Introduction
We want to execute the same command on multiple servers. We had various options to do so but every one of them required to tweak something or the other on the agents to run command.
Background
We have multiple servers in a given environment and sometimes, we need to run a command in parallel at the same time on all servers. There are a lot of tools available for doing this. Like Powershell DSC, Jenkins, Nolio, Octopus, UCDeploy and lot more. But they all had one thing in common, we need to have some sort of installation on the agent machine like a service or small runtime. And we cannot afford to install anything on these servers since they are prod servers and we would need extensive testing to do something like that.
Using the Code
We came up with the plan to use PSexec from Sysinternals. It's good and fast and does not need anything to be installed on the running agents. We can just fire and forget. The logs we could trap for further details on what happened to the process we just launched.
Psexec Command
C:\sysIntern\psexec -u<username> -p <password> \\<machineName/IP> /accepteula
-w <working dir on Client Machine> -h <command to run>. >{8}\{1}.log 2>$null' `
Powershell Integration
Multithreading in Powershell is more of a tweak. It does not work straight out of the box. You don't have delegation or threading libraries.
To do parallel runs in Powershell, you need to call Jobs. Jobs can run in asynchronous mode. You can use fire and forget or you can track them down. But ideally, whatever Jobs script starts should always clean it up. We should not leave the Jobs behind in the process. The Jobs can also be queried from other Powershell shells.
Code for creating and sending Jobs. Below is an example of parameterized Job which we are launching. Here, the Job
will accept one parameter Command
to execute. Below, you see -Name
that is a parameter which is giving Name
to the job
which we will query later and close based on checking if there is an error or not.
$job = Start-Job -ScriptBlock{
param([string] $command)
$output = iex $command
}-ArgumentList $command -Name $IP
$jobs +=$job
To check if Job has completed, here is the code for the same:
foreach ($job in $jobs) {
Wait-Job $job
$results = receive-job -job $job
Write-Host ("Job Data returned for [{0}][{1}]" -f $job.Name , $results)
remove-job $job
}
Now that we know how everything is working, let's put everything together.
$IPS=@("192.168.1.1","192.168.1.2","192.168.1.3","192.168.1.4")
$jobs = @();
$executable = "iisrest /stop"
foreach($IP in $IPS){
$command = '{0} -u {1} -p {2} \\{3} /accepteula -w {4}
-h cmd /c {5} >{6}\{1}.log 2>$null' `
-f ("C:\SysIntern\Psexec.exe",$username,$password,$IP,
"C:\Windows",$executable,"\\LogServer\Logs","iisresetlog")
Write-Host "Command to Execute [$command]"
$job = Start-Job -ScriptBlock{
param([string] $command)
$output = iex $command
}-ArgumentList $command -Name $IP
$jobs +=$job
}
$totalJobs = $jobs.count
$jobsCompleted = 0
$printCounter = 0
while($jobsCompleted -lt $totalJobs){
foreach ($job in $jobs) {
$IP=$job.Name
if(($job.State -ne "Completed") -or ($job.State -ne "Failed")){
if (0 -eq $printCounter % 300){
$totalMins=$printCounter/60
$statusPrint = "Command is still going on {0} for last {1} mins.
Current jobState is {2}." -f ($IP,$totalMins ,$job.State)
write-host $statusPrint
}
continue
}
$jobsCompleted = $jobsCompleted + 1
}
$printCounter = $printCounter + 1
Start-Sleep 1
}
foreach ($job in $jobs) {
Wait-Job $job
$results = receive-job -job $job
Write-Host ("Job Data returned for [{0}][{1}]" -f $job.Name , $results)
remove-job $job
}
Points of Interest
We could have used C# code in Powershell or System.Threading
but they are not native calls of Powershell, rather would be coming from .NET API which will defeat the purpose of the script.
History
- 7th November, 2019: Initial version