SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

12Aug/1062

Getting an Inventory of All SharePoint Documents Using Windows PowerShell

I got an email today asking if I had anything that would generate a report detailing all the documents throughout an entire SharePoint Farm. As this wasn’t the first time I’ve been asked this same question I decided that I’d just go ahead and post the script for generating such a report.

The script is really quite straightforward – it simply iterates through all Web Applications, Site Collections, Webs, Lists, and finally, List Items. I skip any List that is not a Document Library (as well as the Central Admin site) and then build a hash table containing all the data I want to capture. I then convert that hash table to an object which is written to the pipeline.

All of this is placed in a function which I can call and then pipe the output to something like the Out-GridView cmdlet or the Export-Csv cmdlet. I also wrote the script so that it works with either SharePoint 2007 or SharePoint 2010 so that I don’t have to maintain two versions (I could have used cmdlets such as Get-SPWebApplication, Get-SPSite, and Get-SPWeb but there was little benefit to doing so and the script would be limited to SharePoint 2010).

One word of caution – in a large Farm this script should be run off hours or at least on a back facing server (not your WFE) – it’s going to generate a lot of traffic to your database.

function Get-DocInventory() {
    [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
    $farm = [Microsoft.SharePoint.Administration.SPFarm]::Local
    foreach ($spService in $farm.Services) {
        if (!($spService -is [Microsoft.SharePoint.Administration.SPWebService])) {
            continue;
        }

        foreach ($webApp in $spService.WebApplications) {
            if ($webApp -is [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]) { continue }

            foreach ($site in $webApp.Sites) {
                foreach ($web in $site.AllWebs) {
                    foreach ($list in $web.Lists) {
                        if ($list.BaseType -ne "DocumentLibrary") {
                            continue
                        }
                        foreach ($item in $list.Items) {
                            $data = @{
                                "Web Application" = $webApp.ToString()
                                "Site" = $site.Url
                                "Web" = $web.Url
                                "list" = $list.Title
                                "Item ID" = $item.ID
                                "Item URL" = $item.Url
                                "Item Title" = $item.Title
                                "Item Created" = $item["Created"]
                                "Item Modified" = $item["Modified"]
                                "File Size" = $item.File.Length/1KB
                            }
                            New-Object PSObject -Property $data
                        }
                    }
                    $web.Dispose();
                }
                $site.Dispose()
            }
        }
    }
}
Get-DocInventory | Out-GridView
#Get-DocInventory | Export-Csv -NoTypeInformation -Path c:\inventory.csv

12Jun/1017

Deploying SharePoint 2010 Solution Packages Using PowerShell

Update 4/19/2011: I've reworked this script completely. You can find the update here: http://blog.falchionconsulting.com/index.php/2011/04/deploying-sharepoint-2010-solution-package-using-powershell-revisited/

With SharePoint 2010 we can now deploy our Solution Packages using PowerShell. What’s cool about this is that it’s a bit easier than it was with 2007 to check if a package is already deployed and conditionally retract, delete, and then re-add and re-deploy. By now most people already know how to do this as it’s fairly straightforward but I thought I’d go ahead and share the script that I use as it’s great for deploying lots of Solution Packages to my various client environments in bulk.

Like most of my scripts this one is driven by an XML file but I have a core function which can be called directly – I just wrap that in another function which can iterate through the XML file thus facilitating bulk installs of packages. First lets look at the XML file:

<Solutions>
<Solution Path="W:\my.sharepoint.package.wsp" CASPolicies="false" GACDeployment="true">
<WebApplications>
<WebApplication>http://portal</WebApplication>
</WebApplications>
</Solution>
</Solutions>

As you can see the structure is fairly simplistic – just provide the path to the WSP file and whether it contains CAS policies and whether it should be deployed to the GAC or not. If it’s a Farm level solution (no web application resources) then simply omit the <WebApplications /> element. If you have more than one solution just add another <Solution /> element. If you’re deploying to multiple web applications then add as many <WebApplication /> elements as is needed.

Now we’ll take a look at the wrapper function which loops through the XML:

function Install-Solutions([string]$configFile)
{
    if ([string]::IsNullOrEmpty($configFile)) { return }

    [xml]$solutionsConfig = Get-Content $configFile
    if ($solutionsConfig -eq $null) { return }

    $solutionsConfig.Solutions.Solution | ForEach-Object {
        [string]$path = $_.Path
        [bool]$gac = [bool]::Parse($_.GACDeployment)
        [bool]$cas = [bool]::Parse($_.CASPolicies)
        $webApps = $_.WebApplications.WebApplication
        Install-Solution $path $gac $cas $webApps
    }
}

As you can see the code just loads the passed in file as an XmlDocument object and grabs each Solution element ($solutionsConfig.Solutions.Solution) and then iterates through each object using the ForEach-Object cmdlet. For pure convenience I grab each attribute and assign it to a local variable. And finally I call the Install-Solution function which is shown below:

function Install-Solution([string]$path, [bool]$gac, [bool]$cas, [string[]]$webApps = @())
{
    $spAdminServiceName = "SPAdminV4"

    [string]$name = Split-Path -Path $path -Leaf
    $solution = Get-SPSolution $name -ErrorAction SilentlyContinue

    if ($solution -ne $null) {
        #Retract the solution
        if ($solution.Deployed) {
            Write-Host "Retracting solution $name..."
            if ($solution.ContainsWebApplicationResource) {
                $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
            } else {
                $solution | Uninstall-SPSolution -Confirm:$false
            }
            Stop-Service -Name $spAdminServiceName
            Start-SPAdminJob -Verbose
            Start-Service -Name $spAdminServiceName

            #Block until we're sure the solution is no longer deployed.
            do { Start-Sleep 2 } while ((Get-SPSolution $name).Deployed)
        }

        #Delete the solution
        Write-Host "Removing solution $name..."
        Get-SPSolution $name | Remove-SPSolution -Confirm:$false
    }

    #Add the solution
    Write-Host "Adding solution $name..."
    $solution = Add-SPSolution $path

    #Deploy the solution
    if (!$solution.ContainsWebApplicationResource) {
        Write-Host "Deploying solution $name to the Farm..."
        $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -Confirm:$false
    } else {
        if ($webApps -eq $null -or $webApps.Length -eq 0) {
            Write-Warning "The solution $name contains web application resources but no web applications were specified to deploy to."
            return
        }
        $webApps | ForEach-Object {
            Write-Host "Deploying solution $name to $_..."
            $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $_ -Confirm:$false
        }
    }
    Stop-Service -Name $spAdminServiceName
    Start-SPAdminJob -Verbose
    Start-Service -Name $spAdminServiceName

    #Block until we're sure the solution is deployed.
    do { Start-Sleep 2 } while (!((Get-SPSolution $name).Deployed))
}

The code looks more complicated than it really is. I first start out by getting the solution name which I always assume to be the filename of the WSP file. I then use that to get the SPSolution object using Get-SPSolution. If a value comes back then I check if it has been deployed and if it has then I retract it by calling Uninstall-SPSolution. The trick is knowing whether it has web application scoped resources and if it does then we need to retract using the -AllWebApplications parameter. Once retracted I stop the SharePoint Administration Service (SPAdminV4) so that I can call Start-SPAdminJob and force the retraction timer job to execute. Once the Start-SPAdminJob cmdlet returns I then restart the SharePoint Administration Service. With the solution retracted I can now delete the solution from the solution store using Remove-SPSolution (I re-get the solution to make sure that I get no errors due to the current variables state being invalid).

Once deleted I can then add the new solution to the store using the path provided (Add-SPSolution). Now that it’s in the store I can check if it has web application resources or not – if it does not then the deployment is simply a matter of calling Install-SPSolution and specifying whether it should be deployed to the GAC and if it contains CAS policies. If it does contain web application resources then I have to loop through all the web application items in the provided string array ($webApps) and then pass each one into a separate call to Install-SPSolution.

You now have two options for adding your solution: you can call the first function and pass in an XML file or you can call the second function directly. I’ll first show how to call the second function directly:

PS C:\>Install-Solution "w:\my.sharepoint.package.wsp" $true $false @("http://portal","http://mysites")

Now lets look at how to call the first function given an XML file named “solutions.xml” containing a structure similar to that shown above:

PS C:\>Install-Solutions "w:\solutions.xml"

Hopefully you’ll find this script useful for deploying your custom SharePoint 2010 Solution Packages.

29Apr/1015

Discovering Who Has Access to SharePoint 2010 Securable Objects

I've talked on several occasions about how we can easily use the SharePoint 2010 object model (OM) to discover who has access to a securable object (SPWeb, SPList, or SPListItem) and the fact that we can use the same mechanisms within PowerShell to create useful security/audit reports. On some of those occasions I've shown a version of a PowerShell script which gives you a dump to the screen or a text file of every securable object and who has access to it and how they were given access to it - today I'd like to share a new version of that script.

Before we get to the actual script let's first talk about how to get the information. All securable objects have a method named GetUserEffectivePermissionInfo which is defined in the abstract base class SPSecurableObject (in 2007 this method was defined directly on the SPWeb, SPList, and SPListItem objects). This method returns back an SPPermissionInfo object which we can use to inspect the various role definition bindings and corresponding permission levels.

Once we have the permission details we simple loop through the SPRoleAssignments objects via the RoleAssignments property. This will give us information about how the user is given access to the resource. Next we look at the RoleDefinitionBindings property which returns back a collection of SPRoleDefinition objects that tell us about the type of access granted (e.g., Full Control, etc.).

I then take all this information, stick it in a hash table which I then use to create a new object which gets written to the pipeline.

So with that, let's take a look at the code:

function Get-SPUserEffectivePermissions(
    [object[]]$users, 
    [Microsoft.SharePoint.SPSecurableObject]$InputObject) {
    
    begin { }
    process {
        $so = $InputObject
        if ($so -eq $null) { $so = $_ }
        
        if ($so -isnot [Microsoft.SharePoint.SPSecurableObject]) {
            throw "A valid SPWeb, SPList, or SPListItem must be provided."
        }
        
        foreach ($user in $users) {
            # Set the users login name
            $loginName = $user
            if ($user -is [Microsoft.SharePoint.SPUser] -or $user -is [PSCustomObject]) {
                $loginName = $user.LoginName
            }
            if ($loginName -eq $null) {
                throw "The provided user is null or empty. Specify a valid SPUser object or login name."
            }
            
            # Get the users permission details.
            $permInfo = $so.GetUserEffectivePermissionInfo($loginName)
            
            # Determine the URL to the securable object being evaluated
            $resource = $null
            if ($so -is [Microsoft.SharePoint.SPWeb]) {
                $resource = $so.Url
            } elseif ($so -is [Microsoft.SharePoint.SPList]) {
                $resource = $so.ParentWeb.Site.MakeFullUrl($so.RootFolder.ServerRelativeUrl)
            } elseif ($so -is [Microsoft.SharePoint.SPListItem]) {
                $resource = $so.ParentList.ParentWeb.Site.MakeFullUrl($so.Url)
            }

            # Get the role assignments and iterate through them
            $roleAssignments = $permInfo.RoleAssignments
            if ($roleAssignments.Count -gt 0) {
                foreach ($roleAssignment in $roleAssignments) {
                    $member = $roleAssignment.Member
                    
                    # Build a string array of all the permission level names
                    $permName = @()
                    foreach ($definition in $roleAssignment.RoleDefinitionBindings) {
                        $permName += $definition.Name
                    }
                    
                    # Determine how the users permissions were assigned
                    $assignment = "Direct Assignment"
                    if ($member -is [Microsoft.SharePoint.SPGroup]) {
                        $assignment = $member.Name
                    } else {
                        if ($member.IsDomainGroup -and ($member.LoginName -ne $loginName)) {
                            $assignment = $member.LoginName
                        }
                    }
                    
                    # Create a hash table with all the data
                    $hash = @{
                        Resource = $resource
                        "Resource Type" = $so.GetType().Name
                        User = $loginName
                        Permission = $permName -join ", "
                        "Granted By" = $assignment
                    }
                    
                    # Convert the hash to an object and output to the pipeline
                    New-Object PSObject -Property $hash
                }
            }
        }
    }
    end {}
}

Great - we've got the code - so now you're probably asking, "how the heck do I use it?" Well the first thing you need to do is save it to a file, let's call it SecurityReport.ps1 and we'll put it in the root of the C drive. Once saved we can load it in memory using the following:

C:\ PS> . .\SecurityReport.ps1

Now for the fun stuff :) . The examples I'm going to show will build off of each other and will eventually conclude with an example that gives me a report for all users and all securable objects throughout the entire farm. The first example I want to show is how to retrieve a report for a single user and a single web (we'll reuse the $user variable throughout the script so I'll only define it once here):

