Sandbox solutions are a great feature of the SharePoint 2010 platform. But as these solutions are deployed in various locations across your farm or site collection, the question of how to manage them arises.
We're providing a PowerShell script to help you with this. It's pretty simple but can be extended to do more in order to fit into the tools you're already using to manage your farms and site collections.
Let me describe what this script does.
Every Sandbox Solution that is deployed has a Solution ID associated with it. Inside that solution, you will find one or more features with a Feature ID and a version number. For a given Solution Id, this script will walk your entire farm or site collection and generate a log telling you all the locations where that solution is found. It will then look at each feature within the Solution and add a log entry indicating which version of that feature is deployed. It can also perform the upgrade if a new version of the Solution is provided. This is a flag so the script can perform the upgrade or simply be used as a means for gathering information on the solution.
The script takes the following parameters:
- File Path and Name of Solution (.wsp file) to be upgraded
- Switch Parameter indicating if the upgrade should be performed or not
Permissions
- Script assumes the person running it is the box admin and is permitted to perform the upgrade actions.
- Permission levels are modified to grant Full Control for the admin.
- After upgrade is complete, permissions are restored to original settings.
The log format is as follows:
- Details of the file path and name of the solution you are examining including Solution Id.
- Detail on each site collection that is examined
- For each site collection:
Information on whether or not a matching solution was found
If found, details of the solution file that is being removed so it may be replaced by the new version.
If an upgrade is being performed, details on success or failure
- A summary that includes:
Number of site collections are examined
If an upgrade is not being performed, a summary list of the sites that will require an upgrade along with details of the current version for each feature (id) in the solution.
If an upgrade is being performed, the feature and version information accompanied by details of the successful upgrade, the successful new install, or the feature being unchanged because it's already current.
A few Things to Note
To upgrade a solution, the SharePoint 2010 platform expects the solution filenames to be different. If they are the same, you will get an error telling you that the solution is already active. When you upgrade a solution, the solution id will be used to find existing versions of that solution and THEN look at the version details. The upgrade will only move you to a new version. It will not roll back to an old version. And if the versions are the same, it will do nothing.
We hope this script helps you manage the solutions you are deploying and makes upgrades much easier.
# SharePoint DLL
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
#Cmdlet Install
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
#Log file definition
$intro = "User Solution Upgrader v1.0 - Log File"
$date = (Get-Date).ToString('yyyyMMdd')
Set-Variable -Name ForReading -value 1 -Option Constant
Set-Variable -Name ForWriting -value 2 -Option Constant
Set-Variable -Name ForAppending -value 8 -Option Constant
#Assume that the current "Domain\User" is the box admin entitled to perform the reconaissance and upgrade actions.
$admin = $env:UserDomain + "\" + $env:UserName
if ($env:UserDomain -eq $null)
{
$admin = $admin.substring(1)
}
# Get the script's parent folder. This is where the log file will be written.
$logFolder = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$getACL = Get-Acl $logFolder
$access = $getACL.Access | Where { $_.IdentityReference -eq $admin }
if ($access -eq $null -or
($access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::FullControl -and
($access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::Write -and
$access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::CreateFiles)))
{
$logFolder = $env:userprofile
}
# Add the log file name to the path.
$logFileFullPath = Join-Path $logFolder "usersolutionupgrader-$date.txt"
# Open file
$fo = New-Object -com Scripting.FileSystemObject
$f = $fo.OpenTextFile($logFileFullPath, $ForAppending, $true)
if ($f -eq $null)
{
$logFolder = $env:userprofile
$logFileFullPath = Join-Path $logFolder "usersolutionupgrader-$date.txt"
$f = $fo.OpenTextFile($logFileFullPath, $ForAppending, $true)
}
#Confirm log file on screen
Write-Host ("Log Path: " + $logFileFullPath)
#Write intro line
$f.WriteLine()
$f.WriteLine($intro)
function WriteLog
{
$dt = Get-Date
$logMsg = $dt.ToShortDateString() + " " + $dt.ToShortTimeString() + "`t" + $args[0]
$f.WriteLine($logMsg)
Write-Output($logMsg) #Turn this on when you need console output
}
function exitScript()
{
if ($f -ne $null)
{
$f.Close()
}
Write-Host "Exiting script due to error."
exit
}
WriteLog ("Current user (admin): $admin")
#solution details
$solFile = ""
$targetSolutionId = ""
$upgrade = $false
$myError = $null
$haveCurrentFeatures = $false
$haveNewFeatures = $false
$numSites = 0
$oldFeatures = @{}
$newFeatures = @{}
$solSites = New-Object System.Collections.ArrayList
$solFailSites = New-Object System.Collections.ArrayList
#Parse the commandline parameters
if (($args -eq $null) -or ($args.Count -lt 2))
{
WriteLog("Error: This script has been called without enough parameters. Listing your parameters below:")
WriteLog($args)
if ($f -ne $null)
{
$f.Close()
}
exitScript
}
else
{
$badParameters = $false
foreach ($arg in $args)
{
switch ($arg)
{
"-upgrade" {$upgrade = $true}
"-path" {[void] $foreach.MoveNext(); $solFile = $foreach.Current}
default {$badParameters = $true}
}
}
if ($badParameters -eq $true)
{
WriteLog("You passed in a bad parameter.")
exitScript
}
#Validate parameters
$extension = [IO.Path]::GetExtension($solFile)
if (($extension -eq $null) -or ($extension -ne ".wsp") -or (!(Test-Path $solFile)))
{
WriteLog("Error: The solution file name is not of WSP format or is an invalid file.")
exitScript
}
#Confirm parameter values captured
$fileName = Split-Path $solFile -Leaf
WriteLog("Solution Path is: `t" + $solFile)
WriteLog("Solution Name is: `t" + $fileName)
if ($upgrade)
{
WriteLog "The tool will attempt to perform UPGRADE on the site collections"
}
WriteLog ""
}
#Get solution ID from given WSP
$tempPath = Join-Path $env:temp "upgrader" | Join-Path -ChildPath $fileName
$shell = New-Object -ComObject "Shell.Application" -ErrorAction:SilentlyContinue -ErrorVariable myError
if ($myError -ne $null)
{
WriteLog("FAILED to create the Shell.Application object.")
WriteLog("Error: " + $myError)
$myError = $null
}
[IO.Directory]::CreateDirectory($tempPath) | Out-Null
$tempFolder = $shell.NameSpace($tempPath)
$tempFolder.CopyHere($solFile)
#Take the file name ("yourfile.wsp") from the original path
$tempSolPath = Join-Path $tempPath $fileName
if (!(Test-Path $tempSolPath))
{
WriteLog "Error: Failed to copy WSP file to temp location."
exitScript
}
#Rename the WSP file to have CAB extension in order to facilitate CAB extraction
$cabFileName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) + ".cab"
Rename-Item $tempSolPath $cabFileName
$cabPath = Join-Path $tempPath $cabFileName
$sourceWsp = $shell.NameSpace($cabPath).items()
$tempFolder.CopyHere($sourceWsp)
$manifestPath = Join-Path $tempPath "manifest.xml"
[xml]$manifest = Get-Content $manifestPath
$targetSolutionId = $manifest.Solution.SolutionId
#Validate Solution GUID
[Guid]$testGuid = "B80D56EC-5899-459d-83B4-1AE0BB8418E4"
if (($targetSolutionId -eq $null) -or ($targetSolutionId.Length -lt 36) -or
([System.ComponentModel.TypeDescriptor]::GetConverter($testGuid).ConvertFromString($targetSolutionId) -eq $null))
{
WriteLog("Error: Target solution ID is invalid: " + $stringSolutionId)
exitScript
}
WriteLog("Extracted solution ID: $targetSolutionId from manifest.xml")
WriteLog("")
#Now delete temp folder.
Remove-Item $tempPath\*
Remove-Item $tempPath
#Go through Content DBs
WriteLog ("Looking for Solution Id: " + $targetSolutionId + " in all Content Databases`n")
$dbs = Get-SPContentDatabase
foreach ($contentdb in $dbs)
{
#Web App Level
$webAppUrl = $contentdb.WebApplication.Url
#Get WebApp
$webApp = Get-SPWebApplication -Identity $webAppUrl
$policy = $webApp.Policies[$admin]
$policyAdded = $false
$roleAdded = $false
#If the admin doesn't have Full Control, it will be granted as follows:
if ($policy -eq $null)
{
$policy = $webApp.Policies.Add($admin, "")
$webAppModified = $true
$policyAdded = $true
WriteLog "Added a policy entry for user '$admin'."
}
$fullRole = $webApp.PolicyRoles.GetSpecialRole([Microsoft.SharePoint.Administration.SPPolicyRoleType]::FullControl)
if ($policy.PolicyRoleBindings[$fullRole] -eq $null)
{
$policy.PolicyRoleBindings.Add($fullRole)
$webAppModified = $true
$roleAdded = $true
WriteLog "Full Control added for '$admin' to Web Application '$webAppUrl'"
}
if ($webAppModified)
{
$webApp.Update()
$webAppModified = $false
}
#Done. Have Full Control
WriteLog ("Entering Web Application '$webAppUrl'.`n")
Get-SPSite -WebApplication $webApp -Limit ALL | % {
$site = $_
$solution = $null
$foundSolution = $false
#Scan for solution here
Get-SPUserSolution -Site $_ | Where { $_.Status -eq [Microsoft.Sharepoint.SPUserSolutionStatus]::Activated -and $_.SolutionId -eq $targetSolutionId } | % {
if ($foundSolution -eq $false)
{
$foundSolution = $true
$solution = $_
if ($haveCurrentFeatures -eq $false)
{
Get-SPFeature -Sandboxed -Site $site | Where { $_.SolutionId -eq $targetSolutionId } | % {$oldFeatures.Add($_.Id, $_)}
$haveCurrentFeatures = $true
}
if ($upgrade -eq $false)
{
$solSites.Add($site.Url) | Out-Null
}
$solutionHash = $_.Signature
WriteLog ("Found site collection: " + $site.Url)
}
}
$numSites ++
#DoUpgrade here
if ($upgrade -and $foundSolution)
{
$successAdd = $false
WriteLog ("Uploading new solution file as: $fileName")
#Add + Upgrade solution
$myError = $null
Add-SPUserSolution -LiteralPath $solFile -Site $site -ErrorAction:SilentlyContinue -ErrorVariable myError -Confirm:$false
if ($myError -ne $null)
{
WriteLog("Site collection '" + $site.Url + "' FAILED to upload the new solution.")
WriteLog("Error: " + $myError)
$myError = $null
}
else
{
$successAdd = $true
}
$addedSolution = Get-SPUserSolution -Identity $fileName -Site $site
if ($addedSolution -ne $null)
{
WriteLog ("Found solution $fileName in the Solutions Gallery. Attempting to use it...")
#First check for already updated solution
if ($addedSolution.Signature -eq $solutionHash)
{
#This means we have the same version installed. Just skip it. And delete our copy!
WriteLog ("New solution is already active on this site collection.")
if ($successAdd -eq $true)
{
#Remove the new solution (the dupe)
WriteLog ("Removing file: $fileName")
Remove-SPUserSolution -Identity $addedSolution -Site $site -Confirm:$false
if (!($solFailSites.Contains($site.Url)))
{
$solFailSites.Add($site.Url) | Out-Null
}
}
}
else
{
#Perform upgrade
Update-SPUserSolution -Identity $solution -Site $site -ToSolution $addedSolution -ErrorAction:SilentlyContinue -ErrorVariable myError -Confirm:$false
if ($myError -ne $null)
{
WriteLog("Site collection '" + $site.Url + "' FAILED to upgrade to the new solution.")
WriteLog("Error: " + $myError)
if (!($solFailSites.Contains($site.Url)))
{
$solFailSites.Add($site.Url) | Out-Null
}
$myError = $null
}
if (!($solFailSites.Contains($site.Url)))
{
#Upgrade succeeded
WriteLog("Site collection '" + $site.Url + "' has been upgraded to the new solution.")
WriteLog ""
$solSites.Add($site.Url) | Out-Null
#Record results AFTER upgrade
if ($haveNewFeatures -eq $false)
{
Get-SPFeature -Sandboxed -Site $site | Where { $_.SolutionId -eq $targetSolutionId } | %{$newFeatures.Add($_.Id, $_)}
$haveNewFeatures = $true
}
}
}
}
else
{
if (!($solFailSites.Contains($site.Url)))
{
$solFailSites.Add($site.Url) | Out-Null
}
}
}
$site.Close();
}
#Close permissions and webApp
if ($roleAdded)
{
$policy.PolicyRoleBindings.RemoveById($fullRole.Id)
$webAppModified = $true
WriteLog "Removed Full Control for '$admin' from Web Application '$webAppUrl'"
}
if ($policyAdded)
{
$webApp.Policies.Remove($admin)
$webAppModified = $true
WriteLog "Removed the policy entry for user '$admin'."
}
if ($webAppModified)
{
$webApp.Update()
$webAppModified = $false
}
WriteLog ""
WriteLog "Done with Web Application."
WriteLog ""
}
#Final tally of site collections
WriteLog("Analysis of site collection upgrades for solution ID $targetSolutionId ...")
WriteLog("We have processed a total of $numSites site collections.")
WriteLog("")
if ($upgrade)
{
#Site Collection Summary
if ($solSites.Count -eq 0)
{
WriteLog("No site collections were upgraded. Refer to this log for any upgrade errors.")
}
else
{
WriteLog("Listing site collections that have been upgraded:")
foreach ($siteUrl in $solSites)
{
WriteLog($siteUrl)
}
}
if ($solFailSites.Count -ne 0)
{
WriteLog("Listing site collections that FAILED to upgrade:")
foreach ($siteUrl in $solFailSites)
{
WriteLog($siteUrl)
}
}
#Feature Upgrade Summary
if ($newFeatures.Count -gt 0)
{
WriteLog ""
WriteLog "Feature upgrade summary for the New User Solution:"
foreach($fKey in $newFeatures.Keys)
{
$fDef = $oldFeatures[$fKey]
$fDef2 = $newFeatures[$fKey]
#Feature ID and DisplayName
WriteLog("Feature ID: " + $fDef2.Id + "`t DisplayName: " + $fDef2.DisplayName)
#Feature Version
if ($fDef -eq $null)
{
#New feature added.
WriteLog("This feature has been newly added.`t`t`t Version " + $fDef2.Version)
}
else
{
if ($fDef.Version -eq $fDef2.Version)
{
WriteLog("This feature has been unchanged.`t`t`t Version " + $fDef2.Version)
}
else
{
WriteLog("Feature went from Version " + $fDef.Version + " to Version " + $fDef2.Version)
}
}
WriteLog ""
}
WriteLog ("The new solution holds a total of " + $newFeatures.Count + " feature(s).")
}
}
else
{
if ($solSites.Count -eq 0)
{
WriteLog("No site collections have been found.")
}
else
{
WriteLog("We have found " + $solSites.Count + " site collections, as follows (no upgrade action performed):")
foreach ($siteUrl in $solSites)
{
WriteLog($siteUrl)
}
}
WriteLog ""
WriteLog "Feature summary for current User Solution:"
foreach($fKey in $oldFeatures.Keys)
{
$fDef = $oldFeatures[$fKey]
WriteLog ("Feature DisplayName: `t" + $fDef.DisplayName)
WriteLog ("Feature Version: `t" + $fDef.Version)
WriteLog ("Feature Id: `t`t" + $fDef.Id)
}
WriteLog ""
WriteLog ("Found a total of " + $oldFeatures.Count + " feature(s).")
WriteLog ""
}
WriteLog ""
#Close the file handle.
if ($f -ne $null)
{
$f.Close()
}