Remotely Install Software Using WMI And Powershell

by Brandon Dillinger

Today I’d like to talk about remote process creation using Powershell and WMI. Specifically for software installation, but you could start any process on a remote machine by modifying the code to your liking. As always, I’m sure there is a better way to do this, but this is the way I’ve found to make everything work. I wrote this because we had a large group of machines that needed the same thing installed and we didn’t have authorization to do it any way other than manually (yuk!) or via script. If you’re not using a MSI install OR an EXE that has a silent install trigger you are going to need to wrap it up in a silent install package. I’ll cover that in another post another day and update this with a link to it in case you need to know how.

If you need to start a local process powershell comes with a built in way to accomplish that. Start-Process will start any process locally. However, if you need to start a process remotely Start-Process will leave you wanting. The way I’ve chosen to start the remote process to install a piece of software is by using win32_process. You can call win32_process using the Invoke-WmiMethod cmdlet inside powershell to invoke the create method and pass it arguments. If that’s the only part you need I’ll save you further reading and provide a snippet of it here. The specific line of code looks something like this.


Invoke-WMIMethod win32_process -name create -ArgumentList 'msiexec.exe /i "c:\path\to\file.msi" /qb REBOOT=ReallySuppress'

Alright, so that line will execute the file.msi installer located in c:\path\to\ on the TARGET machine. This means we will need to copy the file to the target machine. I typically use the temp directory and then clean the file when the install is done. I do this by monitoring the process that gets created to see when it ends and the file can be deleted, but lets not get ahead of ourselves.

So, we start off with the list of workstations that will be imported and then have the install ran against each machine in the list using a foreach loop.


$colComputer = Get-Content c:\targets.txt

Foreach ($strComputer in $colComputer) {

Now that we have our workstation list together and ready for processing we will need to verify that the RemoteRegistry service is running or we won’t be able to monitor the process creation. If your environment always has this enabled then you can skip the next piece of the script, but it doesn’t hurt anything to leave it in as it will only start the service if it is found to be stopped.


$service = Get-Service 'RemoteRegistry' -cn $strComputer

If ($service.Status -match 'Stopped') {

    $service.Start()

}

Now, we could also use WMI here if we felt like it. Just for comparison here is the code to use WMI instead

$service = gwmi win32_service -cn $strComputer | Where { $_.name -match 'RemoteRegistry' }

If ($service.state -match 'Stopped') {

    $service.StartService()

}

Notice the differences. Using the Get-Service cmdlet it’s the status that tells you if the service is running or stopped, versus win32_service it is the state that designates whether the service is running. The message is the same with both however, they will either be “Running” or “Stopped”. To start or stop the service with the Get-Service cmdlet you need to call the Start() method or the Stop() method. With win32_service you have to StartService() or StopService(). If you forget the proper method you can always pipe the command to Get-Member to be shown a list of Methods and Properties for the given item. You can pipe variables that contain the results of a cmdlet or WMI call to Get-Member as well, like so.


$service = Get-Service RemoteRegistry -cn $strComputer

$service | Get-Member

OK, so we have our list of machines, we’ve initiated our Foreach loop, and we’ve taken care of the RemoteRegistry service. Next we will cover getting our installation files onto the target machine. I’m assuming that your piece of software has more than one file, if you only have one install file you can remove the foreach loop and just directly copy the file.


#If the target directory doesn't exist, create it

If (!(Test-Path \\$strComputer\c$\temp\MyInstall\)) {

mkdir \\$strComputer\c$\temp\MyInstall\

}

#Get all the files for the setup program. This can be on a network share or your local machine.

$files = Get-ChildItem "\\path\to\software\"

Foreach ($file in $file) {

Copy-Item $file.fullname "\\$strComputer\c$\temp\MyInstall\"

}

The next step is to create a variable with our process. Remember, the path you put to the file needs to point to an existing file on the target workstation.


#Start the process on the remote machine

$process = Invoke-WMIMethod win32_process -name create -cn $strComputer -ArgumentList 'msiexec /i "c:\temp\MyInstall\install.msi" /qb /norestart'

#Get the process ID for the newly created process

$id = $process.processid

#Create a DO WHILE loop to monitor for process exit

$a = ""
do {
$a = Get-Process -cn $strComputer | Where{$_.ID -match "$ID"}
}
While ($a -ne $null)

#Delete the MyInstall directory on the remote machine
del \\$strComputer\c$\temp\MyInstall -force -recurse

#Lets leave the machine the way we found it in regards to running services
If ($service.status -match 'Stopped') {</pre>
<pre>$service.Stop()
}

That’s pretty much all there is to it. You could put in a check to verify that the software is installed by reading the registry, or checking for a file that is created upon installation after this if you wanted to. If you’re upgrading software then I recommend checking the registry key to do a version match instead of verifying that the install path has the programs files. Here is everything we have written in pieces combined into the final script. Remember, you will have to run your powershell terminal or ISE using an account that has admin rights on the target workstations.

$colComputer = Get-Content c:\targets.txt

Foreach ($strComputer in $colComputer) {

 $service = Get-Service 'RemoteRegistry' -cn $strComputer

 If ($service.Status -match 'Stopped') {
 $service.Start()
 }

 #If the target directory doesn't exist, create it
 If (!(Test-Path \\$strComputer\c$\temp\MyInstall\)) {
 mkdir \\$strComputer\c$\temp\MyInstall\
 }

 #Get all the files for the setup program. This can be on a network share or your local machine.
 $files = Get-ChildItem "\\path\to\software\"

 Foreach ($file in $file) {
 Copy-Item $file.fullname "\\$strComputer\c$\temp\MyInstall\"
 }

 #Start the process on the remote machine
 $process = Invoke-WMIMethod win32_process -name create -cn $strComputer -ArgumentList 'msiexec /i "c:\temp\MyInstall\install.msi" /qb /norestart'

 #Get the process ID for the newly created process
 $id = $process.processid

 #Create a DO WHILE loop to monitor for process exit
 $a = ""

 do {
 $a = Get-Process -cn $strComputer | Where{$_.ID -match "$ID"}
 }

 While ($a -ne $null)

 #Delete the MyInstall directory on the remote machine
 del \\$strComputer\c$\temp\MyInstall -force -recurse

 #Lets leave the machine the way we found it in regards to running services
 If ($service.status -match 'Stopped') {
 $service.Stop()
 }
}

As always, I welcome comments and suggestions on how to improve the blog and what might be useful for future articles. Have an idea, suggestion, or correction? Please leave a comment!

Advertisements