$user = "sp2010\siteowner2"
Get-SPWeb http://portal | Get-SPUserEffectivePermissions $user | Out-GridView -Title "Web Permissions for $user"

Running this command will generate a grid view as shown here:

image

Note that I could have just as easily saved the results to a CSV file which I could then open in Excel using the Export-Csv cmdlet:

Get-SPWeb http://portal | Get-SPUserEffectivePermissions $user | Export-Csv -NoTypeInformation -Path c:\perms.csv

For this next example I'm going to show the permissions for the same user for ALL webs throughout the entire farm (note that this won't include lists or items):

Get-SPSite -Limit All | Get-SPWeb | Get-SPUserEffectivePermissions $user | Out-GridView -Title "All Web Permissions for $user"

Now I want to get the permissions for the same user for all lists throughout the entire farm:

Get-SPSite -Limit All | Get-SPWeb | %{$_.Lists | Get-SPUserEffectivePermissions $user} | Out-GridView -Title "List Permissions for $user"

Now we're going to get nice and deep and show the permissions for every single item throughout the entire farm (probably don't want to run this on any front-end servers):

Get-SPSite -Limit All | Get-SPWeb | %{$_.Lists | %{$_.Items | Get-SPUserEffectivePermissions $user}} | Out-GridView -Title "Item Permissions for $user"

