Deploying SharePoint 2010 Solution Package Using PowerShell (Revisited)
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):
function global:Deploy-SPSolutions() { <# .Synopsis Deploys one or more Farm Solution Packages to the Farm. .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. .Example 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 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 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 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 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 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. .Parameter Config The XML configuration file containing the WSP files to deploy. .Parameter Identity The directory, WSP file, or XML configuration file containing the WSP files to deploy. .Parameter UpgradeExisting 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). .Parameter AllWebApplications If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable). .Parameter WebApplication Specifies the Web Application(s) to deploy the WSP file to. .Link Get-Content Get-SPSolution Add-SPSolution Install-SPSolution Update-SPSolution Uninstall-SPSolution Remove-SPSolution #> [CmdletBinding(DefaultParameterSetName="FileOrDirectory")] param ( [Parameter(Mandatory=$true, Position=0, ParameterSetName="Xml")] [ValidateNotNullOrEmpty()] [xml]$Config, [Parameter(Mandatory=$true, Position=0, ParameterSetName="FileOrDirectory")] [ValidateNotNullOrEmpty()] [string]$Identity, [Parameter(Mandatory=$false, Position=1, ParameterSetName="FileOrDirectory")] [switch]$UpgradeExisting, [Parameter(Mandatory=$false, Position=2, ParameterSetName="FileOrDirectory")] [switch]$AllWebApplications, [Parameter(Mandatory=$false, Position=3, ParameterSetName="FileOrDirectory")] [Microsoft.SharePoint.PowerShell.SPWebApplicationPipeBind[]]$WebApplication ) function Block-SPDeployment($solution, [bool]$deploying, [string]$status, [int]$percentComplete) { do { Start-Sleep 2 Write-Progress -Activity "Deploying solution $($solution.Name)" -Status $status -PercentComplete $percentComplete $solution = Get-SPSolution $solution if ($solution.LastOperationResult -like "*Failed*") { throw "An error occurred during the solution retraction, deployment, or update." } if (!$solution.JobExists -and (($deploying -and $solution.Deployed) -or (!$deploying -and !$solution.Deployed))) { break } } while ($true) sleep 5 } switch ($PsCmdlet.ParameterSetName) { "Xml" { # An XML document was provided so iterate through all the defined solutions and call the other parameter set version of the function $Config.Solutions.Solution | ForEach-Object { [string]$path = $_.Path [bool]$upgrade = $false if (![string]::IsNullOrEmpty($_.UpgradeExisting)) { $upgrade = [bool]::Parse($_.UpgradeExisting) } $webApps = $_.WebApplications.WebApplication Deploy-SPSolutions -Identity $path -UpgradeExisting:$upgrade -WebApplication $webApps -AllWebApplications:$(($webApps -eq $null) -or ($webApps.Length -eq 0)) } break } "FileOrDirectory" { $item = Get-Item (Resolve-Path $Identity) if ($item -is [System.IO.DirectoryInfo]) { # A directory was provided so iterate through all files in the directory and deploy if the file is a WSP (based on the extension) Get-ChildItem $item | ForEach-Object { if ($_.Name.ToLower().EndsWith(".wsp")) { Deploy-SPSolutions -Identity $_.FullName -UpgradeExisting:$UpgradeExisting -WebApplication $WebApplication } } } elseif ($item -is [System.IO.FileInfo]) { # A specific file was provided so assume that the file is a WSP if it does not have an XML extension. [string]$name = $item.Name if ($name.ToLower().EndsWith(".xml")) { Deploy-SPSolutions -Config ([xml](Get-Content $item.FullName)) return } $solution = Get-SPSolution $name -ErrorAction SilentlyContinue if ($solution -ne $null -and $UpgradeExisting) { # Just update the solution, don't retract and redeploy. Write-Progress -Activity "Deploying solution $name" -Status "Updating $name" -PercentComplete -1 $solution | Update-SPSolution -CASPolicies:$($solution.ContainsCasPolicy) ` -GACDeployment:$($solution.ContainsGlobalAssembly) ` -LiteralPath $item.FullName Block-SPDeployment $solution $true "Updating $name" -1 Write-Progress -Activity "Deploying solution $name" -Status "Updated" -Completed return } if ($solution -ne $null) { #Retract the solution if ($solution.Deployed) { Write-Progress -Activity "Deploying solution $name" -Status "Retracting $name" -PercentComplete 0 if ($solution.ContainsWebApplicationResource) { $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false } else { $solution | Uninstall-SPSolution -Confirm:$false } #Block until we're sure the solution is no longer deployed. Block-SPDeployment $solution $false "Retracting $name" 12 Write-Progress -Activity "Deploying solution $name" -Status "Solution retracted" -PercentComplete 25 } #Delete the solution Write-Progress -Activity "Deploying solution $name" -Status "Removing $name" -PercentComplete 30 Get-SPSolution $name | Remove-SPSolution -Confirm:$false Write-Progress -Activity "Deploying solution $name" -Status "Solution removed" -PercentComplete 50 } #Add the solution Write-Progress -Activity "Deploying solution $name" -Status "Adding $name" -PercentComplete 50 $solution = Add-SPSolution $item.FullName Write-Progress -Activity "Deploying solution $name" -Status "Solution added" -PercentComplete 75 #Deploy the solution if (!$solution.ContainsWebApplicationResource) { Write-Progress -Activity "Deploying solution $name" -Status "Installing $name" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -Confirm:$false Block-SPDeployment $solution $true "Installing $name" 85 } else { if ($WebApplication -eq $null -or $WebApplication.Length -eq 0) { Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to all Web Applications" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -AllWebApplications -Confirm:$false Block-SPDeployment $solution $true "Installing $name to all Web Applications" 85 } else { $WebApplication | ForEach-Object { $webApp = $_.Read() Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to $($webApp.Url)" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false Block-SPDeployment $solution $true "Installing $name to $($webApp.Url)" 85 } } } Write-Progress -Activity "Deploying solution $name" -Status "Deployed" -Completed } break } } }
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
Retrieving SharePoint 2010 Feature Activations Using Windows PowerShell
During my PowerShell for Developers presentation in London last week I promised to show and demonstrate a script for retrieving Feature activations; unfortunately I ran out of time and was not able to show this script to the degree that I’d intended so I decided to throw together this blog post.
When developing custom Features it is very common to expect that there will need to be some level of update required for those Features. Typically this means that, after deploying the Feature via a Solution Package, you will need to re-activate that Feature in order to trigger any additional code to run (or, if you are using the new SharePoint 2010 Feature upgrade capabilities you will need to run the Upgrade(Boolean) method of the SPFeature object). The problem is knowing where the Feature is activated throughout the Farm. Using PowerShell there are two ways to do this – you can use the Get-SPFeature cmdlet and test the results against the appropriate scope or you can use the various “Query” methods that have been provided for each scope. I don’t recommend that you use the Get-SPFeature cmdlet as it is very inefficient, and as such, I won’t bother showing an example of that here. Instead I’ll focus on the “Query” methods approach.
Whether your Feature is scoped to the Farm, Web Application, Site Collection, or Site, there is a method that you can call to get an SPFeature object which effectively corresponds to a Feature activation. For Farm scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebService class, obtainable via the SPWebService class’ static AdministrationService property; for Web Application scoped Features you use the static QueryFeaturesInAllWebServices(Guid, Boolean) method of the SPWebService class; for Site Collection scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebApplication class; and for Site scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPSite class.
To create our PowerShell function we’ll simply take in a SPFeatureDefinition object and use a switch statement to call the appropriate method based on the scope of the Feature. To make the function more versatile we can use the Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind type which will allow the caller to pass in either the name of the Feature, its ID, or an actual SPFeatureDefinition object; additionally, we can use parameter attributes to easily allow the value to be passed in via the object pipeline. And finally, we’ll add an additional parameter stating that we wish to retrieve only those activations that require upgrading and we’ll add some basic help for the function.
The following code listing represents the completed function – I recommend that you save this to a file named Get-SPFeatureActivations.ps1. Note that I plan on adding this as a cmdlet to my downloadable extensions thereby making the need for this script unnecessary, however, I believe that this example provides a great template to use for creating professional looking, production ready scripts that both IT administrators and developers can use.
function Get-SPFeatureActivations() { <# .Synopsis Retrieves Feature activations for the given Feature Definition. .Description Retrieves the SPFeature object for each activation of the SPFeatureDefinition object. .Example Get-SPFeatureActivations TeamCollab .Parameter Identity The Feature name, ID, or SPFeatureDefinition object whose activations will be retrieved. .Parameter NeedsUpgrade If specified, only Feature activations needing upgrading will be retrieved. .Link Get-SPFeature #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [Alias("Feature")] [ValidateNotNullOrEmpty()] [Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind]$Identity, [Parameter(Mandatory=$false, Position=1)] [switch]$NeedsUpgrade ) begin { } process { $fd = $Identity.Read() switch ($fd.Scope) { "Farm" { [Microsoft.SharePoint.Administration.SPWebService]::AdministrationService.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) break } "WebApplication" { [Microsoft.SharePoint.Administration.SPWebService]::QueryFeaturesInAllWebServices($fd.ID, $NeedsUpgrade.IsPresent) break } "Site" { foreach ($webApp in Get-SPWebApplication) { $webApp.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) } break } "Web" { foreach ($site in Get-SPSite -Limit All) { $site.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) $site.Dispose() } break } } } end { } }
Assuming you’ve saved the file to the root of the C drive (not recommended but its what I do when I’m doing demos) then you can load the function into memory using dot sourcing as shown in the following example (note that the help for the function shows the help information specified by the block comment help):
Once the function is loaded into memory you can start using it. In the following example I’m returning back all the locations where the MyCustomFeature Feature is activated; I then use the Select-Object cmdlet to return just the URL for each activation:
Get-SPFeatureActivations MyCustomFeature | select @{Expression={$_.Parent.Url};Label="Url"}In this next example, instead of simply outputting the URL of each activation, I’m forcing the Feature to be reactivated using the Enable-SPFeature cmdlet (use the -Force parameter to force the Feature to be reactivated – you could also change the code to deactivate the Feature using the Disable-SPFeature cmdlet and then activate using the Enable-SPFeature cmdlet):
Get-SPFeatureActivations MyCustomFeature | ForEach-Object {
Enable-SPFeature -Identity MyCustomFeature -Url $_.Parent.Url -Force
}
Similarly you can retrieve only those Features needing upgrade and then call the Upgrade() method, as shown in this next example:
Get-SPFeatureActivations MyCustomFeature -NeedsUpgrade | ForEach-Object {$_.Upgrade($false)
}
I strongly recommend that, before you re-deploy a Feature that may be activated at an unknown number of scopes, you run this function (or something similar to it) so that you fully understand the impact of upgrading your Feature. One more thing to watch out for, if your environment is very large you may wish to modify this function so that it does not return the SPFeature object but instead just returns the URL corresponding to the activation – you can then use the Get-SPFeature cmdlet to retrieve the SPFeature object; the benefit of this is that you can immediately dispose of the parent object and prevent potential out of memory errors (I’m particularly concerned with Site Collection and Site scoped Features here where the Parent property of the SPFeature object corresponds to an SPSite or SPWeb object which must be disposed).
That’s all I’ve got for now; hopefully you’ve found this useful!
-Gary









