Issue getting pssessions to loop properly for multiple servers listed in a txt file

456 Views Asked by At

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”
}
} 

1

There are 1 best solutions below

14
On

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.