So now that I've shown you how to get the individual securable objects results throughout the farm for a single user let's now go ahead and stitch them together into one report:

Get-SPSite -Limit All | ForEach-Object {
    $site = $_
    $webPermissions += $site | Get-SPWeb –Limit All | Get-SPUserEffectivePermissions $user
    $listPermissions += $site | Get-SPWeb –Limit All | %{$_.Lists | Get-SPUserEffectivePermissions $user}
    $itemPermissions += $site | Get-SPWeb –Limit All | %{$_.Lists | %{$_.Items | Get-SPUserEffectivePermissions $user}}
    $site.Dispose();
}
$webPermissions + $listPermissions + $itemPermissions | Out-GridView -Title "Web, List, and Item Permissions for $user"

In this example I'm simply performing the same calls but appending to an array of objects and then dumping the combination of those arrays to the grid. Note that in this case I'm calling $site.Dispose() but below I'll be using the SPAssignmentCollection to dispose of objects - keep reading for an explanation.

So now lets take it one step further and see how we can get the same reports but this time for every user. We'll start with webs again - in this example we'll get the permissions for all users for a given site:

$gc = Start-SPAssignment
$site = $gc | Get-SPSite http://portal
$site | Get-SPWeb –Limit All | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName) | Out-GridView -Title "Web Permissions for All Users In $($site.Url)"
$gc | Stop-SPAssignment

As you can see I'm basically using the SiteUsers property from the root web and passing the login name for each user into the function. Note that here I'm using the Start-SPAssignment and Stop-SPAssignment cmdlets - that's because I'm using the SPSite object after the pipeline execution finishes (as opposed to the above) so I need to make sure it gets disposed (I could just as easily called Dispose on the object as I did above but I'm attempting to demonstrate when/why you'd use the assignment collections).

Now lets see the lists:

$gc = Start-SPAssignment
$site = $gc | Get-SPSite http://portal
$site | Get-SPWeb –Limit All | %{$_.Lists | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)} | Out-GridView -Title "List Permissions for All Users in $($site.Url)"
$gc | Stop-SPAssignment

Starting to see a pattern? Let's take a look at the list items now:

$gc = Start-SPAssignment
$site = $gc | Get-SPSite http://portal
$site | Get-SPWeb –Limit All | %{$_.Lists | %{$_.Items | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)}} | Out-GridView -Title "Item Permissions for All Users in $($site.Url)"
$gc | Stop-SPAssignment

Great! So now lets piece this last bit together so we can see the permissions for all webs, lists, and list items for every user within a single site collection:

$gc = Start-SPAssignment
$site = $gc | Get-SPSite http://portal
$webPermissions = $site | Get-SPWeb –Limit All | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)
$listPermissions = $site | Get-SPWeb –Limit All | %{$_.Lists | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)}
$itemPermissions = $site | Get-SPWeb –Limit All | %{$_.Lists | %{$_.Items | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)}}
$webPermissions + $listPermissions + $itemPermissions Out-GridView -Title "Web, List, and Item Permissions for All Users in $($site.Url)"
$gc | Stop-SPAssignment

Alright, we're almost done - let's now stitch this all together and generate a single report showing all permissions for all securable objects (webs, lists, and list items) for every user within every site collection:

Get-SPSite -Limit All | ForEach-Object {
    $site = $_
    $webPermissions += $site | Get-SPWeb –Limit All | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)
    $listPermissions += $site | Get-SPWeb –Limit All | %{$_.Lists | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)}
    $itemPermissions += $site | Get-SPWeb –Limit All | %{$_.Lists | %{$_.Items | Get-SPUserEffectivePermissions ($site.RootWeb.SiteUsers | select LoginName)}}
    $site.Dispose();
}
$webPermissions + $listPermissions + $itemPermissions | Out-GridView -Title "Web, List, and Item Permissions for All Users in All Sites"

Note in this last example, as I did previously when looping through all site collections, I'm calling the Dispose() method inside the ForEach-Object script block. I do this because objects wouldn't otherwise get disposed until the pipeline execution has finished and because it's continuing to iterate so the pipeline has not yet completed. If I used the assignment collection I wouldn't get a disposal until after I'm done iterating which would be too late - I want to dispose right when I'm done with the individual SPSite objects to avoid out of memory errors.

Reporting on who has access to what is one of the things I get asked about most frequently so hopefully this code sample and corresponding examples will prove to be useful to people. One possible area of improvement to the script would be to accommodate groups being passed in - right now I'm only considering users; and of course you could easily turn the example usages into functions. As always, if anyone has any feedback (bugs, improvements, etc.) please post here so that myself and others may benefit.

4Apr/106

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.

19Dec/0928

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!

18Dec/099

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.

5Mar/092

Deleting Orphaned Event Receivers using PowerShell

