Starting the SharePoint 2010 Foundation Search Service using PowerShell
It's been a while since my last real SharePoint 2010 scripting post but we're getting close to RTM so I figured I need to buckle down and play some catch up and get some long overdue posts published. So, continuing my series of posts on scripting the various services and service applications within SharePoint 2010 I decided that I would share something that I know a lot of people have been struggling with recently - scripting the SharePoint Foundation Search Service.
This one threw me for a bit of a loop because all the other services and service applications can be configured almost exclusively using PowerShell cmdlets - this one though has to be configured almost exclusively using the object model. We basically have four cmdlets available to help with the configuration and unfortunately they're not much help at all:
- Get-SPSearchService - Returns back an object representing the actual service
- Get-SPSearchServiceInstance - Returns an object representing a service configuration for the service
- Set-SPSearchService - Updates a few select properties associated with the service
- Set-SPSearchServiceInstance - Updates the ProxyType for the service
The main failing with these cmdlets is that you can't set the services process identity, the database name and server or failover server, and you can't trigger the provisioning of the service instances which is required for the service to be considered fully "started". All of these things I can do through Central Admin but there's no way to do it using any provided cmdlets - so how do we solve the problem? By getting our hands dirty and writing a boat load of code against the object model.
So let's get started. As before we'll use an XML file to drive the setup process:
<Services>
<FoundationSearchService Enable="true"
AddStartAddressForNonNTZone="false"
MaxBackupDuration="2880"
PerformanceLevel="PartlyReduced"
DatabaseServer="SPSQL1"
DatabaseName="SharePoint_Search_Help"
FailoverDatabaseServer="">
<SvcAccount Name="sp2010\spsearch" />
<CrawlAccount Name="sp2010\spcrawl" />
<Servers>
<Server Name="sp2010svr" ProxyType="Default" />
</Servers>
</FoundationSearchService>
</Services>
As you can see the configuration file is pretty simple. We define two accounts that we'll use, one for the process identity of the service and the other for the crawl account. There's a few simple attributes for the database and some miscellaneous configurations and a list of all the servers in which the service should be started on.
Okay, let's start digging into the actual script. The first thing I do is load the XML file to a variable, $svcConfig, which I use throughout the function:
1: [xml]$config = Get-Content $settingsFile
2: $svcConfig = $config.Services.FoundationSearchService
Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <FoundationSearchService /> element and set that to the $svcConfig variable. Next I need to determine if the script should continue on this server by checking the <Servers /> element to see if there's a match for the current machine:
1: #See if we want to start the svc on the current server.
2: $install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
3: if (!$install) {
4: Write-Host "Machine not specified in Servers element, service will not be started on this server."
5: return
6: }
So at this point we know that we're on a target machine so the first thing we want to do is use the Start-SPServiceInstance to start the Foundation Search Service:
1: #Start the service instance
2: $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
3: if ($svc -eq $null) {
4: $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
5: }
6: Start-SPServiceInstance -Identity $svc
The trick with this is that if we're not using SharePoint Foundation then once the service is initially started it renames itself to "SharePoint Foundation Help Search" so I had to put a provision to look for one name or the other to allow this script to be run multiple times and from multiple machines. Now that the service is started lets set a few variables that we'll use throughout the rest of the script:
1: #Get the service and service instance
2: $searchSvc = Get-SPSearchService
3: $searchSvcInstance = Get-SPSearchServiceInstance -Local
4:
5: $dbServer = $svcConfig.DatabaseServer
6: $failoverDbServer = $svcConfig.FailoverDatabaseServer
We'll use the $searchSvc and $searchSvcInstance variables extensively. Note that we'll also need to repeat lines one and two at least a couple of times to avoid update conflicts as a result of timer jobs modifying those objects.
The next step will be to set the process identity for the service. We'll go ahead and also get the crawl account information while we're at it to avoid prompting for passwords in more than one location:
1: #Get the service account details
2: Write-Host "Provide the username and password for the search crawl account..."
3: $crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
4: Write-Host "Provide the username and password for the search service account..."
5: $searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name
6:
7: #Get or Create a managed account for the search service account.
8: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
9: if ($err) {
10: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
11: }
12:
13: #Set the account details if different than what is current.
14: $processIdentity = $searchSvc.ProcessIdentity
15: if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
16: $processIdentity = $searchSvc.ProcessIdentity
17: $processIdentity.CurrentIdentityType = "SpecificUser"
18: $processIdentity.ManagedAccount = $searchSvcManagedAccount
19: Write-Host "Updating the service process identity..."
20: $processIdentity.Update()
21: $searchSvc.Update()
22: }
This is where things start to get interesting. I use the Get-Credential cmdlet to return back the credentials of the user to use for the service but once I have that there's no parameter on any cmdlet that will allow me to set the credential so I have to do it using the object model. I use the $searchSvc variable from earlier and edit the object returned by the ProcessIdentity property (after confirming that the value needs to be changed).
Once we have the process set we can go ahead and set the other simple properties on the service - fortunately the cmdlet Set-SPSearchService can actually help us out with this one:
1: #It doesn't hurt if this runs more than once so we don't bother checking before running.
2: Write-Host "Updating the search service properties..."
3: $searchSvc | Set-SPSearchService `
4: -CrawlAccount $crawlAccount.Username `
5: -CrawlPassword $crawlAccount.Password `
6: -AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
7: -MaxBackupDuration $svcConfig.MaxBackupDuration `
8: -PerformanceLevel $svcConfig.PerformanceLevel `
9: -ErrorVariable err `
10: -ErrorAction SilentlyContinue
11: if ($err) {
12: throw $err
13: }
Alright, that was the easy stuff - now we have to deal with the database. The first step is to see if there's already a database defined for the service and if it matches what we want. This is important as we want to be able to run the script more than once so we don't want to just blindly delete and recreate the database. The first bit of code builds a connection string using the SqlConnectionStringBuilder object (note that in PowerShell you have to use the PSBase property to access the properties on this object) and then compares that to what is currently set. If a match is not found then the existing database is deleted and the search service updated:
1: #Build the connection string to the new database.
2: [System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
3: $builder1.psbase.DataSource = $dbServer
4: $builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
5: $builder1.psbase.IntegratedSecurity = $true
6: Write-Host "Proposed database connection: {$builder1}"
7:
8: [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
9: $dbMatch = $false
10: if ($searchDb -ne $null) {
11: #A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
12: [System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
13: Write-Host "Existing database connection: {$builder2}"
14: if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
15: $dbMatch = $true
16: }
17: if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
18: $dbMatch = $true
19: }
20: if (!$dbMatch) {
21: #The database does not match the configuration provided so delete it.
22: Write-Host "The specified database details do not match existing details. Clearing existing."
23: $searchSvcInstance.SearchDatabase = $null
24: $searchSvcInstance.Update()
25: Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
26: $searchDb.Delete()
27: Write-Host "Finished deleting search DB."
28: $searchDb = $null
29: } else {
30: Write-Host "Existing Database details match provided details ($($builder2))"
31: }
32: }
At this point if the $searchDb variable is null then we want to go ahead and create a new search database:
1: #If we don't have a DB go ahead and create one.
2: if ($searchDb -eq $null) {
3: $dbCreated = $false
4: try
5: {
6: Write-Host "Creating new search database {$builder1}..."
7: $searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
8: Write-Host "Provisioning new search database..."
9: $searchDb.Provision()
10: Write-Host "Provisioning search database complete."
11: $dbCreated = $true
12:
13: #Re-get the service to avoid update conflicts
14: $searchSvc = Get-SPSearchService
15: $searchSvcInstance = Get-SPSearchServiceInstance -Local
16:
17: Write-Host "Associating new database with search service instance..."
18: $searchSvcInstance.SearchDatabase = $searchDb
19: Write-Host "Updating search service instance..."
20: $searchSvcInstance.Update()
21:
22: #Re-get the service to avoid update conflicts
23: $searchSvc = Get-SPSearchService
24: $searchSvcInstance = Get-SPSearchServiceInstance -Local
25: }
26: catch
27: {
28: if ($searchDb -ne $null -and $dbCreated) {
29: Write-Warning "An error occurred updating the search service instance, deleting search database..."
30: try
31: {
32: #Clean up
33: $searchDb.Delete()
34: }
35: catch
36: {
37: Write-Warning "Unable to delete search database."
38: Write-Error $_
39: }
40: }
41: throw $_
42: }
43: }
I first create a new SPSearchDatabase object by calling the static Create() method and passing in the SqlConnectionStringBuilder object that was previously created. I then call the Provision() method to actually create the database on the SQL server instance. Once it's created we can associate the database with the service by setting the SearchDatabase property on the $searchSvcInstance variable. If an error occurs then I attempt to delete the database from SQL Server if it's not yet associated with the service.
Now that we have our database provisioned we can go ahead and set the failover server:
1: #Set the database failover server
2: if (![string]::IsNullOrEmpty($failoverDbServer)) {
3: if (($searchDb.FailoverServiceInstance -eq $null) -or `
4: ![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
5: {
6: try
7: {
8: Write-Host "Adding failover database instance..."
9: $searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
10: Write-Host "Updating search service instance..."
11: $searchSvcInstance.Update()
12: }
13: catch
14: {
15: Write-Warning "Unable to set failover database server. $_"
16: }
17: }
18: }
Most of the logic here is just in determining whether or not to set the failover server. Basically you just call the AddFailoverServiceInstance() method of the SearchDatabase property (SPSearchDatabase) and then update the service instance.
We're almost there - we've set all the properties we can now we need to complete the provisioning process:
1: $status = $searchSvcInstance.Status
2: #Provision the service instance on the current server
3: if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
4: if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
5: try
6: {
7: Write-Host "Provisioning search service instance..."
8: $searchSvcInstance.Provision()
9: }
10: catch
11: {
12: Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
13: if ($status -ne $searchSvcInstance.Status) {
14: try
15: {
16: $searchSvcInstance.Status = $status
17: $searchSvcInstance.Update()
18: }
19: catch
20: {
21: Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
22: }
23: }
24: throw $_
25: }
26: }
27: }
If the service instance is not currently marked as Online (again, accounting for multiple runs) and the service instance we're working with is for the current machine then we call the Provision() method on the service instance. If an error occurs provisioning the service then I try to set the status back to its previous value.
Only two steps left; First we need to create a timer job to trigger the search service instance to be provisioned on the other servers in the farm:
1: #Re-get the service to avoid update conflicts
2: $searchSvc = Get-SPSearchService
3:
4: #Create the timer job to update the instances for the other servers.
5: foreach ($serviceInstance in $searchSvc.Instances) {
6: if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
7: -and $serviceInstance -ne $searchSvcInstance `
8: -and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
9: $definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
10: if ($definition -ne $null) {
11: Write-Host "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
12: } else {
13: Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
14: $job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
15: $job.Update($true)
16: }
17: }
18: }
And finally, we need to set the ProxyType for the service instances so I loop through the <Server /> elements and call the Set-SPSearchServiceInstance cmdlet, providing the ProxyType attribute as defined in the XML:
1: #Set the proxy type for all the service instances.
2: $svcConfig.Servers.Server | ForEach-Object {
3: $server = $_
4: $instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
5: if ($instance -ne $null `
6: -and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
7: Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
8: $instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType
9: }
10: }
Phew - we're done! Let's put it all together now - here's the complete script:
function Start-FoundationSearch([string]$settingsFile = "Configurations.xml") {
[xml]$config = Get-Content $settingsFile
$svcConfig = $config.Services.FoundationSearchService
#See if we want to start the svc on the current server.
$install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
if (!$install) {
Write-Host "Machine not specified in Servers element, service will not be started on this server."
return
}
#Start the service instance
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
if ($svc -eq $null) {
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
}
Start-SPServiceInstance -Identity $svc
#Get the service and service instance
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
$dbServer = $svcConfig.DatabaseServer
$failoverDbServer = $svcConfig.FailoverDatabaseServer
#Get the service account details
Write-Host "Provide the username and password for the search crawl account..."
$crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
Write-Host "Provide the username and password for the search service account..."
$searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name
#Get or Create a managed account for the search service account.
$searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
}
#Set the account details if different than what is current.
$processIdentity = $searchSvc.ProcessIdentity
if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
$processIdentity = $searchSvc.ProcessIdentity
$processIdentity.CurrentIdentityType = "SpecificUser"
$processIdentity.ManagedAccount = $searchSvcManagedAccount
Write-Host "Updating the service process identity..."
$processIdentity.Update()
$searchSvc.Update()
}
#It doesn't hurt if this runs more than once so we don't bother checking before running.
Write-Host "Updating the search service properties..."
$searchSvc | Set-SPSearchService `
-CrawlAccount $crawlAccount.Username `
-CrawlPassword $crawlAccount.Password `
-AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
-MaxBackupDuration $svcConfig.MaxBackupDuration `
-PerformanceLevel $svcConfig.PerformanceLevel `
-ErrorVariable err `
-ErrorAction SilentlyContinue
if ($err) {
throw $err
}
#Build the connection string to the new database.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder1.psbase.DataSource = $dbServer
$builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
$builder1.psbase.IntegratedSecurity = $true
Write-Host "Proposed database connection: {$builder1}"
[Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
$dbMatch = $false
if ($searchDb -ne $null) {
#A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
Write-Host "Existing database connection: {$builder2}"
if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch) {
#The database does not match the configuration provided so delete it.
Write-Host "The specified database details do not match existing details. Clearing existing."
$searchSvcInstance.SearchDatabase = $null
$searchSvcInstance.Update()
Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
$searchDb.Delete()
Write-Host "Finished deleting search DB."
$searchDb = $null
} else {
Write-Host "Existing Database details match provided details ($($builder2))"
}
}
#If we don't have a DB go ahead and create one.
if ($searchDb -eq $null) {
$dbCreated = $false
try
{
Write-Host "Creating new search database {$builder1}..."
$searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
Write-Host "Provisioning new search database..."
$searchDb.Provision()
Write-Host "Provisioning search database complete."
$dbCreated = $true
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
Write-Host "Associating new database with search service instance..."
$searchSvcInstance.SearchDatabase = $searchDb
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
}
catch
{
if ($searchDb -ne $null -and $dbCreated) {
Write-Warning "An error occurred updating the search service instance, deleting search database..."
try
{
#Clean up
$searchDb.Delete()
}
catch
{
Write-Warning "Unable to delete search database."
Write-Error $_
}
}
throw $_
}
}
#Set the database failover server
if (![string]::IsNullOrEmpty($failoverDbServer)) {
if (($searchDb.FailoverServiceInstance -eq $null) -or `
![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
{
try
{
Write-Host "Adding failover database instance..."
$searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Unable to set failover database server. $_"
}
}
}
$status = $searchSvcInstance.Status
#Provision the service instance on the current server
if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
try
{
Write-Host "Provisioning search service instance..."
$searchSvcInstance.Provision()
}
catch
{
Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
if ($status -ne $searchSvcInstance.Status) {
try
{
$searchSvcInstance.Status = $status
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
}
}
throw $_
}
}
}
#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
#Create the timer job to update the instances for the other servers.
foreach ($serviceInstance in $searchSvc.Instances) {
if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
-and $serviceInstance -ne $searchSvcInstance `
-and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
$definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
if ($definition -ne $null) {
Write-Host "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
} else {
Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
$job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
$job.Update($true)
}
}
}
#Set the proxy type for all the service instances.
$svcConfig.Servers.Server | ForEach-Object {
$server = $_
$instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
if ($instance -ne $null `
-and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
$instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType
}
}
}
One thing you should note is that I'm not setting the schedule for the service. This is because the timer job class that I'd need to use to set the schedule is marked internal thus making it impossible for me to set the schedule without using reflection.
As you can see we're in a bit of a conundrum with SharePoint 2010 - scripting your installations is considered to be a best practice and you should strive to do so whenever possible but the level of complexity involved with scripting such simple things has made it prohibitively complex for the average administrator to do.
I recognized this issue the very first day I started working with SharePoint 2010 and to solve the problem I've been working on a product for ShareSquared called SharePoint Composer which will allow administrators, architects, and developers to visually design their SharePoint configurations and then build out the entire Farm using the model they create in the design tool. This tool will allow you to enforce your corporate standards by clearly documenting every configuration and building the farm based on those configurations in a single-click, automated way - all without having to know any PowerShell at all! Keep a watch here for more information about SharePoint Composer.
Note - I've not had a chance to test this in a multi-server farm so if anyone can give me some feedback about their experiences with it I'd greatly appreciate it.
Creating a SharePoint 2010 Enterprise Search Service Application using PowerShell
The information in this post is specific to SharePoint 2010 Beta 2 and may need adjusting for the RTM version.
In an effort to continue with my previous posts where I demonstrated how to build a basic farm and it's site structure using XML configuration files and PowerShell for SharePoint 2010 I would like to now share how to create a search service application. An automated install of the service applications is, without a doubt, the most difficult PowerShell task you'll undertake when scripting your SharePoint 2010 install, specifically the search application is the most difficult which is why I've chosen to explain it first as I expect it to be one of the most needed and one of the least understood. Note that I'm not planning on giving any depth to what the various components are, there's plenty of other resources that will explain what the admin component is, for example.
To start off let's look at the XML file that will drive our setup. Like my previous examples I have a fairly simplistic XML structure that drives all my configurations. This structure allows me to create as many service application instances as needed, each with their own configurations:
<Services>
<EnterpriseSearchService ContactEmail="no-reply@sp2010.com"
ConnectionTimeout="60"
AcknowledgementTimeout="60"
ProxyType="Default"
IgnoreSSLWarnings="false"
InternetIdentity="Mozilla/4.0 (compatible; MSIE 4.01; Windows NT; MS Search 6.0 Robot)"
IndexLocation="c:\sharepoint\indexes"
PerformanceLevel="PartlyReduced"
Account="sp2010\spsearch">
<EnterpriseSearchServiceApplications>
<EnterpriseSearchServiceApplication Name="Enterprise Search Service Application"
DatabaseServer="spsql1"
DatabaseName="SharePoint_Search"
FailoverDatabaseServer=""
Partitioned="false"
Partitions="1"
SearchServiceApplicationType="Regular">
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearch" />
<CrawlServers>
<Server Name="sp2010b2" />
</CrawlServers>
<QueryServers>
<Server Name="sp2010b2" />
</QueryServers>
<SearchQueryAndSiteSettingsServers>
<Server Name="sp2010b2" />
</SearchQueryAndSiteSettingsServers>
<AdminComponent>
<Server Name="sp2010b2" />
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearchsvc" />
</AdminComponent>
<Proxy Name="Enterprise Search Service Application Proxy" Partitioned="false">
<ProxyGroup Name="Default" />
</Proxy>
</EnterpriseSearchServiceApplication>
</EnterpriseSearchServiceApplications>
</EnterpriseSearchService>
</Services>
Examining the structure above you can see that I chose to put the <EnterpriseSearchService /> element under a <Services /> element - this will allow me to have all my service configurations in one file rather than a separate file for each service (note that there can be only one <EnterpriseSearchService /> element). Under the <EnterpriseSearchService /> element I have a container element for the applications - there should be only one <EnterpriseSearchServiceApplications /> elements but you can have as many <EnterpriseSearchServiceApplication /> elements under it. The application element is where all the meat of the configurations are. Within this element you define the application pool to use, the crawl and query servers to use, and the server for the administrative component, and finally the proxy definition and it's proxy group memberships. The <CrawlServers /> and <QueryServers /> elements can have as many <Server /> child elements as needed but the <AdminComponent /> element can have only one <Server /> child element. And finally the <Proxy /> element can have as many <ProxyGroup /> child elements as desired.
Okay, so that's the easy part - hopefully you can begin to see the power and flexibility of this simple XML file. No for the scripts - first we need to look at a couple of helper functions, one to get/create our application pools and another for the proxy group memberships. Let's take a look at the application pool function which I called Get-ApplicationPool:
function Get-ApplicationPool([System.Xml.XmlElement]$appPoolConfig) {
#Try and get the application pool if it already exists
$pool = Get-SPIisWebServiceApplicationPool -Identity $appPoolConfig.Name -ErrorVariable err -ErrorAction SilentlyContinue
if ($err) {
#The application pool does not exist so create.
Write-Host "Getting $($appPoolConfig.Account) account for application pool..."
$managedAccount = (Get-SPManagedAccount -Identity $appPoolConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$accountCred = Get-Credential $appPoolConfig.Account
$managedAccount = New-SPManagedAccount -Credential $accountCred
}
Write-Host "Creating application pool $($appPoolConfig.Name)..."
$pool = New-SPIisWebServiceApplicationPool -Name $appPoolConfig.Name -Account $managedAccount
}
return $pool
}
In this function I'm attempting to get the application pool if it already exists and if it doesn't then I proceed to attempt to get the managed account that will be associated with the application pool. If the managed account doesn't exist then I prompt for credentials and then create the managed account which I then use to create the application pool which gets returned to the calling function.
The next function, which I've named Set-ProxyGroupMembership associates my service application proxy with one or more proxy groups:
function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject)
{
begin {}
process {
$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 {}
}
This function is probably a bit more complicated than it needs to be but I'm going to use it with every service application script so I'll explain it briefly here and just reference this post in my future posts. For this function I wanted to be able to pass the proxy object that I created into the function using the pipeline rather than a parameter (it just flowed better that way and allowed me to pass more than one proxy if I desired without having to write a loop within the function). The first thing I'm doing in this function is clearing out any existing proxy group assignments that may have been set automatically but are not what I want per the XML file. Once I've cleared undesired assignments then I add any missing assignments. Some service applications will automatically add the proxy to the default proxy group which may not be what you want.
Now that we have our two helper functions out of the way we can start looking at the core function. I'll talk about it in chunks and then at the end of this post provide the complete function.
The first thing I do is load the XML file to a variable, $svcConfig, which I use throughout the function:
1: [xml]$config = Get-Content $settingsFile
2: $svcConfig = $config.Services.EnterpriseSearchService
Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <EnterpriseSearchService /> element and set that to the $svcConfig variable. Next I need to get the search service itself and set that to a variable which I'll use throughout the function as well. I pass the -Local switch in to get the service instance on the current machien. If I'm unable to find a service instance then something is wrong and I throw an error:
1: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
2: if ($searchSvc -eq $null) {
3: throw "Unable to retrieve search service."
4: }
Next I need to get the managed account that will be used for the search service. I first try to retrieve the account in case it already exists and if it doesn't exist then I create after asking the user for the password:
1: Write-Host "Getting $($svcConfig.Account) account for search service..."
2: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
3: if ($err) {
4: $searchSvcAccount = Get-Credential $svcConfig.Account
5: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
6: }
Now that we have a managed account and service instance we can set the core properties for the search service. I end up doing this on every machine but it only needs to be done once - just easier to set it every time rather than try and figure out if it's been set yet and doing so has no negative repercussions:
1: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
2: -ServiceAccount $searchSvcManagedAccount.Username `
3: -ServicePassword $searchSvcManagedAccount.SecurePassword `
4: -ContactEmail $svcConfig.ContactEmail `
5: -ConnectionTimeout $svcConfig.ConnectionTimeout `
6: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
7: -ProxyType $svcConfig.ProxyType `
8: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
9: -InternetIdentity $svcConfig.InternetIdentity `
10: -PerformanceLevel $svcConfig.PerformanceLevel
11:
12: Write-Host "Setting default index location on search service..."
13: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
The core service settings are in place, now it's time to create all the service applications. In the example XML we have just one but we could have more so I use the ForEach-Object cmdlet to loop through all the definitions:
1: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
The first thing we need to do to create our app is to create the application pool for the service application itself and the administration component:
1: $appConfig = $_
2:
3: #Try and get the application pool if it already exists
4: $pool = Get-ApplicationPool $appConfig.ApplicationPool
5: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
Before creating the application pools I store the current XML element in the $appConfig node for easier reference and to avoid conflicts with sub-loops. I then call the helper function I showed earlier to create the two application pools which I'll use later. Next I check to see if the service application has already been created (line 1 below) by calling Get-SPEnterpriseSearchServiceApplication and if it does not exist then I create a new one. This helps when you have to run the script again due to possible errors that may occur later in the script (I've often seen update conflict errors occur randomly, running the script again is usually all that's necessary):
1: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
2: if ($searchApp -eq $null) {
3: Write-Host "Creating enterprise search service application..."
4: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
5: -DatabaseServer $appConfig.DatabaseServer `
6: -DatabaseName $appConfig.DatabaseName `
7: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
8: -ApplicationPool $pool `
9: -AdminApplicationPool $adminPool `
10: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
11: -SearchApplicationType $appConfig.SearchServiceApplicationType
12: } else {
13: Write-Host "Enterprise search service application already exists, skipping creation."
14: }
Now that the service application exists we can go ahead and create the proxy and set the proxy group memberships:
1: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
2: if ($proxy -eq $null) {
3: Write-Host "Creating enterprise search service application proxy..."
4: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
5: } else {
6: Write-Host "Enterprise search service application proxy already exists, skipping creation."
7: }
8: if ($proxy.Status -ne "Online") {
9: $proxy.Status = "Online"
10: $proxy.Update()
11: }
12: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
Like with the service application I first try and get the proxy in case it has already been created and if I don't find it then I create it. Once I have a reference to the proxy object I check to see if it's online and if not then I set it online and call Update() to commit the change. And finally I call the Set-ProxyGroupsMembership function that I previously defined.
The intent of the script is to allow it to be run on multiple servers to support a multi-server scripted deployment. That's where this next bit comes in:
1: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
2: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
3: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
4: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
For both the crawl servers, query servers, and admin component I get the name of the current computer ($env:computername) and then check to see if an <Server /> element has been declared with a matching name for the specific component. The variables declared are then used throughout the rest of the script.
Before I can create the crawl or query component I need start search service instance that we previously acquired:
1: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
2: $searchSvc | Start-SPEnterpriseSearchServiceInstance
3: }
If the service isn't already online and if we're on an appropriate server then I start the service by passing the service instance to the Start-SPEnterpriseSearchServiceInstance cmdlet. Next I need to set the administration component:
1: if ($installAdminCmpnt) {
2: Write-Host "Setting administration component..."
3: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
4: }
The trick with this bit is that you have to set the administration component before you can set the query or crawl components so the first time you run this script it must be on the sever that is to run the administration component - short of having the user run the script multiple times on the same server and adding appropriate code to handle that I've not come up with any way around this - frankly, it sucks, big time - so be careful with this one!
Okay, we're about halfway through, still with me?
Now it's time to create the crawl topology:
1: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($crawlTopology -eq $null) {
3: Write-Host "Creating new crawl topology..."
4: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
5: } else {
6: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
7: }
8:
9: if ($installCrawlSvc) {
10: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
11: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
12: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
13: Write-Host "Creating new crawl component..."
14: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
15: } else {
16: Write-Host "Crawl component already exist, skipping crawl component creation."
17: }
18: }
On line 1 I'm getting all existing crawl topologies for the service application (Get-SPEnterpriseSearchCrawlTopology) and filtering on whether or not the crawl topology has components and is active or not. I do this because when the search application is created it automatically creates a crawl topology for us but that topology is not configured correctly (there are no crawl components) but once the topology has been made active it doesn't let us change it in order to add crawl components. When I create our new topology it will be inactive so I will use this fact when I run the script on the next server. Once I have the crawl topology I can then add the crawl components using the New-SPEnterpriseSearchCrawlComponent cmdlet (note that you have to pass in the crawl store ID so I have to get that ID as shown in line 12).
After we create crawl topology and components we do essentially the exact same thing for the query topology and components:
1: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($queryTopology -eq $null) {
3: Write-Host "Creating new query topology..."
4: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
5: } else {
6: Write-Host "A query topology with query components already exists, skipping query topology creation."
7: }
8: if ($installQuerySvc) {
9: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
10: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
11: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
12: Write-Host "Creating new query component..."
13: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
14: Write-Host "Setting index partition and property store database..."
15: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
16: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
17: } else {
18: Write-Host "Query component already exist, skipping query component creation."
19: }
20: }
Great! We have our admin component created, our crawl topology and components created, and our query topology and components created. Now we just need to make things active. There's nothing more to do with the admin component so we'll first start the "Search Query and Site Settings Service" and then continue with the crawl topology:
1: if ($installSyncSvc) {
2: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
3: }
So starting the query and site settings service was easy, now lets move on to the hard stuff:
1: #Don't activate until we've added all components
2: $allCrawlServersDone = $true
3: $appConfig.CrawlServers.Server | ForEach-Object {
4: $server = $_.Name
5: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
6: if ($top -eq $null) { $allCrawlServersDone = $false }
7: }
8:
9: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
10: Write-Host "Setting new crawl topology to active..."
11: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
14: while ($true) {
15: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
16: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
17: if ($ct.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original crawl topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
25: }
The first thing I do is set a variable to indicate whether I've gotten all designated crawl servers configured - we don't want to set the crawl topology active until all the servers have been configured because once we make it active we can't change it (this is critical if you are planning on doing a phased server roll-out - you will need to rebuild your topology if you need to add additional crawl or query components). On line 11 I set the topology as active using the Set-SPEnterpriseSearchCrawlTopology cmdlet. Problem is not quite that simple - you see, this cmdlet runs asynchronously, meaning that it returns immediately and does not wait until the service is made active - this is critical because we can't proceed to the query piece until the crawl topology is active so all I'm doing in lines 14 through 22 is checking the status and if it's not "Ready" then I sleep for 2 seconds and try again.
Only one more thing - now that the crawl topology is active we do, once again, the same thing for the query topology:
1: $allQueryServersDone = $true
2: $appConfig.QueryServers.Server | ForEach-Object {
3: $server = $_.Name
4: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
5: if ($top -eq $null) { $allQueryServersDone = $false }
6: }
7:
8: #Make sure we have a crawl component added and started before trying to enable the query component
9: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
10: Write-Host "Setting query topology as active..."
11: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
14: while ($true) {
15: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
16: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
17: if ($qt.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original query topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
25: }
This code is identical to that of the crawl topology but uses the query specific cmdlets.
And, finally, after about 236 lines of code, we're done! Makes me miss the days of MOSS 2007 where I could start search with one line of STSADM (maybe I need to create a Start-OSearch cmdlet
). So, putting it all together, here's the complete function:
1: function Start-EnterpriseSearch([string]$settingsFile = "Configurations.xml") {
2: [xml]$config = Get-Content $settingsFile
3: $svcConfig = $config.Services.EnterpriseSearchService
4:
5: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
6: if ($searchSvc -eq $null) {
7: throw "Unable to retrieve search service."
8: }
9:
10: Write-Host "Getting $($svcConfig.Account) account for search service..."
11: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
12: if ($err) {
13: $searchSvcAccount = Get-Credential $svcConfig.Account
14: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
15: }
16:
17: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
18: -ServiceAccount $searchSvcManagedAccount.Username `
19: -ServicePassword $searchSvcManagedAccount.SecurePassword `
20: -ContactEmail $svcConfig.ContactEmail `
21: -ConnectionTimeout $svcConfig.ConnectionTimeout `
22: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
23: -ProxyType $svcConfig.ProxyType `
24: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
25: -InternetIdentity $svcConfig.InternetIdentity `
26: -PerformanceLevel $svcConfig.PerformanceLevel
27:
28: Write-Host "Setting default index location on search service..."
29: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
30:
31: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
32: $appConfig = $_
33:
34: #Try and get the application pool if it already exists
35: $pool = Get-ApplicationPool $appConfig.ApplicationPool
36: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
37:
38: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
39: if ($searchApp -eq $null) {
40: Write-Host "Creating enterprise search service application..."
41: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
42: -DatabaseServer $appConfig.DatabaseServer `
43: -DatabaseName $appConfig.DatabaseName `
44: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
45: -ApplicationPool $pool `
46: -AdminApplicationPool $adminPool `
47: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
48: -SearchApplicationType $appConfig.SearchServiceApplicationType
49: } else {
50: Write-Host "Enterprise search service application already exists, skipping creation."
51: }
52:
53: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
54: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
55: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
56: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
57:
58: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
59: $searchSvc | Start-SPEnterpriseSearchServiceInstance
60: }
61:
62: if ($installAdminCmpnt) {
63: Write-Host "Setting administration component..."
64: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
65: }
66:
67: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
68: if ($crawlTopology -eq $null) {
69: Write-Host "Creating new crawl topology..."
70: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
71: } else {
72: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
73: }
74:
75: if ($installCrawlSvc) {
76: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
77: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
78: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
79: Write-Host "Creating new crawl component..."
80: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
81: } else {
82: Write-Host "Crawl component already exist, skipping crawl component creation."
83: }
84: }
85:
86: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
87: if ($queryTopology -eq $null) {
88: Write-Host "Creating new query topology..."
89: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
90: } else {
91: Write-Host "A query topology with query components already exists, skipping query topology creation."
92: }
93: if ($installQuerySvc) {
94: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
95: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
96: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
97: Write-Host "Creating new query component..."
98: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
99: Write-Host "Setting index partition and property store database..."
100: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
101: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
102: } else {
103: Write-Host "Query component already exist, skipping query component creation."
104: }
105: }
106:
107: if ($installSyncSvc) {
108: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
109: }
110:
111: #Don't activate until we've added all components
112: $allCrawlServersDone = $true
113: $appConfig.CrawlServers.Server | ForEach-Object {
114: $server = $_.Name
115: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
116: if ($top -eq $null) { $allCrawlServersDone = $false }
117: }
118:
119: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
120: Write-Host "Setting new crawl topology to active..."
121: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
122:
123: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
124: while ($true) {
125: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
126: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
127: if ($ct.State -eq "Active" -and $state -eq $null) {
128: break
129: }
130: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
131: Start-Sleep 2
132: }
133: # Need to delete the original crawl topology that was created by default
134: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
135: }
136:
137: $allQueryServersDone = $true
138: $appConfig.QueryServers.Server | ForEach-Object {
139: $server = $_.Name
140: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
141: if ($top -eq $null) { $allQueryServersDone = $false }
142: }
143:
144: #Make sure we have a crawl component added and started before trying to enable the query component
145: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
146: Write-Host "Setting query topology as active..."
147: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
148:
149: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
150: while ($true) {
151: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
152: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
153: if ($qt.State -eq "Active" -and $state -eq $null) {
154: break
155: }
156: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
157: Start-Sleep 2
158: }
159: # Need to delete the original query topology that was created by default
160: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
161: }
162:
163: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
164: if ($proxy -eq $null) {
165: Write-Host "Creating enterprise search service application proxy..."
166: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
167: } else {
168: Write-Host "Enterprise search service application proxy already exists, skipping creation."
169: }
170: if ($proxy.Status -ne "Online") {
171: $proxy.Status = "Online"
172: $proxy.Update()
173: }
174: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
175:
176: }
177: }
178:
This script took me an incredible amount of time to figure out and I really hope others are able to benefit from it. If you find areas of improvement or anything that requires correction please, please, please post a comment so that I and others can benefit from your experiences with it.
Also, this script is a derivative of a slightly more complex one that I use for all my stuff and though that more complex script has gone through many rounds of testing this one has not - mainly I've not had a chance to test in a multi-server environment and have only had time to do a single server deploy (though the changes related to the servers were very small and, if they were to fail, would likely have failed on the single server). Mainly try to remember that the product is still in beta so you should expect that things may either change between now and RTM or things may just not work from one environment to the next.
Good luck and happy scripting!
Creating a SharePoint 2010 Site Structure Using PowerShell
In a previous post I detailed how to use PowerShell to perform what would be otherwise done using PSConfig to create an initial SharePoint Farm. In this post I will continue the example and show how to create your web applications using a simple XML configuration file and a reusable script.
Like the previous example I have a very basic XML file that defines my web application structure. In this example I've included not only the web application and application pool but also the content databases and site collections, along with the SharePoint Designer settings. Consider the XML and corresponding PowerShell a starting place to extend further if needed by adding elements for managed paths, quota templates, sites and even lists. Here's the XML which I store in a file called WebAppConfigurations.xml:
<WebApplications>
<WebApplication Name="SharePoint Portal (80)"
DefaultTimeZone="12"
DefaultQuotaTemplate="Portal"
AllowAnonymous="false"
AuthenticationMethod="NTLM"
HostHeader="portal"
Path="c:\sharepoint\webs\portal"
Port="80"
LoadBalancedUrl="http://portal"
Ssl="false">
<ApplicationPool Name="SharePoint Portal App Pool"
Account="sp2010\spportalapppool" />
<SPDesigner AllowDesigner="true" AllowRevertFromTemplate="true"
AllowMasterPageEditing="true" ShowURLStructure="true" />
<ContentDatabases>
<ContentDatabase Server="spsql1"
Name="SharePoint_Content_Portal1"
MaxSiteCount="100" WarningSiteCount="80"
Default="true">
<SiteCollections>
<SiteCollection Name="Portal"
Description=""
Url="http://portal"
LCID="1033"
Template="SPSPORTAL#0"
OwnerLogin="sp2010\siteowner1"
OwnerEmail="siteowner1@sp2010.com"
SecondaryLogin="sp2010\spadmin"
SecondaryEmail="spadmin@sp2010.com">
</SiteCollection>
</SiteCollections>
</ContentDatabase>
</ContentDatabases>
</WebApplication>
</WebApplications>
Note that you could easily adapt the file by having the <WebApplications /> element be a child of the <Farm /> element shown in my previous post resulting in a single configuration file rather than multiple files. One thing to note is that I'm not storing the password for the application pool account which I assume exists - the password will be asked for when the script runs.
Let's take a look at the script that does all the work:
function Start-WebApplicationsBuild( [string]$settingsFile = "Configurations.xml") { [xml]$config = Get-Content $settingsFile #Creating individual web applications $config.WebApplications.WebApplication | ForEach-Object { $webAppConfig = $_ $webApp = New-WebApplication $webAppConfig #Configuring SharePoint Designer Settings $spd = $webAppConfig.SPDesigner $allowRevert = ([bool]::Parse($spd.AllowRevertFromTemplate)) $allowMasterEdit = ([bool]::Parse($spd.AllowMasterPageEditing)) Write-Host "Setting SP Designer settings..." $webApp | Set-SPDesignerSettings ` -AllowDesigner:([bool]::Parse($spd.AllowDesigner)) ` -AllowRevertFromTemplate:$allowRevert ` -AllowMasterPageEditing:$allowMasterEdit ` -ShowURLStructure:([bool]::Parse($spd.ShowURLStructure)) $webAppConfig.ContentDatabases.ContentDatabase | ForEach-Object { #Creating content database Write-Host "Creating content database $($_.Name)..." $db = New-SPContentDatabase -Name $_.Name ` -WebApplication $webApp ` -DatabaseServer $_.Server ` -MaxSiteCount $_.MaxSiteCount ` -WarningSiteCount $_.WarningSiteCount $_.SiteCollections.SiteCollection | ForEach-Object { #Creating site collection Write-Host "Creating site collection $($_.Url)..." $gc = Start-SPAssignment $site = $gc | New-SPSite ` -Url $_.Url ` -ContentDatabase $db ` -Description $_.Description ` -Language $_.LCID ` -Name $_.Name ` -Template $_.Template ` -OwnerAlias $_.OwnerLogin ` -OwnerEmail $_.OwnerEmail ` -SecondaryOwnerAlias $_.SecondaryLogin ` -SecondaryEmail $_.SecondaryEmail Stop-SPAssignment -SemiGlobal $gc } } } } function New-WebApplication([System.Xml.XmlElement]$webAppConfig) { $poolAccount = $null $tempAppPool = $null $poolName = $webAppConfig.ApplicationPool.Name if ([Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools.Count -gt 0) { $tempAppPool = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools | ? {$_.Name -eq $poolName} } if ($tempAppPool -eq $null) { Write-Host "Getting $($webAppConfig.ApplicationPool.Account) account for application pool..." $accountCred = Get-Credential $webAppConfig.ApplicationPool.Account $poolAccount = (Get-SPManagedAccount -Identity $accountCred.Username -ErrorVariable err -ErrorAction SilentlyContinue) if ($err) { $poolAccount = New-SPManagedAccount -Credential $accountCred } } $allowAnon = [bool]::Parse($webAppConfig.AllowAnonymous.ToString()) $ssl = [bool]::Parse($webAppConfig.Ssl.ToString()) $db = $null if ($webAppConfig.ContentDatabases.ChildNodes.Count -gt 1) { $db = $webAppConfig.ContentDatabases.ContentDatabase | ` where {$_.Default -eq "true"} if ($db -is [array]) { $db = $db[0] } } else { $db = $webAppConfig.ContentDatabases.ContentDatabase } #Create the web application Write-Host "Creating web application $($webAppConfig.Name)..." $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl ` -AllowAnonymousAccess:$allowAnon ` -ApplicationPool $poolName ` -ApplicationPoolAccount $poolAccount ` -Name $webAppConfig.Name ` -AuthenticationMethod $webAppConfig.AuthenticationMethod ` -DatabaseServer $db.DatabaseServer ` -DatabaseName $db.DatabaseName ` -HostHeader $webAppConfig.HostHeader ` -Path $webAppConfig.Path ` -Port $webAppConfig.Port ` -Url $webAppConfig.LoadBalancedUrl ` -ErrorVariable err return $webApp }
I've put the script in two different functions with Start-WebApplicationsBuild being the primary function that is called by the logged in user. The other function, New-WebApplication, is just there for readability (I wanted to separate out the code that created the application pool and web application itself). Note that, like in my previous post, I use a more complex version of this script which has the various elements broken out into many different shared helper functions and considerably more tracing and error handling added - this script is a fairly simplistic version which lets you focus on the core SharePoint 2010 PowerShell stuff without polluting the code with lots of plumbing.
With this script and XML file structure you can create as many web applications, content databases, and site collections as needed by only modifying the XML file - the script will support any number of each. One thing to be careful of - make sure you have only one <ContentDatabase /> element with a Default attribute set to "true" (this is the database that will be created when the web application is created - you may have as many <ContentDatabase /> elements as needed but you need at least one with a Default value of true).
Hopefully this script proves useful to anyone who needs to automatically create their SharePoint 2010 site structure. Stay tuned for the next piece of the scripts which will cover provisioning service applications.
Creating Custom SharePoint 2010 Cmdlets using Visual Studio 2010
With SharePoint 2010 we now have the ability to create custom PowerShell cmdlets that can be deployed just like any other SharePoint artifact using SharePoint Solution Packages (WSP) created with Visual Studio 2010. With SharePoint 2007 it was necessary to build a custom setup (MSI) package which had to be run on every server in the farm. This setup package would register a custom snap-in that you'd have to create which would be responsible for registering all of your custom cmdlets with the PowerShell runtime.
With SharePoint 2010 we no longer have to create a custom snap-in or setup package. When the Microsoft.SharePoint.PowerShell snap-in is loaded it examines the {SharePointRoot}/Config/PowerShell/Registration folder for any XML files and dynamically registers the cmdlets specified in the XML. As long as the SharePoint binaries have been installed on the server then you can utilize this feature (if the farm has not yet been created then you'll have to manually GAC the assembly and deploy the registration XML file as solution deployments only work when the farm exists).
To facilitate a standard and consistent scripting experience SharePoint 2010 introduces five new base classes that all SharePoint 2010 PowerShell cmdlets should be derived from:
When creating your custom cmdlet you should carefully choose the correct base class for your cmdlet. When creating a cmdlet that is meant to work with persistent objects (objects that are to be used across calls) you should utilize one of the four task based base classes: SPRemoveCmdletBase, SPNewCmdletBase, SPSetCmdletBase, or SPGetCmdletBase. When creating cmdlets that return non-persistent objects/data or perform tasks that do not require a persistent object (e.g., Start-SP*) then you should use the SPCmdlet base class. A good example of a cmdlet that would use the SPCmdlet base class would be one what returns a report or some other information without returning back any specific objects.
Let's now take a look at an example of a custom cmdlet that we'll eventually package up in a SharePoint Solution Package:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;
using System.Management.Automation;
namespace Lapointe.SharePoint2010.PowerShell.Demo.Quotas
{
[Cmdlet(VerbsCommon.Get, "SPQuotaTemplate"),
SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = true)]
public class SPCmdletGetQuotaTemplate : SPGetCmdletBase<SPQuotaTemplate>
{
protected override void InternalValidate()
{
if (this.Identity != null)
{
base.DataObject = this.Identity.Read();
if (base.DataObject == null)
{
base.WriteError(new PSArgumentException("The quota template does not exist."), ErrorCategory.InvalidArgument, this.Identity);
base.SkipProcessCurrentRecord();
}
}
}
protected override IEnumerable<SPQuotaTemplate> RetrieveDataObjects()
{
List<SPQuotaTemplate> list = new List<SPQuotaTemplate>();
if (base.DataObject != null)
{
list.Add(base.DataObject);
return list;
}
SPWebService webService = SPWebService.ContentService;
if (webService != null)
{
foreach (SPQuotaTemplate quota in webService.QuotaTemplates)
{
list.Add(quota);
}
}
return list;
}
[Parameter(Mandatory = false, ValueFromPipeline = true, Position = 0), Alias(new string[] { "Name" })]
public SPQuotaTemplatePipeBind Identity
{
get;
set;
}
}
}
In the code example above I'm returning back SPQuotaTemplate objects based on the Identity (or Name) passed into the cmdlet. If the Identity parameter is not provided then all quota templates are returned to the pipeline. In the InternalValidate method I'm checking if the Identity parameter has been provided, and if it has, I set the base class's DataObject property by calling the Read method of the SPQuotaTemplatePipeBind object. In the override RetrieveDataObjects method I then check the DataObject property and return the value as an item in a generic list. If the DataObject property has not been set then I loop through all existing quota templates and return them as generic list. Note that if you are returning lots of items or large items it is better, and preferable, to directly call the WriteResult method and return back null - for this case I know there are typically not a lot of templates and they are not large so I just return back a single collection rather than calling WriteResult.
Pay particular attention to the SPQuotaTemplatePipeBind type - In SharePoint an object can be represented in numerous ways, for example, an SPSite object can be represented by either an URL or a GUID. In order to prevent the need to multiple parameters to support these various types Microsoft has introduced the PipeBind object which eliminates the need for these superfluous parameters and from having to create multiple parameter sets to support them. In the case of the SPQuotaTemplatePipeBind object I can pass in either an actual instance of an SPQuotaTemplate object or a name representing a quota template.
You're not limited to what is available out of the box. You can easily create your own PipeBind objects by simply inheriting from the SPCmdletPipeBind class. Take a look at the following example which demonstrates how to create a custom SPListPipeBind object:
using System;
using System.Collections.Generic;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using System.Management.Automation;
using System.Globalization;
namespace Lapointe.SharePoint2010.PowerShell.Demo.Lists
{
public sealed class SPListPipeBind : SPCmdletPipeBind<SPList>
{
private bool m_IsAbsoluteUrl;
private bool m_IsCollection;
private Guid m_SiteGuid;
private Guid m_WebGuid;
private Guid m_ListGuid;
private string m_WebUrl;
private string m_ListUrl;
public SPListPipeBind(SPList instance)
: base(instance)
{
}
public SPListPipeBind(Guid guid)
{
this.m_ListGuid = guid;
}
public SPListPipeBind(string inputString)
{
if (inputString != null)
{
inputString = inputString.Trim();
try
{
this.m_ListGuid = new Guid(inputString);
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
if (this.m_ListGuid.Equals(Guid.Empty))
{
this.m_ListUrl = inputString;
if (this.m_ListUrl.StartsWith("http", true, CultureInfo.CurrentCulture))
{
this.m_IsAbsoluteUrl = true;
}
if (WildcardPattern.ContainsWildcardCharacters(this.m_ListUrl))
{
this.m_IsCollection = true;
}
}
}
}
public SPListPipeBind(Uri listUri)
{
this.m_ListUrl = listUri.ToString();
}
protected override void Discover(SPList instance)
{
this.m_ListGuid = instance.ID;
this.m_WebGuid = instance.ParentWeb.ID;
this.m_SiteGuid = instance.ParentWeb.Site.ID;
}
public override SPList Read()
{
return this.Read(null);
}
public SPList Read(SPWeb web)
{
SPList list = null;
string parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0}", new object[] { "Empty or Null" });
if (this.IsCollection)
{
return null;
}
try
{
if (Guid.Empty != this.ListGuid)
{
if (web == null && Guid.Empty != this.m_WebGuid && Guid.Empty != this.m_SiteGuid)
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url: {0} and Web Id: {1}", new object[] { this.ListGuid.ToString(), this.m_WebGuid.ToString() });
using (SPSite site = new SPSite(this.m_SiteGuid))
{
web = site.OpenWeb(this.m_WebGuid);
list = web.Lists[ListGuid];
}
}
else
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url: {0} and web Url {1}", new object[] { this.ListUrl, web.Url });
list = web.Lists[ListGuid];
}
}
else if (!string.IsNullOrEmpty(this.ListUrl))
{
string serverRelativeListUrl = null;
if (this.m_IsAbsoluteUrl)
{
serverRelativeListUrl = Utilities.GetServerRelUrlFromFullUrl(this.ListUrl).Trim('/');
}
else
{
serverRelativeListUrl = this.ListUrl.Trim('/');
}
if (web == null)
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0}", new object[] { this.ListUrl });
using (SPSite site = new SPSite(this.ListUrl))
{
web = site.OpenWeb();
}
}
else
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0} and web Url {1}", new object[] { this.ListUrl, web.Url });
}
if (!web.Exists)
{
list = null;
}
else
{
list = web.GetList(serverRelativeListUrl);
}
}
}
catch (Exception exception)
{
throw new SPCmdletPipeBindException(string.Format("The SPList Pipebind object could not be found ({0}).", parameterDetails), exception);
}
if (list == null)
{
throw new SPCmdletPipeBindException(string.Format("The SPList Pipebind object could not be found ({0}).", parameterDetails));
}
return list;
}
public bool IsCollection
{
get
{
return this.m_IsCollection;
}
}
public Guid ListGuid
{
get
{
return this.m_ListGuid;
}
}
public string ListUrl
{
get
{
return this.m_ListUrl;
}
}
}
}
There are two core components that are required for a custom PipeBind object. The first is to have a constructor that takes in the type that you wish to convert (in this example, a string, URI, or GUID) to the target object. The second is to override the Read method which is used to convert the argument value passed into the constructor into the target type. In some cases you'll need additional information which must be provided by the calling code - for example, if a GUID is passed in, representing the List ID, then you will also need to provide the SPWeb object which contains the List; this is done by creating an overload for the Read method which accepts an SPWeb object. It's up to the calling code to determine which overload to call.
Let's now look at how we can package our SPCmdletGetQuotaTemplate class into a SharePoint Solution Package using Visual Studio 2010.
From a new instance of Visual Studio 2010:
- Click File > New > Project to create a new Visual Studio Project
- In the New Project dialog select Visual C#/SharePoint/2010 in the Installed Templates panel and then select Empty Project:

