Suppose we are migrating the public Internet site for Fabrikam Technologies (http://www.fabrikam.com) to SharePoint Server 2010. We know that we'll need some customization and therefore we've created a custom SharePoint solution (Fabrikam.Demo.Web.wsp) that includes the following features:
- Fabrikam.Demo.Web_HomeSiteConfiguration
- Fabrikam.Demo.Web_WebAppConfiguration
- Fabrikam.Demo.Web_WebParts
Let's start by assuming we've got a brand new SharePoint Server 2010 environment setup with "nothing" on it. In other words, we haven't yet created a Web application or deployed/activated any custom features. Therefore we first need a script to quickly create a new Web application in SharePoint.
Create Web Application.ps1
This script is used to create the Fabrikam Web application in SharePoint:
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function Main()
{
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
Write-Host "Creating Web application ($webAppUrl)..."
$hostHeader = $webAppUrl.Substring("http://".Length)
$webAppName = "SharePoint - " + $hostHeader + "80"
$membershipProviderName = "FabrikamSqlMembershipProvider"
$roleProviderName = "FabrikamSqlRoleProvider"
$contentDatabaseName = "WSS_Content_FabrikamDemo"
$appPoolName = $webAppName
$appPoolUserName = $env:USERDOMAIN + "\svc-web-fabrikam"
if (($webAppUrl -eq "http://www-local.fabrikam.com") -or
($webAppUrl -eq "http://www-dev.fabrikam.com"))
{
Write-Debug "Overriding variables for Development environment..."
$appPoolUserName = $env:USERDOMAIN + "\svc-web-fabrikam-dev"
}
elseif ($webAppUrl -eq "http://www-test.fabrikam.com")
{
Write-Debug "Overriding variables for Test environment..."
$appPoolUserName = $env:USERDOMAIN + "\svc-web-fabrikam-test"
}
Write-Debug "hostHeader: $hostHeader"
Write-Debug "webAppName: $webAppName"
Write-Debug "appPoolName: $appPoolName"
Write-Debug "appPoolUserName: $appPoolUserName"
Write-Debug "contentDatabaseName: $contentDatabaseName"
Write-Debug "Get service account for application pool ($appPoolUserName)..."
$appPoolAccount = Get-SPManagedAccount -Identity $appPoolUserName `
-Debug:$false -EA 0
If ($appPoolAccount -eq $null)
{
Write-Host "Registering managed account ($appPoolUserName)..."
Write-Debug "Get credential ($appPoolUserName)..."
$appPoolCredential = Get-Credential $appPoolUserName
$appPoolAccount = New-SPManagedAccount -Credential $appPoolCredential `
-Debug:$false
}
$windowsAuthProvider = New-SPAuthenticationProvider -Debug:$false
$formsAuthProvider = New-SPAuthenticationProvider `
-ASPNETMembershipProvider $membershipProviderName `
-ASPNETRoleProviderName $roleProviderName `
-Debug:$false
$authProviders = $windowsAuthProvider, $formsAuthProvider
$webApp = New-SPWebApplication -Name $webAppName -AllowAnonymousAccess `
-ApplicationPool $appPoolName -AuthenticationMethod "NTLM" `
-ApplicationPoolAccount $appPoolAccount -Url $webAppUrl -Port 80 `
-AuthenticationProvider $authProviders -HostHeader $hostHeader `
-DatabaseName $contentDatabaseName `
-Debug:$false
Write-Host -Fore Green "Successfully created Web application ($webAppUrl)."
}
Main
If you aren't familiar with the "-EA 0
" abbreviated syntax, just realize that it's a short way of saying "-ErrorAction SilentlyContinue
", or in other words you are telling PowerShell, "there's a chance this command may generate an error, but I'm okay with that -- just ignore it." For example, if you try to add the SharePoint PowerShell snap-in but the snap-in has already been added, then you get an error. However, in order to run the script from Windows PowerShell ISE (Integrated Shell Environment) or from a plain ol' PowerShell prompt (i.e. not the SharePoint 2010 Management Shell shortcut), then we need to ensure the SharePoint snap-in is loaded.
The rest of the script should be pretty obvious. However, it might be worth pointing out that I explicitly add the "-Debug:$false
" parameter to the various SharePoint cmdlets because I often set $DebugPreference ="Continue"
in order to get debug messages from my scripts, but I don't want to see the SharePoint debug messages.
It's also worth mentioning that the URL for the Fabrikam Web site is expected to vary by environment (e.g. DEV, TEST, and PROD) and therefore the FABRIKAM_DEMO_URL environment variable can be used to specify the URL of the SharePoint Web application. For example, in the Development integration environment, the URL of the site ishttp://www-dev.fabrikam.com. This becomes important later on when deploying and retracting solutions.
Also keep in mind that this script creates a Web application with claims authentication enabled. Therefore you might need to tweak it slightly if you just want "Classic Mode Authentication."
Create Site Collections.ps1
At this point, we have a brand new Web application but it doesn't contain any site collections. While the Fabrikam Internet site will likely end up having numerous site collections, let's start out by simply creating the top-level site (i.e. "/") using the Publishing Portal site definition.
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function CreateSiteCollection(
[string] $siteUrl = $(Throw "Value cannot be null: siteUrl"),
[string] $ownerAlias = $(Throw "Value cannot be null: ownerAlias"),
[string] $siteName = $(Throw "Value cannot be null: siteName"),
[string] $siteTemplate = $(Throw "Value cannot be null: siteTemplate"),
[string] $siteDescription)
{
Write-Host "Creating site collection ($siteUrl)..."
Write-Debug "ownerAlias: $ownerAlias"
Write-Debug "siteName: $siteName"
Write-Debug "siteTemplate: $siteTemplate"
Write-Debug "siteDescription: $siteDescription"
New-SPSite $siteUrl -OwnerAlias $ownerAlias -Name $siteName `
-Description $siteDescription -Template $siteTemplate -Debug:$false > $null
Write-Host -Fore Green "Successfully created site collection ($siteUrl)."
}
function Main
{
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
Write-Debug "webAppUrl: $webAppUrl"
$ownerAlias = $env:USERDOMAIN + "\" + $env:USERNAME
$siteUrl = $webAppUrl + "/"
$siteName = "Fabrikam"
$siteTemplate = "BLANKINTERNETCONTAINER#0"
$siteDescription = "Public Internet site for Fabrikam Technologies"
CreateSiteCollection $siteUrl $ownerAlias $siteName $siteTemplate `
$siteDescription
}
Main
There's not much worth noting about this script, except perhaps to be aware that it is hard-coded to set the primary site collection administrator to the user running the script.
Enable Anonymous Access.ps1
For an Internet-facing site, I can't think of any scenario where we wouldn't want to allow anonymous access to at least some part of the site (even for an extranet site, at a minimum, we would probably want to support a custom login page as well as some generic content, such as terms and conditions for using the site).
For the Fabrikam site, most of the content will be available to anonymous users. Therefore, we use a script to avoid having to repeatedly configure anonymous access through Site Actions -> Site Permissions (a.k.a. /_layouts/user.aspx). By "repeatedly" I am referring to performing this configuration change in each environment (DEV, TEST, and PROD) or whenever a developer decides to rebuild the Web application in his or her local SharePoint environment.
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function EnableAnonymousAccess(
[Microsoft.SharePoint.SPWeb] $web = $(Throw "Value cannot be null: web"))
{
Write-Host "Enabling anonymous access on site ($($web.Url))..."
$anonymousPermissionMask =
[Microsoft.SharePoint.SPBasePermissions]::Open `
-bor [Microsoft.SharePoint.SPBasePermissions]::ViewFormPages `
-bor [Microsoft.SharePoint.SPBasePermissions]::ViewListItems `
-bor [Microsoft.SharePoint.SPBasePermissions]::ViewPages `
-bor [Microsoft.SharePoint.SPBasePermissions]::ViewVersions
If ($web.AnonymousPermMask64 -eq $anonymousPermissionMask)
{
Write-Host `
"Anonymous access is already enabled on the site ($($web.Url))."
return;
}
If ($web.HasUniqueRoleAssignments -eq $false)
{
$web.BreakRoleInheritance($true);
}
$web.AnonymousPermMask64 = $anonymousPermissionMask;
$web.Update();
Write-Host -Fore Green `
"Successfully enabled anonymous access on site ($($web.Url))."
}
function Main()
{
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
$webUrl = $webAppUrl + "/"
$web = Get-SPWeb $webUrl -Debug:$false
EnableAnonymousAccess $web
$web.Dispose()
}
Main
Configure Object Cache User Accounts.ps1
If you haven't yet discovered errors in the event log after creating Publishing sites in SharePoint Server 2010, then you probably don't even bother to look at your event logs. In that case, shame on you ;-)
If you have seen the errors I'm referring to, then you're probably familiar with the following TechNet article:
Configure object cache user accounts
Here's a script to get rid of those pesky errors. It assumes the Portal Super User is {DOMAIN}\svc-sp-psu (or perhaps some variant depending on environment, such as {DOMAIN}\svc-sp-psu-dev) and the Portal Super Reader is {DOMAIN}\svc-sp-psr (or, again, some variant of this).
It takes care of adding the appropriate user policies on the Web application (Full Control to {DOMAIN}\svc-sp-psu, and Full Read to {DOMAIN}\svc-sp-psr), as well as setting the corresponding properties on the Web application -- as described in the above TechNet article. It also ensures the specified service accounts are indeed valid (via the GetUserDisplayName
function).
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function ConfigureObjectCacheUserAccounts(
[string] $webAppUrl = $(Throw "Value cannot be null: webAppUrl"),
[string] $portalSuperUserAccount =
$(Throw "Value cannot be null: portalSuperUserAccount"),
[string] $portalSuperReaderAccount =
$(Throw "Value cannot be null: portalSuperReaderAccount"))
{
Write-Host ("Configuring object cache user accounts for Web application" `
+ " ($webAppUrl)...")
Write-Debug "portalSuperUserAccount: $portalSuperUserAccount"
Write-Debug "portalSuperReaderAccount: $portalSuperReaderAccount"
$webApp = Get-SPWebApplication -Identity $webAppUrl -Debug:$false
SetWebAppUserPolicy $webApp $portalSuperUserAccount "Full Control"
SetWebAppProperty $webApp "portalsuperuseraccount" $portalSuperUserAccount
SetWebAppUserPolicy $webApp $portalSuperReaderAccount "Full Read"
SetWebAppProperty $webApp "portalsuperreaderaccount" $portalSuperReaderAccount
Write-Host -Fore Green ("Successfully configured object cache user accounts" `
+ " for Web application ($webAppUrl).")
}
function GetUserDisplayName(
[string] $userName = $(Throw "Value cannot be null: userName"))
{
Write-Debug "Getting display name for user ($userName)..."
$userNameParts = $userName.Split("\")
$samAccountName = $userNameParts[1]
$filter = "(&(objectCategory=User)(samAccountName=$samAccountName))"
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = $filter
$path = $searcher.FindOne()
If ($path -eq $null)
{
Throw "User not found ($userName)."
}
$user = $path.GetDirectoryEntry()
Write-Debug "Found display name for user ($($user.DisplayName))."
return $user.DisplayName
}
function SetWebAppProperty(
[Microsoft.SharePoint.Administration.SPWebApplication] $webApp =
$(Throw "Value cannot be null: webApp"),
[string] $propertyName = $(Throw "Value cannot be null: propertyName"),
[string] $propertyValue)
{
Write-Debug ("Setting property ($propertyName) on Web application" `
+ " ($($webApp.Url)...")
If ($webApp.Properties[$propertyName] -eq $propertyValue)
{
Write-Debug ("The Web application property ($propertyName) is already set" `
+ " to the expected value ($propertyValue).")
return;
}
$webApp.Properties[$propertyName] = $propertyValue
$webApp.Update()
Write-Debug ("Successfully set property ($propertyName) on Web" `
+ " application ($($webApp.Url)) to '$propertyValue'.")
}
function SetWebAppUserPolicy(
[Microsoft.SharePoint.Administration.SPWebApplication] $webApp =
$(Throw "Value cannot be null: webApp"),
[string] $userName = $(Throw "Value cannot be null: userName"),
[string] $permissions = $(Throw "Value cannot be null: permissions"))
{
Write-Debug ("Setting policy ($permissions) for user" `
+ " ($userName) on Web application ($($webApp.Url))...")
[Microsoft.SharePoint.Administration.SPPolicyRole] $policyRole =
$webApp.PolicyRoles | where {$_.Name -eq $permissions}
if ($policyRole -eq $null)
{
Throw "Invalid permissions ($permissions)."
}
$userDisplayName = GetUserDisplayName $userName
[Microsoft.SharePoint.Administration.SPPolicyCollection] $policies =
$webApp.Policies
[Microsoft.SharePoint.Administration.SPPolicy] $policy = $policies.Add(
$userName,
$userDisplayName)
$policy.PolicyRoleBindings.Add($policyRole)
$webApp.Update()
Write-Debug ("Successfully added policy ($permissions) for user" `
+ " ($userName) to Web application ($($webApp.Url))...")
}
function Main()
{
$portalSuperUserAccount = $env:USERDOMAIN + "\svc-sp-psu"
$portalSuperReaderAccount = $env:USERDOMAIN + "\svc-sp-psr"
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
Write-Debug "webAppUrl: $webAppUrl"
if (($webAppUrl -eq "http://www-local.fabrikam.com") -or
($webAppUrl -eq "http://www-dev.fabrikam.com"))
{
Write-Debug "Overriding variables for Development environment..."
$portalSuperUserAccount = $env:USERDOMAIN + "\svc-sp-psu-dev"
$portalSuperReaderAccount = $env:USERDOMAIN + "\svc-sp-psr-dev"
}
elseif ($webAppUrl -eq "http://www-test.fabrikam.com")
{
Write-Debug "Overriding variables for Test environment..."
$portalSuperUserAccount = $env:USERDOMAIN + "\svc-sp-psu-test"
$portalSuperReaderAccount = $env:USERDOMAIN + "\svc-sp-psr-test"
}
ConfigureObjectCacheUserAccounts $webAppUrl $portalSuperUserAccount `
$portalSuperReaderAccount
}
Main
Add Event Log Sources.ps1
The Fabrikam solution includes a custom SPLogger class for writing trace messages and events. In order to write to the Windows event log with a custom source (e.g. "Fabrikam Demo Site"), you first need to create the event log source (assuming your solution is running with a least-privileged service account, and I certainly hope it is). Otherwise, a nasty error will occur when attempting to log an event. [If you solution is running with administrative privileges, then the custom event log source will be dynamically created as necessary -- but please don't do this. It's just plain wrong.]
The following script ensures the custom event source is registered.
Important
This script must be run on each SharePoint server in the farm.
$ErrorActionPreference = "Stop"
Function AddEventLogSource(
[string] $source = $(Throw "Value cannot be null: source"))
{
$source = $source.Trim()
If ([string]::IsNullOrEmpty($source) -eq $true)
{
Throw "The name of the event source is required."
}
$sourceExists = [System.Diagnostics.EventLog]::SourceExists($source)
If ($sourceExists -eq $true)
{
Write-Host "The event source ($source) already exists."
}
Else
{
Write-Host "Creating event source ($source)..."
[System.Diagnostics.EventLog]::CreateEventSource($source, "Application")
Write-Host -Fore Green "Successfully created event source ($source)."
}
}
function Main
{
AddEventLogSource "Fabrikam Demo Site"
}
Main
Although this script currently adds only one custom event log source, I named it plural just in case additional sources are added in the future (so I wouldn't have to update the script name in the Installation Guide for the Fabrikam site).
At this point, we have a "vanilla" Web site created and configured in SharePoint Server 2010. Now we need to run the "ADA" portion of the "DR.DADA" process in order to Add our custom solution (Fabrikam.Demo.Web.wsp),Deploy the solution to the Fabrikam Web application, and, finally, Activate our features.
Add Solutions.ps1
Let's start with a script to add the solution (note that additional solutions may be added in the future, so the script name is plural).
If you've been developing with SharePoint 2010 for any signficant period of time, then you might have encountered issues due to "stale" assemblies being used for event receivers (unless perhaps you simply do all of your "DR.DADA" operations through Visual Studio and never through PowerShell). This is because your assemblies are actually loaded into the PowerShell process during various deployment operations (for example, when you invoke the Add-SPSolution
cmdlet). I discovered this issue the "hard way" -- in other words, by attaching WinDbg to my PowerShell command prompt.
When I searched the Internet for PowerShell reload assembly (looking for an easy way to unload a specific assembly), I discovered the following blog post:
PowerShell Does Not Reload Upgraded Assemblies
I found Lauri's approach to be invaluable for isolating the AppDomain that loads your assemblies, as shown in the following script.
param(
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function AddSolution(
[string] $solutionPath = $(Throw "Value cannot be null: solutionPath"))
{
Write-Host "Adding solution ($solutionPath)..."
Resolve-Path $solutionPath | Add-SPSolution -Debug:$false > $null
Write-Host -Fore Green "Successfully added solution ($solutionPath)."
}
function Main()
{
Write-Host "Adding solutions..."
$buildConfiguration = $env:FABRIKAM_BUILD_CONFIGURATION
If ($buildConfiguration -eq $null)
{
$buildConfiguration = "Release"
}
Write-Debug "buildConfiguration: $buildConfiguration"
$solutionFile = "..\..\Web\bin\$buildConfiguration\Fabrikam.Demo.Web.wsp"
If ((Test-Path $solutionFile) -eq $false)
{
$solutionFile = "..\..\$buildConfiguration\Fabrikam.Demo.Web.wsp"
}
AddSolution $solutionFile
}
Main
Update (2011-03-02)
I modified the original script above to ensure it works with TFS builds as well as "desktop" builds (i.e. built from within Visual Studio).
The optional -Debug
parameter for this script makes it easy to debug the new PowerShell instance (without requiring you to temporarily specify $DebugPreference = "Continue"
in your PowerShell profile).
Note how I allow the build configuration (Debug or Release) to be specified outside of the script depending on the environment (i.e. using the FABRIKAM_BUILD_CONFIGURATION environment variable). For example, in the Production environment, we always want to deploy Release builds -- not Debug builds. Whereas in development environments, we always want to install the Debug builds. (Whether we install Debug or Release builds to the Test environment depends on where we are at in the release cycle.)
Deploy Solutions.ps1
After adding the solutions to SharePoint, we next need to deploy them to the Web application. When a solution is deployed, SharePoint extracts the contents of the CAB file -- er, I mean WSP file -- including the assembly and any files deployed to the "14 hive", and copies these to the appropriate locations (such as the Global Assembly Cache or a folder under C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14).
Note that in a SharePoint farm comprised of multiple servers, this deployment must be done on each server in the farm. No, you don't execute the PowerShell script on each SharePoint server in the farm. Rather, when you run the script on one of the servers in the farm, SharePoint automatically creates a timer job on each server in the farm to deploy the solution on that server.
param(
[switch] $force,
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function DeploySolution(
[string] $solutionName = $(Throw "Value cannot be null: solutionName"),
[string] $webAppUrl = $(Throw "Value cannot be null: webAppUrl"),
[bool] $force,
[bool] $local)
{
Write-Host ("Deploying solution ($solutionName) to Web application" `
+ " ($webAppUrl)...")
If ($force -eq $true)
{
Write-Debug "The solution deployment will be forced."
}
If ($local -eq $true)
{
Write-Debug ("The solution will be deployed locally (bypassing" `
+ " SharePoint timer job).")
}
Install-SPSolution $solutionName -GACDeployment `
-WebApplication $webAppUrl -Force:([bool]::Parse($force)) `
-Local:([bool]::Parse($local)) -Confirm:$false -Debug:$false
If (-not $local)
{
. '.\Wait for Solution Deployment Jobs to Finish.ps1' $solutionName
}
Write-Host -Fore Green ("Successfully deployed solution ($solutionName)" `
+ " to Web application ($webAppUrl).")
}
function Main(
[bool] $force)
{
Write-Host "Deploying solutions..."
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
[bool] $local = $false
If (($webAppUrl -eq "http://www-local.fabrikam.com") `
-or ($webAppUrl -eq "http://www-dev.fabrikam.com"))
{
$local = $true
}
DeploySolution "Fabrikam.Demo.Web.wsp" $webAppUrl $force $local
}
Main $force
Also note that the script supports an optional parameter to force the solution to be deployed -- in the (hopefully) rare event you need to use that.
The most interesting part about Deploy Solutions.ps1 is the fact that I try to avoid using a SharePoint Timer job to deploy the solution if at all possible. In other words, on my local development VM or in the Development integration environment, there's no need to schedule the deployment through the SharePoint Timer infrastructure since it's just a single server environment. This is another great reason to follow a standard naming convention for your environments.
If the solution is not deployed with the "-Local
" option, then we need to wait for the solution deployment job to finish before we can activate the features. Back in the MOSS 2007 days, I used stsadm.exe -o execadmsvcjobs
to wait for the timer job to finish before continuing. However, since the StsAdm.exe utility is considered "passé" in SharePoint 2010, I now use a PowerShell script instead.
Wait for Solution Deployment Jobs to Finish.ps1
Here's a script that allows you to wait for either specific solution deployment jobs to finish (if one or more solution names are specified as parameters to the script) or to wait for all solution deployment jobs to finish (if no parameters are specified when running the script).
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function WaitForSharePointTimerJobToFinish(
[Microsoft.SharePoint.Administration.SPJobDefinition] $job)
{
If ($job -eq $null)
{
return
}
$jobName = $job.Name
Write-Host -NoNewLine ("Waiting for SharePoint timer job ($jobName) to" `
+ " finish...")
While ((Get-SPTimerJob $jobName -Debug:$false) -ne $null)
{
Write-Host -NoNewLine "."
Start-Sleep -Seconds 5
}
Write-Host
Write-Host "The SharePoint timer job ($jobName) has finished."
}
function WaitForSolutionDeploymentJobsToFinish(
[string] $solutionName)
{
Write-Debug "solutionName: $solutionName"
If ([string]::IsNullOrEmpty($solutionName) -eq $true)
{
Write-Debug "Waiting for all solution deployment jobs to finish..."
$jobNameFilter = "*solution-deployment*"
}
Else
{
Write-Debug ("Waiting for solution deployment " `
+ " ($solutionName) to finish...")
$jobNameFilter = "*solution-deployment*$solutionName*"
}
Write-Debug "jobNameFilter: $jobNameFilter"
$jobs = Get-SPTimerJob -Debug:$false | Where { $_.Name -like $jobNameFilter }
If ($jobs -eq $null)
{
Write-Debug "No solution deployment jobs found"
return
}
If ($jobs -is [Array])
{
Foreach ($job in $jobs)
{
WaitForSharePointTimerJobToFinish $job
}
}
Else
{
WaitForSharePointTimerJobToFinish $jobs
}
}
function Main()
{
If ($args.Count -eq 0)
{
WaitForSolutionDeploymentJobsToFinish
}
Else
{
Foreach ($solutionName in $args)
{
WaitForSolutionDeploymentJobsToFinish $solutionName
}
}
}
Main $args
Activate Features.ps1
With our custom WSP deployed, we are now ready to activate the features. This is easy enough to do using theEnable-SPFeature
cmdlet, but the following script makes this a little more robust. For example, it first determines the scope of each activated feature and then checks to see if the feature is already activated at the corresponding scope. This avoids annoying errors like "The feature ... is already activated..." that terminate the script.
Also note that, like Deploy Solutions.ps1, the following script supports an optional parameter to force the features to be activated.
param(
[switch] $force,
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function ActivateFeature(
[string] $featureName = $(Throw "Value cannot be null: featureName"),
[string] $siteUrl = "",
[bool] $force = $false)
{
Write-Debug "Activating feature ($featureName)..."
Write-Debug "featureName: $featureName"
If ($force -eq $true)
{
Write-Debug "The feature activation will be forced."
}
$feature = Get-SPFeature $featureName -Debug:$false
If ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Farm)
{
$feature = Get-SPFeature $featureName -Farm -Debug:$false -EA 0
If ($feature -ne $null -and ($force -eq $false))
{
Write-Host "The feature ($featureName) is already activated on the farm."
return;
}
Write-Host "Activating feature ($featureName) on farm..."
Enable-SPFeature $featureName -Force:([bool]::Parse($force)) `
-Confirm:$false -Debug:$false
Write-Host -Fore Green ("Successfully activated farm feature" `
+ " ($featureName).")
return
}
ElseIf ($feature.Scope -eq `
[Microsoft.SharePoint.SPFeatureScope]::WebApplication)
{
$feature = Get-SPFeature $featureName -WebApplication $siteUrl `
-Debug:$false -EA 0
}
ElseIf ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Site)
{
$feature = Get-SPFeature $featureName -Site $siteUrl -Debug:$false -EA 0
}
ElseIf ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Web)
{
$feature = Get-SPFeature $featureName -Web $siteUrl -Debug:$false -EA 0
}
If ($feature -ne $null -and ($force -eq $false))
{
Write-Host ("The feature ($featureName) is already activated on the site" `
+ " ($siteUrl)...")
return;
}
Write-Host "Activating feature ($featureName) on site ($siteUrl)..."
Enable-SPFeature $featureName -Url $siteUrl `
-Force:([bool]::Parse($force)) -Confirm:$false -Debug:$false
Write-Host -Fore Green ("Successfully activated feature ($featureName) on" `
+ " site ($siteUrl).")
}
function Main()
{
Write-Host "Activating features..."
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
$siteUrl = $webAppUrl + "/"
ActivateFeature "Fabrikam.Demo.Web_WebAppConfiguration" $siteUrl $force
ActivateFeature "Fabrikam.Demo.Web_WebParts" $siteUrl $force
ActivateFeature "Fabrikam.Demo.Web_HomeSiteConfiguration" $siteUrl $force
}
Main
At this point, the Fabrikam site is fully configured and ready for testing.
Now, imagine that we've fixed some bugs or modified our custom SharePoint solution (for example, to add a custom master page, or some new page layouts). Consequently, we need to "DRD" the old solution and "ADA" the new version.
Let's start by deactivating the features...
Deactivate Features.ps1
If you've carefully examined the Activate Features.ps1 script, then there's really no point in scrutinizing the following script ;-)
param(
[switch] $force,
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function DeactivateFeature(
[string] $featureName = $(Throw "Value cannot be null: featureName"),
[string] $siteUrl = "",
[bool] $force = $false)
{
Write-Debug "Deactivating feature ($featureName)..."
Write-Debug "siteUrl: $siteUrl"
If ($force -eq $true)
{
Write-Debug "The feature deactivation will be forced."
}
$feature = Get-SPFeature $featureName -Debug:$false -EA 0
If ($feature -eq $null)
{
Write-Warning "The specified feature ($featureName) was not found."
return
}
If ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Farm)
{
$feature = Get-SPFeature $featureName -Farm -Debug:$false -EA 0
If ($feature -eq $null)
{
Write-Host "The feature ($featureName) is not activated on the farm."
return
}
Write-Host "Deactivating farm feature ($featureName)..."
Disable-SPFeature $featureName -Force:([bool]::Parse($force)) `
-Confirm:$false -Debug:$false
Write-Host -Fore Green ("Successfully deactivated farm feature" `
+ " ($featureName).")
return
}
ElseIf ($feature.Scope -eq `
[Microsoft.SharePoint.SPFeatureScope]::WebApplication)
{
$feature = Get-SPFeature $featureName -WebApplication $siteUrl `
-Debug:$false -EA 0
}
ElseIf ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Site)
{
$feature = Get-SPFeature $featureName -Site $siteUrl -Debug:$false -EA 0
}
ElseIf ($feature.Scope -eq [Microsoft.SharePoint.SPFeatureScope]::Web)
{
$feature = Get-SPFeature $featureName -Web $siteUrl -Debug:$false -EA 0
}
If ($feature -eq $null)
{
Write-Host ("The feature ($featureName) is not activated on the site" `
+ " ($siteUrl)...")
return
}
Write-Host "Deactivating feature ($featureName) on site ($siteUrl)..."
Disable-SPFeature $featureName -Url $siteUrl -Force:([bool]::Parse($force)) `
-Confirm:$false -Debug:$false
Write-Host -Fore Green ("Successfully deactivated feature ($featureName) on" `
+ " site ($siteUrl).")
}
function Main(
[bool] $force)
{
Write-Host "Deactivating features..."
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
$siteUrl = $webAppUrl + "/"
DeactivateFeature "Fabrikam.Demo.Web_HomeSiteConfiguration" $siteUrl $force
DeactivateFeature "Fabrikam.Demo.Web_WebParts" $siteUrl $force
DeactivateFeature "Fabrikam.Demo.Web_WebAppConfiguration" $siteUrl $force
}
Main $force
Retract Solutions.ps1
After the features are deactivated, we are ready to retract the solution from the Web application. Like the Deploy Solutions.ps1 script, I try to avoid SharePoint timer jobs -- if possible -- so that developers can be as productive as possible.
param(
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function RetractSolution(
[string] $solutionName = $(Throw "Value cannot be null: solutionName"),
[string] $webAppUrl = $(Throw "Value cannot be null: webAppUrl"),
[bool] $local)
{
Write-Host ("Retracting solution ($solutionName) from Web application" `
+ " ($webAppUrl)...")
If ($local -eq $true)
{
Write-Debug ("The solution will be retracted locally (bypassing" `
+ " SharePoint timer job).")
}
$webApp = Get-SPWebApplication $webAppUrl -Debug:$false
$solution = Get-SPSolution $solutionName -Debug:$false -EA 0
If ($solution -eq $null)
{
Write-Warning "The specified solution ($solutionName) was not found."
return
}
$deployedWebApp = $solution.DeployedWebApplications |
Where { $_.Url -eq $webApp.Url }
If ($deployedWebApp -eq $null)
{
Write-Host ("The solution ($solutionName) is not deployed to the" `
+ " specified Web application ($webAppUrl).")
return;
}
Uninstall-SPSolution $solutionName -WebApplication $webAppUrl `
-Local:([bool]::Parse($local)) -Confirm:$false -Debug:$false
If ($local -eq $false)
{
. '.\Wait for Solution Deployment Jobs to Finish.ps1' $solutionName
}
Write-Host -Fore Green ("Successfully retracted solution ($solutionName)" `
+ " from Web application ($webAppUrl).")
}
function Main()
{
Write-Host "Retracting solutions..."
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
$local = $false
If (($webAppUrl -eq "http://www-local.fabrikam.com") `
-or ($webAppUrl -eq "http://www-dev.fabrikam.com"))
{
$local = $true
}
RetractSolution "Fabrikam.Demo.Web.wsp" $webAppUrl $local
}
Main
Delete Solutions.ps1
Lastly, it's time to delete the old solution from the SharePoint farm...
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function DeleteSolution(
[string] $solutionName = $(Throw "Value cannot be null: solutionName"))
{
Write-Host "Deleting solution ($solutionName)..."
$solution = Get-SPSolution $solutionName -EA 0 -Debug:$false
If ($solution -eq $null)
{
Write-Warning "The specified solution ($solutionName) was not found."
return
}
Remove-SPSolution $solutionName -Confirm:$false -Debug:$false
Write-Host -Fore Green "Successfully deleted solution ($solutionName)."
}
function Main()
{
Write-Host "Deleting solutions..."
DeleteSolution "Fabrikam.Demo.Web.wsp"
}
Main
At this point, we are ready to "ADA" the updated Fabrikam.Demo.Web.wsp solution.
Redeploy Features.ps1
If, like me, you get tired of cycling through the command history (F7) to repeatedly execute the "DR.DADA" process, then you can use the following script to save a few dozen keystrokes.
$ErrorActionPreference = "Stop"
function Main()
{
& '.\Deactivate Features.ps1'
& '.\Retract Solutions.ps1'
& '.\Delete Solutions.ps1'
& '.\Add Solutions.ps1'
& '.\Deploy Solutions.ps1'
& '.\Activate Features.ps1'
}
Main
This script essentially performs the same activities as the Default deployment configuration for a SharePoint project in Visual Studio. Consequently, I don't expect this to be used all that much during the development process. I've still found this useful, however, for some scenarios. For example, in the sample SharePoint solution I provided in my previous post, you'll find that I changed the Activate On Default property of the "WebAppConfiguration" feature to False. For the reasons why I did this, refer to the following post (that was originally written for MOSS 2007 but still applies to SharePoint 2010):
SharePoint Features Activated by Default
Consequently, I used the Redeploy Features.ps1 script to ensure the WebAppConfiguration feature is activated (thus ensuring my custom Sign In page for claims authentication was configured on the Web application).
Upgrade Solutions.ps1
While the "DR.DADA" process doesn't typically take very long, there are a limited number of changes that can be made to a SharePoint solution in which a simple "Upgrade Solution" will suffice. Here's a script that performs the equivalent of the old stsadm.exe -o upgradesolution command.
param(
[switch] $force,
[switch] $runInThisAppDomain,
[switch] $debug)
If (-not $runInThisAppDomain)
{
Write-Host -Fore Yellow "Invoking script in a new app domain"
PowerShell.exe -Command $MyInvocation.Line -RunInThisAppDomain
return
}
If ($debug -eq $true)
{
$DebugPreference = "Continue"
}
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function UpgradeSolution(
[string] $solutionPath = $(Throw "Value cannot be null: solutionPath"),
[bool] $force,
[bool] $local)
{
Write-Host "Upgrading solution ($solutionPath)..."
If ($force -eq $true)
{
Write-Debug "The solution upgrade will be forced."
}
If ($local -eq $true)
{
Write-Debug ("The solution will be upgraded locally (bypassing" `
+ " SharePoint timer job).")
}
$literalPath = Resolve-Path $solutionPath
$solutionFile = Get-Item $literalPath
Update-SPSolution $solutionFile.Name -LiteralPath $literalPath -GACDeployment `
-Force:([bool]::Parse($force)) -Local:([bool]::Parse($local)) `
-Debug:$false > $null
If ($local -eq $false)
{
. '.\Wait for Solution Deployment Jobs to Finish.ps1' $solutionFile.Name
}
Write-Host -Fore Green "Successfully upgraded solution ($solutionPath)."
}
function Main(
[bool] $force)
{
Write-Host "Upgrading solutions..."
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
$local = $false
If (($webAppUrl -eq "http://www-local.fabrikam.com") `
-or ($webAppUrl -eq "http://www-dev.fabrikam.com"))
{
$local = $true
}
$buildConfiguration = $env:FABRIKAM_BUILD_CONFIGURATION
If ($buildConfiguration -eq $null)
{
$buildConfiguration = "Release"
}
Write-Debug "buildConfiguration: $buildConfiguration"
$solutionFile = "..\..\Web\bin\$buildConfiguration\Fabrikam.Demo.Web.wsp"
If ((Test-Path $solutionFile) -eq $false)
{
$solutionFile = "..\..\$buildConfiguration\Fabrikam.Demo.Web.wsp"
}
UpgradeSolution $solutionFile $force $local
}
Main $force
Update (2011-03-02)
I modified the original script above to ensure it works with TFS builds as well as "desktop" builds (i.e. built from within Visual Studio).
Delete Web Application.ps1
On my local development VM, I often find it helpful to "nuke" the Fabrikam Web application and start over from scratch. To achieve this, I "DRD" the Fabrikam solution/features and then run the following script and start over with the Create Web Application.ps1 script.
$ErrorActionPreference = "Stop"
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA 0
function Main()
{
$webAppUrl = $env:FABRIKAM_DEMO_URL
If ($webAppUrl -eq $null)
{
$webAppUrl = "http://www.fabrikam.com"
}
Write-Debug "webAppUrl: $webAppUrl"
Remove-SPWebApplication $webAppUrl -DeleteIISSite -RemoveContentDatabases `
-Debug:$false
}
Main
In fact, on several occasions I've found it to be very helpful to rollback my SharePoint 2010 Hyper-V VM to a snapshot that I took shortly after installing SharePoint Server 2010 and creating the farm (but before creating any Web applications or configuring any service applications). Whenever I do that, I simply need to force a "Get Latest" from TFS (since my TFS workspace isn't aware that I've rolled back my VHD to an earlier point in time), compile and package the solution, and then run the scripts described above in the following order:
& '.\Create Web Application.ps1'
& '.\Create Site Collections.ps1'
& '.\Enable Anonymous Access.ps1'
& '.\Configure Object Cache User Accounts.ps1'
& '.\Add Event Log Sources.ps1'
& '.\Add Solutions.ps1'
& '.\Deploy Solutions.ps1'
& '.\Activate Features.ps1'
Thanks to the extremely robust scripting capabilities in SharePoint 2010, I'm able to rebuild my development environment in a matter of minutes.
Warning
If, like me, you decide to use Hyper-V snapshots in your SharePoint development environment, then make darn sure you've checked in any pending changes to TFS (or at least shelved your changes) before you apply a snapshot. [Note that when applying an earlier snapshot, I don't typically take a new snapshot before reverting to the earlier point in time. In other words, I treat my SharePoint development VM as "volatile" -- or "disposable" (if you prefer that term instead).]