While looking through the event logs at a client of mine the other day I came across an odd error that was occurring regularly.  Apparently they had developed a Feature which contained an Event Receiver which was programmatically bound to a series of lists.  As time went on their requirements changed so they deleted the event receiver class.  Problem was that the binding to the lists still exists so every time an item was updated an error would be dumped to the event log about a missing type in the specified assembly.

Fortunately it’s pretty easy to fix this using some simple PowerShell:

$site = get-spsite -url http://portal
foreach ($web in $site.AllWebs) {
    $lists = $web.Lists | where {$_.EventReceivers.Count -gt 0}
    foreach ($list in $lists) {
        $evts = $list.EventReceivers | where {$_.Class -eq MyCompany.SharePoint.MyFeature.EventReceivers.MyEventReceiver"}
        if ($evts.Count -gt 0) {
            foreach ($evt in $evts) {
                Write-Host("Deleting..." + $list.RootFolder.ServerRelativeUrl + ", " + $evt.Type)
                $evt.Delete()
            }
        }
    }
}

The only thing that threw me off when I was putting this little snippet together was the fact that I couldn’t work with the collections directly due to errors about the collection being modified during the enumeration (so a “for” loop rather than a “foreach” loop would have worked just as easily as doing filtering as I am above).

The code above is great for when you know the class name of a specific event receiver that needs to be pulled.  If you don’t know the name then you’ll have to add in some reflection to look for the assembly and type – just be careful as the assembly may or may not be in the GAC.

22Oct/083

Change Password Script

I'd been meaning to post this for quite some time but just haven't gotten around to it - as paranoid administrators we often find the need to change our service account passwords and doing so with a product like SharePoint can be a rather significant effort if you consider all the various accounts that may be used in a least privileges model.  If you're just about to make hit this situation you're likely to do a quick search and find the following support article: http://support.microsoft.com/kb/934838 - this article provides you with the stsadm commands you need as well as a sample script that you can use. 

The problem I have is that the article doesn't provide a complete script - the sample only addresses some SSP related settings and app pools - it makes mention of the commands needed to change the farm account but it doesn't include those commands in the script.  It also has a line where you have to go and manually make a change via the browser - this is because the out of the box stsadm commands don't provide you with all the stuff you need to change all the passwords.  Specifically there's two missing - the default content access account and the user profile import account.  Seeing as I consider myself a developer and not an administrator (though sometimes I wonder) I decided to build those missing commands which I've previously blogged about here and here.

Using these two commands I created the script shown below - note that you don't necessarily need all the execadmsvcjobs calls but I prefer to make sure that all pending jobs complete before moving onto the next step.  Also - you may not have as many accounts - you can either remove the unnecessary lines and/or change the variable values as needed but I'd encourage you to leave the variable names so that it is clearer what each account is used for.  Of course this batch file will not actually make the password changes - if you need a script that will actually make the password change then look here.  And finally - please, please, please do NOT leave this script on your server when you are done - it's a huge security risk storing all the passwords in a script like this so you need to make sure that you either store the file in a secure location and/or blank the passwords out when not being utilized.

As always - if you have any comments or suggestions please let me know as I'm always looking for ways to improve and I'm by now means a batch file expert.

@echo off

SET PATH=C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN;%PATH%
SET DOMAIN=spdev
SET SSP=SSP1

rem *** Farm account (central admin app pool, timer jobs account)
set APP_POOL_CA_USER="%DOMAIN%\spfarm"
set APP_POOL_CA_PWD="pa$$w0rd"

rem *** SharePoint SSP Service Account
set SSPSVC_USER="%DOMAIN%\sspsvc"
set SSPSVC_PWD="pa$$w0rd"

rem *** SharePoint SSP Application Pool Account
SET APP_POOL_SSP_USER="%DOMAIN%\sspapppool"
SET APP_POOL_SSP_PWD="pa$$w0rd"

rem *** SharePoint Server Search Service Account 
set SEARCH_USER="%DOMAIN%\sspsearch"
set SEARCH_PWD="pa$$w0rd"

rem *** SharePoint Services Help Search Service Account 
set SEARCH_HELP_USER="%DOMAIN%\sphelpsearch"
set SEARCH_HELP_PWD="pa$$w0rd"

rem *** Default content access account for office search
set CONTENT_USER="%DOMAIN%\sspcontent"
set CONTENT_PWD="pa$$w0rd"

rem *** content access account for windows sharepoint services help search
set CONTENT_HELP_USER="%DOMAIN%\spcontentsearch"
set CONTENT_HELP_PWD="pa$$w0rd"

rem *** User profile import account
set PROFILE_IMPORT_USER="%DOMAIN%\sspuserprofilesvc"
set PROFILE_IMPORT_PWD="pa$$w0rd"

rem *** Portal application pool account
set APP_POOL_PORTAL_USER="%DOMAIN%\spportalapppool"
set APP_POOL_PORTAL_PWD="pa$$w0rd"

rem *** Teams sites application pool account
set APP_POOL_TEAMS_USER="%DOMAIN%\spcollabapppool"
set APP_POOL_TEAMS_PWD="pa$$w0rd"

rem *** My sites application pool account
set APP_POOL_MYSITE_USER="%DOMAIN%\spmysitesapppool"
set APP_POOL_MYSITE_PWD="pa$$w0rd"

rem *** Excel Services Unattended User Account
set SVC_EXCEL_USER="%DOMAIN%\SPSSAcct_dev"
set SVC_EXCEL_PWD="Pa$$w0rd"

goto startpoint
:startpoint


rem central admin
ECHO %DATE% %TIME%: Updating Central Admin password
stsadm -o updatefarmcredentials -userlogin %APP_POOL_CA_USER% -password %APP_POOL_CA_PWD% -identitytype configurableid
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Run "stsadm -o updatefarmcredentials -userlogin %APP_POOL_CA_USER% -password %APP_POOL_CA_PWD% -identitytype configurableid -local" on each WFE before continuing
pause
ECHO %DATE% %TIME%: Run "stsadm -o execadmsvcjobs" on each WFE before continuing.
pause

iisreset /noforce

rem application pools
ECHO %DATE% %TIME%: Updating app pool passwords for Portal
stsadm -o updateaccountpassword -userlogin %APP_POOL_PORTAL_USER% -password %APP_POOL_PORTAL_PWD% -noadmin
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating app pool passwords for Teams
stsadm -o updateaccountpassword -userlogin %APP_POOL_TEAMS_USER% -password %APP_POOL_TEAMS_PWD% -noadmin
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating app pool passwords for MySite
stsadm -o updateaccountpassword -userlogin %APP_POOL_MYSITE_USER% -password %APP_POOL_MYSITE_PWD% -noadmin
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating app pool passwords for SSP
stsadm -o updateaccountpassword -userlogin %APP_POOL_SSP_USER% -password %APP_POOL_SSP_PWD% -noadmin
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd


rem ssp - new
ECHO %DATE% %TIME%: Updating ssp password for new installs
stsadm -o editssp -title %SSP% -ssplogin %SSPSVC_USER% -ssppassword %SSPSVC_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd


rem osearch
ECHO %DATE% %TIME%: Updating osearch password
stsadm -o osearch -farmserviceaccount %SEARCH_USER% -farmservicepassword %SEARCH_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating default content access account
stsadm -o gl-updatedefaultcontentaccessaccount -username %CONTENT_USER% -password %CONTENT_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

iisreset /noforce

rem spsearch
ECHO %DATE% %TIME%: Updating spsearch password
stsadm -o spsearch -farmserviceaccount %SEARCH_HELP_USER% -farmservicepassword %SEARCH_HELP_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating spsearch content access account
stsadm -o spsearch -farmcontentaccessaccount %CONTENT_HELP_USER% -farmcontentaccesspassword %CONTENT_HELP_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating default profile import account
stsadm -o gl-setuserprofiledefaultaccessaccount -username %PROFILE_IMPORT_USER% -password %PROFILE_IMPORT_PWD% -sspname %SSP%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating excel services unattended service account
stsadm -o set-ecsexternaldata -ssp %SSP% -unattendedserviceaccountname %SVC_EXCEL_USER% -unattendedserviceaccountpassword %SVC_EXCEL_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

rem restarting IIS
ECHO %DATE% %TIME%: Doing soft restart of IIS

iisreset /noforce
echo on
goto end

:errhnd

echo An error occured - terminating script.

:end

To use this script on WSS just remove the unnecessary elements (lines with the following commands: gl-setuserprofiledefaultaccessaccount, gl-updatedefaultcontentaccessaccount, editssp, osearch, and set-ecsexternaldata).

8Mar/0834

Sample Install Script

I've mentioned a few times on my blog that I was going to post a sample install script which can be used to set up and configure a basic MOSS install (will also work with WSS but you'll need to pull a lot out) and now I've finally got around to actually doing it. I've created a zip file which contains two files that you can download. The first file, variables.bat, simply contains the core configuration settings that you'll want to change as you build each of your environments. The second file, install.bat, is the main install file which calls the psconfig tool and numerous stsadm commands (there's also a snapshot (WSP) of my custom extensions which the install.bat file will install immediately after psconfig does what it needs to do). You can use whatever accounts you want but I tend to follow a least priveledges model. The following list details the specific accounts typically needed as well as the actual configurations that must be manually made for each account (if applicable). Note that the sample script and the information below assumes that you are using Kerberos.

  1. SqlSvrSvc: SQL Server Service Account
    1. Machine SQL Server is running on must be trusted for delegation for Kerberos
    2. Needs an SPN for “MSSQLSvc/%DBSERVER%” (host and FQDN)
  2. SpAdmin: SharePoint Administrator
    1. Member of the local admin group on each server in which setup is run
    2. SQL Server logon requiring the following roles
      1. securityadmin
      2. dbcreator
      3. db_owner for each database that stsadm may need to interact with
      4. Should NOT be a member of the local admin group on the SQL Server servers
  3. SpFarm: SharePoint Server Farm Account
    1. Must be trusted for delegation for Kerberos
    2. Needs an SPN for “HTTP/%WEBSERVER%” (host and FQDN)
    3. Must be added to WSS_WPG group on WFE
  4. SspAppPool: SharePoint SSP Application Pool Account
    1. Must be trusted for delegation for Kerberos
    2. Needs an SPN for “HTTP/sspadmin” (host and FQDN)
    3. Must be added to WSS_WPG group on WFE
  5. SspSvc: SharePoint SSP Service Account
  6. SspSearch: SharePoint Server Search Service Account
  7. SspContent: SharePoint Default Content Access Account
  8. SspUserProfileSvc: SharePoint SSP Profile Import Default Access Account
    1. Must have read access to the directory service (AD)
    2. If “Enable Server Side Incremental” is selected for an Active Directory connection and the environment is Windows 2000 Server, the account must have the Replicate Changes permission in Active Directory (this permission is not required for Windows Server 2003 AD environments)
    3. Must be given the “Manage User Profiles” personalization services permission (this is a done within SharePoint)
    4. Must be given “View” permissions on entities used in BDC import connections (this is done within SharePoint)
  9. SspExcelSvc: SharePoint Excel Services Unattended Service Account
  10. SpHelpSearch: SharePoint Services Help Search Service Account
  11. SpContentSearch: SharePoint Services Search Content Access Account
  12. SpPortalAppPool: SharePoint Portal Application Pool Identity
    1. Must be trusted for delegation for Kerberos
    2. Needs an SPN for “HTTP/portal” (host and FQDN)
    3. Must be added to WSS_WPG group on WFE
  13. SpMySitesAppPool: SharePoint MySites Application Pool Identity
    1. Must be trusted for delegation for Kerberos
    2. Needs an SPN for “HTTP/mysites” (host and FQDN)
    3. Must be added to WSS_WPG group on WFE
  14. SpAdminitrators: SharePoint Administrators Security Group
    1. The spadmin account would be in this group along with any users who need full admin rights