- After you click OK you will be taken to the SharePoint Configuration Wizard:

You can specify any site to use for debugging as we won't be using it for PowerShell development (note that when you start the debugger you'll be given a warning if the specified site's web.config does not allow debugging). PowerShell cmdlets must be deployed to the GAC so select Deploy as full-trust solution and click the Finish button to create the project.
The first thing we need to do with our new empty project is to add a couple of project references:
- Right-click the References folder in the project and select Add Reference...
- In the Add Reference dialog's .NET tab select Microsoft.SharePoint.PowerShell and System.Management.Automation
- Click OK to add the references to the project
Now that we have our references added we can setup our project structure. PowerShell cmdlets are not deployed using Features so we can delete the starting Feature folder that is created:
- Expand the Features folder
- Right-click the Feature1 Feature and click Delete
The next step is to add a SharePoint Mapped Folder:
- Right-click the project and click Add > SharePoint Mapped Folder...
- Add the {SharePointRoot}/Config/PowerShell/Registration folder
- Note that you can add the Format and Help folders as well but I won't be using those in this example as creating help and format files are outside the scope of this article (I usually will add the {SharePointRoot}/Config/PowerShell folder and then manually add the three sub-folders so that I can keep things grouped together in one parent folder within my project).
- Click OK to add the mapped folder
- If a folder is created under the Registration folder then go ahead and delete it (this sub-folder is automatically added in Beta1 but may not be added come RTM)
In the new Registration mapped folder create a new XML file (you can name it anything you like but I usually give it the same name as my project) and paste the following XML into the file:
<?xml version="1.0" encoding="utf-8" ?>
<ps:Config xmlns:ps="urn:Microsoft.SharePoint.PowerShell"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:Microsoft.SharePoint.PowerShell SPCmdletSchema.xsd">
<ps:Assembly Name="$SharePoint.Project.AssemblyFullName$">
<ps:Cmdlet>
<ps:VerbName>Get-SPQuotaTemplate</ps:VerbName>
<ps:ClassName>Lapointe.SharePoint2010.PowerShell.Demo.Quotas.SPCmdletGetQuotaTemplate</ps:ClassName>
<ps:HelpFile>Lapointe.SharePoint2010.PowerShell.Demo.dll-help.xml</ps:HelpFile>
</ps:Cmdlet>
</ps:Assembly>
</ps:Config>
Note that the <ps:HelpFile /> element does require a value but the file specified does not have to exist.
Now we simply need to paste in the code for the SPCmdletGetQuotaTemplate class from above:
- Create a folder below the project root called Quotas
- Add a new class file named SPCmdletGetQuotaTemplate.cs
- Paste the code from above into this file (be sure to adjust your namespaces in the class file and the XML file if you used a different project name than the one shown)
You now have a complete SharePoint 2010 PowerShell Solution - all that's left is to build and deploy it:
- Right-click the project name and select Deploy
Notice what is happening in the output window - IIS application pools are being recycled along with the retraction and deployment of the solution. Because this is a PowerShell solution we don't need IIS to be recycled so let's create a new deployment configuration to remove the recycling of the application pools which should speed up our deployment time:
- Right-click the project and select Properties
- In the properties dialog select the Deploy tab
- In the Edit Configurations group select New to create a new deployment action
- Name the new deployment action PowerShell and configure the deployment steps as shown below:
- Click OK to save the new deployment configuration
Now that we have our custom deployment configuration we need to tell our project to use this configuration. Make sure the Properties Window is visible (type F4 if not) and select the project. Select the PowerShell configuration we just created in the Active Deployment Configuration drop-down.
Our final configuration setting change is to configure the project so that it will open PowerShell when we start the debugger:
- Right-click the project and select Properties to return to the project's properties dialog
- Click the Debug tab
- Select the radio button next to Start external program and specify the following value: C:\Windows\System32\WindowsPowerShell\v1.0\PowerShell.exe
- Paste the following into the Command line arguments text box: -NoExit " & ' C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1 ' "
You can now start the debugger (F5) which will load a PowerShell console and register the SharePoint 2010 snap-in which results in the loading of your new custom cmdlet. To verify that the cmdlet is loaded type Get-Command Get-SPQuotaTemplate | Format-List. You should see the following output:
PS C:\> Get-Command Get-SPQuotaTemplate | Format-List
Name : Get-SPQuotaTemplate
CommandType : Cmdlet
Definition : Get-SPQuotaTemplate [[-Identity] <SPQuotaTemplatePipeBind>]
[-AssignmentCollection <SPAssignmentCollection>] [-Verbose]
[-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <
ActionPreference>] [-ErrorVariable <String>] [-WarningVariab
le <String>] [-OutVariable <String>] [-OutBuffer <Int32>]
Path :
AssemblyInfo :
DLL : C:\Windows\assembly\GAC_MSIL\Lapointe.SharePoint2010.PowerSh
ell.Demo\1.0.0.0__xxxxxxxxxxxxxxxx\Lapointe.SharePoint2010.P
owerShell.Demo.dll
HelpFile : C:\Program Files\Common Files\Microsoft Shared\Web Server Ex
tensions\14\CONFIG\PowerShell\Help\Lapointe.SharePoint2010.P
owerShell.Demo.dll-help.xml
ParameterSets : {[[-Identity] <SPQuotaTemplatePipeBind>] [-AssignmentCollect
ion <SPAssignmentCollection>] [-Verbose] [-Debug] [-ErrorAct
ion <ActionPreference>] [-WarningAction <ActionPreference>]
[-ErrorVariable <String>] [-WarningVariable <String>] [-OutV
ariable <String>] [-OutBuffer <Int32>]}
ImplementingType : Lapointe.SharePoint2010.PowerShell.Demo.Quotas.SPCmdletGetQu
otaTemplate
Verb : Get
Noun : SPQuotaTemplate
|
As you can see, creating and deploying custom PowerShell cmdlets for SharePoint 2010 using Visual Studio 2010 is now super easy. The only complexity lies in the logic of the cmdlet itself.
As you probably expected I have already been hard at work on creating some new cmdlets to replace some of my old PowerShell cmdlets as well as a few select STSADM commands. I'll be releasing these new cmdlets with full source shortly - keep checking back here for more example code and downloads!
SharePoint 2010: PSConfig and PowerShell
The information in this article is based on BETA 2 of SharePoint 2010 - there may be differences with the RTM release.
Update 12/18/2009: I've updated the post to reflect BETA 2 changes and have considerably simplified the script removing all the dependencies on helper functions so that the core pieces required to build a basic farm are focused on rather than the complex elements to support tracing and error handling as I previously had it.
If you've ever done a scripted install of SharePoint 2007 then you are familiar with how to script the initial farm creation using psconfig.exe. Unfortunately psconfig is still with us in SharePoint 2010 but we do have some PowerShell cmdlets which replace all the psconfig commands.
Before we look at how to use PowerShell lets refresh our memories by looking at an install script for SharePoint 2007. I usually have two scripts, one for the first server and a second script for additional servers - here's the first script (I've omitted the variable declarations):
1: ECHO %DATE% %TIME%: Building configuration database
2: psconfig -cmd configdb -create -server %SERVER_DB% -database %DB_CONFIG_NAME% -user %ACCT_SPFARM% -password %ACCT_SPFARM_PWD% -admincontentdatabase %DB_CENTRALADMINCONTENT_NAME%
3: if not errorlevel 0 goto errhnd
4:
5: ECHO %DATE% %TIME%: Installing help content
6: psconfig -cmd helpcollections -installall
7: if not errorlevel 0 goto errhnd
8:
9: ECHO %DATE% %TIME%: Securing resources
10: psconfig -cmd secureresources
11: if not errorlevel 0 goto errhnd
12:
13: ECHO %DATE% %TIME%: Installing services
14: psconfig -cmd services -install
15: if not errorlevel 0 goto errhnd
16:
17: ECHO %DATE% %TIME%: Installing features
18: psconfig -cmd installfeatures
19: if not errorlevel 0 goto errhnd
20:
21: ECHO %DATE% %TIME%: Creating central admin site
22: psconfig -cmd adminvs -provision -port %CENTRALADMIN_PORT% -windowsauthprovider enablekerberos
23: if not errorlevel 0 goto errhnd
24:
25: ECHO %DATE% %TIME%: Adding application content to central admin site
26: psconfig -cmd applicationcontent -install
27: if not errorlevel 0 goto errhnd
28:
29: goto end
30:
31: :errhnd
32:
33: echo An error occured - terminating script.
34:
35: :end
And here's the second script (variable declarations omitted):
1: ECHO %DATE% %TIME%: Connecting to farm
2: psconfig -cmd configdb -connect -server %SERVER_DB% -database %DB_CONFIG_NAME% -user %ACCT_SPFARM% -password %ACCT_SPFARM_PWD%
3: if not errorlevel 0 goto errhnd
4:
5: ECHO %DATE% %TIME%: Installing services
6: psconfig -cmd services install
7: if not errorlevel 0 goto errhnd
8:
9: ECHO %DATE% %TIME%: Installing features
10: psconfig -cmd installfeatures
11: if not errorlevel 0 goto errhnd
12:
13: ECHO %DATE% %TIME%: Setting security on registry and file system
14: psconfig -cmd secureresources
15: if not errorlevel 0 goto errhnd
16:
17: goto end
18:
19: :errhnd
20:
21: echo An error occured - terminating script.
22:
23: :end
Obviously the two scripts are very similar with the main difference being the parameters passed to the configdb command and with fewer commands being called. So how would we do this using SharePoint 2010's PowerShell cmdlets? First lets list each psconfig command and what the PowerShell equivalent is:
| PSConfig Command | PowerShell Cmdlet |
|---|---|
| configdb -create | New-SPConfigurationDatabase |
| configdb -connect | Connect-SPConfigurationDatabase |
| helpcollections -installall | Install-SPHelpCollection |
| secureresources | Initialize-SPResourceSecurity |
| services -install | Install-SPService |
| installfeatures | Install-SPFeature (provide the -AllExistingFeatures parameter) |
| adminvs -provision | New-SPCentralAdministration |
| applicationcontent -install | Install-SPApplicationContent |
Now that we know what PowerShell cmdlets to use lets rework our install script. Instead of using a batch file for variables I'm going to use an XML file which can be read in and parsed as needed. I'm also going to make it so that you can run the same script from any server and just specify a switch parameter indicating whether or not you are connecting to an existing farm or not.
Let's first take a look at the XML that we'll use to manage our settings - we'll store this in a file called FarmConfigurations.xml:
<Farm FarmAccount="sp2010\spfarm"
ConfigDB="SharePoint_ConfigDB"
AdminContentDB="SharePoint_Content_Admin"
DatabaseServer="spsql1"
Passphrase="Pa$$w0rd">
<CentralAdmin Port="1234" AuthProvider="NTLM">
<Servers>
<Server Name="spsvr1" />
</Servers>
</CentralAdmin>
</Farm>
In this XML I'm storing the core farm settings in the <Farm /> element which includes attributes for the farm account to use as well as the database server and names for the configuration and central admin content database. I then have a <CentralAdmin /> element which contains the servers that the Central Admin site should be installed on along with the port and authentication provider to use.
So now that we have our configuration settings all we need now is the core script to actually create our farm. I named this script BuildFarm.ps1 but you can name it whatever you want:
function Install-SharePointFarm([bool]$connectToExisting,
[string]$settingsFile = "FarmConfigurations.xml") {
[xml]$config = Get-Content $settingsFile
$farmAcct = Get-Credential $config.Farm.FarmAccount
$configDb = $config.Farm.ConfigDB
$adminContentDb = $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) {
if ($connectToExisting) {
#Connecting to farm
Connect-SPConfigurationDatabase -DatabaseName $configDb `
-DatabaseServer $server -Passphrase $passphrase
} else {
#Creating new farm
New-SPConfigurationDatabase -DatabaseName $configDb `
-DatabaseServer $server `
-AdministrationContentDatabaseName $adminContentDb `
-Passphrase $passphrase -FarmCredentials $farmAcct
}
#Verifying farm creation
$spfarm = Get-SPFarm -ErrorAction SilentlyContinue -ErrorVariable err
if ($spfarm -eq $null -or $err) {
throw "Unable to verify farm creation."
}
#ACLing SharePoint Resources
Initialize-SPResourceSecurity
#Installing Services
Install-SPService
#Installing Features
Install-SPFeature -AllExistingFeatures
} else {
Write-Warning "Farm already exists. Skipping creation."
}
$installSCA = (($config.Farm.CentralAdmin.Servers.Server | `
where {$_.Name -eq $env:computername}) -ne $null)
$url = "http://$($env:computername):$($config.Farm.CentralAdmin.Port)"
$sca=[Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($url)
if ($installSCA -and $sca -eq $null) {
#Provisioning Central Administration
New-SPCentralAdministration -Port $config.Farm.CentralAdmin.Port `
-WindowsAuthProvider $config.Farm.CentralAdmin.AuthProvider
#Installing Help
Install-SPHelpCollection -All
#Installing Application Content
Install-SPApplicationContent
}
}
Notice that the script takes a parameter which allows you to specify whether you are connecting to an existing farm. We can call the script using the following syntax (replace $false with $true if connecting to an existing farm):
PS C:\> . .\buildfarm.ps1
PS C:\> Install-SharePointFarm $false "FarmConfigurations.xml"
In a series of upcoming post I'll be building on what I created here by demonstrating how to create web applications and how to start each of the services that are included, including the extremely complex search service (gone are the days of a single line setup for search - we now have a dozen or so cmdlets that have to be run to get search started).
SharePoint 2010: STSADM and PowerShell
The information in this article is based on BETA 2 of SharePoint 2010 - there will likely be differences with the RTM release.
Finally, the NDA is lifted and we can openly talk about SharePoint 2010! There's so many cool things to talk about it's hard to decide where to begin, but as the focus of my blog has generally been on STSADM then I suppose that's a good place to start. The first thing we should do is load the new SharePoint 2010 Management Shell which can be found under the Microsoft SharePoint 2010 Products section of the start menu:
This new console window is actually just a PowerShell console which pre-loads the SharePoint PowerShell Snap-in. You can see that by looking at the target for the shortcut:
C:\Windows\System32\WindowsPowerShell\v1.0\PowerShell.exe -NoExit " & ' C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\\sharepoint.ps1 ' "
If you look at the SharePoint.ps1 file you'll see the following:
From this you can see that you can easily load up any PowerShell console app (either the native PowerShell console or a any other editor such as PowerGUI or the PowerShell ISE) and simply run the Add-PsSnapin cmdlet to load the new SharePoint 2010 cmdlets as shown above.
Note that the script is also changing the threading model of PowerShell so that commands are entered in the same thread rather than a different thread for each line of execution (I'll cover this in more detail in a later post but it's important to understand that this has a huge impact on how objects are disposed within PowerShell - see Zach's post about threading here: http://sharepoint.microsoft.com/blogs/zach/Lists/Posts/Post.aspx?ID=34)
I'll cover the PowerShell stuff in bit but first lets take a look at the STSADM changes. I did a simple stsadm -help command and saved that to a file in both my 2007 and 2010 environment and then compared those files. Here's what I found:
- The following STSADM commands no longer exist:
- associatewebapp
- createcmsmigrationprofile
- createssp
- deletecmsmigrationprofile
- deletessp
- deletessptimerjob
- editcmsmigrationprofile
- editssp
- enablecmsurlredirect
- enumssp
- enumssptimerjobs
- grantiis7permission
- listqueryprocessoroptions
- mysite
- preupgradecheck
- restoressp
- runcmsmigrationprofile
- setdefaultssp
- setqueryprocessoroptions
- setsharedwebserviceauthn
- setsspport
- trimauditlog
- The following STSADM commands have been added:
- add-adsdefaultapplication
- add-ecsblockedexcelfiletype
- monitordb
- patchpostaction
- remove-adsdefaultapplication
- remove-ecsblockedexcelfiletype
From the list above you can see that the bulk of the items removed have to do with the SSP and CMS migration features which were part of the 2007 product but have been removed from the 2010 product (2010 has a whole new services architecture which is really cool, but outside the scope of this article - stay tuned! - 2010 also does not support upgrading from Microsoft CMS, you must upgrade to SharePoint 2007 and then SharePoint 2010).
Also note that very few new commands have been added - in fact I was surprised to see that any were added - why? Because PowerShell is what we should all be using - STSADM is dead - just don't use it, forget that these new commands have been added as there are PowerShell equivalents (I wouldn't be surprised if they get pulled come RTM). That said, at beta there may be some stuff that still only exists with STSADM but expect that not to be the case going forward.
So now that we have that out of the way, lets talk about some PowerShell (as of this post I'm officially done creating STSADM extensions - at least for 2010, I may still have need for some new stuff with the 2007 product). In your new SharePoint 4.0 Management Console type the following command: gcm -pssnapin microsoft.sharepoint.powershell | select Name, Definition | fl > .\sp2010cmdlets.txt
Take a look at the file you just generated, you now have a listing of the 535 new PowerShell cmdlets that ship with SharePoint 2010. For brevity sake I won't show the syntax but here's a listing of all the new cmdlets that are available:
| Add-PluggableSecurityTrimmer | New-SPPerformancePointServiceApplication |
If you're going to be working with the SharePoint PowerShell cmdlets then there are two cmdlets that you should learn before any others: Start-SPAssignment and Stop-SPAssignment. If you've used the custom cmdlets that I created for the 2007 product and read my articles around them then you should be familiar with the core issue that I was trying to solve with some of them - the handling of disposable objects. When you use a cmdlet such as Get-SPSite you are returning back a disposable object, an object that implements the IDisposable interface in order to release handles to unmanaged resources which cause memory leaks if not released. I chose to solve the issue by simply not returning back disposable objects and instead using a proxy object. Microsoft chose a different route.
If you look closely at the definitions of the cmdlets from the text file you created above you should notice that every cmdlet has an -AssignmentCollection parameter which takes in an SPAssignmentCollection object type. The purpose of this collection is to store disposable objects so that they can be disposed when your operations complete. You create a new assignment collection by calling the Start-SPAssignment cmdlet. You can then optionally pass the created assignment collection object into your subsequent calls by either using the pipeline or directly setting the parameter (otherwise the global collection will be used). Lets look at the help for the cmdlet to understand this better:
PS C:\> help start-spassignment
NAME
Start-SPAssignment
SYNOPSIS
Initiates a new assignment store.
SYNTAX
Start-SPAssignment [-Global <SwitchParameter>] [<CommonParameters>]
DETAILED DESCRIPTION
Use this command to properly dispose of objects used with variable assignme
nts.
Using SPWeb, SPSite, or SPSiteAdminsitration objects can use large amounts
of memory and using these objects, or lists of these objects, in PowerShell
scripts requires proper memory management. By default, all Get commands di
spose of these objects immediately after the pipeline finishes, but using S
PAssignment, you can assign the list of objects to a variable and dispose o
f the objects after they are no longer needed. You can also ensure that the
objects will remain as long as you need them, even throughout multiple ite
rations of commands.
There are three levels of assignment:
* No assignment - The object is not assigned to a variable and is disposed
of after each iteration of the command.
* Simple assignment - All objects are assigned to the global assignment sto
re. This is done by using the Global parameter. When using this level, al
l objects are assigned to a global store and are disposed of when the Sto
p-SPAssignment command is called.
* Advanced assignment - Objects are assigned to named stores for disposal.
You can dispose of objects by using the -Identity parameter with the Stop
-SPAssignment command.
Regardless of the level used, all objects are disposed of when the PowerShe
ll runspace is closed.
RELATED LINKS
REMARKS
To see the examples, type: "get-help Start-SPAssignment -examples".
For more information, type: "get-help Start-SPAssignment -detailed".
For technical information, type: "get-help Start-SPAssignment -full".
|
As you can see from the help text there are three levels of assignment: No assignment (dispose immediately), simple assignment (use a global store), and advanced assignment (use a named store). Let's take a look at the syntax (note that in Beta 1 most of the help files do not match the actual syntax so I rarely rely on the help text to tell me what the cmdlet syntax is - instead I use the Get-Command (gcm) cmdlet):
PS C:\> gcm Start-SPAssignment -syntax Start-SPAssignment [-Global] [-AssignmentCollection <SPAssignmentCollection>] [ -Verbose] [-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <ActionPre ference>] [-ErrorVariable <String>] [-WarningVariable <String>] [-OutVariable < String>] [-OutBuffer <Int32>] |
If you specify the -Global switch then you do not need to store the returned object or pass it into your cmdlets - internally this cmdlet stores the object in a static variable which is used by all subsequent calls - this is the "simple assignment" method mentioned in the help text. If you do not specify the -Global switch then you must store the returned object in a variable and pass that variable into all subsequent cmdlet calls that return disposable objects and provide the variable to the Stop-SPAssignment cmdlet with the same variable via the -SemiGlobal parameter (this may change to Identity for the RTM) - this is the "advanced assignment" mentioned in the help text. So what about the "no assignment" option? The "no assignment" option is basically when you do not create an SPAssignmentCollection using the Start-SPAssignment cmdlet which causes cmdlets that return disposable objects to dispose of the object immediately after the call to WriteObject. So as long as the pipeline is active the object remains un-disposed but at the end of the pipeline the object is disposed.
So now we'll look at the syntax for the Stop-SPAssignment cmdlet:
PS C:\> gcm Stop-SPAssignment -syntax Stop-SPAssignment [[-SemiGlobal] <SPAssignmentCollection>] [-Global] [-Assignme ntCollection <SPAssignmentCollection>] [-Verbose] [-Debug] [-ErrorAction <Actio nPreference>] [-WarningAction <ActionPreference>] [-ErrorVariable <String>] [-W arningVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>] |
As you can see this is pretty straightforward, if you used the -Global switch for the Start-SPAssignment cmdlet then you'll use it here, otherwise you would use the -SemiGlobal parameter and pass the variable you created earlier.
Here's a complete example demonstrating both approaches:
#Use of the Global assignment variable Start-SPAssignment -Global $site = Get-SPSite "http://portal" $site | fl Stop-SPAssignment -Global #Use of a semi-global, or named variable $gc = Start-SPAssignment $site = $gc | Get-SPSite "http://mysites" $site | fl $gc | Stop-SPAssignment
For the second example you can see that I'm passing in the variable using the pipeline rather than setting the parameter directly (for both the Get-SPSite cmdlet and the Stop-SPAssignment cmdlet). I could have easily set the parameter names directly but this just results in less code.
So one thing you should be asking is when do I need to do this - every cmdlet takes an SPAssignmentCollection so does that mean that I always have to deal with this stuff? My simple answer is, no, you don't have to do this all the time, only when you know you are working with disposable objects. The problem is that most people don't know when they are working with disposable objects, and in many cases what disposable objects are. So my first best practice recommendation for SharePoint 2010 will be to always use the simple assignment approach (use the global assignment variable) unless you specifically know to do otherwise. So wrap all your scripts with a call to Start-SPAssignment and Stop-SPAssignment. If you have functions that do not return values then wrap those using the advanced approach as shown below:
function Set-SomethingInteresting([string]$siteUrl) { trap { $gc | Stop-SPAssignment } $gc = Start-SPAssignment $site = $gc | Get-SPSite $siteUrl Write-Host "Doing something interesting..." $gc | Stop-SPAssignment } Set-SomethingInteresting "http://mysites"
So as you can see SharePoint 2010 introduces all kinds of really cool stuff - I've literally not even scratched the surface with what you can do with PowerShell and SharePoint 2010, let alone all the cool new features that 2010 introduces. Over the coming weeks and months I hope to provide details on how to do a full scripted install of SharePoint 2010 including detail on how to script out every single service - this will be critical information that every administrator and developer should have as PowerShell is going to be the recommended way to start and configure your services in the new architecture.