Recently I've been downloading powershell functions and scripts that I find useful and wrapping them into invoke-commands to make them more useful across my network. One thing I haven't figured out is how to accept the common parameters from the [cmdletBinding()] and pass them all into the invoke-command. I know how to pass individual preference variables but not the entirety of them. Is there a common variables collection? Below is some powershell excerpts to help illustrate.
ScriptBlock =
{ #How do I pass the whole of the common variables?
$ErrorActionPreference=$using:ErrorActionPreference
$InformationPreference=$Using:InformationPreference
$VerbosePreference=$Using:VerbosePreference...
Process
{
Write-Verbose "Processing"
$computername=$name
#Used $Name as a parameter originally to be compatible with the get-adcomputer cmdlet
If ($Credential) {Invoke-Command -ComputerName $computername -Credential $Credential -ScriptBlock $ScriptBlock}
Else {Invoke-Command -ComputerName $computername -ScriptBlock $ScriptBlock}
#You will need to be running Powershell with the proper Admin privileges if you don't specify a credential
} #End Process
END{
You can get it from the $PSBoundParameters hashtable.
$VerbosePreference=$PSBoundParameters['Verbose']
$ErrorActionPreference=$PSBoundParameters['ErrorAction']
EDIT:
You can also splat these standard parameters to your cmdlets as well
Invoke-Command -scriptblock $scriptblock #PSBoundParameters
Related
I start with a txt file named vms.txt.
It contains 2 servers like so:
server1
server2
When I run the script shown below, the command that is invoked to install VMware tools only runs on server2 and server1 gets skipped. Does anyone have any suggestions on how to modify this script to make it run on both servers in the txt file? I will have to run this in the future for hundreds of VMs, so I am trying to find an easy way to get this to loop properly.
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
foreach($vm in $vms){
$sessions = New-PSSession -ComputerName $vm -Credential $cred
}
foreach($session in $sessions)
{
Invoke-Command -Session $session -ScriptBlock {
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI”
}
}
In your loop-based approach, the problem is your variable assignment:
# !! This only ever stores the *last* session created in $sessions,
# !! because the assignment is performed in *each iteration*.
foreach($vm in $vms){
$sessions = New-PSSession -ComputerName $vm -Credential $cred
}
The immediate fix is to move the assignment out of the loop:
# OK - captures *all* session objects created in $sessions
$sessions = foreach($vm in $vms){
New-PSSession -ComputerName $vm -Credential $cred
}
Taking a step back:
Both New-PSSession -ComputerName and Invoke-Command -Session accept an array of computer names / sessions, so there's no need for loops.
Passing multiple sessions / computer names to Invoke-Command has the big advantage that the operations run in parallel.
Note:
Invoke-Command has built-in throttling to avoid targeting too many machines at once. It defaults to 32, but can be modified with the -ThrottleLimit parameter.
Output from the targeted computers will arrive in no predictable order, but the output objects are decorated with (among others) a .PSComputerName property reflecting the originating computer - see the bottom section of this answer.
That is, your code can be simplified to:
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
$sessions = New-PSSession -ComputerName $vms -Credential $cred
Invoke-Command -Session $sessions -ScriptBlock {
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI”
}
Important:
Sessions should eventually be cleaned up with Remove-PSSession when no longer needed.
However, that stops any commands running in those sessions, so if you've launched asynchronous operations via your Invoke-Command call, you need to ensure that those operations have finished first - see the comments re potentially asynchronous execution of your VMware-tools-10.3.5.exe application below.
Or, even simpler, if you only need to execute one command on each machine, in which case there is no need to create sessions explicitly, pass all computer names directly to Invoke-Command's -ComputerName parameter:
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
# Note the use of -ComputerName
Invoke-Command -ComputerName $vms -Credential $cred -ScriptBlock {
# Note the use of | Write-Output to ensure synchronous execution.
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI” | Write-Output
}
Important:
If your application (VMware-tools-10.3.5.exe) runs asynchronously, you must ensure its
synchronous execution, otherwise it may not run to completion, because the implicitly created remote session is discarded when a script block returns from a given computer.
A simple trick to ensure synchronous execution of any external (GUI-subsystem) executable is to pipe it to Write-Output, as shown above (or Wait-Process, if it doesn't produce console output) - see this answer for an explanation.
I'm trying to create a script that connects to various servers, should attach a PSDrive and copy over files. The problem resides in that I an unable to pass the variable into the Invoke-Command script-block.
workflow kopijobb {
param ([string[]]$serverList, $creds, $basePath)
foreach -parallel ($server in $serverList){
# Use the sequence keyword, to ensure everything inside of it runs in order on each computer.
sequence {
#Use the inlinescript keyword to allow PowerShell workflow to run regular PowerShell cmdlets
inlineScript{
$path = $using:basePath
Write-Host "Starting $using:server using $path"
#Create session for New-PSSession
$session = New-PSSession -ComputerName $using:server -Credential $using:creds
# Copy Java and recreate symlink
Invoke-Command -Session $session -ScriptBlock {
# Make a PSDrive, since directly copying from UNC-path doesn't work due to credential-issues
New-PSDrive -Name N -PSProvider FileSystem -root $using:path -Credential $using:creds | out-null
I pass the network path to $basePath, and I'm able to read it inside the inlineScript block (where I have tried storing it in a new variable to test), but once I try accessing it in the New-PSDrive command, the variable is suddenly empty/unreachable, and the mounting of the drive fails with the error Cannot bind argument to parameter 'Root' because it is null.
I'm at a loss to why this fails, so I'm turning to the collective wisdom here instead.
If feels embarrassing to answer my own question, especially on the same day, but I bumped into a PowerShell guru at work and he took one glance at the script and saw the problem:
I had to add -Args to the Invoke-Command
Invoke-Command -Session $session -ScriptBlock {
param($srv,$login,$path,$...)
#Make a PSDrive, since directly copying from UNC-path doesn't work due to credential-issues
New-PSDrive -Name N -PSProvider FileSystem -root $path -Credential $login | out-null
} -Args $using:server,$using:creds,$using:basePath,$using:...
This does of course mean that I had to import all the needed arguments from the top level into the workflow, and then into the Invoke-Command.
I wish to use Invoke-Command passing environment variables from the calling machine to the server where Invoke-Command is being executed.
I want this to work:
Invoke-Command -ComputerName MyServer-ScriptBlock {
$env:VAR=$using:env:USERNAME
Write-Host $env:VAR
}
But the output for this command is empty. If I do not use the $using scope modifier, and just assign the variable directly I get the expected output ("VAR").
Invoke-Command -ComputerName MyServer -ScriptBlock {
$env:VAR="VAR"
Write-Host $env:VAR
}
So, can I use $using with environment variables? If not, is there an easy way to pass environment variables over to the remote computer where Invoke-Command is running?
One option would be to assign the environment variable to a standard variable before invoking:
$username = $env:USERNAME
Invoke-Command -ComputerName MyServer-ScriptBlock {
$env:VAR=$using:userName
Write-Host $env:VAR
}
Note that assigning environment variables like this ($env:VAR=<value>) won't persist once your session ends. Use the Environment.SetEnvironmentVariable() method to do that.
I think you could use -ArgumentList. See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-6
Invoke-Command -ComputerName MyServer -ArgumentList $env:USERNAME -ScriptBlock {
Param ($x)
Write-Host $x
}
Update: The bug described below has been fixed in v7.3.
The existing answers show helpful workarounds.
It looks like you've hit a bug, present up to PowerShell 7.2.x:
The $using: scope - which is required for accessing variable values from the caller's scope in all out-of-runspace executions (see this answer) - should also support namespace variable notation (see this answer).
Thus, you should be able to pass a namespace-notation variable reference such as $env:USERNAME as $using:env:USERNAME:
indeed you can in the context of jobs (child-process-based background jobs started with Start-Job and thread-based jobs started with Start-ThreadJob); e.g.:
$env:FOO = 'bar'
# Outputs 'bar', as expected.
Start-Job { $using:env:FOO } | Receive-Job -Wait -AutoRemoveJob
but, as of PowerShell 7.1, that doesn't work with PowerShell remoting, such as Invoke-Command -ComputerName, as in your case.
The potential bug has been reported in GitHub issue #16019.
This question already has an answer here:
Parameter interpretation when running jobs
(1 answer)
Closed 6 years ago.
I have a PoSH script that I can't figure out is not running..
function Connect-AD
{
Param($mod,$cmd)
Write-Host "$mod $cmd"
Write-Host "`tConnecting to AD: $DC`n"
$ADSession = New-PSsession -ComputerName $DC -Credential $MyCredential
Invoke-Command -Command {Import-Module ('$mod') -Cmdlet ('$cmd')} -Session $ADSession
Import-PSSession -Session $ADSession -Module ('$mod') -Prefix r | Out-Null
}
I then try to call this with..
Connect-AD -mod 'ActiveDirectory' -cmd 'Get-ADUser,New-ADUser'
But no mater what I do I keep getting..
The specified module '$mod' was not loaded because no valid module file was found in any module directory.
The Write-Host inside the function outputs the parameters correctly, so it is getting that far. However it is not being passed into the Invoke-Command or Import-PSSession?
I've tried different ways to escape the parameters, etc.. but no luck.
What am I not doing correctly? Anyone able to help me out? Thanks.
Single quoted strings don't interpolate variables, '$mod' is a literal string "dollar m o d".
And you probably need to read all the similar questions on passing parameters to Invoke-Command, because the command {} is running on another computer - how will it know what the variable $mod is on your computer?
Passing string $variable to invoke-command scriptblock parameter -name
Powershell: How to pass parameter with invoke-command and -filepath remotely?
Something like
Invoke-Command -Command {param($mod, $cmd) Import-Module $mod -Cmdlet $cmd} -Session $ADSession -ArgumentList $mod,$cmd
Help Links (if available):
Invoke-Command
Import-Module
I want to call another secondary powershell script from within my primary powershell script. I want to pass it a parameter from the primary script, the secondary script needs the username parameter, I want to pass to it, from the primary, and then have the secondary script Im calling use different credentials. I think I might be able to use invoke-command, I just dont know all the syntax, anyone able to post some examples of what I want to accomplish, and then I'll fill in the blanks if need be?
Thanks in advance! :-)
Assume that your secondary script looks like this:
param (
[string] $Username = $args[0]
)
Write-Output -InputObject $Username;
You can use the Start-Process cmdlet to launch the script with alternate credentials.
$Credential = Get-Credential;
Start-Process -Wait -NoNewWindow -FilePath powershell.exe -ArgumentList '"c:\path\to my\file.ps1" -Username "UsernameGoesHere!"' -Credential $Credential;
Or you can use the Invoke-Command cmdlet:
Invoke-Command -FilePath 'c:\path\to my\script.ps1' -Credential $Credential -ArgumentList "UsernameGoesHere!";
I got it, thanks to Trevor Sullivan for pointing me in the right direction.
I ended up just putting my second ps1 file into a scriptblock, and running it as a job, and passing it the arguments from the main script, like this
$job = Start-Job -scriptblock {
param ($username)
some code to run against the variable that was passed in
} -Args $target -credential $Cred
$target being the variable I want to pass to my scriptblock
$username being the parameter that the scriptblock accepts
Thanks.