If you were at my PowerShell for developers talk at the European SharePoint Best Practices Conference last week then you’ll know that I’ve never been all that happy with how I was approaching Farm Solution deployment, as detailed in an earlier post from sometime last year (Deploying SharePoint 2010 Solution Packages Using PowerShell). So what are some of the issues I have with what I created? Here’s a quick list:

  • There are two functions – I really just want one function to call and let the function figure out what to do based on the parameters provided.
  • I want to be able to provide a directory containing WSP files to deploy (sure, I could use Get-ChildItem to grab all my files, iterate through them, and then call the function, but that means I have to type more each time I want to execute and I’m way too lazy for that).
  • There’s no consideration for simply updating Solution Packages rather than retracting and redeploying.
  • I was using the Start-SPAdminJob cmdlet and stopping and starting the admin service – something that we shouldn’t be doing and is really just an old throwback to 2007. It’s just a bad idea – don’t do it.
  • I was forcing information such as GAC and CAS settings in the XML when I could easily get the information via the SPSolution object once added.
  • And finally, there was no real help available so you had to really know what was going on to understand how to construct the XML file and to then call the file.

For all these reasons I’ve decided to completely rewrite the script. As a result it’s a bit more complicated at first blush but that’s mainly due to some additional error handling, progress reporting, and blocking code that I’ve added; as well as the additional parameter related code and associated help. I’ve essentially followed the pattern that I described with my earlier post on Feature activation and have made the function work more like a cmdlet (with full help, parameter sets, and use of PipeBind objects). Before I share the code, I’d like to show the complete help that is available for the function:

NAME
    Deploy-SPSolutions
    
SYNOPSIS
    Deploys one or more Farm Solution Packages to the Farm.
    
SYNTAX
    Deploy-SPSolutions [-Identity] <String> [[-UpgradeExisting]] [[-AllWebApplications]] [[-WebApplication] <SPWebApplicationPipeBind[]>] [<CommonParameters>]
    
    Deploy-SPSolutions [-Config] <XmlDocument> [<CommonParameters>]
    
DESCRIPTION
    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
    If using an XML configuration file, the format of the file must match the following:
      <Solutions>    
        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
          <WebApplications>            
         <WebApplication>http://example.com/</WebApplication>        
          </WebApplications>    
        </Solution>
      </Solutions>
    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.

PARAMETERS
    -Config <XmlDocument>
        The XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -Identity <String>
        The directory, WSP file, or XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -UpgradeExisting [<SwitchParameter>]
        If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
        
        Required?                    false
        Position?                    2
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -AllWebApplications [<SwitchParameter>]
        If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
        
        Required?                    false
        Position?                    3
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -WebApplication <SPWebApplicationPipeBind[]>
        Specifies the Web Application(s) to deploy the WSP file to.
        
        Required?                    false
        Position?                    4
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    -------------------------- EXAMPLE 1 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
    
    -------------------------- EXAMPLE 2 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
    
    -------------------------- EXAMPLE 3 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 4 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 5 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
    
    -------------------------- EXAMPLE 6 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions C:\Solutions.xml
    
    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
    
RELATED LINKS
    Get-Content
    Get-SPSolution
    Add-SPSolution
    Install-SPSolution
    Update-SPSolution
    Uninstall-SPSolution
    Remove-SPSolution 

As you can see, this is a lot more useful for someone wishing to execute this script as not only does it provide information about the XML structure but it also provides several usage examples.

