First off I apologize for the delay in getting this post out – I’ve been fraught with injuries and illness since my return from London and just haven’t had the time or mental capacity to think about writing anything.

During my sessions at the ISC I demonstrated (along with Spence Harbar and Chan Kulathilake) how we could use PowerShell to provision the entire SharePoint 2010 Farm used throughout the IT track at the conference (yeah, no pressure – everyone presenting after me was depending on my scripts functioning). This Farm consisted of numerous servers beyond the SharePoint servers but my scripts were only responsible for getting SharePoint configured. For reference here’s a screenshot showing the server topology:

Before we could run the scripts on any servers there was some prep work that we had to do ahead of time. Specifically we did the following on each of the SharePoint servers (SP01-06):

  • Installed SharePoint 2010 and Office Web Applications with SP1 and the October 2011 CU
    • The installation of the bits could have been scripted but doing so during a live session at a conference just wasn’t practical due to time limitations.
  • Disable: UAC, Firewall, IE ESC
    • This isn’t something you’d typically do for a production environment but doing so reduced potential complications and annoyances when it came to doing demos. I typically will disable all these things when doing my provisioning and then selectively re-enable them after I’ve confirmed that the Farm is running properly – this makes troubleshooting much easier.
  • PowerShell Prep
    • I updated the all users all hosts profile on each server so that the SharePoint PowerShell snap-in would be loaded for any editor so that we wouldn’t be tied to the management shell. (We also installed the PowerShell ISE).
    • I enabled remoting on all servers so that I could build each server remotely, thereby foregoing the need to have to log into each server to kick off the script in parallel. I also created a custom configuration session which loaded the SharePoint PowerShell snap-in automatically and made sure the threading model was set appropriately. The following shows the commands I executed to enable remoting:
    • Because the we’re using scripts and because those scripts are stored on a network share we had to set the PowerShell execution policy on each server to Bypass.
1Enable-PSRemoting
2Enable-WSmanCredSSP -Role Server
3Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 1024
4Register-PSSessionConfiguration -Name "SharePoint" -StartupScript "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1" -Force -ThreadOptions ReuseThread
5Set-PSSessionConfiguration -Name "SharePoint" -ShowSecurityDescriptorUI -Force
  • SPInstall Permissions
    • The scripts were all run using a single named account which we called SPInstall. In order to perform all the required tasks this account was made a local administrator on all the SharePoint servers (none of the others) and the account was granted the dbcreator and securityadmin roles on SQL01 and SQL02 (or primary and failover SQL instances).
  • Add SQL Alias and DNS Entries
    • The DNS servers were updated with the appropriate entries for all of our Web Applications (we did not use hosts files) and, to connect to SQL Server, we used a SQL alias which needed to be added on each of the SharePoint servers.

Controller Scripts

So that constitutes the extent of the prep work that we did before we could kick off the scripts that I put together. To do the provisioning I used a main “controller” script which I called ConfigureServer.ps1. This script was responsible for loading all the subsequent scripts which created the initial Farm (or connected to it), provisioned the Web Applications and Site Collections, and finally, created all the relevant Service Applications:

  1param(
  2  [switch]$Connect
  3)
  4
  5Write-Host "Script Start Time: $(Get-Date)" -ForegroundColor Green
  6
  7#Main Farm
  8. .\FarmCreation\Build-SPFarm.ps1
  9$farmConfigFile = Resolve-Path .\FarmCreation\FarmConfigurations.xml
 10try {
 11    if ($Connect) {
 12        Join-SPFarm $farmConfigFile
 13    } else {
 14        New-SPFarm $farmConfigFile
 15    }
 16} catch {
 17    Write-Warning "Unable to build or join Farm!"
 18    $_
 19    return
 20}
 21
 22#Web Applications
 23if (!$Connect) {
 24    #Only need to do this once so do it when we create the farm initially and not for each connection.
 25    try {
 26        . .\WebApplications\Provision-WebApplications.ps1
 27        Provision-WebApplications (Resolve-Path .\WebApplications\WebApplications.xml)
 28    } catch {
 29        Write-Warning "Unable to create web applications!"
 30        $_
 31        return
 32    }
 33}
 34
 35#Services
 36try {
 37    #State Service
 38    . .\ServiceApplications\StateServices\Start-StateServices.ps1
 39    Start-StateServices (Resolve-Path .\ServiceApplications\StateServices\StateServices.xml)
 40
 41    #Usage Service
 42    . .\ServiceApplications\UsageService\Start-UsageService.ps1
 43    Start-UsageService (Resolve-Path .\ServiceApplications\UsageService\UsageService.xml)
 44
 45    #Secure Store Services
 46    . .\ServiceApplications\SecureStoreServices\Start-SecureStoreServices.ps1
 47    Start-SecureStoreServices (Resolve-Path .\ServiceApplications\SecureStoreServices\SecureStoreServices.xml)
 48
 49    #Web Analytics Services
 50    . .\ServiceApplications\WebAnalyticsServices\Start-WebAnalyticsServices.ps1
 51    Start-WebAnalyticsServices (Resolve-Path .\ServiceApplications\WebAnalyticsServices\WebAnalyticsServices.xml)
 52
 53    #User Code Services
 54    . .\ServiceApplications\UserCodeServices\Start-UserCodeServices.ps1
 55    Start-UserCodeServices (Resolve-Path .\ServiceApplications\UserCodeServices\UserCodeServices.xml)
 56
 57    #BCS Services
 58    . .\ServiceApplications\BCSServices\Start-BCSServices.ps1
 59    Start-BCSServices (Resolve-Path .\ServiceApplications\BCSServices\BCSServices.xml)
 60
 61    #Excel Services
 62    . .\ServiceApplications\ExcelServices\Start-ExcelServices.ps1
 63    Start-ExcelServices (Resolve-Path .\ServiceApplications\ExcelServices\ExcelServices.xml)
 64
 65    #Word Automation Services
 66    . .\ServiceApplications\WordAutomationServices\Start-WordAutomationServices.ps1
 67    Start-WordAutomationServices (Resolve-Path .\ServiceApplications\WordAutomationServices\WordAutomationServices.xml)
 68
 69    #Metadata Services
 70    . .\ServiceApplications\MetadataServices\Start-MetadataServices.ps1
 71    Start-MetadataServices (Resolve-Path .\ServiceApplications\MetadataServices\MetadataServices.xml)
 72
 73    #C2WTS
 74    . .\ServiceApplications\ClaimsToWindowsTokenServices\Start-ClaimsToWindowsTokenServices.ps1
 75    Start-ClaimsToWindowsTokenServices (Resolve-Path .\ServiceApplications\ClaimsToWindowsTokenServices\ClaimsToWindowsTokenServices.xml)
 76
 77    #PowerPoint Services
 78    . .\ServiceApplications\PowerPointServices\Start-PowerPointServices.ps1
 79    Start-PowerPointServices (Resolve-Path .\ServiceApplications\PowerPointServices\PowerPointServices.xml)
 80
 81    #Word Viewing Services
 82    . .\ServiceApplications\WordViewingServices\Start-WordViewingServices.ps1
 83    Start-WordViewingServices (Resolve-Path .\ServiceApplications\WordViewingServices\WordViewingServices.xml)
 84    
 85    #User Profile Services
 86    . .\ServiceApplications\UserProfileServices\Start-UserProfileServices.ps1
 87    Start-UserProfileServices (Resolve-Path .\ServiceApplications\UserProfileServices\UserProfileServices.xml)
 88
 89    #Enterprise Search Services
 90    . .\ServiceApplications\EnterpriseSearchServices\Start-EnterpriseSearchServices.ps1
 91    Start-EnterpriseSearchServices (Resolve-Path .\ServiceApplications\EnterpriseSearchServices\EnterpriseSearchServices.xml)
 92
 93} catch {
 94    Write-Warning "Service Application creation failed!"
 95    $_
 96    return
 97}
 98finally {
 99    Write-Host "Script End Time: $(Get-Date)" -ForegroundColor Green
100}