The first thing you'll want to do if you download the sample script is to modify the variables.bat file, shown below:

rem SET PATH=C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN;%PATH%

rem ******* SERVERS *********
SET DOMAIN=spdev
SET SERVER_MAIL="sharepoint1.spdev.com"
SET SERVER_INDEX=sharepoint1
SET SERVER_DB=spsql1

rem ******* DATABASES ********
SET DB_CONFIG_NAME="SharePoint_ConfigDB"
SET DB_CENTRALADMINCONTENT_NAME="SharePoint_CentralAdminContent"
SET DB_SEARCHHELP_NAME="SharePoint_Search_HelpData"
SET DB_SSPCONFIG_NAME="SharePoint_SSP_ConfigDB"
SET DB_MYSITES_NAME="SharePoint_MySites"
SET DB_SSPCONTENT_NAME="SharePoint_SSP_Content"
SET DB_SEARCHCONTENT_NAME="SharePoint_SSP_SearchContent1"
SET DB_PORTALCONTENT_NAME="SharePoint_PortalContent1"
SET DB_TEAMSCONTENT_NAME="SharePoint_TeamsContent1"

rem ******* FILE PATHS ********
SET PATH_HELPSEARCH_INDEXES="e:\MOSS\Indexes\HelpData"
SET PATH_SSP_INDEXES="e:\MOSS\Indexes\Office Server\Applications"
SET PATH_SSPVDIR="e:\MOSS\Webs\SSPAdmin"
SET PATH_MYSITESVDIR="e:\MOSS\Webs\MySites"
SET PATH_USAGELOGS="e:\MOSS\Usage"
SET PATH_PORTALVDIR="e:\MOSS\Webs\Portal"
SET PATH_TEAMSVDIR="e:\MOSS\Webs\Teams"
SET PATH_LOGS="e:\MOSS\Logs"

