Managing Sandbox Solutions in Sharepoint 2010

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()

 

}