MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Saturday, June 12, 2010

Deploying SharePoint 2010 Solution Packages Using PowerShell

With SharePoint 2010 we can now deploy our Solution Packages using PowerShell. What’s cool about this is that it’s a bit easier than it was with 2007 to check if a package is already deployed and conditionally retract, delete, and then re-add and re-deploy. By now most people already know how to do this as it’s fairly straightforward but I thought I’d go ahead and share the script that I use as it’s great for deploying lots of Solution Packages to my various client environments in bulk.

Like most of my scripts this one is driven by an XML file but I have a core function which can be called directly – I just wrap that in another function which can iterate through the XML file thus facilitating bulk installs of packages. First lets look at the XML file:

<Solutions>
<Solution Path="W:\my.sharepoint.package.wsp" CASPolicies="false" GACDeployment="true">
<WebApplications>
<WebApplication>http://portal</WebApplication>
</WebApplications>
</Solution>
</Solutions>

As you can see the structure is fairly simplistic – just provide the path to the WSP file and whether it contains CAS policies and whether it should be deployed to the GAC or not. If it’s a Farm level solution (no web application resources) then simply omit the <WebApplications /> element. If you have more than one solution just add another <Solution /> element. If you’re deploying to multiple web applications then add as many <WebApplication /> elements as is needed.

Now we’ll take a look at the wrapper function which loops through the XML:

function Install-Solutions([string]$configFile)
{
    if ([string]::IsNullOrEmpty($configFile)) { return }

    [xml]$solutionsConfig = Get-Content $configFile
    if ($solutionsConfig -eq $null) { return }

    $solutionsConfig.Solutions.Solution | ForEach-Object {
        [string]$path = $_.Path
        [bool]$gac = [bool]::Parse($_.GACDeployment)
        [bool]$cas = [bool]::Parse($_.CASPolicies)
        $webApps = $_.WebApplications.WebApplication
        Install-Solution $path $gac $cas $webApps
    }
}

As you can see the code just loads the passed in file as an XmlDocument object and grabs each Solution element ($solutionsConfig.Solutions.Solution) and then iterates through each object using the ForEach-Object cmdlet. For pure convenience I grab each attribute and assign it to a local variable. And finally I call the Install-Solution function which is shown below:

function Install-Solution([string]$path, [bool]$gac, [bool]$cas, [string[]]$webApps = @())
{
    $spAdminServiceName = "SPAdminV4"

    [string]$name = Split-Path -Path $path -Leaf
    $solution = Get-SPSolution $name -ErrorAction SilentlyContinue
    
    if ($solution -ne $null) {
        #Retract the solution
        if ($solution.Deployed) {
            Write-Host "Retracting solution $name..."
            if ($solution.ContainsWebApplicationResource) {
                $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
            } else {
                $solution | Uninstall-SPSolution -Confirm:$false
            }
            Stop-Service -Name $spAdminServiceName
            Start-SPAdminJob -Verbose
            Start-Service -Name $spAdminServiceName    
        
            #Block until we're sure the solution is no longer deployed.
            do { Start-Sleep 2 } while ((Get-SPSolution $name).Deployed)
        }
        
        #Delete the solution
        Write-Host "Removing solution $name..."
        Get-SPSolution $name | Remove-SPSolution -Confirm:$false
    }
    
    #Add the solution
    Write-Host "Adding solution $name..."
    $solution = Add-SPSolution $path
    
    #Deploy the solution
    if (!$solution.ContainsWebApplicationResource) {
        Write-Host "Deploying solution $name to the Farm..."
        $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -Confirm:$false
    } else {
        if ($webApps -eq $null -or $webApps.Length -eq 0) {
            Write-Warning "The solution $name contains web application resources but no web applications were specified to deploy to."
            return
        }
        $webApps | ForEach-Object {
            Write-Host "Deploying solution $name to $_..."
            $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $_ -Confirm:$false
        }
    }
    Stop-Service -Name $spAdminServiceName
    Start-SPAdminJob -Verbose
    Start-Service -Name $spAdminServiceName    
    
    #Block until we're sure the solution is deployed.
    do { Start-Sleep 2 } while (!((Get-SPSolution $name).Deployed))
}

The code looks more complicated than it really is. I first start out by getting the solution name which I always assume to be the filename of the WSP file. I then use that to get the SPSolution object using Get-SPSolution. If a value comes back then I check if it has been deployed and if it has then I retract it by calling Uninstall-SPSolution. The trick is knowing whether it has web application scoped resources and if it does then we need to retract using the -AllWebApplications parameter. Once retracted I stop the SharePoint Administration Service (SPAdminV4) so that I can call Start-SPAdminJob and force the retraction timer job to execute. Once the Start-SPAdminJob cmdlet returns I then restart the SharePoint Administration Service. With the solution retracted I can now delete the solution from the solution store using Remove-SPSolution (I re-get the solution to make sure that I get no errors due to the current variables state being invalid).