So, without further delay, here’s the new version of the deployment script (note that I changed the function name to Deploy-SPSolutions so it won’t impact environments that depend on the old function):

  1function global:Deploy-SPSolutions() {
  2  <#
  3  .Synopsis
  4    Deploys one or more Farm Solution Packages to the Farm.
  5  .Description
  6    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
  7    If using an XML configuration file, the format of the file must match the following:
  8      <Solutions>    
  9        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
 10          <WebApplications>            
 11            <WebApplication>http://example.com/</WebApplication>        
 12          </WebApplications>    
 13        </Solution>
 14      </Solutions>
 15    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.
 16  .Example
 17    PS C:\> . .\Deploy-SPSolutions.ps1
 18    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
 19    
 20    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
 21  .Example
 22    PS C:\> . .\Deploy-SPSolutions.ps1
 23    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
 24    
 25    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
 26  .Example
 27    PS C:\> . .\Deploy-SPSolutions.ps1
 28    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
 29    
 30    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
 31  .Example
 32    PS C:\> . .\Deploy-SPSolutions.ps1
 33    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
 34    
 35    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
 36  .Example
 37    PS C:\> . .\Deploy-SPSolutions.ps1
 38    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
 39    
 40    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
 41  .Example
 42    PS C:\> . .\Deploy-SPSolutions.ps1
 43    PS C:\> Deploy-SPSolutions C:\Solutions.xml
 44    
 45    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
 46  .Parameter Config
 47    The XML configuration file containing the WSP files to deploy.
 48  .Parameter Identity
 49    The directory, WSP file, or XML configuration file containing the WSP files to deploy.
 50  .Parameter UpgradeExisting
 51    If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
 52  .Parameter AllWebApplications
 53    If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
 54  .Parameter WebApplication
 55    Specifies the Web Application(s) to deploy the WSP file to.
 56  .Link
 57    Get-Content
 58    Get-SPSolution
 59    Add-SPSolution
 60    Install-SPSolution
 61    Update-SPSolution
 62    Uninstall-SPSolution
 63    Remove-SPSolution
 64  #>
 65  [CmdletBinding(DefaultParameterSetName="FileOrDirectory")]
 66  param (
 67    [Parameter(Mandatory=$true, Position=0, ParameterSetName="Xml")]
 68    [ValidateNotNullOrEmpty()]
 69    [xml]$Config,
 70
 71    [Parameter(Mandatory=$true, Position=0, ParameterSetName="FileOrDirectory")]
 72    [ValidateNotNullOrEmpty()]
 73    [string]$Identity,
 74    
 75    [Parameter(Mandatory=$false, Position=1, ParameterSetName="FileOrDirectory")]
 76    [switch]$UpgradeExisting,
 77    
 78    [Parameter(Mandatory=$false, Position=2, ParameterSetName="FileOrDirectory")]
 79    [switch]$AllWebApplications,
 80    
 81    [Parameter(Mandatory=$false, Position=3, ParameterSetName="FileOrDirectory")]
 82    [Microsoft.SharePoint.PowerShell.SPWebApplicationPipeBind[]]$WebApplication
 83  )
 84  function Block-SPDeployment($solution, [bool]$deploying, [string]$status, [int]$percentComplete) {
 85    do { 
 86      Start-Sleep 2
 87      Write-Progress -Activity "Deploying solution $($solution.Name)" -Status $status -PercentComplete $percentComplete
 88      $solution = Get-SPSolution $solution
 89      if ($solution.LastOperationResult -like "*Failed*") { throw "An error occurred during the solution retraction, deployment, or update." }
 90      if (!$solution.JobExists -and (($deploying -and $solution.Deployed) -or (!$deploying -and !$solution.Deployed))) { break }
 91    } while ($true)
 92    sleep 5  
 93  }
 94  switch ($PsCmdlet.ParameterSetName) { 
 95    "Xml" { 
 96      # An XML document was provided so iterate through all the defined solutions and call the other parameter set version of the function
 97      $Config.Solutions.Solution | ForEach-Object {
 98        [string]$path = $_.Path
 99        [bool]$upgrade = $false
100        if (![string]::IsNullOrEmpty($_.UpgradeExisting)) {
101          $upgrade = [bool]::Parse($_.UpgradeExisting)
102        }
103        $webApps = $_.WebApplications.WebApplication
104        Deploy-SPSolutions -Identity $path -UpgradeExisting:$upgrade -WebApplication $webApps -AllWebApplications:$(($webApps -eq $null) -or ($webApps.Length -eq 0))
105      }
106      break
107    }
108    "FileOrDirectory" {
109      $item = Get-Item (Resolve-Path $Identity)
110      if ($item -is [System.IO.DirectoryInfo]) {
111        # A directory was provided so iterate through all files in the directory and deploy if the file is a WSP (based on the extension)
112        Get-ChildItem $item | ForEach-Object {
113          if ($_.Name.ToLower().EndsWith(".wsp")) {
114            Deploy-SPSolutions -Identity $_.FullName -UpgradeExisting:$UpgradeExisting -WebApplication $WebApplication
115          }
116        }
117      } elseif ($item -is [System.IO.FileInfo]) {
118        # A specific file was provided so assume that the file is a WSP if it does not have an XML extension.
119        [string]$name = $item.Name
120        
121        if ($name.ToLower().EndsWith(".xml")) {
122          Deploy-SPSolutions -Config ([xml](Get-Content $item.FullName))
123          return
124        }
125        $solution = Get-SPSolution $name -ErrorAction SilentlyContinue
126        
127        if ($solution -ne $null -and $UpgradeExisting) {
128          # Just update the solution, don't retract and redeploy.
129          Write-Progress -Activity "Deploying solution $name" -Status "Updating $name" -PercentComplete -1
130          $solution | Update-SPSolution -CASPolicies:$($solution.ContainsCasPolicy) `
131            -GACDeployment:$($solution.ContainsGlobalAssembly) `
132            -LiteralPath $item.FullName
133        
134          Block-SPDeployment $solution $true "Updating $name" -1
135          Write-Progress -Activity "Deploying solution $name" -Status "Updated" -Completed
136          
137          return
138        }
139
140        if ($solution -ne $null) {
141          #Retract the solution
142          if ($solution.Deployed) {
143            Write-Progress -Activity "Deploying solution $name" -Status "Retracting $name" -PercentComplete 0
144            if ($solution.ContainsWebApplicationResource) {
145              $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
146            } else {
147              $solution | Uninstall-SPSolution -Confirm:$false
148            }
149            #Block until we're sure the solution is no longer deployed.
150            Block-SPDeployment $solution $false "Retracting $name" 12
151            Write-Progress -Activity "Deploying solution $name" -Status "Solution retracted" -PercentComplete 25
152          }
153
154          #Delete the solution
155          Write-Progress -Activity "Deploying solution $name" -Status "Removing $name" -PercentComplete 30
156          Get-SPSolution $name | Remove-SPSolution -Confirm:$false
157          Write-Progress -Activity "Deploying solution $name" -Status "Solution removed" -PercentComplete 50
158        }
159
160        #Add the solution
161        Write-Progress -Activity "Deploying solution $name" -Status "Adding $name" -PercentComplete 50
162        $solution = Add-SPSolution $item.FullName
163        Write-Progress -Activity "Deploying solution $name" -Status "Solution added" -PercentComplete 75
164
165        #Deploy the solution
166        
167        if (!$solution.ContainsWebApplicationResource) {
168          Write-Progress -Activity "Deploying solution $name" -Status "Installing $name" -PercentComplete 75
169          $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -Confirm:$false
170          Block-SPDeployment $solution $true "Installing $name" 85
171        } else {
172          if ($WebApplication -eq $null -or $WebApplication.Length -eq 0) {
173            Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to all Web Applications" -PercentComplete 75
174            $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -AllWebApplications -Confirm:$false
175            Block-SPDeployment $solution $true "Installing $name to all Web Applications" 85
176          } else {
177            $WebApplication | ForEach-Object {
178              $webApp = $_.Read()
179              Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to $($webApp.Url)" -PercentComplete 75
180              $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false
181              Block-SPDeployment $solution $true "Installing $name to $($webApp.Url)" 85
182            }
183          }
184        }
185        Write-Progress -Activity "Deploying solution $name" -Status "Deployed" -Completed
186      }
187      break 
188    }
189  } 
190}

When it comes to using the function I believe the help documentation speaks for itself so I won’t reiterate it here.

As always, I’m open to suggestions as to how to improve this function so please leave a comment if you find something wrong or have a suggestion for making it better.

-Gary