International SharePoint Conference 2012 Follow-up
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:
Enable-PSRemoting Enable-WSmanCredSSP -Role Server Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 1024 Register-PSSessionConfiguration -Name "SharePoint" -StartupScript "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1" -Force -ThreadOptions ReuseThread Set-PSSessionConfiguration -Name "SharePoint" -ShowSecurityDescriptorUI -Force
- 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.
- 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:
param( [switch]$Connect ) Write-Host "Script Start Time: $(Get-Date)" -ForegroundColor Green #Main Farm . .\FarmCreation\Build-SPFarm.ps1 $farmConfigFile = Resolve-Path .\FarmCreation\FarmConfigurations.xml try { if ($Connect) { Join-SPFarm $farmConfigFile } else { New-SPFarm $farmConfigFile } } catch { Write-Warning "Unable to build or join Farm!" $_ return } #Web Applications if (!$Connect) { #Only need to do this once so do it when we create the farm initially and not for each connection. try { . .\WebApplications\Provision-WebApplications.ps1 Provision-WebApplications (Resolve-Path .\WebApplications\WebApplications.xml) } catch { Write-Warning "Unable to create web applications!" $_ return } } #Services try { #State Service . .\ServiceApplications\StateServices\Start-StateServices.ps1 Start-StateServices (Resolve-Path .\ServiceApplications\StateServices\StateServices.xml) #Usage Service . .\ServiceApplications\UsageService\Start-UsageService.ps1 Start-UsageService (Resolve-Path .\ServiceApplications\UsageService\UsageService.xml) #Secure Store Services . .\ServiceApplications\SecureStoreServices\Start-SecureStoreServices.ps1 Start-SecureStoreServices (Resolve-Path .\ServiceApplications\SecureStoreServices\SecureStoreServices.xml) #Web Analytics Services . .\ServiceApplications\WebAnalyticsServices\Start-WebAnalyticsServices.ps1 Start-WebAnalyticsServices (Resolve-Path .\ServiceApplications\WebAnalyticsServices\WebAnalyticsServices.xml) #User Code Services . .\ServiceApplications\UserCodeServices\Start-UserCodeServices.ps1 Start-UserCodeServices (Resolve-Path .\ServiceApplications\UserCodeServices\UserCodeServices.xml) #BCS Services . .\ServiceApplications\BCSServices\Start-BCSServices.ps1 Start-BCSServices (Resolve-Path .\ServiceApplications\BCSServices\BCSServices.xml) #Excel Services . .\ServiceApplications\ExcelServices\Start-ExcelServices.ps1 Start-ExcelServices (Resolve-Path .\ServiceApplications\ExcelServices\ExcelServices.xml) #Word Automation Services . .\ServiceApplications\WordAutomationServices\Start-WordAutomationServices.ps1 Start-WordAutomationServices (Resolve-Path .\ServiceApplications\WordAutomationServices\WordAutomationServices.xml) #Metadata Services . .\ServiceApplications\MetadataServices\Start-MetadataServices.ps1 Start-MetadataServices (Resolve-Path .\ServiceApplications\MetadataServices\MetadataServices.xml) #C2WTS . .\ServiceApplications\ClaimsToWindowsTokenServices\Start-ClaimsToWindowsTokenServices.ps1 Start-ClaimsToWindowsTokenServices (Resolve-Path .\ServiceApplications\ClaimsToWindowsTokenServices\ClaimsToWindowsTokenServices.xml) #PowerPoint Services . .\ServiceApplications\PowerPointServices\Start-PowerPointServices.ps1 Start-PowerPointServices (Resolve-Path .\ServiceApplications\PowerPointServices\PowerPointServices.xml) #Word Viewing Services . .\ServiceApplications\WordViewingServices\Start-WordViewingServices.ps1 Start-WordViewingServices (Resolve-Path .\ServiceApplications\WordViewingServices\WordViewingServices.xml) #User Profile Services . .\ServiceApplications\UserProfileServices\Start-UserProfileServices.ps1 Start-UserProfileServices (Resolve-Path .\ServiceApplications\UserProfileServices\UserProfileServices.xml) #Enterprise Search Services . .\ServiceApplications\EnterpriseSearchServices\Start-EnterpriseSearchServices.ps1 Start-EnterpriseSearchServices (Resolve-Path .\ServiceApplications\EnterpriseSearchServices\EnterpriseSearchServices.xml) } catch { Write-Warning "Service Application creation failed!" $_ return } finally { Write-Host "Script End Time: $(Get-Date)" -ForegroundColor Green }
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):
$servers = @("SP01","SP02","SP04","SP05","SP06") $cred = Get-Credential "isclondon\spinstall" foreach ($server in $servers) { Write-Host "---------------------------------------------------------------" -ForegroundColor Green Write-Host "$(Get-Date): Connecting to server $server." -ForegroundColor Green Write-Host "---------------------------------------------------------------" -ForegroundColor Green $session = New-PSSession -ComputerName $server -Authentication CredSSP -Credential $cred -ConfigurationName "SharePoint" Invoke-Command -Session $session -ScriptBlock {Set-ExecutionPolicy Bypass -Scope Process} Invoke-Command -Session $session -ScriptBlock {cd \\sp03\c$\Scripts} Invoke-Command -Session $session -ScriptBlock {. .\ConfigureServer.ps1 -Connect} Remove-PSSession $session Write-Host "$(Get-Date): Finished for server $server" -ForegroundColor Green }
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:
<Farm ConfigDB="ISC_Config" ConfigDBFailoverDatabaseServer="" AdminContentDB="ISC_Content_CentralAdmin" AdminContentDBFailoverDatabaseServer="" DatabaseServer="ISCSharePoint1" Passphrase="p@ssw0rd" OutgoingEmailServer="192.168.1.101" OutgoingEmailFromAddr="administrator@isclondon.com" OutgoingEmailReplyToAddr="administrator@isclondon.com"> <FarmAccount AccountName="isclondon\spfarm" AccountPassword="password" /> <CentralAdmin Port="2010" AuthProvider="NTLM"> <Servers> <Server Name="SP01" /> <Server Name="SP02" /> <Server Name="SP03" /> </Servers> </CentralAdmin> <Services> <WebApplicationService> <Servers> <Server Name="SP01" /> <Server Name="SP02" /> </Servers> </WebApplicationService> <WorkflowTimerService> <Servers> <Server Name="SP01" /> <Server Name="SP02" /> </Servers> </WorkflowTimerService> <IncomingEmailService> <Servers> <Server Name="SP01" /> <Server Name="SP02" /> </Servers> </IncomingEmailService> <TraceService> <Account AccountName="isclondon\sptrace" AccountPassword="password" /> </TraceService> </Services> </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:
function Join-SPFarm ([string]$settings = $(throw "-settings is required")) { Build-SPFarm $true $settings } function New-SPFarm ([string]$settings = $(throw "-settings is required")) { Build-SPFarm $false $settings } function Build-SPFarm ([bool]$connectToExisting = $false, [string]$settings = $(throw "-settings is required")) { [xml]$config = Get-Content $settings if ([string]::IsNullOrEmpty($config.Farm.FarmAccount.AccountPassword)) { $farmAcct = Get-Credential $config.Farm.FarmAccount.AccountName } else { $farmAcct = New-Object System.Management.Automation.PSCredential $config.Farm.FarmAccount.AccountName, (ConvertTo-SecureString $config.Farm.FarmAccount.AccountPassword -AsPlainText -force) } $configDb = $config.Farm.ConfigDB $contentDB = $config.Farm.AdminContentDb $server = $config.Farm.DatabaseServer if ($config.Farm.Passphrase.Length -gt 0) { $passphrase = (ConvertTo-SecureString $config.Farm.Passphrase -AsPlainText -force) } else { Write-Warning "Using the Farm Admin's password for a passphrase" $passphrase = $farmAcct.Password } #Only build the farm if we don't currently have a farm created if ([Microsoft.SharePoint.Administration.SPFarm]::Local -eq $null) { psconfig -cmd upgrade -inplace b2b if ($connectToExisting) { #Connecting to farm Write-Host "Connecting to Farm..." Connect-SPConfigurationDatabase -DatabaseName $configDb -DatabaseServer $server -Passphrase $passphrase } else { #Creating new farm Write-Host "Creating Farm..." New-SPConfigurationDatabase ` -DatabaseName $configDb ` -DatabaseServer $server ` -AdministrationContentDatabaseName $contentDB ` -Passphrase $passphrase ` -FarmCredentials $farmAcct if (![string]::IsNullOrEmpty($config.Farm.ConfigDBFailoverDatabaseServer)) { Set-FailoverDatabase $configDb $config.Farm.ConfigDBFailoverDatabaseServer } if (![string]::IsNullOrEmpty($config.Farm.AdminContentDBFailoverDatabaseServer)) { Set-FailoverDatabase $contentDB $config.Farm.AdminContentDBFailoverDatabaseServer } } #Verifying farm creation $spfarm = Get-SPFarm -ErrorAction SilentlyContinue if ($spfarm -eq $null) { throw "Unable to verify farm creation." } #ACLing SharePoint Resources Write-Host "Calling Initialize-SPResourceSecurity..." Initialize-SPResourceSecurity #Installing Services Write-Host "Calling Install-SPService..." Install-SPService #Installing Features Write-Host "Calling Install-SPFeature..." Install-SPFeature -AllExistingFeatures Remove-PsSnapin Microsoft.SharePoint.PowerShell Add-PsSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue } else { Write-Warning "Farm exists. Skipping creation." } if ((Get-Service sptimerv4).Status -ne "Running") { Write-Host "Starting SPTimerV4 Service" Start-Service sptimerv4 } $scaConfig = $config.Farm.CentralAdmin $installSCA = (($scaConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null) if ($installSCA) { $auth = $scaConfig.AuthProvider $port = $scaConfig.Port $url = "http://$($env:computername):$port" $sca = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($url) if ($installSCA -and $sca -eq $null) { #Provisioning Central Administration Write-Host "Creating Central Admin Site at $url..." New-SPCentralAdministration -Port $port -WindowsAuthProvider $auth #Installing Help Write-Host "Calling Install-SPHelpCollection..." Install-SPHelpCollection -All #Installing Application Content Write-Host "Calling Install-SPApplicationContent..." Install-SPApplicationContent } } if (!$connectToExisting) { $server = $config.Farm.OutgoingEmailServer $from = $config.Farm.OutgoingEmailFromAddr $replyTo = $config.Farm.OutgoingEmailReplyToAddr $charSet = 65001 $wa = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]::Local if ($wa -ne $null) { $wa.UpdateMailSettings($server, $from, $replyTo, $charSet) } } #Stop or Start key service instances $startWebAppSvc = (($config.Farm.Services.WebApplicationService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null) Set-ServiceInstanceState "Microsoft SharePoint Foundation Web Application" $startWebAppSvc $startWorkflowSvc = (($config.Farm.Services.WorkflowTimerService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null) Set-ServiceInstanceState "Microsoft SharePoint Foundation Workflow Timer Service" $startWorkflowSvc $startIncomingEmailSvc = (($config.Farm.Services.IncomingEmailService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null) Set-ServiceInstanceState "Microsoft SharePoint Foundation Incoming E-Mail" $startIncomingEmailSvc #Set SPTraceV4 to run as domain account $accountNode = $config.Farm.Services.TraceService.Account if ($accountNode -ne $null -and ![string]::IsNullOrEmpty($accountNode.AccountName)) { if ([string]::IsNullOrEmpty($accountNode.AccountPassword)) { $svcAccount = Get-Credential $accountNode.AccountName } else { $svcAccount = New-Object System.Management.Automation.PSCredential $accountNode.AccountName, (ConvertTo-SecureString $accountNode.AccountPassword -AsPlainText -force) } $user = Get-SPManagedAccount -Identity $svcAccount.Username -ErrorAction SilentlyContinue if ($user -eq $null) { $user = New-SPManagedAccount -Credential $svcAccount } $tracingSvc = (Get-SPFarm).Services | ? {$_.Name -eq "SPTraceV4"} if ($tracingSvc.ProcessIdentity.Username -ne $user.Username) { $tracingSvc.ProcessIdentity.ManagedAccount = $user $tracingSvc.ProcessIdentity.CurrentIdentityType = "SpecificUser" $tracingSvc.ProcessIdentity.Update() $tracingSvc.ProcessIdentity.Deploy() } Write-Host "Adding `"$($user.Username)`" to Performance Log Users group..." $p = Start-Process -PassThru -FilePath "c:\windows\system32\net.exe" -ArgumentList "localgroup `"Performance Log Users`" `"$($user.Username)`" /add" -Wait -NoNewWindow if ($p.ExitCode -ne 0) { Write-Warning "Unable to add `"$($user.Username)`" to Performance Log Users group!" } else { Restart-Service SPTraceV4 iisreset } } } function Set-ServiceInstanceState($typeName, $enable) { $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq $typeName} if ($svc -eq $null) { throw "Unable to retrieve $typeName Service Instance." } if ($svc.Status -ne "Online" -and $enable) { Write-Host "Starting $typeName service instance..." $svc | Start-SPServiceInstance } if ($svc.Status -eq "Online" -and !$enable) { Write-Host "Stopping $typeName service instance..." $svc | Stop-SPServiceInstance -Confirm:$false } } function Set-FailoverDatabase($databaseName, [string]$failoverDbServer) { if (![string]::IsNullOrEmpty($failoverDbServer)) { $db = Get-SPDatabase | where {$_.Name -eq $databaseName} if ($db -eq $null) { throw "Unable to retrieve the database to set the failover server: $databaseName" } if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) { try { Write-Host "Adding failover database instance..." $db.AddFailoverServiceInstance($failoverDbServer) $db.Update() } catch { Write-Warning "Unable to set failover database server. $_" } } } }
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:
<Farm> <WebApplications> <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"> <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" /> <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" /> <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" /> <ManagedPaths> <ManagedPath Explicit="false" RelativeURL="/personal" /> </ManagedPaths> <UserPolicies> <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All"> <Permission Name="Full Control" /> </UserPolicy> </UserPolicies> <ProxyGroup Name="Default" /> <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="true" AllowMasterPageEditing="true" ShowURLStructure="true" /> <ContentDatabases> <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_MySites" MaxSiteCount="15000" WarningSiteCount="9000"> <SiteCollections> <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"> </SiteCollection> </SiteCollections> </ContentDatabase> </ContentDatabases> </WebApplication> <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"> <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" /> <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" /> <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" /> <ManagedPaths> </ManagedPaths> <UserPolicies> <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All"> <Permission Name="Full Control" /> </UserPolicy> </UserPolicies> <ProxyGroup Name="Default" /> <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" /> <ContentDatabases> <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_TeamSites" MaxSiteCount="15000" WarningSiteCount="9000"> <SiteCollections> <SiteCollection Name="Team Sites" Url="http://team.isclondon.local" Template="CMSPUBLISHING#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com"> </SiteCollection> </SiteCollections> </ContentDatabase> </ContentDatabases> <Features /> </WebApplication> <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"> <ApplicationPool Name="SharePoint Content" AccountName="isclondon\SPContent" AccountPassword="password" /> <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" /> <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" /> <ManagedPaths> <ManagedPath Explicit="true" RelativeURL="/CTHub" /> </ManagedPaths> <UserPolicies> <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All"> <Permission Name="Full Control" /> </UserPolicy> </UserPolicies> <ProxyGroup Name="Default" /> <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" /> <ContentDatabases> <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_Intranet" MaxSiteCount="15000" WarningSiteCount="9000"> <SiteCollections> <SiteCollection Name="Intranet" Url="http://intranet.isclondon.local" Template="STS#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com"> </SiteCollection> <SiteCollection Name="Content Type Hub" Url="http://intranet.isclondon.local/CTHub" Template="STS#1" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com"> </SiteCollection> </SiteCollections> </ContentDatabase> </ContentDatabases> <Features /> </WebApplication> </WebApplications> </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:
function Provision-WebApplications() { <# .Synopsis Creates the web applications. .Description Creates the web applications. .Example PS C:\> . .\Provision-WebApplications.ps1 PS C:\> Provision-WebApplication -SettingsFile c:\WebApplications.xml .Example PS C:\> . .\Provision-WebApplications.ps1 PS C:\> Provision-WebApplications -ConfigXml ([xml](Get-Content c:\farmconfiguration.xml)) .Parameter SettingsFile The path to an XML file. #> [CmdletBinding(DefaultParameterSetName="FilePath")] param ( [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlElement")] [ValidateNotNull()] [System.Xml.XmlElement]$ConfigElement, [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlDocument")] [ValidateNotNull()] [xml]$ConfigXml, [Parameter(Mandatory=$true, Position=0, ParameterSetName="FilePath")] [ValidateNotNullOrEmpty()] [string]$SettingsFile ) switch ($PsCmdlet.ParameterSetName) { "XmlDocument" { if ($ConfigXml.Farm.WebApplications.WebApplication -ne $null) { foreach ($appConfig in $ConfigXml.Farm.WebApplications.WebApplication) { Provision-WebApplications -ConfigElement $appConfig } return } } "FilePath" { Provision-WebApplications -ConfigXml ([xml](Get-Content $SettingsFile)) return } } $config = $ConfigElement if ($config -eq $null) { Write-Warning "No web application defined. Skipping." return } $webApp = Get-SPWebApplication -Identity $config.Name -ErrorAction SilentlyContinue if ($webApp -eq $null) { $allowAnon = [bool]::Parse($config.AllowAnonymous.ToString()) $ssl = [bool]::Parse($config.Ssl.ToString()) $db = $null if ($config.ContentDatabases -eq $null) { throw "A content database configuration setting could not be found for `"$($config.Name)`"." } if ($config.ContentDatabases.ChildNodes.Count -gt 1) { $db = $config.ContentDatabases.ContentDatabase | where {$_.Default -eq "true"} if ($db -is [array]) { Write-Warning "Multiple content databases set as default for `"$($config.Name)`" (using first)" $db = $db[0] } } else { $db = $config.ContentDatabases.ContentDatabase } if ($db -eq $null) { throw "A content database configuration setting could not be found for `"$($config.Name)`"." } $poolName = $config.ApplicationPool.Name $poolAcctName = $config.ApplicationPool.AccountName $poolAcctPwd = $config.ApplicationPool.AccountPassword $poolAcct = $null #Check for existing Application Pool $pools = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools if (($pools | ? {$_.Name -eq $poolName}) -eq $null) { $poolAcct = Get-SPManagedAccount $poolAcctName -ErrorAction SilentlyContinue if ($poolAcct -eq $null) { $securePwd = ConvertTo-SecureString $poolAcctPwd -AsPlainText -force $cred = New-Object System.Management.Automation.PSCredential $poolAcctName, $securePwd $poolAcct = New-SPManagedAccount -Credential $cred } } $loadBalancedUrl = $config.LoadBalancedUrl $port = $config.Port if (![string]::IsNullOrEmpty($loadBalancedUrl)) { $loadBalancedUri = New-Object System.Uri $config.LoadBalancedUrl $port = $loadBalancedUri.Port $loadBalancedUrl = "$($loadBalancedUri.Scheme)://$($loadBalancedUri.Host)/" } else { if (![string]::IsNullOrEmpty($config.HostHeader)) { $loadBalancedUrl = "http://$($config.HostHeader)" } } if ([string]::IsNullOrEmpty($port)) { $port = "80" } if ($config.AuthenticationMode -eq "Claims") { $authProviders = @() $enableWindowsAuthentication = $false if (![string]::IsNullOrEmpty($config.EnableWindowsAuthentication)) { $enableWindowsAuthentication = [bool]::Parse($config.EnableWindowsAuthentication) } $enableFormsBasedAuthentication = $false if (![string]::IsNullOrEmpty($config.EnableFormsBasedAuthentication)) { $enableFormsBasedAuthentication = [bool]::Parse($config.EnableFormsBasedAuthentication) } if ($enableWindowsAuthentication) { $enableBasicAuthentication = $false if (![string]::IsNullOrEmpty($config.EnableBasicAuthentication)) { $enableBasicAuthentication = [bool]::Parse($config.EnableBasicAuthentication) } $disableKerberos = $config.AuthenticationMethod -eq "NTLM" $authProviders += New-SPAuthenticationProvider ` -UseWindowsIntegratedAuthentication:$enableWindowsAuthentication ` -DisableKerberos:$disableKerberos ` -UseBasicAuthentication:$enableBasicAuthentication ` -AllowAnonymous:$allowAnon } if ($enableFormsBasedAuthentication) { $authProviders += New-SPAuthenticationProvider ` -ASPNETMembershipProvider $config.ASPNETMembershipProviderName ` -ASPNETRoleProviderName $config.ASPNETRoleManagerName } Write-Host "Creating Web Application: `"$($config.Name)`"" $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl ` -AllowAnonymousAccess:$allowAnon ` -ApplicationPool $poolName ` -ApplicationPoolAccount $poolAcct ` -Name $config.Name ` -AuthenticationProvider $authProviders ` -DatabaseServer $db.Server ` -DatabaseName $db.Name ` -HostHeader $config.HostHeader ` -Path $config.Path ` -Port $port ` -Url $loadBalancedUrl } else { $authMethod = "NTLM" if (![string]::IsNullOrEmpty($config.AuthenticationMethod)) { $authMethod = $config.AuthenticationMethod } Write-Host "Creating Web Application: `"$($config.Name)`"" $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl ` -AllowAnonymousAccess:$allowAnon ` -ApplicationPool $poolName ` -ApplicationPoolAccount $poolAcct ` -Name $config.Name ` -AuthenticationMethod $authMethod ` -DatabaseServer $db.Server ` -DatabaseName $db.Name ` -HostHeader $config.HostHeader ` -Path $config.Path ` -Port $port ` -Url $loadBalancedUrl } Set-SPWebApplication -Identity $webApp -DefaultTimeZone $config.DefaultTimeZone #Re-get the web app to avoid update conflicts $webApp = Get-SPWebApplication -Identity $webApp $webApp.SelfServiceSiteCreationEnabled = [bool]::Parse($config.SelfServiceSiteCreation) $webApp.RequireContactForSelfServiceSiteCreation = [bool]::Parse($config.RequireContactForSsc) if ($config.ProxyGroup -ne $null -and ![string]::IsNullOrEmpty($config.ProxyGroup.Name)) { $proxyGroupName = $config.ProxyGroup.Name if ($proxyGroupName -ne $null) { $webApp.ServiceApplicationProxyGroup = Get-ProxyGroup $proxyGroupName $true } } $webApp.Update() Write-Host """$($config.Name)"" successfully created." } else { #We got the web app so return it and don't attempt to recreate Write-Host """$($config.Name)"" already exists, skipping creation." } $setObjectCacheAccounts = $true if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount) -and ![string]::IsNullOrEmpty($config.PortalSuperReaderAccount)) { if ($config.PortalSuperUserAccount.AccountName -eq $config.PortalSuperReaderAccount.AccountName) { 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." $setObjectCacheAccounts = $false } } $claimsPrefix = "" if ($config.AuthenticationMode -eq "Claims") { $claimsPrefix = "i:0#.w|" } if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount.AccountName) -and $setObjectCacheAccounts) { $webApp.Properties["portalsuperuseraccount"] = "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)" Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)" $config.PortalSuperUserAccount.AccountName "Full Control" } if (![string]::IsNullOrEmpty($config.PortalSuperReaderAccount.AccountName) -and $setObjectCacheAccounts) { $webApp.Properties["portalsuperreaderaccount"] = "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)" Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)" $config.PortalSuperReaderAccount.AccountName "Full Read" } if ($config.UserPolicies) { Write-Host "Creating user policies..." [bool]$updateRequired = $false foreach ($userPolicyConfig in $config.UserPolicies.UserPolicy) { if ($userPolicyConfig -eq $null) { continue } [string]$zoneName = $userPolicyConfig.Zone [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies if ($zoneName.ToLower() -ne "all") { $policies = $webApp.ZonePolicies($zoneName) } Write-Host "Adding user policy for: $claimsPrefix$($userPolicyConfig.UserLogin)..." [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add("$claimsPrefix$($userPolicyConfig.UserLogin)", $userPolicyConfig.UserDisplayName) foreach ($permConfig in $userPolicyConfig.Permission) { [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $permConfig.Name} if ($policyRole -ne $null) { Write-Host "Adding policy role: $($permConfig.Name)..." $policy.PolicyRoleBindings.Add($policyRole) } } $updateRequired = $true } if ($updateRequired) { $webApp.Update() } } if ($config.ManagedPaths) { Write-Host "Creating managed paths..." foreach ($mpConfig in $config.ManagedPaths.ManagedPath) { if ($mpConfig -eq $null) { continue } $path = Get-SPManagedPath -Identity $mpConfig.RelativeURL ` -WebApplication $webApp -ErrorAction SilentlyContinue if ($path -eq $null) { Write-Host "Creating ""$($mpConfig.RelativeURL)""..." New-SPManagedPath -RelativeURL $mpConfig.RelativeURL ` -Explicit[bool]::Parse($mpConfig.Explicit)) -WebApplication $webApp } else { Write-Host "Managed path already exists: ""$($mpConfig.RelativeURL)""." } } } if ($config.SPDesigner) { Write-Host "Configuring SharePoint Designer Settings..." sleep 5 #Get the web app again to avoid an update conflict $webApp = Get-SPWebApplication $webApp $webApp | Set-SPDesignerSettings ` -AllowDesigner ([bool]::Parse($config.SPDesigner.AllowDesigner)) ` -AllowRevertFromTemplate ([bool]::Parse($config.SPDesigner.AllowRevertFromTemplate)) ` -AllowMasterPageEditing ([bool]::Parse($config.SPDesigner.AllowMasterPageEditing)) ` -ShowURLStructure ([bool]::Parse($config.SPDesigner.ShowURLStructure)) $webApp.Update() } foreach ($dbConfig in $config.ContentDatabases.ContentDatabase) { $db = Get-SPContentDatabase $dbConfig.Name -ErrorAction SilentlyContinue if ($db -eq $null) { $db = New-SPContentDatabase -Name $dbConfig.Name ` -WebApplication $webApp ` -DatabaseServer $dbConfig.Server ` -MaxSiteCount $dbConfig.MaxSiteCount ` -WarningSiteCount $dbConfig.WarningSiteCount } $failoverDbServer = $dbConfig.FailoverDatabaseServer if (![string]::IsNullOrEmpty($failoverDbServer)) { if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) { try { Write-Host "Adding failover database instance..." $db.AddFailoverServiceInstance($failoverDbServer) $db.Update() } catch { Write-Warning "Unable to set failover database server. $_" } } } foreach ($siteConfig in $dbConfig.SiteCollections.SiteCollection) { $site = Get-SPSite $siteConfig.Url -ErrorAction SilentlyContinue if ($site -ne $null) { Write-Host "Site Collection $($siteConfig.Url) already exists. Skipping." $site.Dispose() continue } $lcid = $siteConfig.LCID if ([string]::IsNullOrEmpty($lcid)) { $lcid = [Microsoft.SharePoint.SPRegionalSettings]::GlobalServerLanguage.LCID } $optionalParams = @{} if (![string]::IsNullOrEmpty($siteConfig.SecondaryLogin)) { $optionalParams += @{"SecondaryOwnerAlias"=$siteConfig.SecondaryLogin} } if (![string]::IsNullOrEmpty($siteConfig.SecondaryEmail)) { $optionalParams += @{"SecondaryEmail"=$siteConfig.SecondaryEmail} } if (![string]::IsNullOrEmpty($siteConfig.OwnerEmail)) { $optionalParams += @{"OwnerEmail"=$siteConfig.OwnerEmail} } Write-Host "Creating Site Collection $($siteConfig.Url)..." $site = New-SPSite ` -Url $siteConfig.Url ` -Description $siteConfig.Description ` -Language $lcid ` -Name $siteConfig.Name ` -OwnerAlias $siteConfig.OwnerLogin ` -Template $siteConfig.Template ` -ContentDatabase $db ` -ErrorAction Continue @optionalParams if ($site -eq $null) { Write-Warning "Site collection was not created!" } else { Write-Host "Site collection successfully created: $($siteConfig.Url)" Write-Host "Setting default associated security groups..." sleep 5 $refreshedSite = $site | Get-SPSite $secondaryLogin = $siteConfig.SecondaryLogin if (![string]::IsNullOrEmpty($secondaryLogin)) { $secondaryLogin = "$claimsPrefix$secondaryLogin" } $refreshedSite.RootWeb.CreateDefaultAssociatedGroups("$claimsPrefix$($siteConfig.OwnerLogin)", $secondaryLogin, $siteConfig.Name) $refreshedSite.Dispose() $site.Dispose() } } } } function Get-ProxyGroup([string]$name, [bool]$createIfMissing = $true) { if ($name -eq "Default" -or [string]::IsNullOrEmpty($name)) { return [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default } $proxyGroup = Get-SPServiceApplicationProxyGroup $name -ErrorAction SilentlyContinue -ErrorVariable err if ($err -and !$createIfMissing) { throw $err } if ($proxyGroup -eq $null) { $proxyGroup = New-SPServiceApplicationProxyGroup -Name $name } return $proxyGroup } function Set-WebAppUserPolicy($webApp, $userName, $userDisplayName, $perm) { [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add($userName, $userDisplayName) [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $perm} if ($policyRole -ne $null) { $policy.PolicyRoleBindings.Add($policyRole) } $webApp.Update() }
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):
$claim = [Microsoft.SharePoint.Administration.Claims.SPClaimProviderManager]::CreateUserClaim("isclondon\spcacheuser", "Windows") [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:
<Farm> <Services> <SecureStoreServices> <Servers> <Server Name="SP03" /> <Server Name="SP04" /> </Servers> <SecureStoreServiceApplications> <SecureStoreServiceApplication Name="ISC Secure Store Service" DatabaseName="ISC_SecureStore" DatabaseServer="ISCSharePoint1" FailoverDatabaseServer="" AuditingEnabled="true" AuditLogMaxSize="30" Sharing="false" KeyPassphrase="p@ssw0rd" Partitioned="false"> <ApplicationPool Name="SharePoint Services App Pool" AccountName="isclondon\SPServices" AccountPassword="password" /> <Proxy Name="ISC Secure Store Service"> <ProxyGroup Name="Default" /> </Proxy> </SecureStoreServiceApplication> </SecureStoreServiceApplications> </SecureStoreServices> </Services> </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:
function Start-SecureStoreServices { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [ValidateNotNullOrEmpty()] [string]$SettingsFile ) [xml]$config = Get-Content $settingsFile $install = (($config.Farm.Services.SecureStoreServices.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null) if (!$install) { Write-Host "Machine not specified in Servers element, service will not be installed on this server." return } $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"} if ($svc -eq $null) { throw "Unable to retrieve Service Instance." } if ($svc.Status -ne "Online") { Write-Host "Starting service instance..." $svc | Start-SPServiceInstance #Make sure the service is online before attempting to add a svc app. while ($true) { Start-Sleep 2 $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"} if ($svc.Status -eq "Online") { break } } } foreach ($appConfig in $config.Farm.Services.SecureStoreServices.SecureStoreServiceApplications.SecureStoreServiceApplication) { $app = Get-SPServiceApplication | where {$_.Name -eq $appConfig.Name} $updateKey = $false if ($app -eq $null) { $poolName = $appConfig.ApplicationPool.Name $identity = $appConfig.ApplicationPool.AccountName $password = $appConfig.ApplicationPool.AccountPassword $pool = Get-ServicePool $poolName $identity $password Write-Host "Creating secure store service application..." $app = New-SPSecureStoreServiceApplication -Name $appConfig.Name ` -ApplicationPool $pool ` -DatabaseServer $appConfig.DatabaseServer ` -DatabaseName $appConfig.DatabaseName ` -AuditingEnabled[bool]::Parse($appConfig.AuditingEnabled)) ` -AuditLogMaxSize $appConfig.AuditLogMaxSize ` -FailoverDatabaseServer $appConfig.FailoverDatabaseServer ` -PartitionMode:([bool]::Parse($appConfig.Partitioned)) ` -Sharing:([bool]::Parse($appConfig.Sharing)) $updateKey = $true } else { Write-Host "Secure store service application already exists, skipping creation." } $proxy = Get-SPServiceApplicationProxy | where {$_.Name -eq $appConfig.Proxy.Name} if ($proxy -eq $null) { Write-Host "Creating secure store service application proxy..." $proxy = New-SPSecureStoreServiceApplicationProxy ` -Name $appConfig.Proxy.Name ` -ServiceApplication $app ` -DefaultProxyGroup:$false } else { Write-Host "Secure store service application proxy already exists, skipping creation." } $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup if ($updateKey) { Update-SPSecureStoreMasterKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase while ($true) { try { Start-Sleep -Seconds 5 Update-SPSecureStoreApplicationServerKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase break } catch { } } } } } function Get-ServicePool($poolName, $identity, $password) { $pool = Get-SPServiceApplicationPool $poolName -ErrorAction SilentlyContinue if ($pool -eq $null) { if ($identity -ne $null) { $acct = Get-SPManagedAccount $identity -ErrorAction SilentlyContinue } if ($acct -eq $null) { if ([string]::IsNullOrEmpty($password)) { $cred = Get-Credential $identity } else { $cred = New-Object System.Management.Automation.PSCredential $identity, (ConvertTo-SecureString $password -AsPlainText -force) } $acct = Get-SPManagedAccount $cred.UserName -ErrorAction SilentlyContinue if ($acct -eq $null) { $acct = New-SPManagedAccount $cred } } Write-Host "Creating application pool..." $pool = New-SPServiceApplicationPool -Name $poolName -Account $acct } return $pool } function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject) { begin {} process { if ($_ -eq $null -and $InputObject -ne $null) { $InputObject | Set-ProxyGroupsMembership $groups return } $proxy = $_ #Clear any existing proxy group assignments Get-SPServiceApplicationProxyGroup | where {$_.Proxies -contains $proxy} | ForEach-Object { $proxyGroupName = $_.Name if ([string]::IsNullOrEmpty($proxyGroupName)) { $proxyGroupName = "Default" } $group = $null [bool]$matchFound = $false foreach ($g in $groups) { $group = $g.Name if ($group -eq $proxyGroupName) { $matchFound = $true break } } if (!$matchFound) { Write-Host "Removing ""$($proxy.DisplayName)"" from ""$proxyGroupName""" $_ | Remove-SPServiceApplicationProxyGroupMember -Member $proxy -Confirm:$false -ErrorAction SilentlyContinue } } foreach ($g in $groups) { $group = $g.Name $pg = $null if ($group -eq "Default" -or [string]::IsNullOrEmpty($group)) { $pg = [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default } else { $pg = Get-SPServiceApplicationProxyGroup $group -ErrorAction SilentlyContinue -ErrorVariable err if ($pg -eq $null) { $pg = New-SPServiceApplicationProxyGroup -Name $name } } $pg = $pg | where {$_.Proxies -notcontains $proxy} if ($pg -ne $null) { Write-Host "Adding ""$($proxy.DisplayName)"" to ""$($pg.DisplayName)""" $pg | Add-SPServiceApplicationProxyGroupMember -Member $proxy } } } end {} }
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
Deploying SharePoint 2010 Solution Package Using PowerShell (Revisited)
If you were at my PowerShell for developers talk at the European SharePoint Best Practices Conference last week then you’ll know that I’ve never been all that happy with how I was approaching Farm Solution deployment, as detailed in an earlier post from sometime last year (Deploying SharePoint 2010 Solution Packages Using PowerShell). So what are some of the issues I have with what I created? Here’s a quick list:
- There are two functions – I really just want one function to call and let the function figure out what to do based on the parameters provided.
- I want to be able to provide a directory containing WSP files to deploy (sure, I could use Get-ChildItem to grab all my files, iterate through them, and then call the function, but that means I have to type more each time I want to execute and I’m way too lazy for that).
- There’s no consideration for simply updating Solution Packages rather than retracting and redeploying.
- I was using the Start-SPAdminJob cmdlet and stopping and starting the admin service – something that we shouldn’t be doing and is really just an old throwback to 2007. It’s just a bad idea – don’t do it.
- I was forcing information such as GAC and CAS settings in the XML when I could easily get the information via the SPSolution object once added.
- And finally, there was no real help available so you had to really know what was going on to understand how to construct the XML file and to then call the file.
For all these reasons I’ve decided to completely rewrite the script. As a result it’s a bit more complicated at first blush but that’s mainly due to some additional error handling, progress reporting, and blocking code that I’ve added; as well as the additional parameter related code and associated help. I’ve essentially followed the pattern that I described with my earlier post on Feature activation and have made the function work more like a cmdlet (with full help, parameter sets, and use of PipeBind objects). Before I share the code, I’d like to show the complete help that is available for the function:
NAME
Deploy-SPSolutions
SYNOPSIS
Deploys one or more Farm Solution Packages to the Farm.
SYNTAX
Deploy-SPSolutions [-Identity] <String> [[-UpgradeExisting]] [[-AllWebApplications]] [[-WebApplication] <SPWebApplicationPipeBind[]>] [<CommonParameters>]
Deploy-SPSolutions [-Config] <XmlDocument> [<CommonParameters>]
DESCRIPTION
Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
If using an XML configuration file, the format of the file must match the following:
<Solutions>
<Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
<WebApplications>
<WebApplication>http://example.com/</WebApplication>
</WebApplications>
</Solution>
</Solutions>
Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.
PARAMETERS
-Config <XmlDocument>
The XML configuration file containing the WSP files to deploy.
Required? true
Position? 1
Default value
Accept pipeline input? false
Accept wildcard characters?
-Identity <String>
The directory, WSP file, or XML configuration file containing the WSP files to deploy.
Required? true
Position? 1
Default value
Accept pipeline input? false
Accept wildcard characters?
-UpgradeExisting [<SwitchParameter>]
If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
Required? false
Position? 2
Default value
Accept pipeline input? false
Accept wildcard characters?
-AllWebApplications [<SwitchParameter>]
If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
Required? false
Position? 3
Default value
Accept pipeline input? false
Accept wildcard characters?
-WebApplication <SPWebApplicationPipeBind[]>
Specifies the Web Application(s) to deploy the WSP file to.
Required? false
Position? 4
Default value
Accept pipeline input? false
Accept wildcard characters?
<CommonParameters>
This cmdlet supports the common parameters: Verbose, Debug,
ErrorAction, ErrorVariable, WarningAction, WarningVariable,
OutBuffer and OutVariable. For more information, type,
"get-help about_commonparameters".
INPUTS
OUTPUTS
-------------------------- EXAMPLE 1 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
-------------------------- EXAMPLE 2 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
-------------------------- EXAMPLE 3 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
-------------------------- EXAMPLE 4 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
-------------------------- EXAMPLE 5 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
-------------------------- EXAMPLE 6 --------------------------
PS C:\>. .\Deploy-SPSolutions.ps1
PS C:\> Deploy-SPSolutions C:\Solutions.xml
This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
RELATED LINKS
Get-Content
Get-SPSolution
Add-SPSolution
Install-SPSolution
Update-SPSolution
Uninstall-SPSolution
Remove-SPSolution
As you can see, this is a lot more useful for someone wishing to execute this script as not only does it provide information about the XML structure but it also provides several usage examples.
So, without further delay, here’s the new version of the deployment script (note that I changed the function name to Deploy-SPSolutions so it won’t impact environments that depend on the old function):
function global:Deploy-SPSolutions() { <# .Synopsis Deploys one or more Farm Solution Packages to the Farm. .Description Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy. If using an XML configuration file, the format of the file must match the following: <Solutions> <Solution Path="<full path and filename to WSP>" UpgradeExisting="false"> <WebApplications> <WebApplication>http://example.com/</WebApplication> </WebApplications> </Solution> </Solutions> Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed. .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable). .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable). .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable). .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable). .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed. .Example PS C:\> . .\Deploy-SPSolutions.ps1 PS C:\> Deploy-SPSolutions C:\Solutions.xml This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file. .Parameter Config The XML configuration file containing the WSP files to deploy. .Parameter Identity The directory, WSP file, or XML configuration file containing the WSP files to deploy. .Parameter UpgradeExisting If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect). .Parameter AllWebApplications If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable). .Parameter WebApplication Specifies the Web Application(s) to deploy the WSP file to. .Link Get-Content Get-SPSolution Add-SPSolution Install-SPSolution Update-SPSolution Uninstall-SPSolution Remove-SPSolution #> [CmdletBinding(DefaultParameterSetName="FileOrDirectory")] param ( [Parameter(Mandatory=$true, Position=0, ParameterSetName="Xml")] [ValidateNotNullOrEmpty()] [xml]$Config, [Parameter(Mandatory=$true, Position=0, ParameterSetName="FileOrDirectory")] [ValidateNotNullOrEmpty()] [string]$Identity, [Parameter(Mandatory=$false, Position=1, ParameterSetName="FileOrDirectory")] [switch]$UpgradeExisting, [Parameter(Mandatory=$false, Position=2, ParameterSetName="FileOrDirectory")] [switch]$AllWebApplications, [Parameter(Mandatory=$false, Position=3, ParameterSetName="FileOrDirectory")] [Microsoft.SharePoint.PowerShell.SPWebApplicationPipeBind[]]$WebApplication ) function Block-SPDeployment($solution, [bool]$deploying, [string]$status, [int]$percentComplete) { do { Start-Sleep 2 Write-Progress -Activity "Deploying solution $($solution.Name)" -Status $status -PercentComplete $percentComplete $solution = Get-SPSolution $solution if ($solution.LastOperationResult -like "*Failed*") { throw "An error occurred during the solution retraction, deployment, or update." } if (!$solution.JobExists -and (($deploying -and $solution.Deployed) -or (!$deploying -and !$solution.Deployed))) { break } } while ($true) sleep 5 } switch ($PsCmdlet.ParameterSetName) { "Xml" { # An XML document was provided so iterate through all the defined solutions and call the other parameter set version of the function $Config.Solutions.Solution | ForEach-Object { [string]$path = $_.Path [bool]$upgrade = $false if (![string]::IsNullOrEmpty($_.UpgradeExisting)) { $upgrade = [bool]::Parse($_.UpgradeExisting) } $webApps = $_.WebApplications.WebApplication Deploy-SPSolutions -Identity $path -UpgradeExisting:$upgrade -WebApplication $webApps -AllWebApplications:$(($webApps -eq $null) -or ($webApps.Length -eq 0)) } break } "FileOrDirectory" { $item = Get-Item (Resolve-Path $Identity) if ($item -is [System.IO.DirectoryInfo]) { # A directory was provided so iterate through all files in the directory and deploy if the file is a WSP (based on the extension) Get-ChildItem $item | ForEach-Object { if ($_.Name.ToLower().EndsWith(".wsp")) { Deploy-SPSolutions -Identity $_.FullName -UpgradeExisting:$UpgradeExisting -WebApplication $WebApplication } } } elseif ($item -is [System.IO.FileInfo]) { # A specific file was provided so assume that the file is a WSP if it does not have an XML extension. [string]$name = $item.Name if ($name.ToLower().EndsWith(".xml")) { Deploy-SPSolutions -Config ([xml](Get-Content $item.FullName)) return } $solution = Get-SPSolution $name -ErrorAction SilentlyContinue if ($solution -ne $null -and $UpgradeExisting) { # Just update the solution, don't retract and redeploy. Write-Progress -Activity "Deploying solution $name" -Status "Updating $name" -PercentComplete -1 $solution | Update-SPSolution -CASPolicies:$($solution.ContainsCasPolicy) ` -GACDeployment:$($solution.ContainsGlobalAssembly) ` -LiteralPath $item.FullName Block-SPDeployment $solution $true "Updating $name" -1 Write-Progress -Activity "Deploying solution $name" -Status "Updated" -Completed return } if ($solution -ne $null) { #Retract the solution if ($solution.Deployed) { Write-Progress -Activity "Deploying solution $name" -Status "Retracting $name" -PercentComplete 0 if ($solution.ContainsWebApplicationResource) { $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false } else { $solution | Uninstall-SPSolution -Confirm:$false } #Block until we're sure the solution is no longer deployed. Block-SPDeployment $solution $false "Retracting $name" 12 Write-Progress -Activity "Deploying solution $name" -Status "Solution retracted" -PercentComplete 25 } #Delete the solution Write-Progress -Activity "Deploying solution $name" -Status "Removing $name" -PercentComplete 30 Get-SPSolution $name | Remove-SPSolution -Confirm:$false Write-Progress -Activity "Deploying solution $name" -Status "Solution removed" -PercentComplete 50 } #Add the solution Write-Progress -Activity "Deploying solution $name" -Status "Adding $name" -PercentComplete 50 $solution = Add-SPSolution $item.FullName Write-Progress -Activity "Deploying solution $name" -Status "Solution added" -PercentComplete 75 #Deploy the solution if (!$solution.ContainsWebApplicationResource) { Write-Progress -Activity "Deploying solution $name" -Status "Installing $name" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -Confirm:$false Block-SPDeployment $solution $true "Installing $name" 85 } else { if ($WebApplication -eq $null -or $WebApplication.Length -eq 0) { Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to all Web Applications" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -AllWebApplications -Confirm:$false Block-SPDeployment $solution $true "Installing $name to all Web Applications" 85 } else { $WebApplication | ForEach-Object { $webApp = $_.Read() Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to $($webApp.Url)" -PercentComplete 75 $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false Block-SPDeployment $solution $true "Installing $name to $($webApp.Url)" 85 } } } Write-Progress -Activity "Deploying solution $name" -Status "Deployed" -Completed } break } } }
When it comes to using the function I believe the help documentation speaks for itself so I won’t reiterate it here.
As always, I’m open to suggestions as to how to improve this function so please leave a comment if you find something wrong or have a suggestion for making it better.
-Gary
Retrieving SharePoint 2010 Feature Activations Using Windows PowerShell
During my PowerShell for Developers presentation in London last week I promised to show and demonstrate a script for retrieving Feature activations; unfortunately I ran out of time and was not able to show this script to the degree that I’d intended so I decided to throw together this blog post.
When developing custom Features it is very common to expect that there will need to be some level of update required for those Features. Typically this means that, after deploying the Feature via a Solution Package, you will need to re-activate that Feature in order to trigger any additional code to run (or, if you are using the new SharePoint 2010 Feature upgrade capabilities you will need to run the Upgrade(Boolean) method of the SPFeature object). The problem is knowing where the Feature is activated throughout the Farm. Using PowerShell there are two ways to do this – you can use the Get-SPFeature cmdlet and test the results against the appropriate scope or you can use the various “Query” methods that have been provided for each scope. I don’t recommend that you use the Get-SPFeature cmdlet as it is very inefficient, and as such, I won’t bother showing an example of that here. Instead I’ll focus on the “Query” methods approach.
Whether your Feature is scoped to the Farm, Web Application, Site Collection, or Site, there is a method that you can call to get an SPFeature object which effectively corresponds to a Feature activation. For Farm scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebService class, obtainable via the SPWebService class’ static AdministrationService property; for Web Application scoped Features you use the static QueryFeaturesInAllWebServices(Guid, Boolean) method of the SPWebService class; for Site Collection scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebApplication class; and for Site scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPSite class.
To create our PowerShell function we’ll simply take in a SPFeatureDefinition object and use a switch statement to call the appropriate method based on the scope of the Feature. To make the function more versatile we can use the Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind type which will allow the caller to pass in either the name of the Feature, its ID, or an actual SPFeatureDefinition object; additionally, we can use parameter attributes to easily allow the value to be passed in via the object pipeline. And finally, we’ll add an additional parameter stating that we wish to retrieve only those activations that require upgrading and we’ll add some basic help for the function.
The following code listing represents the completed function – I recommend that you save this to a file named Get-SPFeatureActivations.ps1. Note that I plan on adding this as a cmdlet to my downloadable extensions thereby making the need for this script unnecessary, however, I believe that this example provides a great template to use for creating professional looking, production ready scripts that both IT administrators and developers can use.
function Get-SPFeatureActivations() { <# .Synopsis Retrieves Feature activations for the given Feature Definition. .Description Retrieves the SPFeature object for each activation of the SPFeatureDefinition object. .Example Get-SPFeatureActivations TeamCollab .Parameter Identity The Feature name, ID, or SPFeatureDefinition object whose activations will be retrieved. .Parameter NeedsUpgrade If specified, only Feature activations needing upgrading will be retrieved. .Link Get-SPFeature #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [Alias("Feature")] [ValidateNotNullOrEmpty()] [Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind]$Identity, [Parameter(Mandatory=$false, Position=1)] [switch]$NeedsUpgrade ) begin { } process { $fd = $Identity.Read() switch ($fd.Scope) { "Farm" { [Microsoft.SharePoint.Administration.SPWebService]::AdministrationService.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) break } "WebApplication" { [Microsoft.SharePoint.Administration.SPWebService]::QueryFeaturesInAllWebServices($fd.ID, $NeedsUpgrade.IsPresent) break } "Site" { foreach ($webApp in Get-SPWebApplication) { $webApp.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) } break } "Web" { foreach ($site in Get-SPSite -Limit All) { $site.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) $site.Dispose() } break } } } end { } }
Assuming you’ve saved the file to the root of the C drive (not recommended but its what I do when I’m doing demos) then you can load the function into memory using dot sourcing as shown in the following example (note that the help for the function shows the help information specified by the block comment help):
Once the function is loaded into memory you can start using it. In the following example I’m returning back all the locations where the MyCustomFeature Feature is activated; I then use the Select-Object cmdlet to return just the URL for each activation:
Get-SPFeatureActivations MyCustomFeature | select @{Expression={$_.Parent.Url};Label="Url"}In this next example, instead of simply outputting the URL of each activation, I’m forcing the Feature to be reactivated using the Enable-SPFeature cmdlet (use the -Force parameter to force the Feature to be reactivated – you could also change the code to deactivate the Feature using the Disable-SPFeature cmdlet and then activate using the Enable-SPFeature cmdlet):
Get-SPFeatureActivations MyCustomFeature | ForEach-Object {
Enable-SPFeature -Identity MyCustomFeature -Url $_.Parent.Url -Force
}
Similarly you can retrieve only those Features needing upgrade and then call the Upgrade() method, as shown in this next example:
Get-SPFeatureActivations MyCustomFeature -NeedsUpgrade | ForEach-Object {$_.Upgrade($false)
}
I strongly recommend that, before you re-deploy a Feature that may be activated at an unknown number of scopes, you run this function (or something similar to it) so that you fully understand the impact of upgrading your Feature. One more thing to watch out for, if your environment is very large you may wish to modify this function so that it does not return the SPFeature object but instead just returns the URL corresponding to the activation – you can then use the Get-SPFeature cmdlet to retrieve the SPFeature object; the benefit of this is that you can immediately dispose of the parent object and prevent potential out of memory errors (I’m particularly concerned with Site Collection and Site scoped Features here where the Parent property of the SPFeature object corresponds to an SPSite or SPWeb object which must be disposed).
That’s all I’ve got for now; hopefully you’ve found this useful!
-Gary
Windows PowerShell Cheat Sheet
While preparing for my two PowerShell talks that I presented at the European SharePoint Best Practices Conference last week in London, I soon discovered that I had way too much content to present. Specifically, I wanted to begin both talks with a “PowerShell 101” piece where I walked through some of the more basic concepts relevant to each audience (mainly those that I always got tripped up on); unfortunately there’s just too many things to choose from and I couldn’t possibly show everything I wanted. Well, I happened to be in Orlando the week before I left for the conference and I had a chance to talk with Rob Bogue about what my plans were – well, to make a long story short, Rob had stated that the one thing he’d love to have is a simple, one-page cheat sheet showing some common syntactical examples. I thought this was not only a great idea in general but could also help solve my problem for my presentations – now I didn’t have to show everything during my talk, I could just show those bits that warranted more explanation and then provide everyone the cheat sheet as a handout for later review. And of course, I never planned on limiting the audience to just the attendees of the conference, though I did want them to be the first recipients. So, feel free to download my Windows PowerShell Cheat Sheet and good luck with all your PowerShell endeavors!
4th Grade Math and PowerShell
My 10 year old daughter came home from school today with a simple math problem that she had to solve and she asked if there was an easier way to solve the problem. Basically the teacher explained that the number six was a perfect number because the sum of it's factors (excluding itself) is equal to the number and she wanted to know what the next perfect number was. The kids basically had to go through the numbers and manually add up the factors until they found a match. So naturally, the programmer in me said, sure we can write a quick algorithm to find the next few perfect numbers.
After writing the code, using PowerShell of course, I realized that this little math algorithm demonstrated a few key PowerShell concepts that would be good for anyone starting out with PowerShell to know. Here's the code I wrote for her along with the output:
So in this really simple math example you can see how to use a basic for loop construct, dynamic arrays, dynamic typing, static method calls, and variable replacement within strings.
As you can see, for loops in PowerShell are identical to those in C# so there's nothing new there (unless you're new to C# that is). Dynamic arrays on the other hand are kind of cool - you can declare an empty array by simply using @(). Don't confuse this with declaring empty hash tables which use curly braces instead of parenthesis, @{}. Adding elements to these arrays is as easy as using the += operator: $factors += $i.
You can see the dynamic typing where I'm dividing $i by $j - if it divides evenly then the type returned would be an integer, otherwise it would be a float. So a really easy way to check if it divided evenly is to see if the returned type is equal to [int] (we can work with a type by wrapping a type name in brackets).
Calling static methods is a little different than what you may be used to in C#. As mentioned above types are defined by wrapping the type name in brackets - if we want to call a static method (or access a static property) of a type then we simply separate the method or property name and the type name with double colons. So in this example I'm finding the square root of the number by using the static Sqrt method on the System.Math class: [Math]::Sqrt($i).
The last little bit, dynamic variable replacement, just demonstrates how we can use $() to force the contents of the parentheses to be evaluated before they are used within the string for the Write-Host command. In this case I wanted to show the number of factors but if I did not wrap the $factors.Length bit in parentheses my output would look like this: 6 (1 2 3.Length factors)= 1 2 3.
So, as you can see, PowerShell is a really great tool and can be used for things other than SharePoint, even helping your 4th grader with her math homework
Setting PowerShell Switch Parameters Dynamically
I've been doing a lot of configuration driven PowerShell scripts lately and I had a bit of trouble figuring this bit out so I thought I'd write up a short post about it. A lot of the cmdlets that I've been working with take in switch parameters and I needed a way to set those parameters dynamically based on settings in a configuration file. For those new or unfamiliar with PowerShell, switch parameters are just parameters that don't have any corresponding value. Here's an example:
Get-Command -Name Start-Sleep -Syntax
The "-Syntax" parameter above is a switch parameter - it takes no value and simply tells the Get-Command cmdlet to return only the syntax of the command. This particular cmdlet isn't the best example of where you would want to set the parameters dynamically but you get the idea. So how would you set the -Syntax parameter dynamically? Here's an example:
[xml]$config = Get-Content .\Configurations.xml Get-Command -Name Start-Sleep -Syntax[bool]::Parse($config.Settings.ShowSyntax))
You can imagine an XML structure where there may be some kind of configuration settings (in this case: <Settings ShowSyntax="true" />). So you basically just add a colon (:) after the parameter and then provide a Boolean value; in this example because the value is in the form of an XmlElement we have to convert it to a Boolean.
This is one of those simple things that really pissed me off because it took forever for me to figure it out so I figured I'd share this simple tip with others (if you're a SharePoint person you'll want to know about all this stuff for the next version - believe me!).









