When you think about it, software deployment is just the act of copying some files to a remote computer, executing an installer, and maybe reporting success or failure. That's it! PowerShell does all this easily.

PowerShell never ceases to amaze me with its flexibility. One reason I love it so much is that it can do just about anything you'd like. If you're good enough, PowerShell code can replace just about any piece of software you have. I might argue, though, that just because you can doesn't mean you should. However, there are times when you need a simple solution to get a job done. One of those jobs is software deployment.

I come from a Microsoft System Center Configuration Manager (SCCM) background. SCCM is expensive; it’s a huge product that does a lot of stuff. Software deployment is one of its features. SCCM is for large software deployments, and I always thought it was overkill for small deployments of fewer than a dozen computers. That's why I sometimes used good ol' PowerShell to do the job. Let's go into how to do that.

I'm going to assume you've already figured out how to install the software silently. Depending on the installer type, you're probably using Windows Installer, InstallShield, or perhaps some other homegrown installer. Regardless, test the install, get it working locally, and then you can look into deploying it remotely.

The next step will require PowerShell remoting. You'll need to ensure the appropriate firewall ports are open and that you have a WinRM listener configured on each computer. For the super-quick way, just run 'winrm quickconfig' on each computer and it will do the rest. I'm also going to assume that your computers are in a domain. This technique is still usable for computers in a workgroup but requires further tweaking to make WinRM work.

Next, you should have a shared folder on your network that contains one folder for each piece of software you'd like to deploy. Here I have folders called SomeClient and vnc representing all the files necessary to install each piece of software.

Directory of all the files necessary

Directory of all the files necessary

Now that we have a source repository set up, we need a list of computers to target. For simplicity, I'll use a text file, but as long as you're able to pull a list of computer names from somewhere like Active Directory or some other database, that works too.

Generate list of computers to target using Get-Content

Generate list of computers to target using Get-Content

I have three PCs I'd like to deploy software to, as you can see.

Next, we'll copy each of the software folders to each of the clients. I prefer copying the entire folder (temporarily) to the client to remove any network hiccups that might occur during install. To do this, I'll use Copy-Item to copy each folder to a temporary location on each client.

$computers = Get-Content –Path C:\Computers.txt
foreach ($pc in $computers) {
    Get-ChildItem \\MEMBERSRV1\Software -Recurse | Copy-Item \\$pc\c$\Windows\Temp -Force    
}

This will create a C:\Windows\Temp\vnc and a C:\Windows\Temp\SomeClient folder on each computer.

We now need to execute the installer on each computer and pass it the correct arguments. I'm going to assume each piece of software is an MSI file, the MSI is called install.msi, and each installs silently with the following syntax locally.

Msiexec /I install.msi /qn

With this, I can execute the command on each computer by using PowerShell to issue the remote command with the Invoke-Command cmdlet inside the loop.

$softwareFolders = Get-ChildItem \\MEMBERSRV1\Software -Directory
foreach ($pc in $computers) {
    foreach ($sw in $softwareFolders) {
        Copy-Item –Path $sw.FullName  -Destination  \\$pc\c$\Windows\Temp -Force -Recurse
        Invoke-Command –ComputerName $pc –ScriptBlock {msiexec /I "C:\Windows\Temp\$($using:sw.Name)\install.msi" /qn}
    }
}

Notice that I had to add some code to account for the software folder names. This was because I needed a way to reference the folder name inside of the scriptblock that Invoke-Command uses. This will ensure install.msi executes on each computer for each piece of software.

Finally, the only thing left to do is to clean up our temporary mess.

Subscribe to 4sysops newsletter!