Once deleted I can then add the new solution to the store using the path provided (Add-SPSolution). Now that it’s in the store I can check if it has web application resources or not – if it does not then the deployment is simply a matter of calling Install-SPSolution and specifying whether it should be deployed to the GAC and if it contains CAS policies. If it does contain web application resources then I have to loop through all the web application items in the provided string array ($webApps) and then pass each one into a separate call to Install-SPSolution.

You now have two options for adding your solution: you can call the first function and pass in an XML file or you can call the second function directly. I’ll first show how to call the second function directly:

PS C:\>Install-Solution "w:\my.sharepoint.package.wsp" $true $false @("http://portal","http://mysites")

Now lets look at how to call the first function given an XML file named “solutions.xml” containing a structure similar to that shown above:

PS C:\>Install-Solutions "w:\solutions.xml"

Hopefully you’ll find this script useful for deploying your custom SharePoint 2010 Solution Packages.

14 comments:

Jeremy Thake said...

this is awesome, so much easier to see whether it's finished or not! good job!
I'll use this in a demo at Australian SharePoint Conference on Wednesday! Perfect for me as I'm demonstrating deploying versioned features in PowerShell etc.

Pavel Novotný said...

However, there is a GUI tool that all those manual tasks can be performed via mouse clicks :)

http://www.devit.eu/products/122-sharepoint-administration-toolkit-2010-sat.aspx

noebierzo said...

I think that's really cool!! Thanks for sharing.

Anonymous said...

Great post!

When I add more than a single -section to the xml its breaks when trying to DeActivate and Active the features.

When debugging I have found that the $featureName is empty or not defined.

Have you got an xml-example containing more than a single feature that works? :)

Thanks in advance.

Gary Lapointe said...

Nothing in the XML or code in this article is dealing with features. Are you sure you have the right blog post :)

Ronak said...

Thanks Gary this script is veru useful to deploy solution but i have question what is SilentlyContinue in script is it function or is it out of box function.

Gary Lapointe said...

SilentlyContinue is just a parameter that you can pass to any cmdlet to tell it to silently continue if an error occurs.

asif said...

Thanks for sharing such an extraordinary explanation. It is really helpful.

John Powell said...

There is a problem with this implementation. If deployment fails, it will get stuck waiting for the solution to be deployed. Here are some suggested changes:

$solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false -Force:$force
Execute-AdminServiceJobs

# block until deployed
do
{
Start-Sleep 2
$retryCount++
Write-Host "." -NoNewLine
if($retryCount -gt 60)
{
Write-Host ""
Write-Error " Timeout deploying solution $name to $webApp"
break
}
} while ((Get-SPSolution $name).JobExists)

Write-Host ""
$solution = (Get-SPSolution $name)
if($solution.LastOperationResult -ne "DeploymentSucceeded")
{
Write-Error $solution.LastOperationDetails
}

NahoY said...

Thanks Gary!
Great work. A nice addition would be to pass the folder path where all the solutions are located instead of type the absolute path for each file.
Anyways, great start for anyone wanting to deploy through Powershell!

Arun said...

Im getting the following error Pls help me...
The term 'Get-SPSolution' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of
the name, or if a path was included, verify that the path is correct and try again.

Gary Lapointe said...

Make sure you're using the SharePoint 2010 Management Shell or otherwise loading the Microsoft.SharePoint.PowerShell snap-in.

NahoY said...

When using more than one web application, the solution deployment fails because it doesn't wait for the previous deployment to finish.

I think the deployment code should look more like this:

#Deploy the solution
if (!$solution.ContainsWebApplicationResource) {
Write-Host "Deploying solution $name to the Farm..."
$solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -Confirm:$false
} else {
if ($webApps -eq $null -or $webApps.Length -eq 0) {
Write-Warning "The solution $name contains web application resources but no web applications were specified to deploy to."
return
}
$webApps | ForEach-Object {
Write-Host "Deploying solution $name to $_..."
$solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $_ -Confirm:$false
#Block until we're sure the solution is deployed.
do { Start-Sleep 2 } while ((-not (Get-SPSolution $name).Deployed) -and (Get-SPSolution $name).JobExists)
}
}
Stop-Service -Name $spAdminServiceName
Start-SPAdminJob -Verbose
Start-Service -Name $spAdminServiceName

Anonymous said...

We have host header site collection in a web app in sharepoint 2010 and when we do redeployment of wsp, nothing gets updated but suprising thing is root site collection which is not host header site collection inside same web app gets updated master page/ page layouts. I tried to activate/deactivate feature but nothing getting updated and I can not delete existing page layouts/ master page as they are being used by number of pages and that is not the solution I am looking for.

Can anybody guide on how to deploy wsp in host header site collection in sharepoint 2010/

amitloh@gmail.com