rem ******* ACCOUNTS ********
SET ACCT_SPFARM="%DOMAIN%\spfarm"
SET ACCT_SPFARM_PWD="pa$$w0rd"

SET ACCT_SPADMIN="%DOMAIN%\spadmin"
SET ACCT_SPADMIN_EMAIL="no-reply@spdev.com"
SET ACCT_SPADMIN_NAME="SharePoint Administrator"
SET ACCT_SPADMIN_GROUPNAME="%DOMAIN%\spadministrators"

rem *** SharePoint Server Search Service Account
SET ACCT_SSPSEARCH="%DOMAIN%\sspsearch"
SET ACCT_SSPSEARCH_PWD="pa$$w0rd"

rem *** SharePoint Services Help Search Service Account
SET ACCT_SEARCH_HELP="%DOMAIN%\sphelpsearch"
SET ACCT_SEARCH_HELP_PWD="pa$$w0rd"

rem *** content access account for windows sharepoint services help search
set ACCT_CONTENT_HELP="%DOMAIN%\spcontentsearch"
set ACCT_CONTENT_HELP_PWD="pa$$w0rd"

rem *** Default content access account for office search
SET ACCT_SSPCONTENT="%DOMAIN%\sspcontent"
SET ACCT_SSPCONTENT_PWD="pa$$w0rd"

rem *** SharePoint SSP Application Pool Account
SET ACCT_SSPAPPPOOL="%DOMAIN%\sspapppool"
SET ACCT_SSPAPPPOOL_PWD="pa$$w0rd"

rem *** My sites application pool account
SET ACCT_MYSITESAPPPOOL="%DOMAIN%\spmysitesapppool"
SET ACCT_MYSITESAPPPOOL_PWD="pa$$w0rd"
SET ACCT_MYSITESUSERS_GROUP="%DOMAIN%\SPMySiteUsers"

rem *** SharePoint SSP Service Account
SET ACCT_SSPSVC="%DOMAIN%\sspsvc"
SET ACCT_SSPSVC_PWD="pa$$w0rd"

rem *** User profile import account
SET ACCT_SSPUSERPROFILESVC="%DOMAIN%\sspuserprofilesvc"
SET ACCT_SSPUSERPROFILESVC_PWD="pa$$w0rd"

rem *** Portal application pool account
SET ACCT_SPPORTALAPPPOOL="%DOMAIN%\spportalapppool"
SET ACCT_SPPORTALAPPPOOL_PWD="pa$$w0rd"

rem *** Teams sites application pool account
SET ACCT_SPTEAMSAPPPOOL="%DOMAIN%\spteamsapppool"
SET ACCT_SPTEAMSAPPPOOL_PWD="pa$$w0rd"

rem *** Excel Services unattended access account
set ACCT_EXCEL_USER="%DOMAIN%\sspexcelsvc"
set ACCT_EXCEL_PWD="pa$$w0rd"

SET ACCT_PORTAL_SECONDARYSITEOWNER="%DOMAIN%\siteowner1"
SET ACCT_PORTAL_SECONDARYSITEOWNER_EMAIL="siteowner1@spdev.com"
SET ACCT_PORTAL_SECONDARYSITEOWNER_NAME="Site Owner1"


SET ACCT_TEAMS_SECONDARYSITEOWNER="%DOMAIN%\siteowner1"
SET ACCT_TEAMS_SECONDARYSITEOWNER_EMAIL="siteowner1@spdev.com"
SET ACCT_TEAMS_SECONDARYSITEOWNER_NAME="Site Owner1"


rem ******** WEB APPLICATIONS **********
SET CENTRALADMIN_PORT=1234

SET WEB_SSP_URL="http://sspadmin/"
SET WEB_SSP_IISDESC="SharePoint Shared Services Admin (80)"
SET WEB_SSP_APPIDNAME="SharePoint_SSP_AppPool"
SET WEB_SSP_NAME="SSP1"

SET WEB_MYSITES_URL="http://mysites/"
SET WEB_MYSITES_IISDESC="SharePoint My Sites (80)"
SET WEB_MYSITES_APPIDNAME="SharePoint_MySites_AppPool"

SET WEB_PORTAL_URL=http://portal/
SET WEB_PORTAL_SITEDIR_URL="%WEB_PORTAL_URL%SiteDirectory"
SET WEB_PORTAL_NAME="Portal"
SET WEB_PORTAL_DESC=""
SET WEB_PORTAL_IISDESC="SharePoint Portal (80)"
SET WEB_PORTAL_APPIDNAME="SharePoint_Portal_AppPool"

SET WEB_TEAMS_URL=http://teams/
SET WEB_TEAMS_SITEDIR_URL="%WEB_PORTAL_URL%SiteDirectory"
SET WEB_TEAMS_NAME="Teams"
SET WEB_TEAMS_DESC=""
SET WEB_TEAMS_IISDESC="SharePoint Teams (80)"
SET WEB_TEAMS_APPIDNAME="SharePoint_Collaboration_AppPool"

The variables.bat file details all the servers, accounts, web addresses and names, database names, etc. For the sample I'm assuming a single server (named "SharePoint1") which is acting as the database server, WFE and index/query server (typical of a single user development environment). Examine each line carefully and make sure that you set the variables to meet your needs. For this sample I'm assuming that four web applications will be created: Central Admin, Shared Services Provider Admin, My Sites, and a single Portal. The "Portal" web application is the one that will be almost definitely changed by everyone - if you need to add more web applications just follow the pattern for the "Portal" web application. The install.bat file executes all the commands necessary to build out the MOSS configuration. The install.bat file can be seen below:

echo off

echo %DATE% %TIME%: Starting script

call variables.bat

goto startpoint
:startpoint
rem *** NOTE: The order of the following psconfig statements is critical - do not re-order.
ECHO %DATE% %TIME%:  Building configuration database
psconfig -cmd configdb -create -server %SERVER_DB% -database %DB_CONFIG_NAME% -user %ACCT_SPFARM% -password %ACCT_SPFARM_PWD% -admincontentdatabase %DB_CENTRALADMINCONTENT_NAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Installing help content
psconfig -cmd helpcollections -installall
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Securing resources
psconfig -cmd secureresources
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Installing services
psconfig -cmd services -install
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Installing features
psconfig -cmd installfeatures
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Creating central admin site
psconfig -cmd adminvs -provision -port %CENTRALADMIN_PORT% -windowsauthprovider enablekerberos
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Adding application content to central admin site
psconfig -cmd applicationcontent -install
if not errorlevel 0 goto errhnd

pause

ECHO %DATE% %TIME%: Installing custom stsadm extensions
stsadm -o addsolution -filename "Lapointe.SharePoint.STSADM.Commands.wsp"
stsadm -o deploysolution -local -allowgacdeployment -name "Lapointe.SharePoint.STSADM.Commands.wsp"
stsadm -o execadmsvcjobs

REM ====================================
REM ======= BEGIN SERVICES CONFIG ======
REM ====================================

echo %DATE% %TIME%: BEGINNING SERVICES CONFIGURATIONS...
echo

ECHO %DATE% %TIME%: Enabling sharepoint services help search service
stsadm -o spsearch -action start -farmserviceaccount %ACCT_SEARCH_HELP% -farmservicepassword %ACCT_SEARCH_HELP_PWD% -farmperformancelevel maximum -farmcontentaccessaccount %ACCT_CONTENT_HELP% -farmcontentaccesspassword %ACCT_CONTENT_HELP_PWD% -indexlocation %PATH_HELPSEARCH_INDEXES% -databaseserver %SERVER_DB% -databasename %DB_SEARCHHELP_NAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Enabling the office sharepoint search service
stsadm -o osearch -action start -role Index -farmcontactemail %ACCT_SPADMIN_EMAIL% -farmperformancelevel maximum -farmserviceaccount %ACCT_SSPSEARCH% -farmservicepassword %ACCT_SSPSEARCH_PWD% -defaultindexlocation %PATH_SSP_INDEXES%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Starting excel calculation services
stsadm -o provisionservice -action start -servicetype "Microsoft.Office.Excel.Server.ExcelServerSharedWebService, Microsoft.Office.Excel.Server, Version = 12.0.0.0, Culture = neutral, PublicKeyToken = 71e9bce111e9429c"
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Starting Document Conversions Load Balancer Service
stsadm -o provisionservice -action start -servicetype "Microsoft.Office.Server.Conversions.LoadBalancerService, Microsoft.Office.Server.Conversions, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -servicename DCLoadBalancer
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Starting Document Conversions Launcher Service
stsadm -o provisionservice -action start -servicetype "Microsoft.Office.Server.Conversions.LauncherService, Microsoft.Office.Server.Conversions, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -servicename DCLauncher
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Resetting IIS
iisreset /noforce
if not errorlevel 0 goto errhnd


ECHO %DATE% %TIME%: Adding %ACCT_SPADMIN% to Farm Administrators group
stsadm -o adduser -url "http://localhost:%CENTRALADMIN_PORT%" -userlogin %ACCT_SPADMIN% -group "Farm Administrators" -username %ACCT_SPADMIN_NAME% -useremail %ACCT_SPADMIN_EMAIL%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Adding %ACCT_SPADMIN_GROUPNAME% to Farm Administrators group
stsadm -o gl-adduser2 -url "http://localhost:%CENTRALADMIN_PORT%" -userlogin %ACCT_SPADMIN_GROUPNAME% -group "Farm Administrators" -username %ACCT_SPADMIN_NAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting outbound email settings
stsadm -o email -outsmtpserver %SERVER_MAIL% -fromaddress %ACCT_SPADMIN_EMAIL% -replytoaddress %ACCT_SPADMIN_EMAIL% -codepage 65001
if not errorlevel 0 goto errhnd


echo
echo %DATE% %TIME%: FINISHED SERVICES CONFIGURATIONS
echo TODO: Set Load Balancer Server and Port
pause

REM ====================================
REM ======= END SERVICES CONFIG ========
REM ====================================

echo
echo


REM ====================================
REM ========= BEGIN SSP ================
REM ====================================
echo %DATE% %TIME%: BEGINNING SSP SETTINGS
echo