$softwareFolders = Get-ChildItem \\MEMBERSRV1\Software -Directory
foreach ($pc in $computers) {
    foreach ($sw in $softwareFolders) {
        Copy-Item -Path $sw.FullName  -Destination "\\$pc\c$\Windows\Temp" -Force -Recurse
        Invoke-Command –ComputerName $pc –ScriptBlock {msiexec /I "C:\Windows\Temp\$($using:sw.Name)\install.msi" /qn}
    }
    Remove-Item -Path "\\$pc\c$\Windows\Temp" -Recurse -Force
}
avatar
30 Comments
  1. Phonce 6 years ago

    Nice job.  But is there a way to retrieve status codes for the installation?  Just to know if the installation went fine,  wrong or have a reboot pending?

    Thanks!!

  2. Author

    Yes. You can use $LastExitCode or you could use the ResultCode property from Start-Process.

  3. Dan Bone 4 years ago

    "Adam the Automator". Deltron 3030 fan?

  4. Adithya 3 years ago

    what if the remote machines has username and password. The same username and password for all the remote machines. Can you please tell me how to include that in the code

  5. Adithya: When you use  your Invoke-Command, you can pass it a Credential object.  So, create your credential with just the username & password, and it should work.

    If you really want to *completely* automate it, you could look at using the Credential Store on the system you are going to invoke the script from.  You'd log in as the account that will run the script, and store the password in the credential store (it is encrypted for that user only on that computer only).  Then your script could retrieve that stored credential information to create the Credential Object.

    David F.

    avatar
  6. Adithya Srinivasan 3 years ago

    Thank you very much for your reply. Can you please tell me how can add it to the code. Just a code and where should i place it. It will be really helpful to me.

  7. Sure..  🙂

    Line 2 - get the credential
    Line 6 - add the -Credential parameter  that's it..

    $softwareFolders = Get-ChildItem \\MEMBERSRV1\Software -Directory
    $MyCredential = Get-Credential 
    foreach ($pc in $computers) {
         foreach ($sw in $softwareFolders) {
              Copy-Item -Path $sw.FullName -Destination "\\$pc\c$\Windows\Temp" -Force -Recurse
              Invoke-Command –ComputerName $pc –ScriptBlock {msiexec /I "C:\Windows\Temp\$($using:sw.Name)\install.msi" /qn} -Credential $MyCredential
    }
       Remove-Item -Path "\\$pc\c$\Windows\Temp" -Recurse -Force
    }

    David F

  8. Adithya Srinivasan 3 years ago

    Thank you very much David. I truly appreciate your help.

    Will give a try and let you know 🙂

  9. Adithya Srinivasan 3 years ago

    $softwareFolders = Get-ChildItem \\MEMBERSRV1\Software -Directory
    $MyCredential = Get-Credential $user="adithya" $pass="abc@123"----- Is this the correct way to give the credentials
    foreach ($pc in $computers) {
         foreach ($sw in $softwareFolders) {
              Copy-Item -Path $sw.FullName -Destination "\\$pc\c$\Windows\Temp" -Force -Recurse
              Invoke-CommandComputerName $pcScriptBlock {msiexec /I "C:\Windows\Temp\$($using:sw.Name)\install.msi" /qn} -Credential $MyCredential
    }
       Remove-Item -Path "\\$pc\c$\Windows\Temp" -Recurse -Force
    }

  10. Get-Credential normally pops up a GUI dialog asking for the credentials.  This is where things can get messy..

    Normal best practice is to never put the credentials in the script in any form, and of course, that's frequently not realistic.

    Now.. let's say your circumstances say that it is ok to have the creds in the script.. then you could do this:

    $softwareFolders = Get-ChildItem -Path \\MEMBERSRV1\Software -Directory
    $MyPassword = 'abc@123' | ConvertTo-SecureString
    $MyUserID = 'adithya'
    
    $MyCredential = [System.Management.Automation.PSCredential]::new($MyUserID, $MyPassword)
    
    #$MyCredential = Get-Credential $user="adithya" $pass="abc@123"----- Is this the correct way to give the credentials (No, this is not)
    
    foreach ($pc in $computers) {
        foreach ($swin$softwareFolders) {
             Copy-Item-Path $sw.FullName-Destination "\\$pc\c$\Windows\Temp"-Force -Recurse
            Invoke-Command –ComputerName $pc –ScriptBlock {msiexec /I "C:\Windows\Temp\$($using:sw.Name)\install.msi"/qn} -Credential $MyCredential
        }
        Remove-Item-Path "\\$pc\c$\Windows\Temp"-Recurse -Force
    }

    I have started working with some "new" modules/capabilities that should give you a *much* better solution, but it requires that you store the credentials using whatever account is going to run the script..

    https://gallery.technet.microsoft.com/Manipulate-credentials-in-58e0f761

    So, you use this module to store the credentials, and then in the script you use the same module to retrieve the credentials from the vault, and then convert them from the security type credential to a pscredential (that function is already in the module).  So, using this you can have your secured credential under your account to run the script, and use it that way.

    This module has been the best way I have found to manage credentials for scripts. The downside being that your system needs to be at least Win8/2012.

    David F.

    avatar
  11. Rick 3 years ago

    Hi Adam,

    Everything works except the install.

    The files are copied over to the temp directory but the app (adobe reader) doesn't get installed.

    Powershell doesn't report any errors.

    Any suggestions?

    • @Rick

      Can you share the script?

      • Rick 3 years ago

        Yes sir but I really didn't change the original script. I just did a copy paste.

        Thank you for your help.

        I commented out the "Remove-Item" line because I was receiving the error:

        Remove-Item : Access to the path '\\Test66\C$\Documents and Settings' is
        denied.
        At line:11 char:4
        + Remove-Item -Path "\\$pc\C$" -Recurse -Force......

        $computers = Get-Content "C:\PowerShell\Servers.txt"
        
        $softwareFolders = Get-ChildItem "C:\Adobe_Reader\Adobe Reader DC"
        
        
        foreach ($pc in $computers) {
            foreach ($sw in $softwareFolders) {
                Copy-Item -Path $sw.FullName -Destination \\$pc\C$ -Force -Recurse
                Invoke-Command -ComputerName $pc -ScriptBlock {msiexec /I "C$\$($using:sw.Name)\AcroRead.msi" /qn}
            }
          # Remove-Item -Path "\\$pc\C$" -Recurse -Force
        }

         

        • @Rick

          Fortunately, you get an access denied error for the Remove-Item line,
          because your line is removing everything which is under C:\ from the remote computer.

          Then to come back to your installation line, you are using C$ instead of C: in line 9.

          Another problem is that the original code has been designed to enumerate a list of folders containing an MSI.
          You are instead enumerating the files in that folder.

          By replacing the C$ in line 9 and replacing line 3 with the following line it should work.

          $softwareFolders = Get-ChildItem "C:\Adobe_Reader"

          • Rick 3 years ago

            Well when you put it that way it makes sense 😀...lol!!!

            I guess you can tell I’m a newbie.

            Thank you for your help and explanation I really appreciate it.

            avatar
  12. Steven Newman 3 years ago

    Is anyone willing to help me out?  First, I'm not a PowerShell guy.  I've kludged together some small scripts here and there, but this is really me trying to learn how to do a specific thing, so I get concepts, but I could be missing some basic stuff.

    I'm trying to do exactly what this script is for.  Deploy an MSI file to a number of servers.  I am running PowerShell on my local machine, launching it with credentials that also have admin rights on each of the servers I am trying to interact with.

    I have the source files stored on my local machine.  

    $computers = Get-Content -Path c:deployallservers.txt
    
    $softwareFolders = Get-ChildItem c:deploysoftware -Directory
    
    foreach ($pc in $computers) {
    
         foreach ($sw in $softwareFolders) {
    
              Copy-Item -Path $sw.FullName -Destination "\$pcc$windowstemp" -Force -Recurse
    
              Invoke-Command -ComputerName $pc -ScriptBlock {msiexec /I                "C:WindowsTemp$($using:sw.Name)installer.msi" /qn}
    
         }
    
         Remove-Item -Path "\$pcc$windowstemp" -Recurse -Force

    When I run it, it seems to pause (presumably running) for a time, then it starts generating a ton of errors.  For example:  Remote-Item : Cannot remove item.  \[machinename]c$WindowsTemppdk-SYSTEM...per1516.dll: Access to the path 'perl516.dll' is denied.

    Tons of those.  It's like I've got something wrong with the folder stuff and it's trying to kill everything off the WindowsTemp folder and not just the folder we copied.

    But additionally, the software doesn't install, either.  

    For testing, I tried this line manually:

    Copy-Item c:deploysoftware -Recurse \[machinename]c$windowstemp -Force and it did copy SoftwareFirefox (and the msi).  

    I then tried:

    Invoke-Command -ComputerName [machinename] -ScriptBlock {msiexec /I "c:windowstempsoftwarefirefoxinstaller.msi" /qn}

    It pauses for about two seconds and then is back at the command prompt with no error, yet the software does not install on the named server.

    I sincerely appreciate any help offered.

    Thank

  13. @Steven Newman

    First concerning the deletion step:

    The following line removes the C:\Windows\Temp folder and everything inside (files and subfolders)

    Remove-Item -Path "\\$pc\c$\windows\temp" -Recurse -Force

    However, keep in mind that this is a system folder.
    Windows uses it to store its temporary data.
    Thus, when you delete the content, you also try to delete files that Windows has stored there, and which are sometimes still in use.

    Now concerning the installation part:

    What happens when you execute the following command line on the computer directly?

    msiexec /I "c:\windows\temp\software\firefoxinstaller.msi" 

  14. Steven Newman 3 years ago

    On the target server, if I remote in and launch powershell and then run

    msiexec /I "c:\windows\temp\software\firefox\installer.msi" it runs fine, and the installer pops up and it completes the install.

    How would I need to change the script so that rather than trying to empty c:\windows\temp, I was only emptying c:\windows\temp\software?  Would I just need to make the last line be:

    Remove-Item -Path "\\$pc\c$\Windows\Temp\Software" -Recurse -Force

    • I would try this.

      Invoke-Command -ComputerName [machinename] -ScriptBlock {Start-process -Wait msiexec -Argumentlist '/I "c:windowstempsoftwarefirefoxinstaller.msi" /qn'}

      • Steven Newman 3 years ago

        OK, I assume I'm entering that in PowerShell from my local machine.  After I type the line in (replacing machinename with server name and adding backslashes to paths, I hit enter and I get the two >>s

        • Hehe something went wrong here 🙂 I suggest to request deletion 🙂

        • Seems you tried to paste an image into the comment field. This doesn't work. I removed the corresponding code from your comment.

        • @Steven Newman

          Yes, as you found it out the following line is the way to go in order to remove your temporary files.

          Remove-Item -Path "\\$pc\c$\Windows\Temp\Software" -Recurse -Force

          Can you please run the following code and post the result?

          $computers = Get-Content -Path c:\deployallservers.txt
          
          $softwareFolders = Get-ChildItem c:\deploysoftware -Directory
          
          foreach ($pc in $computers) {
              
              foreach ($sw in $softwareFolders) {
                  
                  Copy-Item -Path $sw.FullName -Destination "\\$pc\c$\windows\temp" -Force -Recurse
                  
                  Invoke-Command -ComputerName $pc -ScriptBlock {
                      
                      get-item -Path "C:\Windows\Temp\$($using:sw.Name)\installer.msi"
          
                      msiexec.exe /I "C:\Windows\Temp\$($using:sw.Name)\installer.msi" /qn
          
                      Remove-Item "C:\Windows\Temp\$($using:sw.Name)" -Recurse -Force
                  }
              }
          }

          • Steven Newman 3 years ago

            Thanks, Luc.  So I ran exactly what you had above.  It copies the folder and file to the targets, then removes it, but without ever installing.

            • Steven Newman 3 years ago

              An additional follow up.  If I open a PowerShell session on my local machine, and use the Enter-PSSession [ServerName], and once the connection is established I browse to the windows\temp\firefox\installer.msi file and ran "msiexec /I installer.msi /qn" it did install on the server fine.

              If that provides any insight... 🙂

  15. @Steven Newman

    Please have a look at the Setup and Application logs when you run the msiexec.

    Also, did you try Leos' suggestion with the Start-Process cmdlet and the -Wait parameter?
    Just in case the Invoke-Command would return before the msiexec was able to finish its work...

  16. Jim 3 years ago

    Hi,

    Instead of getting from computer.txt can I set it as AD group?

  17. Dinakar 1 year ago

    Hi Guys,

    Can any one help me with a script to remote install windows offline patches (KB's) and get the result if installed or failed

    We have 500 servers and patching them manually is very tough.

    Can anyone really hep please.

    You can share the script here or to my mail

    dinakar.tech@gmail.com

    Thanks

  18. Eugene 8 months ago

    Hello, i modified the code a bit and it looks like this.

    #Variables
    $computername = Get-Content "C:\Computer Lists\WiFiUpdate.txt"
    $sourcefile = "\\3474-VM\Wifi Driver\WiFi_22.30.0_Driver64_Win10.exe"
    #This section will install the software 
    foreach ($computer in $computername) 
    {
        $destinationFolder = "\\$computer\C$\Temp"
        #It will copy $sourcefile to the $destinationfolder. If the Folder does not exist it will create it.
    
        if (!(Test-Path -path $destinationFolder))
        {
            New-Item $destinationFolder -Type Directory
        }
        Copy-Item -Path $sourcefile -Destination $destinationFolder
        Invoke-Command -ComputerName $computer -ScriptBlock {Start-Process 'c:\temp\WiFi_22.30.0_Driver64_Win10.exe -s' -Wait}
    
    }
    

    how ever when i run it my output is as follows..

     Directory: \\LVN-LT-3363\C$
    
    
    Mode               LastWriteTime       Length Name                                                                                                                                                                
    ----               -------------       ------ ----                                                                                                                                                                
    d-----         4/1/2021 2:16 PM               Temp                                                                                                                                                                
    This command cannot be run due to the error: The system cannot find the file specified.
        + CategoryInfo         : InvalidOperation: (:) [Start-Process], InvalidOperationException
        + FullyQualifiedErrorId : InvalidOperationException,Microsoft.PowerShell.Commands.StartProcessCommand
        + PSComputerName       : LVN-LT-3363
    

    it seems like its creating a directory, but does not copy the file there fore it can not run it.

Leave a reply

Please enclose code in pre tags

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2021

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account