I am building an Azure Runbook helper function, that ultimately executes on a Hybrid Runbook Worker.
To generalize my code as much as possible I came up with the idea to have a function dynamically build a scriptblock that defines the Exchange Cmdlet name and parameter.This is what I have so far - the comments are experiments that reveal some of the steps I tried before posting this thread.
function Invoke-BSExchangeCommand{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$CmdletName,
[Parameter(Mandatory = $false)]
[hashtable]
$ArgumentList,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Runspaces.PSSession]
$Session
)
$scriptBlock = {
param ($invokeParams)
# Get-DistributionGroup #invokeParams
# $_::$CmdletName.Invoke($invokeParams)
$cmd = Get-Command -Name $CmdletName
& $cmd #invokeParams
}
$params = #{
Session = $Session
ArgumentList = $ArgumentList
ScriptBlock = $scriptBlock
}
Invoke-Command #params
}
Edit: I have found that the "Get-Command" cmdlet is exposed/not restricted on the Exchange endpoint.
The error I am getting is a generic one and gives many different (irrelevant) Google search results: A Begin statement block, Process statement block, or parameter statement is not allowed in a Data section
Edit: My starting point that I am trying to improve is that this scriptblock with the defined Cmdlet name works:
$scriptBlock = {
param ($invokeParams)
Get-DistributionGroup #invokeParams
}
Thank you!
-Anders
Related
I'm new to PowerShell and have been trying to pass credentials to dot-sourced scripts. The idea is to have one main file that I can just load and then use the sourced functions and it'll be cleaner.
Here's an example of what I've tried so far:
Functions.ps1
$credentials = Get-Credential -Message "Please enter your credentials."
. .\Unlock.ps1 -Credential $credentials
Unlock.ps1
function Unlock-Account {
param(
[Parameter(Mandatory = $true)]
[string] $username,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
[PSCredential] $Credential
)
try {
Unlock-ADAccount -Identity $username -Credential $credentials
}
catch {
Write-Output " Could not unlock $username"
}
}
Dot-sourcing a script does not automatically invoke functions in the sourced script. What you'd normally do in your scenario is this:
$credentials = Get-Credential -Message "Please enter your credentials."
. .\Unlock.ps1
Unlock-Account 'somename' -Credential $credentials
If you want to avoid having to specify -Credential $credentials with every invocation of the function you could make the credential parameter optional and assign a default value:
Param(
[Parameter(Mandatory=$true)]
[string]$Username,
[Parameter(
Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[PSCredential]$Credential = $global:Credential
)
That way $Credential will automatically assume the value from the variable $Credential in the global scope if that variable has already been defined. Beware, though, that the parameter will have an empty value if the global variable has not been defined when the function is invoked.
Technically speaking you can dot-source a script and pass parameters to it:
Param(
[Parameter(Mandatory=$true)]
[PSCredential]$Credential
)
function Unlock-Account {
Param(
[Parameter(Mandatory = $true)]
[string]$Username,
[Parameter(
Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromPipeline=$true
)]
[PSCredential]$Credential = $script:Credential
)
...
}
and then dot-source the script and invoke the function like this:
$credentials = Get-Credential -Message "Please enter your credentials."
. .\Unlock.ps1 -Credential $credentials
Unlock-Account 'somename'
However, that approach would be quite ... unconventional. I do not recommend taking this route.
Dot sourcing a PS1 is effectively running the contents of that file in your session as if you had copied and pasted the whole thing into your command line. There's not really a concept of passing arguments to a dot sourced file.
However, there are a number of ways to effectively do what you want to do.
One way is to add the command that you want to run to the end of your script. So add the following to the last line of your script file (after the function's closing curly brace):
# You could optionally add the line that gathers the credential here as well.
Unlock-Account -Credential $credentials
I don't personally like this method because it forces you to always run the function when sometimes you just want the function to be available. Another way you can do this is by just making the script file run like a function. Essentially, you just remove the function declaration in the script and keep the rest. Then you just call the script like a function instead of dot sourcing it.
So your script contents becomes:
param(
[Parameter(Mandatory = $true)]
[string] $username,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)]
[PSCredential] $Credential
)
try {
Unlock-ADAccount -Identity $username -Credential $credentials
}
catch {
Write-Output " Could not unlock $username"
}
And you call it like this:
.\Unlock.ps1 -Credential $credentials
This method is pretty useful for a quick one-off script. But if you end up with a lot of these that are all related to each other, you're probably better off creating a real PowerShell module.
The last method is probably the least used, but still perfectly valid if you want to keep the script dot source'able and not actually call anything when it's dot sourced. You can just combine the dot source command and your function call into a one-liner command like this:
. .\Unlock.ps1; Unlock-Account -Credential $credentials
I am editing a function which it will invoke a command directly on the VM. The issue I keep running into is if someone passes a function declaration as a scriptblock, I get and error when calling create, because params() is not at the top of the scriptblock.
Trying to figure out how I can still set-fulllanguage first then execute a function with params.
function Invoke-DirectOnVM
{
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[CloudEngine.Configurations.EceInterfaceParameters]
$Parameters,
[Parameter(Mandatory = $true)]
[String[]]$VMNames,
[Parameter(Mandatory = $true)]
[Object]$VMCredential,
[Parameter(Mandatory = $true)]
[ScriptBlock]$ScriptBlock,
[Object[]]$ArgumentList = $null
)
{
Invoke-Command -VMName $localVMs -Credential $using:VMCredential -ScriptBlock ([ScriptBlock]::Create($("Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $using:ScriptBlock)))
}
Remove the $using: from the scriptblock and it should work properly. I took the liberty of cleaning up the code a bit. The result looks like:
function Invoke-DirectOnVM
{
[CmdletBinding()]
Param (
[Parameter(Mandatory)]
[CloudEngine.Configurations.EceInterfaceParameters]
$Parameters,
[Parameter(Mandatory)]
[String[]]
$VMNames,
[Parameter(Mandatory)]
$VMCredential,
[Parameter(Mandatory)]
[ScriptBlock]
$ScriptBlock,
[Parameter()]
[Object[]]
$ArgumentList = $null
)
$PSBoundParameters.Remove("ScriptBlock")
Invoke-Command #PSBoundParameters -ScriptBlock ([ScriptBlock]::Create( "Import-Module OpenUpSession; Set-FullLanguage; `r`n" + $ScriptBlock ))
}
I tried to pass a hashtable to a scriptblock like this where $arg3 is my hashtable. However, it failed. How do I do it in the correct way?
It doesn't seem like it is passing anything to the script block.
$commandParameters.ComputerName = $ComputerName
$commandParameters.ScriptBlock = {
param(
[Parameter()]
[switch]$arg1 = $false,
[Parameter()]
[array]$arg2,
[Parameter()]
[hashtable]$arg3
)
enter code here
Doing something here
}
Invoke-Command #commandParameters -ArgumentList $arg1, #($arg2), $arg3
=================================================================
I found the answer myself and it works for me. This is how I build the associative array and then pass it to the script block.
I am not sure why, but I was using the dot notation ($hash.a.b) to reference the hash table in a function and it works, but it doesn't work for a script block. It looks like I need to use [ ] (e.g. $hash[a][b])in the script block.
$compADGroups = #{}
foreach ( $adGroup in $adGroups ) {
if ( $compADGroups.$computerNameGroup -eq $null ) {
$compADGroups[$computerName] = #{}
$compADGroups[$computerName]["Group"] = #{}
$compADGroups[$computerName]["Group"] = $hashString
}
}
$session = New-PSSession -ComputerName 'Computer1'
Invoke-Command -Session $session -ArgumentList $compADGroups -ScriptBlock { param($compADGroups) $compADGroups[$env:computername]["Group"]}
Get-PSSession | Remove-PSSession
Make sure you're using Invoke-Command correctly.
$ScriptBlock = {
param(
[Parameter(Mandatory=$True, Position=1)]
[hashtable]$myHashTable
)
# Code here
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ([hashtable]$hashtable)
If you specify certain arguments for a scriptblock, make sure you also describe the position value, and more often than not whether it's mandatory. If you are trying to pass your hashtable in as the second argument in an implicitly defined array of arguments, write your scriptblock so that it takes the hashtable at that specific position.
For example,
$ScriptBlock= {
param(
[Parameter(Position=2)] # Take note of the position you set here
[hashtable]$myHashTable,
[Parameter(Position=1)]
[string]$myString,
[Parameter(Position=3)]
[int]$myInteger
)
# Do stuff
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ($myString, $myHashTable, $myInteger);
# ^ variable is in second position
I need to write a function in PowerShell that receives a bunch of parameters, one of them being a ps1 file. I need to execute this file as part of my function code, but I don't know how to do that.
This is probably a very silly detail, but I failed horribly in trying to search for this.
This is my function at the moment. I tried using Invoke-Command there but it's not working:
Function Start-Dsc {
Param(
[Parameter(Mandatory = $true)]
[string] $configurationFile,
[Parameter(Mandatory = $true)]
[string] $configurationName,
[Parameter()]
[string] $configurationData,
[Parameter(Mandatory = $true)]
[string] $computerName
)
Begin {}
Process
{
Invoke-Command -Command "$configurationFile -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
End{}
}
UPDATE:
With Bacon Bits' help, I managed to make it work. The final script is a bit different than what I initially posted though. This is the final process block:
Process
{
Invoke-Command -FilePath $configurationFile -ComputerName 'localhost';
Invoke-Expression -Command "$configurationName -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
Arguments are a separate option in Invoke-Command. Try:
Invoke-Command -Command "$configurationFile" -ArgumentList "-ConfigurationData $configurationData";
You may also need to change -Command to -FilePath.
I have an issue running the following script in a pipeline:
Get-Process | Get-MoreInfo.ps1
The issue is that only the last process of the collection is being displayed. How do I work with all members of the collection in the following script:
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
function Get-Stats($Process)
{
New-Object PSObject -Property #{
Name = $Process.Processname
}
}
Get-Stats($Process)
try this:
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
process{
New-Object PSObject -Property #{
Name = $Process.Processname}
}
Edit:
if you need a function:
function Get-MoreInfo {
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
process{
New-Object PSObject -Property #{
Name = $Process.Processname}
}
}
then you can use:
. .\get-moreinfo.ps1 #
Get-Process | Get-MoreInfo
Edit after Comment:
Read about dot sourcing a script
I you simply create Get-MoreInfo as a Filter instead of Function, you will get the desired effect.
Filter Get-MoreInfo
{
param( [Parameter(Mandatory = $true,ValueFromPipeline = $true)]
$Process
)
...
Actually, both Christian's answer and tbergstedt's answer are both valid--and they are essentially equivalent. You can learn more about how and why in my recent article on Simple-Talk.com: Down the Rabbit Hole- A Study in PowerShell Pipelines, Functions, and Parameters.
In a nutshell, here are the salient points:
A function body includes begin, process, and end blocks.
A function not explicitly specifying any of the above 3 blocks operates as if all code is in the end block; hence the result you initially observed.
A filter is just another way to write a function without any of the above 3 blocks but all the code is in the process block. That is why the above two answers are equivalent.