This script was executed on each server, however, we ran it first on SP03 which is where the User Profile Synchronization Service was to be run. Due to issues with getting this guy to work using remoting we chose to RDP directly into the server and run this one script while logged into the server. Once this server was configured and the initial Farm created then we’d go ahead and run the script on the remaining servers by using PowerShell remoting to execute the script from a single location.

This is the script, which I named simply go.ps1, that I used to call the ConfigureServer.ps1 script for all the remaining SharePoint servers (everything other than SP03):

 1$servers = @("SP01","SP02","SP04","SP05","SP06")
 2$cred = Get-Credential "isclondon\spinstall"
 3
 4foreach ($server in $servers) {
 5    Write-Host "---------------------------------------------------------------" -ForegroundColor Green
 6    Write-Host "$(Get-Date): Connecting to server $server." -ForegroundColor Green
 7    Write-Host "---------------------------------------------------------------" -ForegroundColor Green
 8    $session = New-PSSession -ComputerName $server -Authentication CredSSP -Credential $cred -ConfigurationName "SharePoint"
 9    Invoke-Command -Session $session -ScriptBlock {Set-ExecutionPolicy Bypass -Scope Process}
10    Invoke-Command -Session $session -ScriptBlock {cd \\sp03\c$\Scripts}
11    Invoke-Command -Session $session -ScriptBlock {. .\ConfigureServer.ps1 -Connect}
12    Remove-PSSession $session
13    Write-Host "$(Get-Date): Finished for server $server" -ForegroundColor Green
14}

Build-SPFarm.ps1

So the preceding two scripts constitute the “controller” scripts that I used to call out to the core scripts which did all the real work. Now let’s look at the first and most important script as it is responsible for creating the Farm (or connecting to the Farm): Build-SPFarm.ps1.

This script, like all the remaining scripts is driven via an XML file; in other words, I store all the configuration information in a series of XML files that I pass into the function contained within the script. By taking this approach I can write a single script which is not tied to any particular environment and only change the contents of the XML file.

I think something that a lot of people forget when writing PowerShell scripts is that you are writing code, and just as you would never deploy code written by any old developer to your production environment without it first going through proper testing in a test environment, so should you never deploy (or execute in this case) scripts to production without first testing those scripts. And, with any code, if you make changes to that code you should follow proper testing practices and do a full regression test of any code that was modified (yes, this includes changing variable values because the value you specify could actually break the script if improperly formatted – think of someone forgetting a closing quote or adding a special character to the contents of a string variable). For these reasons, and many others, I encourage people to reduce the changes made to scripts by pulling the configuration information out into an XML file.

So lets look at the FarmConfigurations.xml XML file that I used for the Build-SPFarm.ps1 script:

 1<Farm ConfigDB="ISC_Config"
 2      ConfigDBFailoverDatabaseServer=""
 3      AdminContentDB="ISC_Content_CentralAdmin" 
 4      AdminContentDBFailoverDatabaseServer=""
 5      DatabaseServer="ISCSharePoint1" 
 6      Passphrase="p@ssw0rd"
 7      OutgoingEmailServer="192.168.1.101"
 8      OutgoingEmailFromAddr="administrator@isclondon.com"
 9      OutgoingEmailReplyToAddr="administrator@isclondon.com">
10    <FarmAccount AccountName="isclondon\spfarm" AccountPassword="password" />
11    <CentralAdmin Port="2010" AuthProvider="NTLM">
12        <Servers>
13            <Server Name="SP01" />
14            <Server Name="SP02" />
15            <Server Name="SP03" />
16        </Servers>
17    </CentralAdmin>
18    <Services>
19        <WebApplicationService>
20            <Servers>
21                <Server Name="SP01" />
22                <Server Name="SP02" />
23            </Servers>
24        </WebApplicationService>
25        <WorkflowTimerService>
26            <Servers>
27                <Server Name="SP01" />
28                <Server Name="SP02" />
29            </Servers>
30        </WorkflowTimerService>
31        <IncomingEmailService>
32            <Servers>
33                <Server Name="SP01" />
34                <Server Name="SP02" />
35            </Servers>
36        </IncomingEmailService>
37        <TraceService>
38            <Account AccountName="isclondon\sptrace" AccountPassword="password" />
39        </TraceService>
40    </Services>
41 </Farm>

Note that in this XML we’ve embedded the passwords used – I don’t actually recommend doing this, but if you do you should make sure the files are properly secured and, ideally, remove the password after provisioning is complete (we embedded the passwords so that we wouldn’t be continually prompted for credentials during the build as this would make it very difficult to talk through our presentation).

Before I show the contents of the Build-SPFarm.ps1 script I just wanted to point out one more design consideration which you may have clued in on by looking at the ConfigureServer.ps1 script. With rare exceptions (the ConfigureServer.ps1 script being one of them), I always make my scripts so that running the script will actually do nothing other than load a function in memory which must then be called in order to perform any action. This is to prevent someone from accidentally causing a script to execute (for example, you intend to edit the file so you double-click it and rather than open in an editor it opens in the console and is executed). The only reason the ConfigureServer.ps1 script does not work this way is because I wanted to show that you can pass parameters into a script file.