ECHO %DATE% %TIME%: Creating the My Sites web application
stsadm -o gl-createwebapp -url %WEB_MYSITES_URL% -directory %PATH_MYSITESVDIR% -sethostheader -ownerlogin %ACCT_SPADMIN% -owneremail %ACCT_SPADMIN_EMAIL% -description %WEB_MYSITES_IISDESC% -apidname %WEB_MYSITES_APPIDNAME% -apidtype configurableid -apidlogin %ACCT_MYSITESAPPPOOL% -apidpwd %ACCT_MYSITESAPPPOOL_PWD% -databasename %DB_MYSITES_NAME% -donotcreatesite -timezone 12
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Creating SSP Admin site.
stsadm -o gl-createwebapp -url %WEB_SSP_URL% -directory %PATH_SSPVDIR% -sethostheader -ownerlogin %ACCT_SPADMIN% -owneremail %ACCT_SPADMIN_EMAIL% -description %WEB_SSP_IISDESC% -apidname %WEB_SSP_APPIDNAME% -apidtype configurableid -apidlogin %ACCT_SSPAPPPOOL% -apidpwd %ACCT_SSPAPPPOOL_PWD% -databasename %DB_SSPCONFIG_NAME% -donotcreatesite -timezone 12
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Creating the Shared Service Provider
stsadm -o createssp -title %WEB_SSP_NAME% -url %WEB_SSP_URL% -mysiteurl %WEB_MYSITES_URL% -ssplogin %ACCT_SSPSVC% -indexserver %SERVER_INDEX% -indexlocation %PATH_SSP_INDEXES% -ssppassword %ACCT_SSPSVC_PWD% -sspdatabaseserver %SERVER_DB% -sspdatabasename %DB_SSPCONTENT_NAME% -searchdatabaseserver %SERVER_DB% -searchdatabasename %DB_SEARCHCONTENT_NAME% -ssl no
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Resetting IIS
iisreset /noforce
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting the new SSP as the default SSP
stsadm -o setdefaultssp -title %WEB_SSP_NAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Giving %ACCT_SPADMIN_GROUPNAME% all permissions to SSP
stsadm -o gl-setsspacl -sspname %WEB_SSP_NAME% -rights All -user %ACCT_SPADMIN_GROUPNAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Giving %ACCT_SSPUSERPROFILESVC% profile management permissions
stsadm -o gl-setsspacl -sspname %WEB_SSP_NAME% -rights ManageUserProfiles -user %ACCT_SSPUSERPROFILESVC%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Giving %ACCT_SSPSEARCH% profile management permissions
stsadm -o gl-setsspacl -sspname %WEB_SSP_NAME% -rights ManageUserProfiles -user %ACCT_SSPSEARCH%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting usage analysis settings
mkdir %PATH_USAGELOGS%
stsadm -o gl-setusageanalysis -enablelogging true -enableusageprocessing true -logfilelocation %PATH_USAGELOGS% -numberoflogfiles 30 -processingstarttime "10:00PM" -processingendtime "1:00AM" -sspname %WEB_SSP_NAME% -enableadvancedprocessing true -enablequerylogging true
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Enabling kerberos on the SSP
stsadm -o setsharedwebserviceauthn -negotiate
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting delegation for excel services (to enable Kerberos)
stsadm -o set-ecssecurity -ssp %WEB_SSP_NAME% -accessmodel delegation
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating excel services unattended service account
stsadm -o set-ecsexternaldata -ssp %SSP% -unattendedserviceaccountname %ACCT_EXCEL_USER% -unattendedserviceaccountpassword %ACCT_EXCEL_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Updating default content access account
stsadm -o gl-updatedefaultcontentaccessaccount -username %ACCT_SSPCONTENT% -password %ACCT_SSPCONTENT_PWD%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting user profile default access account
stsadm -o gl-setuserprofiledefaultaccessaccount -username %ACCT_SSPUSERPROFILESVC% -password %ACCT_SSPUSERPROFILESVC_PWD% -sspname %WEB_SSP_NAME%
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Running pending jobs
stsadm -o execadmsvcjobs
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting user profile full import schedule
stsadm -o gl-setuserprofileimportschedule -sspname %WEB_SSP_NAME% -type full -occurrence weekly -hour 3 -dayofweek Saturday -enabled true -runjob
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Setting user profile incremental import schedule
stsadm -o gl-setuserprofileimportschedule -sspname %WEB_SSP_NAME% -type incremental -occurrence daily -hour 22 -enabled true
if not errorlevel 0 goto errhnd

ECHO %DATE% %TIME%: Executing pending timer jobs
stsadm.exe -o execadmsvcjobs
if not errorlevel 0 goto errhnd

echo
echo %DATE% %TIME%: FINISHED SSP SETTINGS

REM ====================================
REM =========== END SSP ================
REM ====================================

echo
echo

REM ====================================
REM =========== BEGIN PORTAL =============
REM ====================================
:portal
echo %DATE% %TIME%: BEGINNING PORTAL CORE SETTINGS
echo
call portal.bat
echo
echo %DATE% %TIME%: FINISHED PORTAL CORE SETTINGS
pause

REM ====================================
REM ============ END PORTAL ==============
REM ====================================



echo
echo


REM ====================================
REM =========== BEGIN TEAMS =============
REM ====================================
:teams
echo %DATE% %TIME%: BEGINNING TEAMS CORE SETTINGS
echo
call teams.bat
echo
echo %DATE% %TIME%: FINISHED TEAMS CORE SETTINGS
pause
goto end
REM ====================================
REM ============ END TEAMS ==============
REM ====================================


echo
echo

REM ====================================
REM ======= BEGIN MY SITES =============
REM ====================================
:mysites
echo %DATE% %TIME%: BEGINNING MY SITES SETTINGS
echo
call mysites.bat
echo
echo %DATE% %TIME%: FINISHED MY SITES SETTINGS
pause
REM ====================================
REM ========= END MY SITES =============
REM ====================================

echo
echo

ECHO ******************* Run Connect.bat on each WFE **************************
pause

ECHO %DATE% %TIME%: Setting log file path
mkdir %PATH_LOGS%
stsadm -o gl-tracelog -logdirectory %PATH_LOGS%
if not errorlevel 0 goto errhnd

echo
echo

ECHO %DATE% %TIME%: TODO 1 - Configure searching (http://sspadmin/ssp/admin/_layouts/listcontentsources.aspx)

goto end

:errhnd

echo An error occured - terminating script.

:end

If you look at the file you'll see that the first thing it does is "goto startpoint" and that "startpoint" marker is right below the goto statement. I put that there so that if the file should fail for any reason I can simply move the startpoint marker to the next command I want to have run and then re-execute the script (I often find that the "setuserprofiledefaultaccessaccount" command will fail on the first run if I haven't loaded the SSP in the browser before it runs - moving the startpoint to right before this command I can re-run the batch file and it will continue right on). The next thing the script does is call psconfig. There's tons of help/guidance on how to script psconfig so I won't bother hear - just make sure you review the settings and make any necessary changes to suit your environment. Once psconfig completes you now have a farm created with a working Central Admin site. The script then goes on to enable services and build out the SSP, MySites, and Portal web applications making various configurations to each. Again, there's lots of resources on how to use the various commands that this sample script uses (including my own blog) so I won't go through all the steps that it does. Just make sure that you review every line of this script and make sure you understand what it is doing (this is meant to get you started and various assumptions are made with the configuration so that I can demonstrate what can be done so make sure you look at every settings and adjust/remove as needed). One final note - I've never been a fan of batch files or command line "programming" (despite the subject of this blog) and I am certainly no expert at creating batch files - there's probably considerably better ways to do some of what I've done in my samples. If you have any suggestions as to how this can be done better I'm all ears :)

Tagged as: , , 34 Comments