2

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

mklement0
  • 382,024
  • 64
  • 607
  • 775
jeff4765
  • 21
  • 4
  • 1
    Can you confirm both those files exist on those machines? `c:\users\jsmith\documents\VMware-tools-10.3.5.exe`, this file path needs to exists on both machines if you want to run it on the remote systems. Other than that, you should get rid of those sessions as they tend to hang onto resources unlike cimsessions. – Abraham Zinala Apr 06 '22 at 21:28

1 Answers1

4

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • For some reason when I run the simpler version you mentioned above to not create sessions and to pass all computer names directly to ```Invoke-Command```'s ```-ComputerName``` it tells me access is denied until I add ```-Credential $cred``` after the last bracket of scriptblock. Then it runs without an access is denied error, but the installer never runs properly within the server. It works when using the ```pssession``` method but not when using this method for some reason. – jeff4765 Apr 07 '22 at 14:36
  • @jeff4765, yes, `-Credential $cred` is needed - I simply forgot to add it - fixed now. – mklement0 Apr 07 '22 at 14:38
  • @jeff4765, as for the installer not running properly: does the executable run _asynchronously_? If so, you can try `c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI” | Write-Output` as a quick way to make it run _synchronously_. That ensures that the script block doesn't exit until installation has finished. – mklement0 Apr 07 '22 at 14:40
  • @jeff4765, conversely, if you need your sessions to live on beyond running the script block remotely, you indeed need explicit session creation with `New-PSSession` – mklement0 Apr 07 '22 at 14:49
  • Thank you for the explanation! I will try this. If you need to also add some code to the script to copy that exe file from a UNC path ```\\fileserver\share\VMware-tools-10.3.5.exe``` to the local folder ```c:\users\jsmith\documents\``` on each server listed in the txt file before the ```invoke-command``` kicks off the install on each server listed in the txt file, what would be the best way to lay this out? – jeff4765 Apr 07 '22 at 16:13
  • I have tried the following and I receive "Cannot find path '\\fileserver\tools\VMware-tools-10.3.5.exe' because it does not exist." ```$cred = Get-Credential $vms = Get-Content C:\Scripts\Tools\vms.txt $sessions = New-PSSession -ComputerName $vms -Credential $cred Invoke-Command -Session $sessions -ScriptBlock { Copy-Item "\\fileserver\tools\VMware-tools-10.3.5.exe" -Destination "c:\users\jsmith\documents" c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI” }``` – jeff4765 Apr 07 '22 at 16:16
  • @jeff4765, you either need explicit sessions and use `Copy-Item` with the `-ToSession` locally, before calling `Invoke-Command`, or - in order to be able to access UNC paths from remote sessions - you need to pass credentials and (temporarily) map a drive with those credentials, to work around the double-hop problem - see [this answer](https://stackoverflow.com/a/58265273/45375). However, this is way beyond the scope of your original question, so I suggest you accept this answer and - if you need additional help - ask a _new_ question. – mklement0 Apr 07 '22 at 16:22
  • Thank you for that! Question for you, if I use explicit sessions and use ```Copy-Item``` with the ```ToSession``` locally, is there a way to do this in bulk? What I mean is if I have hundreds of servers in the txt file I am getting the content from and passing to a variable, would I be able to do these file copies easily in bulk? If so, please give me an example of how this would look. I am struggling a bit to picture what this would look like. This is much appreciated! – jeff4765 Apr 07 '22 at 16:39
  • @jeff4765, `-ToSession` only accepts a _single_ `PSSession` object, so you'll need a loop to target all sessions: `$sessions | ForEach-Object { Copy-Item ... -ToSession $_ ... }` – mklement0 Apr 07 '22 at 16:43
  • Altogether, would it look like the following? ```$cred = Get-Credential $vms = Get-Content C:\Scripts\Tools\vms.txt``` ```$sessions = New-PSSession -ComputerName $vms -Credential $cred``` ```$sessions | ForEach-Object { Copy-Item "\\fileserver\share\vmtools.exe" -ToSession $_ "c:\users\jsmith\documents" }``` ```Invoke-Command -Session $sessions -ScriptBlock { ``` ```c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI”``` ```}``` – jeff4765 Apr 07 '22 at 17:28
  • @jeff4765: Looks about right, but it's hard to visually parse multi-line code in comments, which is one reason why comments aren't the place to discuss this. Please also see my update re lifetime of your sessions relative to the execution of your `.exe` file. – mklement0 Apr 07 '22 at 17:36
  • For testing purposes I have two servers (server1 and server2) in a txt file. For some reason, when running the most recent code I posted above, the file copy starts on both server1 and server2, but the copy hangs on server 1 and never completes. It does complete on server2, jut never finishes copying to server1 for some reason. – jeff4765 Apr 07 '22 at 18:16
  • Yes I see your previous comment. If I can't figure out that last question and post a new question, won't my new question get taken down because of duplicate code? – jeff4765 Apr 07 '22 at 21:00
  • @jeff4765, no, it won't be taken down, assuming you describe the specific problem you're now having, ideally in the form of a [mcve]. Duplicate code per se isn't a problem. However, it's good form to link to where the code came from. – mklement0 Apr 07 '22 at 21:16