With that out of the way, let’s look at the Build-SPFarm.ps1 script:

  1function Join-SPFarm ([string]$settings = $(throw "-settings is required")) {
  2    Build-SPFarm $true $settings
  3}
  4
  5function New-SPFarm ([string]$settings = $(throw "-settings is required")) {
  6    Build-SPFarm $false $settings
  7}
  8
  9function Build-SPFarm ([bool]$connectToExisting = $false, [string]$settings = $(throw "-settings is required")) {   
 10    [xml]$config = Get-Content $settings
 11
 12    if ([string]::IsNullOrEmpty($config.Farm.FarmAccount.AccountPassword)) {
 13        $farmAcct = Get-Credential $config.Farm.FarmAccount.AccountName
 14    } else {
 15        $farmAcct = New-Object System.Management.Automation.PSCredential $config.Farm.FarmAccount.AccountName, (ConvertTo-SecureString $config.Farm.FarmAccount.AccountPassword -AsPlainText -force)
 16    }
 17
 18    $configDb = $config.Farm.ConfigDB
 19    $contentDB = $config.Farm.AdminContentDb
 20    $server = $config.Farm.DatabaseServer
 21    if ($config.Farm.Passphrase.Length -gt 0) {
 22        $passphrase = (ConvertTo-SecureString $config.Farm.Passphrase -AsPlainText -force)
 23    } else {
 24        Write-Warning "Using the Farm Admin's password for a passphrase"
 25        $passphrase = $farmAcct.Password
 26    }
 27    
 28    #Only build the farm if we don't currently have a farm created
 29    if ([Microsoft.SharePoint.Administration.SPFarm]::Local -eq $null) {
 30        psconfig -cmd upgrade -inplace b2b
 31        
 32        if ($connectToExisting) {
 33            #Connecting to farm
 34            Write-Host "Connecting to Farm..."
 35            Connect-SPConfigurationDatabase -DatabaseName $configDb -DatabaseServer $server -Passphrase $passphrase
 36        } else {
 37            #Creating new farm
 38            Write-Host "Creating Farm..."
 39            New-SPConfigurationDatabase `
 40                -DatabaseName $configDb `
 41                -DatabaseServer $server `
 42                -AdministrationContentDatabaseName $contentDB `
 43                -Passphrase $passphrase `
 44                -FarmCredentials $farmAcct
 45            
 46            if (![string]::IsNullOrEmpty($config.Farm.ConfigDBFailoverDatabaseServer)) {
 47                Set-FailoverDatabase $configDb $config.Farm.ConfigDBFailoverDatabaseServer
 48            }
 49            if (![string]::IsNullOrEmpty($config.Farm.AdminContentDBFailoverDatabaseServer)) {
 50                Set-FailoverDatabase $contentDB $config.Farm.AdminContentDBFailoverDatabaseServer
 51            }
 52        }
 53        #Verifying farm creation
 54        $spfarm = Get-SPFarm -ErrorAction SilentlyContinue
 55        if ($spfarm -eq $null) {
 56            throw "Unable to verify farm creation."
 57        }
 58
 59        #ACLing SharePoint Resources
 60        Write-Host "Calling Initialize-SPResourceSecurity..."
 61        Initialize-SPResourceSecurity
 62
 63        #Installing Services
 64        Write-Host "Calling Install-SPService..."
 65        Install-SPService
 66
 67        #Installing Features
 68        Write-Host "Calling Install-SPFeature..."
 69        Install-SPFeature -AllExistingFeatures
 70        
 71        Remove-PsSnapin Microsoft.SharePoint.PowerShell
 72        Add-PsSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
 73    } else {
 74        Write-Warning "Farm exists. Skipping creation."
 75    }
 76    
 77    if ((Get-Service sptimerv4).Status -ne "Running") {
 78        Write-Host "Starting SPTimerV4 Service"
 79        Start-Service sptimerv4
 80    }
 81
 82    $scaConfig = $config.Farm.CentralAdmin
 83    $installSCA = (($scaConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
 84    if ($installSCA) {
 85        $auth = $scaConfig.AuthProvider
 86        $port = $scaConfig.Port
 87        $url = "http://$($env:computername):$port"
 88        $sca = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($url)
 89        if ($installSCA -and $sca -eq $null) {
 90            #Provisioning Central Administration
 91            Write-Host "Creating Central Admin Site at $url..."
 92            New-SPCentralAdministration -Port $port -WindowsAuthProvider $auth
 93
 94            #Installing Help
 95            Write-Host "Calling Install-SPHelpCollection..."
 96            Install-SPHelpCollection -All
 97
 98            #Installing Application Content
 99            Write-Host "Calling Install-SPApplicationContent..."
100            Install-SPApplicationContent
101        }
102    }
103    
104    if (!$connectToExisting) {
105        $server = $config.Farm.OutgoingEmailServer
106        $from = $config.Farm.OutgoingEmailFromAddr
107        $replyTo = $config.Farm.OutgoingEmailReplyToAddr
108        $charSet = 65001
109        $wa = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]::Local
110        if ($wa -ne $null) {
111            $wa.UpdateMailSettings($server, $from, $replyTo, $charSet)
112        }
113    }
114    
115    #Stop or Start key service instances
116    $startWebAppSvc = (($config.Farm.Services.WebApplicationService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
117    Set-ServiceInstanceState "Microsoft SharePoint Foundation Web Application" $startWebAppSvc
118
119    $startWorkflowSvc = (($config.Farm.Services.WorkflowTimerService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
120    Set-ServiceInstanceState "Microsoft SharePoint Foundation Workflow Timer Service" $startWorkflowSvc
121
122    $startIncomingEmailSvc = (($config.Farm.Services.IncomingEmailService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
123    Set-ServiceInstanceState "Microsoft SharePoint Foundation Incoming E-Mail" $startIncomingEmailSvc
124    
125    
126    #Set SPTraceV4 to run as domain account
127    $accountNode = $config.Farm.Services.TraceService.Account
128    if ($accountNode -ne $null -and ![string]::IsNullOrEmpty($accountNode.AccountName)) {
129        if ([string]::IsNullOrEmpty($accountNode.AccountPassword)) {
130            $svcAccount = Get-Credential $accountNode.AccountName
131        } else {
132            $svcAccount = New-Object System.Management.Automation.PSCredential $accountNode.AccountName, (ConvertTo-SecureString $accountNode.AccountPassword -AsPlainText -force)
133        }
134        $user = Get-SPManagedAccount -Identity $svcAccount.Username -ErrorAction SilentlyContinue
135        if ($user -eq $null) {
136            $user = New-SPManagedAccount -Credential $svcAccount
137        }
138        $tracingSvc = (Get-SPFarm).Services | ? {$_.Name -eq "SPTraceV4"}
139        if ($tracingSvc.ProcessIdentity.Username -ne $user.Username) {
140            $tracingSvc.ProcessIdentity.ManagedAccount = $user
141            $tracingSvc.ProcessIdentity.CurrentIdentityType = "SpecificUser"
142            $tracingSvc.ProcessIdentity.Update()
143            $tracingSvc.ProcessIdentity.Deploy()
144        }
145        Write-Host "Adding `"$($user.Username)`" to Performance Log Users group..."
146        $p = Start-Process -PassThru -FilePath "c:\windows\system32\net.exe" -ArgumentList "localgroup `"Performance Log Users`" `"$($user.Username)`" /add" -Wait -NoNewWindow
147        if ($p.ExitCode -ne 0) { 
148            Write-Warning "Unable to add `"$($user.Username)`" to Performance Log Users group!" 
149        } else { 
150            Restart-Service SPTraceV4
151            iisreset
152        }
153    }
154
155} 
156function Set-ServiceInstanceState($typeName, $enable) {
157    $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq $typeName}
158    if ($svc -eq $null) {
159        throw "Unable to retrieve $typeName Service Instance."
160    }
161    if ($svc.Status -ne "Online" -and $enable) {
162        Write-Host "Starting $typeName service instance..."
163        $svc | Start-SPServiceInstance
164    }
165    if ($svc.Status -eq "Online" -and !$enable) {
166        Write-Host "Stopping $typeName service instance..."
167        $svc | Stop-SPServiceInstance -Confirm:$false
168    }
169}
170
171function Set-FailoverDatabase($databaseName, [string]$failoverDbServer) {
172    if (![string]::IsNullOrEmpty($failoverDbServer)) {
173        $db = Get-SPDatabase | where {$_.Name -eq $databaseName}
174        if ($db -eq $null) { 
175            throw "Unable to retrieve the database to set the failover server: $databaseName" 
176        }
177        if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) {
178            try {
179                Write-Host "Adding failover database instance..."
180                $db.AddFailoverServiceInstance($failoverDbServer)
181                $db.Update()
182            } catch {
183                Write-Warning "Unable to set failover database server. $_"
184            }
185        }
186    }
187}

I honestly don’t have the time to explain everything that this script is doing but suffice it to say it is doing a bit more than just creating or connecting to the Farm. One thing in particular is that I chose to put the enabling and disabling of some of the simple services (Web Application, Workflow, and Incoming Email) in this script rather than in the script(s) that provision the Service Applications (this was really just a decision of convenience for me and in my personal scripts this stuff is stored elsewhere).

One other thing I wanted to highlight was this line within the script: “psconfig -cmd upgrade -inplace b2b”. The reason this is there is because we installed the Office Web Applications which incorrectly sets a registry key that indicates that an upgrade is required. We can either fix the registry key or simply open and then close the configuration wizard on each server (without actually letting the wizard run) or we can just run this command on each server just prior to calling the New-SPConfigurationDatabase or Connect-SPConfigurationDatabase cmdlets. The command will actually generate an error but you can safely ignore it – all we care is that it fixes the registry key for us so that we can move on to the actual provisioning.

Provision-WebApplications.ps1

The next script I demoed during the first session was the Provision-WebApplications.ps1 script. This script, aside from the obvious of provisioning Web Applications, also provisioned any initial Site Collections and managed paths that we needed. The following illustration details the three Web Applications we created along with their Application Pool association and other relevant details:

Now let’s take a look at the XML file that I used to provide all the configuration information:

 1<Farm>
 2  <WebApplications>
 3    <WebApplication Name="SharePoint MySites" HostHeader="my.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\mysites" Ssl="false" LoadBalancedUrl="http://my.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="true" RequireContactForSsc="false" AuthenticationMode="Classic" EnableWindowsAuthentication="false" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
 4      <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" />
 5      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
 6      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
 7      <ManagedPaths>
 8        <ManagedPath Explicit="false" RelativeURL="/personal" />
 9      </ManagedPaths>
10      <UserPolicies>
11        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
12          <Permission Name="Full Control" />
13        </UserPolicy>
14      </UserPolicies>
15      <ProxyGroup Name="Default" />
16      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="true" AllowMasterPageEditing="true" ShowURLStructure="true" />
17      <ContentDatabases>
18        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_MySites" MaxSiteCount="15000" WarningSiteCount="9000">
19          <SiteCollections>
20            <SiteCollection Name="My Sites" Description="My Sites Host Site Collection" Url="http://my.isclondon.local" Template="SPSMSITEHOST#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
21            </SiteCollection>
22          </SiteCollections>
23        </ContentDatabase>
24      </ContentDatabases>
25    </WebApplication>
26    <WebApplication Name="SharePoint Team Sites" HostHeader="team.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\teams" Ssl="false" LoadBalancedUrl="http://team.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="false" RequireContactForSsc="false" AuthenticationMode="Classic" EnableWindowsAuthentication="false" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
27      <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" />
28      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
29      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
30      <ManagedPaths>
31      </ManagedPaths>
32      <UserPolicies>
33        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
34          <Permission Name="Full Control" />
35        </UserPolicy>
36      </UserPolicies>
37      <ProxyGroup Name="Default" />
38      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" />
39      <ContentDatabases>
40        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_TeamSites" MaxSiteCount="15000" WarningSiteCount="9000">
41          <SiteCollections>
42            <SiteCollection Name="Team Sites" Url="http://team.isclondon.local" Template="CMSPUBLISHING#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
43            </SiteCollection>
44          </SiteCollections>
45        </ContentDatabase>
46      </ContentDatabases>
47      <Features />
48    </WebApplication>
49    <WebApplication Name="SharePoint Intranet" HostHeader="intranet.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\intranet" Ssl="false" LoadBalancedUrl="http://intranet.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="false" RequireContactForSsc="false" AuthenticationMode="Claims" EnableWindowsAuthentication="true" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
50      <ApplicationPool Name="SharePoint Content" AccountName="isclondon\SPContent" AccountPassword="password" />
51      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
52      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
53      <ManagedPaths>
54        <ManagedPath Explicit="true" RelativeURL="/CTHub" />
55      </ManagedPaths>
56      <UserPolicies>
57        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
58          <Permission Name="Full Control" />
59        </UserPolicy>
60      </UserPolicies>
61      <ProxyGroup Name="Default" />
62      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" />
63      <ContentDatabases>
64        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_Intranet" MaxSiteCount="15000" WarningSiteCount="9000">
65          <SiteCollections>
66            <SiteCollection Name="Intranet" Url="http://intranet.isclondon.local" Template="STS#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
67            </SiteCollection>
68            <SiteCollection Name="Content Type Hub" Url="http://intranet.isclondon.local/CTHub" Template="STS#1" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
69            </SiteCollection>
70          </SiteCollections>
71        </ContentDatabase>
72      </ContentDatabases>
73      <Features />
74    </WebApplication>    
75  </WebApplications>
76</Farm>

At first glance the XML file, due to it’s size, may seem a bit overwhelming but hopefully as you go through it you’ll see that it really is a pretty natural and somewhat obvious structure. (Think about the possibilities if you had a great UI to manage this XML structure – wink, wink).

Okay, now for the code – and there’s a fair bit of it:

  1function Provision-WebApplications() {
  2    <#
  3    .Synopsis
  4        Creates the web applications.
  5    .Description
  6        Creates the web applications.
  7    .Example
  8        PS C:\> . .\Provision-WebApplications.ps1
  9        PS C:\> Provision-WebApplication -SettingsFile c:\WebApplications.xml
 10    .Example
 11        PS C:\> . .\Provision-WebApplications.ps1
 12        PS C:\> Provision-WebApplications -ConfigXml ((Get-Content c:\farmconfiguration.xml))
 13    .Parameter SettingsFile
 14        The path to an XML file.
 15    #>
 16    [CmdletBinding(DefaultParameterSetName="FilePath")]
 17    param (
 18        [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlElement")]
 19        [ValidateNotNull()]
 20        [System.Xml.XmlElement]$ConfigElement,
 21
 22        [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlDocument")]
 23        [ValidateNotNull()]
 24        [xml]$ConfigXml,
 25
 26        [Parameter(Mandatory=$true, Position=0, ParameterSetName="FilePath")]
 27        [ValidateNotNullOrEmpty()]
 28        [string]$SettingsFile
 29    )
 30    switch ($PsCmdlet.ParameterSetName) { 
 31        "XmlDocument" { 
 32            if ($ConfigXml.Farm.WebApplications.WebApplication -ne $null) {
 33                foreach ($appConfig in $ConfigXml.Farm.WebApplications.WebApplication) {
 34                    Provision-WebApplications -ConfigElement $appConfig
 35                }
 36                return
 37            }
 38        }
 39        "FilePath" {
 40            Provision-WebApplications -ConfigXml ([xml](Get-Content $SettingsFile))
 41            return
 42        }
 43    }
 44    $config = $ConfigElement
 45
 46    if ($config -eq $null) {
 47        Write-Warning "No web application defined. Skipping."
 48        return
 49    }
 50
 51    
 52    $webApp = Get-SPWebApplication -Identity $config.Name -ErrorAction SilentlyContinue
 53    
 54    if ($webApp -eq $null) {
 55        $allowAnon = [bool]::Parse($config.AllowAnonymous.ToString())
 56        $ssl = [bool]::Parse($config.Ssl.ToString())
 57
 58        $db = $null
 59        if ($config.ContentDatabases -eq $null) {
 60            throw "A content database configuration setting could not be found for `"$($config.Name)`"."
 61        }
 62        if ($config.ContentDatabases.ChildNodes.Count -gt 1) {
 63            $db = $config.ContentDatabases.ContentDatabase | where {$_.Default -eq "true"}
 64            if ($db -is [array]) {
 65                Write-Warning "Multiple content databases set as default for `"$($config.Name)`" (using first)"
 66                $db = $db[0]
 67            }
 68        } else {
 69            $db = $config.ContentDatabases.ContentDatabase
 70        }
 71        if ($db -eq $null) {
 72            throw "A content database configuration setting could not be found for `"$($config.Name)`"."
 73        }
 74        
 75        $poolName = $config.ApplicationPool.Name
 76        $poolAcctName = $config.ApplicationPool.AccountName
 77        $poolAcctPwd = $config.ApplicationPool.AccountPassword
 78        $poolAcct = $null
 79        
 80        #Check for existing Application Pool
 81        $pools = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools
 82        if (($pools | ? {$_.Name -eq $poolName}) -eq $null) {
 83            $poolAcct = Get-SPManagedAccount $poolAcctName -ErrorAction SilentlyContinue
 84            if ($poolAcct -eq $null) {
 85                $securePwd = ConvertTo-SecureString $poolAcctPwd -AsPlainText -force
 86                $cred = New-Object System.Management.Automation.PSCredential $poolAcctName, $securePwd
 87                $poolAcct = New-SPManagedAccount -Credential $cred
 88            }
 89        }
 90        
 91        $loadBalancedUrl = $config.LoadBalancedUrl
 92        $port = $config.Port
 93        if (![string]::IsNullOrEmpty($loadBalancedUrl)) {
 94            $loadBalancedUri = New-Object System.Uri $config.LoadBalancedUrl
 95            $port = $loadBalancedUri.Port
 96            $loadBalancedUrl = "$($loadBalancedUri.Scheme)://$($loadBalancedUri.Host)/"
 97        } else {
 98            if (![string]::IsNullOrEmpty($config.HostHeader)) {
 99                $loadBalancedUrl = "http://$($config.HostHeader)"
100            }
101        }
102        if ([string]::IsNullOrEmpty($port)) { $port = "80" }
103
104        if ($config.AuthenticationMode -eq "Claims") {
105            $authProviders = @()
106            $enableWindowsAuthentication = $false
107            if (![string]::IsNullOrEmpty($config.EnableWindowsAuthentication)) { $enableWindowsAuthentication = [bool]::Parse($config.EnableWindowsAuthentication) }
108            $enableFormsBasedAuthentication = $false
109            if (![string]::IsNullOrEmpty($config.EnableFormsBasedAuthentication)) { $enableFormsBasedAuthentication = [bool]::Parse($config.EnableFormsBasedAuthentication) }
110
111            if ($enableWindowsAuthentication) {
112                $enableBasicAuthentication = $false
113                if (![string]::IsNullOrEmpty($config.EnableBasicAuthentication)) { $enableBasicAuthentication = [bool]::Parse($config.EnableBasicAuthentication) }
114                $disableKerberos = $config.AuthenticationMethod -eq "NTLM"
115                $authProviders += New-SPAuthenticationProvider `
116                    -UseWindowsIntegratedAuthentication:$enableWindowsAuthentication `
117                    -DisableKerberos:$disableKerberos `
118                    -UseBasicAuthentication:$enableBasicAuthentication `
119                    -AllowAnonymous:$allowAnon
120            }
121            if ($enableFormsBasedAuthentication) {
122                $authProviders += New-SPAuthenticationProvider `
123                    -ASPNETMembershipProvider $config.ASPNETMembershipProviderName `
124                    -ASPNETRoleProviderName $config.ASPNETRoleManagerName 
125            }
126            Write-Host "Creating Web Application: `"$($config.Name)`""
127            $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl `
128                -AllowAnonymousAccess:$allowAnon `
129                -ApplicationPool $poolName `
130                -ApplicationPoolAccount $poolAcct `
131                -Name $config.Name `
132                -AuthenticationProvider $authProviders `
133                -DatabaseServer $db.Server `
134                -DatabaseName $db.Name `
135                -HostHeader $config.HostHeader `
136                -Path $config.Path `
137                -Port $port `
138                -Url $loadBalancedUrl
139        } else {
140            $authMethod = "NTLM"
141            if (![string]::IsNullOrEmpty($config.AuthenticationMethod)) { $authMethod = $config.AuthenticationMethod }
142
143            Write-Host "Creating Web Application: `"$($config.Name)`""
144            $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl `
145                -AllowAnonymousAccess:$allowAnon `
146                -ApplicationPool $poolName `
147                -ApplicationPoolAccount $poolAcct `
148                -Name $config.Name `
149                -AuthenticationMethod $authMethod `
150                -DatabaseServer $db.Server `
151                -DatabaseName $db.Name `
152                -HostHeader $config.HostHeader `
153                -Path $config.Path `
154                -Port $port `
155                -Url $loadBalancedUrl
156        }
157
158        Set-SPWebApplication -Identity $webApp -DefaultTimeZone $config.DefaultTimeZone
159
160        #Re-get the web app to avoid update conflicts
161        $webApp = Get-SPWebApplication -Identity  $webApp
162        $webApp.SelfServiceSiteCreationEnabled = [bool]::Parse($config.SelfServiceSiteCreation)
163        $webApp.RequireContactForSelfServiceSiteCreation = [bool]::Parse($config.RequireContactForSsc)
164        if ($config.ProxyGroup -ne $null -and ![string]::IsNullOrEmpty($config.ProxyGroup.Name)) {
165            $proxyGroupName = $config.ProxyGroup.Name
166            if ($proxyGroupName -ne $null) {
167                $webApp.ServiceApplicationProxyGroup = Get-ProxyGroup $proxyGroupName $true
168            }
169        }
170        $webApp.Update()
171        Write-Host """$($config.Name)"" successfully created."
172    } else {
173        #We got the web app so return it and don't attempt to recreate
174        Write-Host """$($config.Name)"" already exists, skipping creation."
175    }
176    $setObjectCacheAccounts = $true
177    if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount) -and ![string]::IsNullOrEmpty($config.PortalSuperReaderAccount)) {
178        if ($config.PortalSuperUserAccount.AccountName -eq $config.PortalSuperReaderAccount.AccountName) {
179            Write-Warning "The Portal Super User Account and Portal Super Reader Account must not be the same account as this will result in a security hole. The accounts will not be set."
180            $setObjectCacheAccounts = $false
181        }
182    }
183    $claimsPrefix = ""
184    if ($config.AuthenticationMode -eq "Claims") {
185        $claimsPrefix = "i:0#.w|"
186    }
187    if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount.AccountName) -and $setObjectCacheAccounts) {
188        $webApp.Properties["portalsuperuseraccount"] = "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)"
189        Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)" $config.PortalSuperUserAccount.AccountName "Full Control"
190    }
191    if (![string]::IsNullOrEmpty($config.PortalSuperReaderAccount.AccountName) -and $setObjectCacheAccounts) {
192        $webApp.Properties["portalsuperreaderaccount"] = "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)"
193        Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)" $config.PortalSuperReaderAccount.AccountName "Full Read"
194    }
195
196
197    if ($config.UserPolicies) {
198        Write-Host "Creating user policies..."
199        [bool]$updateRequired = $false
200        foreach ($userPolicyConfig in $config.UserPolicies.UserPolicy) {
201            if ($userPolicyConfig -eq $null) { continue }
202
203            [string]$zoneName = $userPolicyConfig.Zone
204            [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies
205            if ($zoneName.ToLower() -ne "all") {
206                $policies = $webApp.ZonePolicies($zoneName)
207            }
208            Write-Host "Adding user policy for: $claimsPrefix$($userPolicyConfig.UserLogin)..."
209            [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add("$claimsPrefix$($userPolicyConfig.UserLogin)", $userPolicyConfig.UserDisplayName)
210            foreach ($permConfig in $userPolicyConfig.Permission) {
211                [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $permConfig.Name}
212                if ($policyRole -ne $null) {
213                    Write-Host "Adding policy role: $($permConfig.Name)..."
214                    $policy.PolicyRoleBindings.Add($policyRole)
215                }
216            }
217            $updateRequired = $true
218        }
219        if ($updateRequired) {
220            $webApp.Update()
221        }
222    }
223    
224    
225    if ($config.ManagedPaths) {
226        Write-Host "Creating managed paths..."
227        foreach ($mpConfig in $config.ManagedPaths.ManagedPath) {
228            if ($mpConfig -eq $null) { continue }
229
230            $path = Get-SPManagedPath -Identity $mpConfig.RelativeURL `
231                -WebApplication $webApp -ErrorAction SilentlyContinue
232            if ($path -eq $null) {
233                Write-Host "Creating ""$($mpConfig.RelativeURL)""..."
234                New-SPManagedPath -RelativeURL $mpConfig.RelativeURL `
235                    -Explicit:([bool]::Parse($mpConfig.Explicit)) -WebApplication $webApp
236            } else {
237                Write-Host "Managed path already exists: ""$($mpConfig.RelativeURL)""."
238            }
239        }
240    }
241    
242    if ($config.SPDesigner) {
243        Write-Host "Configuring SharePoint Designer Settings..."
244        sleep 5
245        #Get the web app again to avoid an update conflict
246        $webApp = Get-SPWebApplication $webApp
247        $webApp | Set-SPDesignerSettings `
248            -AllowDesigner ([bool]::Parse($config.SPDesigner.AllowDesigner)) `
249            -AllowRevertFromTemplate ([bool]::Parse($config.SPDesigner.AllowRevertFromTemplate)) `
250            -AllowMasterPageEditing ([bool]::Parse($config.SPDesigner.AllowMasterPageEditing)) `
251            -ShowURLStructure ([bool]::Parse($config.SPDesigner.ShowURLStructure))
252        $webApp.Update()
253    }
254
255    foreach ($dbConfig in $config.ContentDatabases.ContentDatabase) {
256        $db = Get-SPContentDatabase $dbConfig.Name -ErrorAction SilentlyContinue
257        if ($db -eq $null) { 
258            $db = New-SPContentDatabase -Name $dbConfig.Name `
259                -WebApplication $webApp `
260                -DatabaseServer $dbConfig.Server `
261                -MaxSiteCount $dbConfig.MaxSiteCount `
262                -WarningSiteCount $dbConfig.WarningSiteCount
263        }
264        $failoverDbServer = $dbConfig.FailoverDatabaseServer
265        if (![string]::IsNullOrEmpty($failoverDbServer)) {
266            if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) {
267                try {
268                    Write-Host "Adding failover database instance..."
269                    $db.AddFailoverServiceInstance($failoverDbServer)
270                    $db.Update()
271                } catch {
272                    Write-Warning "Unable to set failover database server. $_"
273                }
274            }
275        }
276        foreach ($siteConfig in $dbConfig.SiteCollections.SiteCollection) {
277            $site = Get-SPSite $siteConfig.Url -ErrorAction SilentlyContinue
278            if ($site -ne $null) {
279                Write-Host "Site Collection $($siteConfig.Url) already exists. Skipping."
280                $site.Dispose()
281                continue
282            }
283            
284            $lcid = $siteConfig.LCID
285            if ([string]::IsNullOrEmpty($lcid)) { $lcid = [Microsoft.SharePoint.SPRegionalSettings]::GlobalServerLanguage.LCID }
286
287            $optionalParams = @{}
288            if (![string]::IsNullOrEmpty($siteConfig.SecondaryLogin)) { 
289                $optionalParams += @{"SecondaryOwnerAlias"=$siteConfig.SecondaryLogin} }
290            if (![string]::IsNullOrEmpty($siteConfig.SecondaryEmail)) { 
291                $optionalParams += @{"SecondaryEmail"=$siteConfig.SecondaryEmail} }
292            if (![string]::IsNullOrEmpty($siteConfig.OwnerEmail)) { 
293                $optionalParams += @{"OwnerEmail"=$siteConfig.OwnerEmail} }
294
295            Write-Host "Creating Site Collection $($siteConfig.Url)..."
296            $site = New-SPSite `
297                -Url $siteConfig.Url `
298                -Description $siteConfig.Description `
299                -Language $lcid `
300                -Name $siteConfig.Name `
301                -OwnerAlias $siteConfig.OwnerLogin `
302                -Template $siteConfig.Template `
303                -ContentDatabase $db `
304                -ErrorAction Continue @optionalParams
305
306            if ($site -eq $null) { 
307                Write-Warning "Site collection was not created!"
308            } else {
309                Write-Host "Site collection successfully created: $($siteConfig.Url)"
310                Write-Host "Setting default associated security groups..."
311                sleep 5
312                $refreshedSite = $site | Get-SPSite
313                $secondaryLogin = $siteConfig.SecondaryLogin
314                if (![string]::IsNullOrEmpty($secondaryLogin)) { $secondaryLogin = "$claimsPrefix$secondaryLogin" }
315                $refreshedSite.RootWeb.CreateDefaultAssociatedGroups("$claimsPrefix$($siteConfig.OwnerLogin)", $secondaryLogin, $siteConfig.Name)
316                $refreshedSite.Dispose()
317                $site.Dispose()
318            }
319        }
320    }
321}
322
323function Get-ProxyGroup([string]$name, [bool]$createIfMissing = $true) {
324    if ($name -eq "Default" -or [string]::IsNullOrEmpty($name)) { return [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default }
325    
326    $proxyGroup = Get-SPServiceApplicationProxyGroup $name -ErrorAction SilentlyContinue -ErrorVariable err
327    if ($err -and !$createIfMissing) { throw $err }
328    if ($proxyGroup -eq $null) {
329        $proxyGroup = New-SPServiceApplicationProxyGroup -Name $name
330    }
331    return $proxyGroup
332}
333
334function Set-WebAppUserPolicy($webApp, $userName, $userDisplayName, $perm) {
335    [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies
336    [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add($userName, $userDisplayName)
337    [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $perm}
338    if ($policyRole -ne $null) {
339        $policy.PolicyRoleBindings.Add($policyRole)
340    }
341    $webApp.Update()
342}

I think one of the more interesting things to point out about this script is that if you’re provisioning a Web Application for Claims versus Classic mode there are differences in how the New-SPWebApplication cmdlet must be called and, when using Claims, there’s more work that needs to be done to make sure the authentication providers are set properly. Outside of that I really think the script is pretty straightforward, if not a bit lengthy. One little hack/cheat that I wanted to point out is my use of the claims prefix, “i:0#.w|” – most of the time my little hack to get this prefix will be more than sufficient but technically, if I wanted my script to be truly generic, I should have done this to get the encoded username (as opposed to just assuming the prefix value and prepending it to the username):

1$claim = [Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager]::CreateUserClaim("isclondon\spcacheuser", "Windows")
2[Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager]::Local.EncodeClaim($claim)

SecureStoreServices.ps1

The last script that I walked through during the sessions was the SecureStoreServices.ps1 script. (It would have obviously been impossible to walk through all the Service Application related scripts during a single 1 hour presentation so the presentation itself focused on the general patterns, and lack thereof, and we then walked through this one script as a simple example which ties it all together).

Like all the other scripts this one was driven by another XML file:

 1<Farm>
 2    <Services>
 3        <SecureStoreServices>
 4          <Servers>
 5            <Server Name="SP03" />
 6            <Server Name="SP04" />
 7          </Servers>
 8          <SecureStoreServiceApplications>
 9            <SecureStoreServiceApplication 
10                Name="ISC Secure Store Service" 
11                DatabaseName="ISC_SecureStore" 
12                DatabaseServer="ISCSharePoint1" 
13                FailoverDatabaseServer=""
14                AuditingEnabled="true" 
15                AuditLogMaxSize="30" 
16                Sharing="false" 
17                KeyPassphrase="p@ssw0rd" 
18                Partitioned="false">
19              <ApplicationPool Name="SharePoint Services App Pool" 
20                      AccountName="isclondon\SPServices" 
21                    AccountPassword="password" />
22              <Proxy Name="ISC Secure Store Service">
23                <ProxyGroup Name="Default" />
24              </Proxy>
25            </SecureStoreServiceApplication>
26          </SecureStoreServiceApplications>
27        </SecureStoreServices>    
28    </Services>
29</Farm>

Hopefully you’ve picked up on the fact that the XML node structure is such that you could easily stitch all the XML files together into a single XML file which would be easier to maintain (think search and replace); I’ve intentionally structured my scripts and XML files for this conference so that they are all essentially standalone which made walking through the code in a presentation much easier as there was a lot less context switching. For my personal scripts I use a series of shared library scripts where I’ve put common functions and all my XML is contained in one XML file (which I manage using a custom WPF application that I’ve developed).

Time for the actual script:

  1function Start-SecureStoreServices {
  2    [CmdletBinding()]
  3    param 
  4    (  
  5        [Parameter(Mandatory=$true, Position=0)]
  6        [ValidateNotNullOrEmpty()]
  7        [string]$SettingsFile
  8    )
  9    
 10    [xml]$config = Get-Content $settingsFile
 11
 12    $install = (($config.Farm.Services.SecureStoreServices.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
 13    if (!$install) { 
 14        Write-Host "Machine not specified in Servers element, service will not be installed on this server."
 15        return
 16    }
 17    
 18    $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"}
 19    if ($svc -eq $null) {
 20        throw "Unable to retrieve Service Instance."
 21    }
 22    if ($svc.Status -ne "Online") {
 23        Write-Host "Starting service instance..."
 24        $svc | Start-SPServiceInstance
 25    
 26        #Make sure the service is online before attempting to add a svc app.
 27        while ($true) {
 28            Start-Sleep 2
 29            $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"}
 30            if ($svc.Status -eq "Online") { break }
 31        }
 32    }
 33
 34
 35    foreach ($appConfig in $config.Farm.Services.SecureStoreServices.SecureStoreServiceApplications.SecureStoreServiceApplication) {
 36        $app = Get-SPServiceApplication | where {$_.Name -eq $appConfig.Name}
 37        $updateKey = $false
 38        if ($app -eq $null) {
 39            $poolName = $appConfig.ApplicationPool.Name
 40            $identity = $appConfig.ApplicationPool.AccountName
 41            $password = $appConfig.ApplicationPool.AccountPassword
 42            $pool = Get-ServicePool $poolName $identity $password
 43
 44            Write-Host "Creating secure store service application..."
 45            $app = New-SPSecureStoreServiceApplication -Name $appConfig.Name `
 46                -ApplicationPool $pool `
 47                -DatabaseServer $appConfig.DatabaseServer `
 48                -DatabaseName $appConfig.DatabaseName `
 49                -AuditingEnabled:([bool]::Parse($appConfig.AuditingEnabled)) `
 50                -AuditLogMaxSize $appConfig.AuditLogMaxSize `
 51                -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
 52                -PartitionMode:([bool]::Parse($appConfig.Partitioned)) `
 53                -Sharing:([bool]::Parse($appConfig.Sharing))
 54            $updateKey = $true
 55        } else {
 56            Write-Host "Secure store service application already exists, skipping creation."
 57        }
 58
 59        $proxy = Get-SPServiceApplicationProxy | where {$_.Name -eq $appConfig.Proxy.Name}
 60        if ($proxy -eq $null) {
 61            Write-Host "Creating secure store service application proxy..."
 62            $proxy = New-SPSecureStoreServiceApplicationProxy `
 63                -Name $appConfig.Proxy.Name `
 64                -ServiceApplication $app `
 65                -DefaultProxyGroup:$false
 66        } else {
 67            Write-Host "Secure store service application proxy already exists, skipping creation."
 68        }
 69        $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
 70
 71        
 72        if ($updateKey) {
 73            Update-SPSecureStoreMasterKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase
 74            while ($true) {
 75                try {
 76                    Start-Sleep -Seconds 5
 77                    Update-SPSecureStoreApplicationServerKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase
 78                    break
 79                } catch { }
 80            }
 81        }
 82        
 83    }
 84}
 85
 86
 87
 88function Get-ServicePool($poolName, $identity, $password) {
 89    $pool = Get-SPServiceApplicationPool $poolName -ErrorAction SilentlyContinue
 90    if ($pool -eq $null) {
 91        if ($identity -ne $null) {
 92            $acct = Get-SPManagedAccount $identity -ErrorAction SilentlyContinue
 93        }
 94        if ($acct -eq $null) {
 95            if ([string]::IsNullOrEmpty($password)) {
 96                $cred = Get-Credential $identity
 97            } else {
 98                $cred = New-Object System.Management.Automation.PSCredential $identity, (ConvertTo-SecureString $password -AsPlainText -force)
 99            }
100            $acct = Get-SPManagedAccount $cred.UserName -ErrorAction SilentlyContinue
101            if ($acct -eq $null) {
102                $acct = New-SPManagedAccount $cred
103            }
104        }
105        Write-Host "Creating application pool..."
106        $pool = New-SPServiceApplicationPool  -Name $poolName -Account $acct
107    }
108    return $pool
109}
110
111function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject) {
112    begin {}
113    process {
114        if ($_ -eq $null -and $InputObject -ne $null) {
115            $InputObject | Set-ProxyGroupsMembership $groups
116            return
117        }
118        $proxy = $_
119        
120        #Clear any existing proxy group assignments
121        Get-SPServiceApplicationProxyGroup | where {$_.Proxies -contains $proxy} | ForEach-Object {
122            $proxyGroupName = $_.Name
123            if ([string]::IsNullOrEmpty($proxyGroupName)) { $proxyGroupName = "Default" }
124            $group = $null
125            [bool]$matchFound = $false
126            foreach ($g in $groups) {
127                $group = $g.Name
128                if ($group -eq $proxyGroupName) { 
129                    $matchFound = $true
130                    break 
131                }
132            }
133            if (!$matchFound) {
134                Write-Host "Removing ""$($proxy.DisplayName)"" from ""$proxyGroupName"""
135                $_ | Remove-SPServiceApplicationProxyGroupMember -Member $proxy -Confirm:$false -ErrorAction SilentlyContinue
136            }
137        }
138        
139        foreach ($g in $groups) {
140            $group = $g.Name
141
142            $pg = $null
143            if ($group -eq "Default" -or [string]::IsNullOrEmpty($group)) {
144                $pg = [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default
145            } else {
146                $pg = Get-SPServiceApplicationProxyGroup $group -ErrorAction SilentlyContinue -ErrorVariable err
147                if ($pg -eq $null) {
148                    $pg = New-SPServiceApplicationProxyGroup -Name $name
149                }
150            }
151            
152            $pg = $pg | where {$_.Proxies -notcontains $proxy}
153            if ($pg -ne $null) { 
154                Write-Host "Adding ""$($proxy.DisplayName)"" to ""$($pg.DisplayName)"""
155                $pg | Add-SPServiceApplicationProxyGroupMember -Member $proxy 
156            }
157        }
158    }
159    end {}
160}

Again, I’m not going to explain what’s going on in this script – my book covers it in detail.

Final Thoughts

So on that final note about my book I’d like to reiterate that everything I demoed during the conference and virtually everything that my scripts did to provision the conference Farm is detailed in great depth in my Automating Microsoft SharePoint 2010 Administration with Windows PowerShell 2.0 book (I don’t cover the Office Web Applications in the book). Not only that, but most of the scripts are also available for download as part of the book. Are all of them available? No. Why? Because I’m a self employed consultant who needs to be able to provide for my family and if I give away everything I ever produce then I’ll slowly be putting myself out of a job and my daughter can forget about going to college so please, please, please do not ask me to post the full scripts because it’s simply not going to happen – if you want the full scripts then hire me to build your Farm or talk to me about purchasing my SharePoint 2010 Farm Builder application. (I know during the conference people were tweeting and blogging that I would be providing all my scripts – I never said I would do this – I simply said that I would provide the scripts which I walked through during the session and this is what I have done in this post).

To those that attended my sessions I truly hope you got some value out of the content that Spence, Chan and I provided and I’d like to thank everyone (speakers, organizers, and attendees) for such a wonderful overall event and for making me feel so welcome so far from home. And of course a special thanks to Spence and Chan for all they did to help pull these sessions off.

-Gary