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

18Jul/130

My First Pluralsight Course

Throughout the years I've done numerous presentations related to using PowerShell with SharePoint and through them all I've often found myself wanting more time so that I could share more details. Recently some friends of mine pointed me in the direction of Pluralsight and the opportunities they had for new authors – I saw this as a fantastic way to take many of the presentations I've done in the past and polish them up, add some details, and create a real course out of them. So with that I'm pleased to announce that my first Pluralsight course is now available: Using Windows PowerShell with SharePoint 2010 and SharePoint 2013. The folks at Pluralsight were amazing to work with and going through the authoring process has given me a whole new level of respect for the existing authors and for Pluralsight as a company; they seem to be doing everything right and every Pluralsight employee I've interacted with exemplifies professionalism and dedication to the goal of producing a great product, and I am humbled by the acceptance of my course into their library.

So if you're a developer or an administrator and you're working with SharePoint then I strongly recommend you go through some or all of my course to help you better understand how to use PowerShell, and more specifically, how to use it with SharePoint. You can see the full course outline by following the previous link but I've gone ahead and included the course and module descriptions here for your direct reference:

Using Windows PowerShell with SharePoint 2010 and SharePoint 2013

When it comes to administering and automating SharePoint 2010, 2013, and Office 365, there is no better tool than Windows PowerShell. After going through this course you'll have the skills and knowledge necessary to be productive with PowerShell. In the first two modules you'll get a jump start into PowerShell where you'll learn everything from basic syntax to creating functions and scripts, all within the context of SharePoint. Next you'll discover what's new when it comes to using PowerShell V3 with SharePoint 2013. Administering SharePoint with PowerShell does not mean that you're limited to what you can do directly on the server and in this course you'll learn everything you need to know to manage your Farm remotely, whether you are using Office 365 or an on-premises installation. And finally, sometimes the out of the box cmdlets just aren't enough so we'll teach you how to create your own custom cmdlets that you can deploy to your SharePoint Farm. After completing this course you'll be on your way to becoming a SharePoint superstar as you'll have all the core knowledge you need to start administering and automating SharePoint using Windows PowerShell.

  1. Introduction to PowerShell for SharePoint

    This module focuses on the basics of Windows PowerShell, all with an emphasis on SharePoint. At the conclusion of this module, you should have enough basic knowledge to start working with SharePoint via the SharePoint Management Shell.

  2. Scripting with PowerShell & SharePoint

    This module builds on the foundations presented in the first module and gets beyond what is typically done in the console. During this module you'll start by learning about conditional logic and looping and then move onto creating functions and scripts.

  3. PowerShell V3 + SharePoint 2013

    In this module you'll learn about many of the new features offered by Windows PowerShell V3 with SharePoint 2013.

  4. PowerShell & Office 365

    In this module we'll shift from SharePoint on-premises to SharePoint Online. You'll learn how to connect to your SharePoint Online tenant and what you can and can't do with the available SharePoint Online cmdlets.

  5. PowerShell Remoting

    In this module, we'll switch gears back to on-premises SharePoint installations as we take a look at how you can use PowerShell from your client machine to remotely connect to and manage your on-premises SharePoint Farm.

  6. Creating Custom Cmdlets for SharePoint

    In this module you'll learn how to extend the out of the box SharePoint cmdlets by creating your own custom cmdlets and PipeBind objects.

I already have some ideas for another course to essentially round out the PowerShell + SharePoint side of things – specifically I'm planning on creating a course that assumes you know PowerShell and now you need to learn how to better apply that knowledge to solve specific problems – so my plan for the next course will be to provide more solution focused education (at least, that's the plan, I'll have to see how this first course does before I commit to anything).

I hope that you find my course useful and please provide feedback (positive or negative) as I'm anxious to know what works and what doesn't so that I can continue to improve and bring better and better stuff to the community.

4Jun/131

Parallel SharePoint Tasks with PowerShell

Today I was working on a deployment for a client which entailed activating a custom SharePoint Feature on about 1000 Site Collections. This Feature did a fair number of things and on average it takes about 10-15 minutes to complete in their test environment (which is pretty slow compared to their production environment which I've not yet deployed to but I expect close to a 5 minute run time per Site Collection once I go to production with it). You can obviously do the math and quickly see that it will take me somewhere around 10 days for this to complete if I did one Site Collection at a time. This is just unacceptable as I personally don't want to be monitoring a Feature activation script for that long. What's worse is that when I look at CPU and memory utilization on the servers I can see that they have plenty of resources so it's not like the operation is actually taxing the system, they're just slow operations. So the solution, for me, is pretty obvious: I need to activate these Features in parallel.

There are two ways that I can achieve this using PowerShell and they depend on which version of PowerShell you're using. In my case I'm running SharePoint 2010 which means that I'm using PowerShell V2; because of this my only option is to use the Start-Job cmdlet with some control logic to dictate how many jobs I'm willing to run at once. If I were using SharePoint 2013 I could use the new workflow capabilities of PowerShell V3 thereby making the whole process a lot easier to understand. I'll show both approaches but I want to first start with what you would do for SharePoint 2010 with PowerShell V2.

Using Start-Job for Parallel Operations

The trick with using the Start-Job cmdlet is knowing when to stop creating new jobs until existing jobs have completed. The key is to use the Get-Job cmdlet and filter on the JobStateInfo property's State property and then, if you have reached your job count threshold, call the Wait-Job cmdlet to block the script until a job completes. The following script is a simple example of what I created for my client and can be used as a template for your own scripts:

$jobThreshold = 10

foreach ($site in (Get-SPSite -Limit All)) {
    # Get all running jobs
    $running = @(Get-Job | where { $_.JobStateInfo.State -eq "Running" })

    # Loop as long as our running job count is >= threshold
    while ($running.Count -ge $jobThreshold) {
        # Block until we get at least one job complete
        $running | Wait-Job -Any | Out-Null
        # Refresh the running job list
        $running = @(Get-Job | where { $_.JobStateInfo.State -eq "Running" })
    }

    Start-Job -InputObject $site.Url {
        $url = $input | %{$_}
        Write-Host "BEGIN: $(Get-Date) Processing $url..."

        # We're in a new process so load the snap-in
        Add-PSSnapin Microsoft.SharePoint.PowerShell

        # Enable the custom feature
        Enable-SPFeature -Url $url -Identity MyCustomFeature

        Write-Host "END: $(Get-Date) Processing $url."
    }
    # Dump the results of any completed jobs
    Get-Job | where { $_.JobStateInfo.State -eq "Completed" } | Receive-Job

    # Remove completed jobs so we don't see their results again
    Get-Job | where { $_.JobStateInfo.State -eq "Completed" } | Remove-Job
}

If you run this script and open up task manager you'll see that it's created a powershell.exe process for each job. You might be able to get away with more processes running at once but I'd recommend starting smaller before you bump it up too high and risk crippling your system.

Using PowerShell V3 Workflow

With PowerShell V3 we now have the ability to create a workflow within which I can specify tasks that should be run in parallel. I actually detailed how to do this in an earlier post so I won't spend much time on it here. I do want to show the code again for the sake of comparison as well as to point out one core difference (I recommend you read the Workflow section of my aforementioned post for more details). First though, here's a slightly modified version of the code so you can compare it to the V2 equivalent:

workflow Enable-SPFeatureInParallel {
    param(
        [string[]]$urls,
        [string]$feature
    )
 
    foreach -parallel($url in $urls) {
        InlineScript {
            # Write-Host doesn't work within a workflow
            Write-Output "BEGIN: $(Get-Date) Processing $($using:url)..."
 
            # We're in a new process so load the snap-in
            Add-PSSnapin Microsoft.SharePoint.PowerShell
 
            # Enable the custom feature
            Enable-SPFeature -Identity $using:feature -Url $using:url
            
            Write-Output "END: $(Get-Date) Processing $($using:url)."
        }
    }
}
Enable-SPFeatureInParallel (Get-SPSite -Limit All).Url "MyCustomFeature"

The first thing you should be asking yourself when you look at this is how many will be processed simultaneously? With the V2 version we could set the limit to whatever arbitrary value made sense for our situation. With this approach, however, we're limited to only 5 processes. You can see this if you run the code and open up task manager where, like the Start-Job approach, you'll see the powershell.exe for each process (note that it's not the workflow that is creating the powershell.exe process, it's the call to the InlineScript activity which is doing it – this call to InlineScript just helps to point out that you'll never see more than five created).

Summary

So, though we're limited by the number of processes and there are some downsides in terms of how we output information (like the fact that we can't use Write-Host and any output generated by one run could be intermixed with output from another run) I think the V3 approach is much cleaner and easier to use. That said, you could make the Start-Job approach generic so that you pass in a script to run along with an array of input values so that this could be easily used without having to look at the details of what's happening.

15Jun/124

Replace SharePoint 2010 Web Parts by Type

Have you ever found yourself in a situation where you needed to replace all occurrences of one web part type with another web part type? No? Consider this scenario: you are using the out of the box content query web part and you discover one of the numerous bugs with this web part or decide that you want to ensure that a specific XSLT file is always used or something like that, so you decide to create a custom content query web part by sub-classing the out of the box one; now you deploy your custom web part and remove the out of the box one from the web part gallery so that any new instance will now be based on your custom type. So this is great, you’ve accomplished your goals and have implemented one of my personal best practices (don’t use the out of the box content query web part and instead use a custom implementation). But now what do you do with the potentially hundreds of existing instances that are deployed on pages throughout your Farm? Well, you need to somehow replace those instances with instances of your new type. For this, PowerShell is your friend!

I’ve encountered this specific scenario as well as numerous other ones (replacing crappy web parts written by other consultants with new versions in different solutions, etc.) and have written lots of different scripts which accomplish this goal. But today I decided I was tired of creating and managing all these scripts so I went ahead and created a custom cmdlet that could achieve what I needed for a given SPFile with a single command: Replace-SPWebPartType.

The Replace-SPWebPartType cmdlet accepts an URL to a web part page (or an instance of an SPFile object) and a string or Type object representing the type of web part to replace and what to replace it with; you can further restrict what web parts are updated by providing a web part title to filter on and you can pass in additional properties to set via a Hashtable object (closed web parts are ignored). The full help for the cmdlet can be seen below:

NAME
    Replace-SPWebPartType

SYNOPSIS
    Replaces instances of one web part type with another web part type.

SYNTAX
    Replace-SPWebPartType [-File] <SPFilePipeBind> -OldType <TypePipeBind> -NewType <TypePipeBind> [-Title <String>] [-Properties <Hashtable>] [-Publish <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]


DESCRIPTION
    Replaces instances of one web part type with another web part type.

    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.


PARAMETERS
    -File <SPFilePipeBind>
        The URL to a web part page or an instance of an SPFile object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -OldType <TypePipeBind>
        The web part type to replace.

        Required?                    true
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -NewType <TypePipeBind>
        The web part type to replace the old type with.

        Required?                    true
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Title [<String>]
        The web part title to restrict the replacement to.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Properties [<Hashtable>]
        Additional properties to set or override after copying the old web part properties.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -Publish [<SwitchParameter>]
        If specified the page will be published after adjusting the Web Part.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".

INPUTS

 

OUTPUTS

 

NOTES


        For more information, type "Get-Help Replace-SPWebPartType -detailed". For technical information, type "Get-Help Replace-SPWebPartType -full".

    ------------------EXAMPLE------------------

    PS C:\> Replace-SPWebPartType -File "http://server_name/pages/default.aspx" -OldType "Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart, Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" -NewType "MyContentByQueryWebPart, MyCompany.SharePoint.WebParts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4ec4b9177b831752" -Publish


    This example replaces all instances of the web part who's class name is ContentByQueryWebPart with the web part who's class name is MyContentByQueryWebPart.


RELATED LINKS
    Get-SPFile

   

 

As previously stated, I’ve created a custom PipeBind object for the type parameters so that you can pass in either a string or an actual type object and you don’t always have to provide the full assembly details. So you could call the cmdlet like this (as opposed to using a string as shown in the example included with the help text):

$oldType = [Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart]
$newType = [Lapointe.SharePoint.MyContentByQueryWebPart]
Replace-SPWebPartType -File http://demo/Pages/default.aspx -OldType $oldType -NewType $oldType

 

The preceding examples all work with a single file but you could of course wrap this in a loop to update all web parts in a specific Library, Site, Site Collection, Web Application or the entire Farm. Additionally, the cmdlet supports providing the –WhatIf parameter so you can see what changes would be made without it actually changing anything. And finally, it also has support for web parts in content fields so when it deletes the old web part it will update the content to make sure it points to the new web part. (Note that I’m simply using reflection to iterate through all the properties of the old web part and to set those same properties on the new web part – if the property doesn’t exist on the new web part then it is ignored).

So to wrap up a final example here is a short script which updates all publishing pages where appropriate:

$newType = [Lapointe.SharePoint.MyContentByQueryWebPart]
$oldType = [Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart]
foreach ($site in (Get-SPWebApplication).Sites) {
    foreach ($web in $site.AllWebs) {
        foreach ($page in (Get-SPPublishingPage -Web $web)) {
            $gc = Start-SPAssignment
            try {
                $mgr = $gc | Get-SPLimitedWebPartManager $page.Uri.ToString() -ErrorAction SilentlyContinue
            } catch {
                Write-Warning "Error retrieving mgr: $($page.Uri.ToString())"
                continue
            }
            if ($mgr -eq $null) { continue }
            $changeNeeded = $false
            foreach ($wp in $mgr.WebParts) {
                if ($wp -eq $null) { continue }
                if ($wp.GetType() –eq $oldType) {
                    Write-Host "Found CQWP => $($page.Uri.ToString())::$($wp.Title)"
                    $changeNeeded = $true
                }
            }
            if ($changeNeeded) {
                Write-Host "About to make a change to $($page.Uri.ToString())" -ForegroundColor Green
                Replace-SPWebPartType -File $page.Uri.ToString() -OldType $oldType -NewType $newType -Publish
            }
            
            $gc | Stop-SPAssignment
        }
        $web.Dispose()
    }
    $site.Dispose()
}
15May/126

International SharePoint Conference 2012 Follow-up

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

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

image

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

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

Controller Scripts

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

param(
  [switch]$Connect
)

Write-Host "Script Start Time: $(Get-Date)" -ForegroundColor Green

#Main Farm
. .\FarmCreation\Build-SPFarm.ps1
$farmConfigFile = Resolve-Path .\FarmCreation\FarmConfigurations.xml
try {
    if ($Connect) {
        Join-SPFarm $farmConfigFile
    } else {
        New-SPFarm $farmConfigFile
    }
} catch {
    Write-Warning "Unable to build or join Farm!"
    $_
    return
}

#Web Applications
if (!$Connect) {
    #Only need to do this once so do it when we create the farm initially and not for each connection.
    try {
        . .\WebApplications\Provision-WebApplications.ps1
        Provision-WebApplications (Resolve-Path .\WebApplications\WebApplications.xml)
    } catch {
        Write-Warning "Unable to create web applications!"
        $_
        return
    }
}

#Services
try {
    #State Service
    . .\ServiceApplications\StateServices\Start-StateServices.ps1
    Start-StateServices (Resolve-Path .\ServiceApplications\StateServices\StateServices.xml)

    #Usage Service
    . .\ServiceApplications\UsageService\Start-UsageService.ps1
    Start-UsageService (Resolve-Path .\ServiceApplications\UsageService\UsageService.xml)

    #Secure Store Services
    . .\ServiceApplications\SecureStoreServices\Start-SecureStoreServices.ps1
    Start-SecureStoreServices (Resolve-Path .\ServiceApplications\SecureStoreServices\SecureStoreServices.xml)

    #Web Analytics Services
    . .\ServiceApplications\WebAnalyticsServices\Start-WebAnalyticsServices.ps1
    Start-WebAnalyticsServices (Resolve-Path .\ServiceApplications\WebAnalyticsServices\WebAnalyticsServices.xml)

    #User Code Services
    . .\ServiceApplications\UserCodeServices\Start-UserCodeServices.ps1
    Start-UserCodeServices (Resolve-Path .\ServiceApplications\UserCodeServices\UserCodeServices.xml)

    #BCS Services
    . .\ServiceApplications\BCSServices\Start-BCSServices.ps1
    Start-BCSServices (Resolve-Path .\ServiceApplications\BCSServices\BCSServices.xml)

    #Excel Services
    . .\ServiceApplications\ExcelServices\Start-ExcelServices.ps1
    Start-ExcelServices (Resolve-Path .\ServiceApplications\ExcelServices\ExcelServices.xml)

    #Word Automation Services
    . .\ServiceApplications\WordAutomationServices\Start-WordAutomationServices.ps1
    Start-WordAutomationServices (Resolve-Path .\ServiceApplications\WordAutomationServices\WordAutomationServices.xml)

    #Metadata Services
    . .\ServiceApplications\MetadataServices\Start-MetadataServices.ps1
    Start-MetadataServices (Resolve-Path .\ServiceApplications\MetadataServices\MetadataServices.xml)

    #C2WTS
    . .\ServiceApplications\ClaimsToWindowsTokenServices\Start-ClaimsToWindowsTokenServices.ps1
    Start-ClaimsToWindowsTokenServices (Resolve-Path .\ServiceApplications\ClaimsToWindowsTokenServices\ClaimsToWindowsTokenServices.xml)

    #PowerPoint Services
    . .\ServiceApplications\PowerPointServices\Start-PowerPointServices.ps1
    Start-PowerPointServices (Resolve-Path .\ServiceApplications\PowerPointServices\PowerPointServices.xml)

    #Word Viewing Services
    . .\ServiceApplications\WordViewingServices\Start-WordViewingServices.ps1
    Start-WordViewingServices (Resolve-Path .\ServiceApplications\WordViewingServices\WordViewingServices.xml)
    
    #User Profile Services
    . .\ServiceApplications\UserProfileServices\Start-UserProfileServices.ps1
    Start-UserProfileServices (Resolve-Path .\ServiceApplications\UserProfileServices\UserProfileServices.xml)

    #Enterprise Search Services
    . .\ServiceApplications\EnterpriseSearchServices\Start-EnterpriseSearchServices.ps1
    Start-EnterpriseSearchServices (Resolve-Path .\ServiceApplications\EnterpriseSearchServices\EnterpriseSearchServices.xml)

} catch {
    Write-Warning "Service Application creation failed!"
    $_
    return
}
finally {
    Write-Host "Script End Time: $(Get-Date)" -ForegroundColor Green
}

 

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

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

$servers = @("SP01","SP02","SP04","SP05","SP06")
$cred = Get-Credential "isclondon\spinstall"

foreach ($server in $servers) {
    Write-Host "---------------------------------------------------------------" -ForegroundColor Green
    Write-Host "$(Get-Date): Connecting to server $server." -ForegroundColor Green
    Write-Host "---------------------------------------------------------------" -ForegroundColor Green
    $session = New-PSSession -ComputerName $server -Authentication CredSSP -Credential $cred -ConfigurationName "SharePoint"
    Invoke-Command -Session $session -ScriptBlock {Set-ExecutionPolicy Bypass -Scope Process}
    Invoke-Command -Session $session -ScriptBlock {cd \\sp03\c$\Scripts}
    Invoke-Command -Session $session -ScriptBlock {. .\ConfigureServer.ps1 -Connect}
    Remove-PSSession $session
    Write-Host "$(Get-Date): Finished for server $server" -ForegroundColor Green
}

 

Build-SPFarm.ps1

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

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

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

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

<Farm ConfigDB="ISC_Config"
      ConfigDBFailoverDatabaseServer=""
      AdminContentDB="ISC_Content_CentralAdmin" 
      AdminContentDBFailoverDatabaseServer=""
      DatabaseServer="ISCSharePoint1" 
      Passphrase="p@ssw0rd"
      OutgoingEmailServer="192.168.1.101"
      OutgoingEmailFromAddr="administrator@isclondon.com"
      OutgoingEmailReplyToAddr="administrator@isclondon.com">
    <FarmAccount AccountName="isclondon\spfarm" AccountPassword="password" />
    <CentralAdmin Port="2010" AuthProvider="NTLM">
        <Servers>
            <Server Name="SP01" />
            <Server Name="SP02" />
            <Server Name="SP03" />
        </Servers>
    </CentralAdmin>
    <Services>
        <WebApplicationService>
            <Servers>
                <Server Name="SP01" />
                <Server Name="SP02" />
            </Servers>
        </WebApplicationService>
        <WorkflowTimerService>
            <Servers>
                <Server Name="SP01" />
                <Server Name="SP02" />
            </Servers>
        </WorkflowTimerService>
        <IncomingEmailService>
            <Servers>
                <Server Name="SP01" />
                <Server Name="SP02" />
            </Servers>
        </IncomingEmailService>
        <TraceService>
            <Account AccountName="isclondon\sptrace" AccountPassword="password" />
        </TraceService>
    </Services>
 </Farm>

 

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

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

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

function Join-SPFarm ([string]$settings = $(throw "-settings is required")) {
    Build-SPFarm $true $settings
}

function New-SPFarm ([string]$settings = $(throw "-settings is required")) {
    Build-SPFarm $false $settings
}

function Build-SPFarm ([bool]$connectToExisting = $false, [string]$settings = $(throw "-settings is required")) {   
    [xml]$config = Get-Content $settings

    if ([string]::IsNullOrEmpty($config.Farm.FarmAccount.AccountPassword)) {
        $farmAcct = Get-Credential $config.Farm.FarmAccount.AccountName
    } else {
        $farmAcct = New-Object System.Management.Automation.PSCredential $config.Farm.FarmAccount.AccountName, (ConvertTo-SecureString $config.Farm.FarmAccount.AccountPassword -AsPlainText -force)
    }

    $configDb = $config.Farm.ConfigDB
    $contentDB = $config.Farm.AdminContentDb
    $server = $config.Farm.DatabaseServer
    if ($config.Farm.Passphrase.Length -gt 0) {
        $passphrase = (ConvertTo-SecureString $config.Farm.Passphrase -AsPlainText -force)
    } else {
        Write-Warning "Using the Farm Admin's password for a passphrase"
        $passphrase = $farmAcct.Password
    }
    
    #Only build the farm if we don't currently have a farm created
    if ([Microsoft.SharePoint.Administration.SPFarm]::Local -eq $null) {
        psconfig -cmd upgrade -inplace b2b
        
        if ($connectToExisting) {
            #Connecting to farm
            Write-Host "Connecting to Farm..."
            Connect-SPConfigurationDatabase -DatabaseName $configDb -DatabaseServer $server -Passphrase $passphrase
        } else {
            #Creating new farm
            Write-Host "Creating Farm..."
            New-SPConfigurationDatabase `
                -DatabaseName $configDb `
                -DatabaseServer $server `
                -AdministrationContentDatabaseName $contentDB `
                -Passphrase $passphrase `
                -FarmCredentials $farmAcct
            
            if (![string]::IsNullOrEmpty($config.Farm.ConfigDBFailoverDatabaseServer)) {
                Set-FailoverDatabase $configDb $config.Farm.ConfigDBFailoverDatabaseServer
            }
            if (![string]::IsNullOrEmpty($config.Farm.AdminContentDBFailoverDatabaseServer)) {
                Set-FailoverDatabase $contentDB $config.Farm.AdminContentDBFailoverDatabaseServer
            }
        }
        #Verifying farm creation
        $spfarm = Get-SPFarm -ErrorAction SilentlyContinue
        if ($spfarm -eq $null) {
            throw "Unable to verify farm creation."
        }

        #ACLing SharePoint Resources
        Write-Host "Calling Initialize-SPResourceSecurity..."
        Initialize-SPResourceSecurity

        #Installing Services
        Write-Host "Calling Install-SPService..."
        Install-SPService

        #Installing Features
        Write-Host "Calling Install-SPFeature..."
        Install-SPFeature -AllExistingFeatures
        
        Remove-PsSnapin Microsoft.SharePoint.PowerShell
        Add-PsSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue
    } else {
        Write-Warning "Farm exists. Skipping creation."
    }
    
    if ((Get-Service sptimerv4).Status -ne "Running") {
        Write-Host "Starting SPTimerV4 Service"
        Start-Service sptimerv4
    }

    $scaConfig = $config.Farm.CentralAdmin
    $installSCA = (($scaConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
    if ($installSCA) {
        $auth = $scaConfig.AuthProvider
        $port = $scaConfig.Port
        $url = "http://$($env:computername):$port"
        $sca = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup($url)
        if ($installSCA -and $sca -eq $null) {
            #Provisioning Central Administration
            Write-Host "Creating Central Admin Site at $url..."
            New-SPCentralAdministration -Port $port -WindowsAuthProvider $auth

            #Installing Help
            Write-Host "Calling Install-SPHelpCollection..."
            Install-SPHelpCollection -All

            #Installing Application Content
            Write-Host "Calling Install-SPApplicationContent..."
            Install-SPApplicationContent
        }
    }
    
    if (!$connectToExisting) {
        $server = $config.Farm.OutgoingEmailServer
        $from = $config.Farm.OutgoingEmailFromAddr
        $replyTo = $config.Farm.OutgoingEmailReplyToAddr
        $charSet = 65001
        $wa = [Microsoft.SharePoint.Administration.SPAdministrationWebApplication]::Local
        if ($wa -ne $null) {
            $wa.UpdateMailSettings($server, $from, $replyTo, $charSet)
        }
    }
    
    #Stop or Start key service instances
    $startWebAppSvc = (($config.Farm.Services.WebApplicationService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
    Set-ServiceInstanceState "Microsoft SharePoint Foundation Web Application" $startWebAppSvc

    $startWorkflowSvc = (($config.Farm.Services.WorkflowTimerService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
    Set-ServiceInstanceState "Microsoft SharePoint Foundation Workflow Timer Service" $startWorkflowSvc

    $startIncomingEmailSvc = (($config.Farm.Services.IncomingEmailService.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
    Set-ServiceInstanceState "Microsoft SharePoint Foundation Incoming E-Mail" $startIncomingEmailSvc
    
    
    #Set SPTraceV4 to run as domain account
    $accountNode = $config.Farm.Services.TraceService.Account
    if ($accountNode -ne $null -and ![string]::IsNullOrEmpty($accountNode.AccountName)) {
        if ([string]::IsNullOrEmpty($accountNode.AccountPassword)) {
            $svcAccount = Get-Credential $accountNode.AccountName
        } else {
            $svcAccount = New-Object System.Management.Automation.PSCredential $accountNode.AccountName, (ConvertTo-SecureString $accountNode.AccountPassword -AsPlainText -force)
        }
        $user = Get-SPManagedAccount -Identity $svcAccount.Username -ErrorAction SilentlyContinue
        if ($user -eq $null) {
            $user = New-SPManagedAccount -Credential $svcAccount
        }
        $tracingSvc = (Get-SPFarm).Services | ? {$_.Name -eq "SPTraceV4"}
        if ($tracingSvc.ProcessIdentity.Username -ne $user.Username) {
            $tracingSvc.ProcessIdentity.ManagedAccount = $user
            $tracingSvc.ProcessIdentity.CurrentIdentityType = "SpecificUser"
            $tracingSvc.ProcessIdentity.Update()
            $tracingSvc.ProcessIdentity.Deploy()
        }
        Write-Host "Adding `"$($user.Username)`" to Performance Log Users group..."
        $p = Start-Process -PassThru -FilePath "c:\windows\system32\net.exe" -ArgumentList "localgroup `"Performance Log Users`" `"$($user.Username)`" /add" -Wait -NoNewWindow
        if ($p.ExitCode -ne 0) { 
            Write-Warning "Unable to add `"$($user.Username)`" to Performance Log Users group!" 
        } else { 
            Restart-Service SPTraceV4
            iisreset
        }
    }

} 
function Set-ServiceInstanceState($typeName, $enable) {
    $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq $typeName}
    if ($svc -eq $null) {
        throw "Unable to retrieve $typeName Service Instance."
    }
    if ($svc.Status -ne "Online" -and $enable) {
        Write-Host "Starting $typeName service instance..."
        $svc | Start-SPServiceInstance
    }
    if ($svc.Status -eq "Online" -and !$enable) {
        Write-Host "Stopping $typeName service instance..."
        $svc | Stop-SPServiceInstance -Confirm:$false
    }
}

function Set-FailoverDatabase($databaseName, [string]$failoverDbServer) {
    if (![string]::IsNullOrEmpty($failoverDbServer)) {
        $db = Get-SPDatabase | where {$_.Name -eq $databaseName}
        if ($db -eq $null) { 
            throw "Unable to retrieve the database to set the failover server: $databaseName" 
        }
        if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) {
            try {
                Write-Host "Adding failover database instance..."
                $db.AddFailoverServiceInstance($failoverDbServer)
                $db.Update()
            } catch {
                Write-Warning "Unable to set failover database server. $_"
            }
        }
    }
}

 

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

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

Provision-WebApplications.ps1

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

image

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

<Farm>
  <WebApplications>
    <WebApplication Name="SharePoint MySites" HostHeader="my.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\mysites" Ssl="false" LoadBalancedUrl="http://my.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="true" RequireContactForSsc="false" AuthenticationMode="Classic" EnableWindowsAuthentication="false" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
      <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" />
      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
      <ManagedPaths>
        <ManagedPath Explicit="false" RelativeURL="/personal" />
      </ManagedPaths>
      <UserPolicies>
        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
          <Permission Name="Full Control" />
        </UserPolicy>
      </UserPolicies>
      <ProxyGroup Name="Default" />
      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="true" AllowMasterPageEditing="true" ShowURLStructure="true" />
      <ContentDatabases>
        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_MySites" MaxSiteCount="15000" WarningSiteCount="9000">
          <SiteCollections>
            <SiteCollection Name="My Sites" Description="My Sites Host Site Collection" Url="http://my.isclondon.local" Template="SPSMSITEHOST#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
            </SiteCollection>
          </SiteCollections>
        </ContentDatabase>
      </ContentDatabases>
    </WebApplication>
    <WebApplication Name="SharePoint Team Sites" HostHeader="team.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\teams" Ssl="false" LoadBalancedUrl="http://team.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="false" RequireContactForSsc="false" AuthenticationMode="Classic" EnableWindowsAuthentication="false" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
      <ApplicationPool Name="SharePoint Collab" AccountName="isclondon\SPCollab" AccountPassword="password" />
      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
      <ManagedPaths>
      </ManagedPaths>
      <UserPolicies>
        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
          <Permission Name="Full Control" />
        </UserPolicy>
      </UserPolicies>
      <ProxyGroup Name="Default" />
      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" />
      <ContentDatabases>
        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_TeamSites" MaxSiteCount="15000" WarningSiteCount="9000">
          <SiteCollections>
            <SiteCollection Name="Team Sites" Url="http://team.isclondon.local" Template="CMSPUBLISHING#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
            </SiteCollection>
          </SiteCollections>
        </ContentDatabase>
      </ContentDatabases>
      <Features />
    </WebApplication>
    <WebApplication Name="SharePoint Intranet" HostHeader="intranet.isclondon.local" Port="80" DefaultTimeZone="2" Path="c:\sharepoint\webs\intranet" Ssl="false" LoadBalancedUrl="http://intranet.isclondon.local" AllowAnonymous="false" SelfServiceSiteCreation="false" RequireContactForSsc="false" AuthenticationMode="Claims" EnableWindowsAuthentication="true" AuthenticationMethod="NTLM" EnableBasicAuthentication="false" EnableFormsBasedAuthentication="false">
      <ApplicationPool Name="SharePoint Content" AccountName="isclondon\SPContent" AccountPassword="password" />
      <PortalSuperUserAccount AccountName="isclondon\spcacheuser" AccountPassword="password" />
      <PortalSuperReaderAccount AccountName="isclondon\spcachereader" AccountPassword="password" />
      <ManagedPaths>
        <ManagedPath Explicit="true" RelativeURL="/CTHub" />
      </ManagedPaths>
      <UserPolicies>
        <UserPolicy UserLogin="isclondon\SPInstall" UserDisplayName="SharePoint Administrator" Zone="All">
          <Permission Name="Full Control" />
        </UserPolicy>
      </UserPolicies>
      <ProxyGroup Name="Default" />
      <SPDesigner AllowDesigner="true" AllowRevertFromTemplate="false" AllowMasterPageEditing="true" ShowURLStructure="true" />
      <ContentDatabases>
        <ContentDatabase Default="true" Server="ISCSharePoint1" FailoverDatabaseServer="" Name="ISC_Content_Intranet" MaxSiteCount="15000" WarningSiteCount="9000">
          <SiteCollections>
            <SiteCollection Name="Intranet" Url="http://intranet.isclondon.local" Template="STS#0" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
            </SiteCollection>
            <SiteCollection Name="Content Type Hub" Url="http://intranet.isclondon.local/CTHub" Template="STS#1" LCID="1033" OwnerLogin="isclondon\administrator" OwnerEmail="administrator@isclondon.com">
            </SiteCollection>
          </SiteCollections>
        </ContentDatabase>
      </ContentDatabases>
      <Features />
    </WebApplication>    
  </WebApplications>
</Farm>

 

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

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

function Provision-WebApplications() {
    <#
    .Synopsis
        Creates the web applications.
    .Description
        Creates the web applications.
    .Example
        PS C:\> . .\Provision-WebApplications.ps1
        PS C:\> Provision-WebApplication -SettingsFile c:\WebApplications.xml
    .Example
        PS C:\> . .\Provision-WebApplications.ps1
        PS C:\> Provision-WebApplications -ConfigXml ([xml](Get-Content c:\farmconfiguration.xml))
    .Parameter SettingsFile
        The path to an XML file.
    #>
    [CmdletBinding(DefaultParameterSetName="FilePath")]
    param (
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlElement")]
        [ValidateNotNull()]
        [System.Xml.XmlElement]$ConfigElement,

        [Parameter(Mandatory=$true, Position=0, ParameterSetName="XmlDocument")]
        [ValidateNotNull()]
        [xml]$ConfigXml,

        [Parameter(Mandatory=$true, Position=0, ParameterSetName="FilePath")]
        [ValidateNotNullOrEmpty()]
        [string]$SettingsFile
    )
    switch ($PsCmdlet.ParameterSetName) { 
        "XmlDocument" { 
            if ($ConfigXml.Farm.WebApplications.WebApplication -ne $null) {
                foreach ($appConfig in $ConfigXml.Farm.WebApplications.WebApplication) {
                    Provision-WebApplications -ConfigElement $appConfig
                }
                return
            }
        }
        "FilePath" {
            Provision-WebApplications -ConfigXml ([xml](Get-Content $SettingsFile))
            return
        }
    }
    $config = $ConfigElement

    if ($config -eq $null) {
        Write-Warning "No web application defined. Skipping."
        return
    }

    
    $webApp = Get-SPWebApplication -Identity $config.Name -ErrorAction SilentlyContinue
    
    if ($webApp -eq $null) {
        $allowAnon = [bool]::Parse($config.AllowAnonymous.ToString())
        $ssl = [bool]::Parse($config.Ssl.ToString())

        $db = $null
        if ($config.ContentDatabases -eq $null) {
            throw "A content database configuration setting could not be found for `"$($config.Name)`"."
        }
        if ($config.ContentDatabases.ChildNodes.Count -gt 1) {
            $db = $config.ContentDatabases.ContentDatabase | where {$_.Default -eq "true"}
            if ($db -is [array]) {
                Write-Warning "Multiple content databases set as default for `"$($config.Name)`" (using first)"
                $db = $db[0]
            }
        } else {
            $db = $config.ContentDatabases.ContentDatabase
        }
        if ($db -eq $null) {
            throw "A content database configuration setting could not be found for `"$($config.Name)`"."
        }
        
        $poolName = $config.ApplicationPool.Name
        $poolAcctName = $config.ApplicationPool.AccountName
        $poolAcctPwd = $config.ApplicationPool.AccountPassword
        $poolAcct = $null
        
        #Check for existing Application Pool
        $pools = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.ApplicationPools
        if (($pools | ? {$_.Name -eq $poolName}) -eq $null) {
            $poolAcct = Get-SPManagedAccount $poolAcctName -ErrorAction SilentlyContinue
            if ($poolAcct -eq $null) {
                $securePwd = ConvertTo-SecureString $poolAcctPwd -AsPlainText -force
                $cred = New-Object System.Management.Automation.PSCredential $poolAcctName, $securePwd
                $poolAcct = New-SPManagedAccount -Credential $cred
            }
        }
        
        $loadBalancedUrl = $config.LoadBalancedUrl
        $port = $config.Port
        if (![string]::IsNullOrEmpty($loadBalancedUrl)) {
            $loadBalancedUri = New-Object System.Uri $config.LoadBalancedUrl
            $port = $loadBalancedUri.Port
            $loadBalancedUrl = "$($loadBalancedUri.Scheme)://$($loadBalancedUri.Host)/"
        } else {
            if (![string]::IsNullOrEmpty($config.HostHeader)) {
                $loadBalancedUrl = "http://$($config.HostHeader)"
            }
        }
        if ([string]::IsNullOrEmpty($port)) { $port = "80" }

        if ($config.AuthenticationMode -eq "Claims") {
            $authProviders = @()
            $enableWindowsAuthentication = $false
            if (![string]::IsNullOrEmpty($config.EnableWindowsAuthentication)) { $enableWindowsAuthentication = [bool]::Parse($config.EnableWindowsAuthentication) }
            $enableFormsBasedAuthentication = $false
            if (![string]::IsNullOrEmpty($config.EnableFormsBasedAuthentication)) { $enableFormsBasedAuthentication = [bool]::Parse($config.EnableFormsBasedAuthentication) }

            if ($enableWindowsAuthentication) {
                $enableBasicAuthentication = $false
                if (![string]::IsNullOrEmpty($config.EnableBasicAuthentication)) { $enableBasicAuthentication = [bool]::Parse($config.EnableBasicAuthentication) }
                $disableKerberos = $config.AuthenticationMethod -eq "NTLM"
                $authProviders += New-SPAuthenticationProvider `
                    -UseWindowsIntegratedAuthentication:$enableWindowsAuthentication `
                    -DisableKerberos:$disableKerberos `
                    -UseBasicAuthentication:$enableBasicAuthentication `
                    -AllowAnonymous:$allowAnon
            }
            if ($enableFormsBasedAuthentication) {
                $authProviders += New-SPAuthenticationProvider `
                    -ASPNETMembershipProvider $config.ASPNETMembershipProviderName `
                    -ASPNETRoleProviderName $config.ASPNETRoleManagerName 
            }
            Write-Host "Creating Web Application: `"$($config.Name)`""
            $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl `
                -AllowAnonymousAccess:$allowAnon `
                -ApplicationPool $poolName `
                -ApplicationPoolAccount $poolAcct `
                -Name $config.Name `
                -AuthenticationProvider $authProviders `
                -DatabaseServer $db.Server `
                -DatabaseName $db.Name `
                -HostHeader $config.HostHeader `
                -Path $config.Path `
                -Port $port `
                -Url $loadBalancedUrl
        } else {
            $authMethod = "NTLM"
            if (![string]::IsNullOrEmpty($config.AuthenticationMethod)) { $authMethod = $config.AuthenticationMethod }

            Write-Host "Creating Web Application: `"$($config.Name)`""
            $webApp = New-SPWebApplication -SecureSocketsLayer:$ssl `
                -AllowAnonymousAccess:$allowAnon `
                -ApplicationPool $poolName `
                -ApplicationPoolAccount $poolAcct `
                -Name $config.Name `
                -AuthenticationMethod $authMethod `
                -DatabaseServer $db.Server `
                -DatabaseName $db.Name `
                -HostHeader $config.HostHeader `
                -Path $config.Path `
                -Port $port `
                -Url $loadBalancedUrl
        }

        Set-SPWebApplication -Identity $webApp -DefaultTimeZone $config.DefaultTimeZone

        #Re-get the web app to avoid update conflicts
        $webApp = Get-SPWebApplication -Identity  $webApp
        $webApp.SelfServiceSiteCreationEnabled = [bool]::Parse($config.SelfServiceSiteCreation)
        $webApp.RequireContactForSelfServiceSiteCreation = [bool]::Parse($config.RequireContactForSsc)
        if ($config.ProxyGroup -ne $null -and ![string]::IsNullOrEmpty($config.ProxyGroup.Name)) {
            $proxyGroupName = $config.ProxyGroup.Name
            if ($proxyGroupName -ne $null) {
                $webApp.ServiceApplicationProxyGroup = Get-ProxyGroup $proxyGroupName $true
            }
        }
        $webApp.Update()
        Write-Host """$($config.Name)"" successfully created."
    } else {
        #We got the web app so return it and don't attempt to recreate
        Write-Host """$($config.Name)"" already exists, skipping creation."
    }
    $setObjectCacheAccounts = $true
    if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount) -and ![string]::IsNullOrEmpty($config.PortalSuperReaderAccount)) {
        if ($config.PortalSuperUserAccount.AccountName -eq $config.PortalSuperReaderAccount.AccountName) {
            Write-Warning "The Portal Super User Account and Portal Super Reader Account must not be the same account as this will result in a security hole. The accounts will not be set."
            $setObjectCacheAccounts = $false
        }
    }
    $claimsPrefix = ""
    if ($config.AuthenticationMode -eq "Claims") {
        $claimsPrefix = "i:0#.w|"
    }
    if (![string]::IsNullOrEmpty($config.PortalSuperUserAccount.AccountName) -and $setObjectCacheAccounts) {
        $webApp.Properties["portalsuperuseraccount"] = "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)"
        Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperUserAccount.AccountName)" $config.PortalSuperUserAccount.AccountName "Full Control"
    }
    if (![string]::IsNullOrEmpty($config.PortalSuperReaderAccount.AccountName) -and $setObjectCacheAccounts) {
        $webApp.Properties["portalsuperreaderaccount"] = "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)"
        Set-WebAppUserPolicy $webApp "$claimsPrefix$($config.PortalSuperReaderAccount.AccountName)" $config.PortalSuperReaderAccount.AccountName "Full Read"
    }


    if ($config.UserPolicies) {
        Write-Host "Creating user policies..."
        [bool]$updateRequired = $false
        foreach ($userPolicyConfig in $config.UserPolicies.UserPolicy) {
            if ($userPolicyConfig -eq $null) { continue }

            [string]$zoneName = $userPolicyConfig.Zone
            [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies
            if ($zoneName.ToLower() -ne "all") {
                $policies = $webApp.ZonePolicies($zoneName)
            }
            Write-Host "Adding user policy for: $claimsPrefix$($userPolicyConfig.UserLogin)..."
            [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add("$claimsPrefix$($userPolicyConfig.UserLogin)", $userPolicyConfig.UserDisplayName)
            foreach ($permConfig in $userPolicyConfig.Permission) {
                [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $permConfig.Name}
                if ($policyRole -ne $null) {
                    Write-Host "Adding policy role: $($permConfig.Name)..."
                    $policy.PolicyRoleBindings.Add($policyRole)
                }
            }
            $updateRequired = $true
        }
        if ($updateRequired) {
            $webApp.Update()
        }
    }
    
    
    if ($config.ManagedPaths) {
        Write-Host "Creating managed paths..."
        foreach ($mpConfig in $config.ManagedPaths.ManagedPath) {
            if ($mpConfig -eq $null) { continue }

            $path = Get-SPManagedPath -Identity $mpConfig.RelativeURL `
                -WebApplication $webApp -ErrorAction SilentlyContinue
            if ($path -eq $null) {
                Write-Host "Creating ""$($mpConfig.RelativeURL)""..."
                New-SPManagedPath -RelativeURL $mpConfig.RelativeURL `
                    -Explicit :( [bool]::Parse($mpConfig.Explicit)) -WebApplication $webApp
            } else {
                Write-Host "Managed path already exists: ""$($mpConfig.RelativeURL)""."
            }
        }
    }
    
    if ($config.SPDesigner) {
        Write-Host "Configuring SharePoint Designer Settings..."
        sleep 5
        #Get the web app again to avoid an update conflict
        $webApp = Get-SPWebApplication $webApp
        $webApp | Set-SPDesignerSettings `
            -AllowDesigner ([bool]::Parse($config.SPDesigner.AllowDesigner)) `
            -AllowRevertFromTemplate ([bool]::Parse($config.SPDesigner.AllowRevertFromTemplate)) `
            -AllowMasterPageEditing ([bool]::Parse($config.SPDesigner.AllowMasterPageEditing)) `
            -ShowURLStructure ([bool]::Parse($config.SPDesigner.ShowURLStructure))
        $webApp.Update()
    }

    foreach ($dbConfig in $config.ContentDatabases.ContentDatabase) {
        $db = Get-SPContentDatabase $dbConfig.Name -ErrorAction SilentlyContinue
        if ($db -eq $null) { 
            $db = New-SPContentDatabase -Name $dbConfig.Name `
                -WebApplication $webApp `
                -DatabaseServer $dbConfig.Server `
                -MaxSiteCount $dbConfig.MaxSiteCount `
                -WarningSiteCount $dbConfig.WarningSiteCount
        }
        $failoverDbServer = $dbConfig.FailoverDatabaseServer
        if (![string]::IsNullOrEmpty($failoverDbServer)) {
            if (($db.FailoverServiceInstance -eq $null) -or ![string]::Equals($db.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase)) {
                try {
                    Write-Host "Adding failover database instance..."
                    $db.AddFailoverServiceInstance($failoverDbServer)
                    $db.Update()
                } catch {
                    Write-Warning "Unable to set failover database server. $_"
                }
            }
        }
        foreach ($siteConfig in $dbConfig.SiteCollections.SiteCollection) {
            $site = Get-SPSite $siteConfig.Url -ErrorAction SilentlyContinue
            if ($site -ne $null) {
                Write-Host "Site Collection $($siteConfig.Url) already exists. Skipping."
                $site.Dispose()
                continue
            }
            
            $lcid = $siteConfig.LCID
            if ([string]::IsNullOrEmpty($lcid)) { $lcid = [Microsoft.SharePoint.SPRegionalSettings]::GlobalServerLanguage.LCID }

            $optionalParams = @{}
            if (![string]::IsNullOrEmpty($siteConfig.SecondaryLogin)) { 
                $optionalParams += @{"SecondaryOwnerAlias"=$siteConfig.SecondaryLogin} }
            if (![string]::IsNullOrEmpty($siteConfig.SecondaryEmail)) { 
                $optionalParams += @{"SecondaryEmail"=$siteConfig.SecondaryEmail} }
            if (![string]::IsNullOrEmpty($siteConfig.OwnerEmail)) { 
                $optionalParams += @{"OwnerEmail"=$siteConfig.OwnerEmail} }

            Write-Host "Creating Site Collection $($siteConfig.Url)..."
            $site = New-SPSite `
                -Url $siteConfig.Url `
                -Description $siteConfig.Description `
                -Language $lcid `
                -Name $siteConfig.Name `
                -OwnerAlias $siteConfig.OwnerLogin `
                -Template $siteConfig.Template `
                -ContentDatabase $db `
                -ErrorAction Continue @optionalParams

            if ($site -eq $null) { 
                Write-Warning "Site collection was not created!"
            } else {
                Write-Host "Site collection successfully created: $($siteConfig.Url)"
                Write-Host "Setting default associated security groups..."
                sleep 5
                $refreshedSite = $site | Get-SPSite
                $secondaryLogin = $siteConfig.SecondaryLogin
                if (![string]::IsNullOrEmpty($secondaryLogin)) { $secondaryLogin = "$claimsPrefix$secondaryLogin" }
                $refreshedSite.RootWeb.CreateDefaultAssociatedGroups("$claimsPrefix$($siteConfig.OwnerLogin)", $secondaryLogin, $siteConfig.Name)
                $refreshedSite.Dispose()
                $site.Dispose()
            }
        }
    }
}

function Get-ProxyGroup([string]$name, [bool]$createIfMissing = $true) {
    if ($name -eq "Default" -or [string]::IsNullOrEmpty($name)) { return [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default }
    
    $proxyGroup = Get-SPServiceApplicationProxyGroup $name -ErrorAction SilentlyContinue -ErrorVariable err
    if ($err -and !$createIfMissing) { throw $err }
    if ($proxyGroup -eq $null) {
        $proxyGroup = New-SPServiceApplicationProxyGroup -Name $name
    }
    return $proxyGroup
}

function Set-WebAppUserPolicy($webApp, $userName, $userDisplayName, $perm) {
    [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webApp.Policies
    [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add($userName, $userDisplayName)
    [Microsoft.SharePoint.Administration.SPPolicyRole]$policyRole = $webApp.PolicyRoles | where {$_.Name -eq $perm}
    if ($policyRole -ne $null) {
        $policy.PolicyRoleBindings.Add($policyRole)
    }
    $webApp.Update()
}

 

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

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

SecureStoreServices.ps1

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

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

<Farm>
    <Services>
        <SecureStoreServices>
          <Servers>
            <Server Name="SP03" />
            <Server Name="SP04" />
          </Servers>
          <SecureStoreServiceApplications>
            <SecureStoreServiceApplication 
                Name="ISC Secure Store Service" 
                DatabaseName="ISC_SecureStore" 
                DatabaseServer="ISCSharePoint1" 
                FailoverDatabaseServer=""
                AuditingEnabled="true" 
                AuditLogMaxSize="30" 
                Sharing="false" 
                KeyPassphrase="p@ssw0rd" 
                Partitioned="false">
              <ApplicationPool Name="SharePoint Services App Pool" 
                      AccountName="isclondon\SPServices" 
                    AccountPassword="password" />
              <Proxy Name="ISC Secure Store Service">
                <ProxyGroup Name="Default" />
              </Proxy>
            </SecureStoreServiceApplication>
          </SecureStoreServiceApplications>
        </SecureStoreServices>    
    </Services>
</Farm>

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

Time for the actual script:

function Start-SecureStoreServices {
    [CmdletBinding()]
    param 
    (  
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$SettingsFile
    )
    
    [xml]$config = Get-Content $settingsFile

    $install = (($config.Farm.Services.SecureStoreServices.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
    if (!$install) { 
        Write-Host "Machine not specified in Servers element, service will not be installed on this server."
        return
    }
    
    $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"}
    if ($svc -eq $null) {
        throw "Unable to retrieve Service Instance."
    }
    if ($svc.Status -ne "Online") {
        Write-Host "Starting service instance..."
        $svc | Start-SPServiceInstance
    
        #Make sure the service is online before attempting to add a svc app.
        while ($true) {
            Start-Sleep 2
            $svc = Get-SPServiceInstance -Server $env:computername | where {$_.TypeName -eq "Secure Store Service"}
            if ($svc.Status -eq "Online") { break }
        }
    }


    foreach ($appConfig in $config.Farm.Services.SecureStoreServices.SecureStoreServiceApplications.SecureStoreServiceApplication) {
        $app = Get-SPServiceApplication | where {$_.Name -eq $appConfig.Name}
        $updateKey = $false
        if ($app -eq $null) {
            $poolName = $appConfig.ApplicationPool.Name
            $identity = $appConfig.ApplicationPool.AccountName
            $password = $appConfig.ApplicationPool.AccountPassword
            $pool = Get-ServicePool $poolName $identity $password

            Write-Host "Creating secure store service application..."
            $app = New-SPSecureStoreServiceApplication -Name $appConfig.Name `
                -ApplicationPool $pool `
                -DatabaseServer $appConfig.DatabaseServer `
                -DatabaseName $appConfig.DatabaseName `
                -AuditingEnabled :( [bool]::Parse($appConfig.AuditingEnabled)) `
                -AuditLogMaxSize $appConfig.AuditLogMaxSize `
                -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
                -PartitionMode:([bool]::Parse($appConfig.Partitioned)) `
                -Sharing:([bool]::Parse($appConfig.Sharing))
            $updateKey = $true
        } else {
            Write-Host "Secure store service application already exists, skipping creation."
        }

        $proxy = Get-SPServiceApplicationProxy | where {$_.Name -eq $appConfig.Proxy.Name}
        if ($proxy -eq $null) {
            Write-Host "Creating secure store service application proxy..."
            $proxy = New-SPSecureStoreServiceApplicationProxy `
                -Name $appConfig.Proxy.Name `
                -ServiceApplication $app `
                -DefaultProxyGroup:$false
        } else {
            Write-Host "Secure store service application proxy already exists, skipping creation."
        }
        $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup

        
        if ($updateKey) {
            Update-SPSecureStoreMasterKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase
            while ($true) {
                try {
                    Start-Sleep -Seconds 5
                    Update-SPSecureStoreApplicationServerKey -ServiceApplicationProxy $proxy -Passphrase $appConfig.KeyPassphrase
                    break
                } catch { }
            }
        }
        
    }
}



function Get-ServicePool($poolName, $identity, $password) {
    $pool = Get-SPServiceApplicationPool $poolName -ErrorAction SilentlyContinue
    if ($pool -eq $null) {
        if ($identity -ne $null) {
            $acct = Get-SPManagedAccount $identity -ErrorAction SilentlyContinue
        }
        if ($acct -eq $null) {
            if ([string]::IsNullOrEmpty($password)) {
                $cred = Get-Credential $identity
            } else {
                $cred = New-Object System.Management.Automation.PSCredential $identity, (ConvertTo-SecureString $password -AsPlainText -force)
            }
            $acct = Get-SPManagedAccount $cred.UserName -ErrorAction SilentlyContinue
            if ($acct -eq $null) {
                $acct = New-SPManagedAccount $cred
            }
        }
        Write-Host "Creating application pool..."
        $pool = New-SPServiceApplicationPool  -Name $poolName -Account $acct
    }
    return $pool
}

function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject) {
    begin {}
    process {
        if ($_ -eq $null -and $InputObject -ne $null) {
            $InputObject | Set-ProxyGroupsMembership $groups
            return
        }
        $proxy = $_
        
        #Clear any existing proxy group assignments
        Get-SPServiceApplicationProxyGroup | where {$_.Proxies -contains $proxy} | ForEach-Object {
            $proxyGroupName = $_.Name
            if ([string]::IsNullOrEmpty($proxyGroupName)) { $proxyGroupName = "Default" }
            $group = $null
            [bool]$matchFound = $false
            foreach ($g in $groups) {
                $group = $g.Name
                if ($group -eq $proxyGroupName) { 
                    $matchFound = $true
                    break 
                }
            }
            if (!$matchFound) {
                Write-Host "Removing ""$($proxy.DisplayName)"" from ""$proxyGroupName"""
                $_ | Remove-SPServiceApplicationProxyGroupMember -Member $proxy -Confirm:$false -ErrorAction SilentlyContinue
            }
        }
        
        foreach ($g in $groups) {
            $group = $g.Name

            $pg = $null
            if ($group -eq "Default" -or [string]::IsNullOrEmpty($group)) {
                $pg = [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default
            } else {
                $pg = Get-SPServiceApplicationProxyGroup $group -ErrorAction SilentlyContinue -ErrorVariable err
                if ($pg -eq $null) {
                    $pg = New-SPServiceApplicationProxyGroup -Name $name
                }
            }
            
            $pg = $pg | where {$_.Proxies -notcontains $proxy}
            if ($pg -ne $null) { 
                Write-Host "Adding ""$($proxy.DisplayName)"" to ""$($pg.DisplayName)"""
                $pg | Add-SPServiceApplicationProxyGroupMember -Member $proxy 
            }
        }
    }
    end {}
}

 

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

Final Thoughts

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

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

-Gary

22Dec/1151

Updating SharePoint 2010 User Information

One of my clients recently had an issue where a particularly high profile user (CEO) had their title spelled incorrectly in Active Directory; unfortunately the error wasn’t noticed right away and now, despite changing the information in Active Directory, SharePoint was still showing the wrong title in the People Picker when granting the user rights to a Site Collection. Fortunately I had a partial PowerShell script to fix the issue and just needed to only slightly modify it – you can see the original script on pages 299 and 300 of my book. So before I show the modified script it’s first important to understand the problem and why I needed to use a script and why what I had in the book is somewhat incomplete.

Whenever you grant a user rights to a Site Collection or when that user creates/updates/deletes any item within a Site Collection, an entry for the user will be added to a hidden user information list, if not already there. This “User Information List” is located at http://<SiteCollectionUrl>/_catalogs/users/detail.aspx:

SNAGHTMLa064e71

By looking at this list you can see that several key pieces of information are stored here – unfortunately, when you change this information in Active Directory the information stored here is not updated (even after running a full or incremental import via UPS). To complicate matters there is no way to edit the information via the browser, thus the need for a PowerShell script. If you click the user’s name you’ll see the additional properties, including an “Edit Item” option, however, the edit dialog is simply a read-only display of the username, helpful right?:

SNAGHTMLa089b49

So let’s first consider the scenario that my book addresses and assume that a user had had their name and/or email address changed. To accommodate this scenario we simply use the Set-SPUser cmdlet along with the -SyncFromAD parameter. The following script is taken directly from my book and simply iterates through all Site Collections and calls the Set-SPUser cmdlet for the provided user:

function Sync-SPUser([string]$userName) {
  Get-SPSite -Limit All | foreach {
    $web = $_.RootWeb
    if ($_.WebApplication.UseClaimsAuthentication) {
      $claim = New-SPClaimsPrincipal $userName -IdentityType WindowsSamAccountName
      $user = $web | Get-SPUser -Identity $claim -ErrorAction SilentlyContinue
    } else {
      $user = $web | Get-SPUser -Identity $userName -ErrorAction SilentlyContinue
    }
    if ($user -ne $null) {
      $web | Set-SPUser -Identity $user -SyncFromAD
    }
    $web.Dispose()
    $_.Dispose()
  }
}

 

Before I make any changes to demonstrate this script and the modifications we’ll make to it, let’s first see how my user is currently set in the Site Collection:

image

And as shown in the People Picker:

SNAGHTMLa14e91b

Note the “Name”/”Display Name”, “Work e-mail”/”E-Mail”, and “Title” fields.

Now I’ll change these values in Active Directory (make the “p” in my last name capitalized, change the title, and set the email) and then run the script (I saved the script as Sync-SPUser.ps1):

SNAGHTMLa17239b

(Note that lowercase “p” is the correct spelling for my name, just in case you were wondering Smile). Now if we look at the user details in the Site Collection and the People Picker we should see the following:

image

SNAGHTMLa1a344d

Notice that the the “Name” / “Display Name” and “Work e-mail” / “E-Mail” fields were updated but not the “Title” field. This is because the Set-SPUser cmdlet and -SyncFromAD parameter only updates these two fields. So how do you update the remaining fields? We simply need to add some code to our function which will grab the SPListItem corresponding to the user from the hidden “User Information List” and then update the corresponding fields manually. The following modified script does this for the “Title” field (note that I’ve changed the function signature to take the title in as a parameter):

function Sync-SPUser([string]$userName, [string]$title) {
  Get-SPSite -Limit All | foreach {
    $web = $_.RootWeb
    if ($_.WebApplication.UseClaimsAuthentication) {
      $claim = New-SPClaimsPrincipal $userName -IdentityType WindowsSamAccountName
      $user = $web | Get-SPUser -Identity $claim -ErrorAction SilentlyContinue
    } else {
      $user = $web | Get-SPUser -Identity $userName -ErrorAction SilentlyContinue
    }
    if ($user -ne $null) {
      $web | Set-SPUser -Identity $user -SyncFromAD
      
      $list = $web.Lists["User Information List"]
      $query = New-Object Microsoft.SharePoint.SPQuery
      $query.Query = "<Where><Eq><FieldRef Name='Name' /><Value Type='Text'>$userName</Value></Eq></Where>"
      foreach ($item in $list.GetItems($query)) {
        $item["JobTitle"] = $title
        $item.SystemUpdate()
      }
    }
    $web.Dispose()
    $_.Dispose()
  }
}

The changes to the original function have been highlighted. Note that the internal field name for the “Title” field is “JobTitle” and that is what we are using to set the Title. Now if we run this modified script we should see the Title field updated:

SNAGHTMLa21bc70

SNAGHTMLa236af7

Okay, so what about the other fields (Department, Mobile Number, etc.)? You can see what fields are available to edit by running the following:

SNAGHTMLa265249

In the preceding example I’m grabbing a specific item (in this case the item corresponding to my user) so that I can see the internal field names in context with the data stored by the field – this helps to make sure that I grab the correct field name (i.e., “JobTitle” vs. “Title”). Now you can just add additional fields to update right before the call to SystemUpdate() – simply follow the pattern established for the title field.

So, add this guy to your script library and you’ll be good to go next time someone changes their name, email, or job title.

-Gary

20Aug/1111

Resetting SharePoint 2010 Themes – Part 2, the Reset-SPTheme cmdlet

Yesterday I threw up a quick post showing how to reset a SharePoint 2010 theme using a reasonably simple Windows PowerShell script. In that post I promised that I’d convert the script to a cmdlet and make it part of my downloadable extensions. Well, as promised I’ve updated my extensions so that they now include a Reset-SPTheme cmdlet. I added on minor enhancement over the previously shown script in that I allow you to pass in either an SPSite or an SPWeb object and by default it will not force all child webs to inherit from the relevant SPWeb object. This way, if you have a child Site with it’s own theme it won’t wipe out that theme. If you have multiple Sites with a custom theme setting within a Site Collection then you’ll want to provide the -Site parameter and pass in an SPSite reference – this will result in all Sites with custom themes within the Site Collection to be reset. If you only wish to reset a single Site then use the -Web parameter and pass in a SPWeb reference.

Here’s the full help for the Reset-SPTheme cmdlet:

NAME
    Reset-SPTheme

SYNOPSIS
    Resets a theme by applying all user specified theme configuration settings to the original source files. This is particularly helpful when the original source files have changed to a Feature upgrade.

SYNTAX
    Reset-SPTheme [-Web] <SPWebPipeBind> [-SetSubWebsToInherit <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

    Reset-SPTheme [-Site] <SPSitePipeBind> [-SetSubWebsToInherit <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

DESCRIPTION
    Resets a theme by applying all user specified theme configuration settings to the original source files. This is particularly helpful when the original source files have changed to a Feature upgrade.

    Copyright 2011 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    >
http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -Web <SPWebPipeBind>
        Specifies the URL or GUID of the Web containing the theme to reset.

        The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid name of Microsoft SharePoint Foundation 2010 Web site (for example, MySPSite1); or an instance of a valid SPWeb object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -Site <SPSitePipeBind>
        The site containing the theme to reset.

        The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form http://server_name; or an instance of a valid SPSite object.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false

    -SetSubWebsToInherit [<SwitchParameter>]
        If specified, all child webs will be reset to inherit the theme of the specified web or root web.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

        When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".

INPUTS

OUTPUTS

NOTES

        For more information, type "Get-Help Reset-SPTheme -detailed". For technical information, type "Get-Help Reset-SPTheme -full".

    ------------------EXAMPLE 1-----------------------

    PS C:\> Get-SPSite http://server_name | Reset-SPTheme -SetSubWebsToInherit

    This example resets the theme for the site collection http://server_name and resets all child webs to inherit from the root web.

    ------------------EXAMPLE 2-----------------------

    PS C:\> Get-SPWeb http://server_name/sub-web | Reset-SPTheme

    This example resets the theme for the web http://server_name/sub-web.

RELATED LINKS
    Get-SPWeb
    Get-SPSite

 

In the following example I’m resetting the theme(s) for an entire Site Collection. If one any child Sites within the Site Collection have a custom theme then they’ll be updated, not just the root (inheritance will not be changed):

PS C:\> Reset-SPTheme -Site http://example.com

In this next example I’m resetting all child Sites to inherit whatever theme has been specified for the root Site and I’m updating the root Site’s theme with changes to the source files:

PS C:\> Reset-SPTheme -Site http://example.com -SetSubWebsToInherit

For this last example I’m resetting the theme of a specific sub-Site:

PS C:\> Reset-SPTheme -Web http://example.com

As you can see, this is pretty easy to use and, if you’re deploying your branding via Features and you have theme support then a cmdlet like this can be quite critical when you need to push out updates to that brand.

19Aug/113

Resetting SharePoint 2010 Themes

UPDATE 8/20/2011: I’ve reworked this script and included it as part of my SharePoint 2010 cmdlet downloads. See “Resetting SharePoint 2010 Themes – Part 2, the Reset-SPTheme cmdlet” for details.

One of my current clients is a local school district here in Denver and we (Aptillon) have recently helped them release a new public facing site for the main district office as well as all the schools in the district. The district has chosen a somewhat fixed brand which has had full theming support added so that the individual schools can adjust the color scheme to match the school’s colors. The master page, page layouts, CSS files, and associated images were all deployed to the Farm using various Solution Packages (WSPs). This allows us to make corrections/additions to the brand related files and quickly deploy them to the Farm, thereby updating the district and school sites quite easily. The problem, however, is that the way theming works within SharePoint 2010 is that it makes a copy of all the CSS and image files and stores them in the /_catalogs/theme/Themed/{THEMEID} folder, as shown in the following figure:

ThemedFolder

Whenever you apply a theme it will copy all the necessary CSS and image files to a new folder in the Themed folder. This means that the site is no longer basing its look and feel off of the Feature deployed files. So if a school has gone in and customized their site to use themes then any updates that we push out to the style sheets and images will be ignored. So I needed a way to effectively “reset” the applied theme after we push out an update to our branding solution. By reset I mean preserve the various color and font settings but re-apply them against the Feature deployed files.

I did some digging with my favorite tool, Reflector, and found that the out of the box theme settings page just uses the Microsoft.SharePoint.Utilities.ThmxTheme class to manipulate the themes. So, after a little experimenting I ended up with some code which will regenerate the current theme using all the source files and the user provided theme settings:

#NOTE: Run in a seperate console instance each time otherwise the changes won't get applied
function Reset-SPTheme([Microsoft.SharePoint.PowerShell.SPSitePipeBind]$spSite) {
    $site = $spSite.Read()
    try {
        # Store some variables for later use
        $tempFolderName = "TEMP"
        $themedFolderName = "$($site.ServerRelativeUrl)/_catalogs/theme/Themed"
        $themesUrlForWeb = [Microsoft.SharePoint.Utilities.ThmxTheme]::GetThemeUrlForWeb($site.RootWeb)
        Write-Host "Old Theme URL: $themesUrlForWeb"
        
        # Open the existing theme
        $webThmxTheme = [Microsoft.SharePoint.Utilities.ThmxTheme]::Open($site, $themesUrlForWeb)
        
        # Generate a new theme using the settings defined for the existing theme
        # (this will generate a temporary theme folder that we'll need to delete)
        $webThmxTheme.GenerateThemedStyles($true, $site.RootWeb, $tempFolderName)
        
        # Apply the newly generated theme to the root web
        [Microsoft.SharePoint.Utilities.ThmxTheme]::SetThemeUrlForWeb($site.RootWeb, "$themedFolderName/$tempFolderName/theme.thmx", $true)
        
        # Delete the TEMP folder created earlier
        $site.RootWeb.GetFolder("$themedFolderName/$tempFolderName").Delete()
        
        # Reset the theme URL just in case it has changed (sometimes it will)
        $site.Dispose()
        $site = $spSite.Read()
        $themesUrlForWeb = [Microsoft.SharePoint.Utilities.ThmxTheme]::GetThemeUrlForWeb($site.RootWeb)
        Write-Host "New Theme URL: $themesUrlForWeb"

        # Set all child webs to inherit.
        if ([Microsoft.SharePoint.Publishing.PublishingWeb]::IsPublishingWeb($site.RootWeb)) {
            $pubWeb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($site.RootWeb)
            $pubWeb.ThemedCssFolderUrl.SetValue($pubWeb.Web.ThemedCssFolderUrl, $true)
        } else {
            foreach ($web in $site.AllWebs) {
                if ($web.isRootWeb) { continue }
                Write-Host "Setting theme for $($web.ServerRelativeUrl)..."
                try {
                    [Microsoft.SharePoint.Utilities.ThmxTheme]::SetThemeUrlForWeb($web, $themesUrlForWeb)
                } finally {
                    $web.Dispose()
                }
            }   
        }
    } finally {
        if ($site -ne $null) { $site.Dispose() }
    }
}

I saved this to a file named Reset-SPTheme so I can then call the function like so:

. c:\Scripts\Reset-SPTheme.ps1
Reset-SPTheme "http://example.com/"

One odd thing I found, however, is that every time I run this it must be run in a new console instance; otherwise the changes are not picked up (this is basically a combination of a threading and caching issue within the code when executed from a PowerShell context). So remember, start a new PowerShell console every time you need to run this script (yeah, I wasted a couple of hours banging my head against the wall trying to figure that little nugget out).

BTW: I intend to add this to my cmdlet extensions as I believe it will be something I’ll need often so look for an updated build to come out this weekend.

28Jun/110

SharePoint Server 2010 Service Pack 1 PowerShell Changes

As most people know by now, Service Pack 1 for SharePoint 2010 was released to the public today. There’s already been a lot of hype over some of the new capabilities such as the site recycle bin and some folks have documented/demonstrated some of the new PowerShell cmdlets that are available to manage this new feature; but what about all the other new and changed PowerShell cmdlets – there’s a bunch! So, let’s take a look at what is new, and what has changed.

We’ll start with the new stuff – here’s a quick bulleted list of all the new cmdlets:

  • Add-SPProfileLeader
  • Get-SPProfileLeader
  • Remove-SPProfileLeader
  • Remove-SPProfileSyncConnection
  • Add-SPProfileSyncConnection
  • Disable-SPHealthAnalysisRule
  • Enable-SPHealthAnalysisRule
  • Get-SPHealthAnalysisRule
  • Get-SPDeletedSite
  • Remove-SPDeletedSite
  • Restore-SPDeletedSite
  • Move-SPSocialComments

I haven’t had a chance to try any of these out but I think there’s some cool new functionality here beyond just the site recycle bin. There’s absolutely no documentation for any of these but some of them are fairly straightforward based on the names. For instance, the Add-SPProfileSyncConnection cmdlet (and equivalent Remove cmdlet) are obviously for managing the synchronization connections for UPS. This was a big whole in RTM when it came to doing an end-to-end scripted installation as there was no practical way to add a synchronization connection using PowerShell. The health analysis rule cmdlets are also pretty obvious and, again, this goes a long way towards enabling administrators to script a deployment and enable or disable rules consistently across Farms.

I think the Move-SPSocialComments also has a lot of potential; if it does what I’m guessing it does then this could potentially solve the issue where comments and tags are stored with the absolute URL of the item that has been tagged/commented on – I’m *guessing* that running this command will effectively retarget those items, which is great in situations in which you’ve moved a list or site.

As for the *-SPProfileLeader cmdlets – I have no idea what those guys do so I’ll have to revisit them when I learn more. (The *-SPDeletedSite cmdlets have already been covered quite a bit by others so I’ll forgo any further discussion here).

Alright, so that’s the new stuff – what about the stuff that’s changed? Here’s a quick list with the changes:

  • Mount-SPContentDatabase
    • New Switch Parameter: ChangeSyncKnowledge (I’ve no idea what this does – it’s not yet documented)
  • New-SPContentDatabase
    • New Switch Parameter: ChangeSyncKnowledge (again, not yet documented)
  • Move-SPSite
    • New Parameter: RbsProviderMapping <Hashtable>
      • From TechNet: “Used to move an RBS-enabled site collection from one RBS-enabled content database to another RBS-enabled content database without moving the underlying BLOB content. If the content database has more than one RBS provider associated with it, you must specify all providers. The same providers must be enabled on the target content database and the source content database.”
  • New-SPPerformancePointServiceApplication
    • New Parameter: AnalyticResultCacheMinimumHitCount <Int32> (not yet documented but I think it’s fairly obvious what it does)
    • New Parameters: DatabaseServer <string>, DatabaseName <string>, DatabaseFailoverServer <string>, DatabaseSQLAuthenticationCredential <PSCredential>
      • Can I get a hurray for this! This was the only Service Application that didn’t allow us to set the database information when we created it so we were left with this nasty GUID in the name. Hurray! We can finally get rid of the last database GUID! Woohoo!
  • Set-SPPerformancePointServiceApplication
    • New Parameter: AnalyticResultCacheMinimumHitCount <Int32> (not yet documented but I think it’s fairly obvious what it does)
    • New Parameters: DatabaseServer <string>, DatabaseName <string>, DatabaseFailoverServer <string>, DatabaseSQLAuthenticationCredential <PSCredential>, DatabaseUseWindowsAuthentication
  • Remove-SPWeb
    • New Switch Parameter: Recycle
      • That’s right, you can in fact cause an SPWeb to go to the new recycle bin by simply providing this switch parameter.
  • Update-SPProfilePhotoStore (I think this one is a bit of mess and may need a CU or two but I could just be reading the code wrong – it is kind of late right now)
    • Update 6/29/2011: From Spence Harbar: “Update-SPProfilePhotoStore change is to address common bug/issue with resize and is for upgrade scenarios”
    • New Switch Parameter: CreateThumbnailsForImportedPhotos
      • I’m not entirely sure what this is supposed to do as thumbnails were created previously and will be created without this; however, I should note that they coded this wrong so both of the following syntaxes have the same affect as they are simply checking that the bool? type has a value and not what the value is:
        • -CreateThumbnailsForImportedPhotos $true
        • -CreateThumbnailsForImportedPhotos $false
    • New Switch Parameter: NoDelete
      • I’m not 100% on this but I believe that the original behavior of this cmdlet was to copy the image and not actually move it (despite the name); they appear to have changed the behavior to delete the original images after the copy but you can preserve the original behavior by simply adding this switch parameter. However, this is *only* true when the CreateThumbnailsForImportedPhotos parameter is provided (in my opinion this is a bug – it should be irrelevant if that parameter is provided).

Well, that’s all I’ve been able to discover with the core SharePoint Server 2010 cmdlets – I may update this post to account for the Office Web Applications but I don’t currently have that installed on the server in which I just installed SP1 so it may take me a bit to do that analysis. At some point I may write something up to inspect the public classes and their members and do a similar post for the developers out there who want to know what API changes have occurred so keep an eye out for that.

So I think it’s pretty cool that we’ve got some improvements in the PowerShell cmdlets, especially in some of those that have frustrated me when it comes to automated deployments; the real frustrating thing, however, is that some of my just released book content is already out of date! Ugh! Maybe there’ll be a second edition Smile

Happy PowerShelling!

-Gary

26Jun/1110

Getting (and taking ownership of) Checked Out Files using Windows PowerShell

Often when I’m working on a project I need to generate a list of all checked out files and provide that to my client just prior to release to production. Sometimes the client will manually inspect each of them and act as they see fit and other times they’ll ask me to just batch publish all of them (for which I use my Publish-SPListItems cmdlet). So, how do I generate the report for the client? It’s actually pretty easy using PowerShell and a couple of quick loops. Here’s an example that loops through every Site Collection in the Farm and generates a nice report:

function Get-CheckedOutFiles() {
    foreach ($web in (Get-SPSite -Limit All | Get-SPWeb -Limit All)) {
        Write-Host "Processing Web: $($web.Url)..."
        foreach ($list in ($web.Lists | ? {$_ -is [Microsoft.SharePoint.SPDocumentLibrary]})) {
            Write-Host "`tProcessing List: $($list.RootFolder.ServerRelativeUrl)..."
            foreach ($item in $list.CheckedOutFiles) {
                if (!$item.Url.EndsWith(".aspx")) { continue }
                $hash = @{
                    "URL"=$web.Site.MakeFullUrl("$($web.ServerRelativeUrl.TrimEnd('/'))/$($item.Url)");
                    "CheckedOutBy"=$item.CheckedOutBy;
                    "CheckedOutByEmail"=$item.CheckedOutByEmail
                }
                New-Object PSObject -Property $hash
            }
            foreach ($item in $list.Items) {
                if ($item.File.CheckOutStatus -ne "None") {
                    if (($list.CheckedOutFiles | where {$_.ListItemId -eq $item.ID}) -ne $null) { continue }
                    $hash = @{
                        "URL"=$web.Site.MakeFullUrl("$($web.ServerRelativeUrl.TrimEnd('/'))/$($item.Url)");
                        "CheckedOutBy"=$item.File.CheckedOutByUser;
                        "CheckedOutByEmail"=$item.File.CheckedOutByUser.Email
                    }
                    New-Object PSObject -Property $hash
                }
            }
        }
        $web.Dispose()
    }
}
Get-CheckedOutFiles | Out-GridView

Running the above will generate a fairly nice report with URLs and usernames and whatnot; you could also use the Export-Csv cmdlet to dump the results to a CSV file that you can then hand off to your end-users. One cool thing to point out about this is that it will also show you files that you normally can’t see – that is files that have been created by other users but have never had a check in. This is actually pretty cool and I stumbled upon this when trying to fine tune my Publish-SPListItems cmdlet. You see, if the file has never been checked in then iterating through the SPListItemCollection object will not reveal the item (or file I should say); this meant that my cmdlet, as it was previously written, was missing a bunch of files. So to work around this all I had to do was add an additional loop to iterate over the collection returned by the SPDocumentLibrary’s CheckedOutFiles property. For each SPCheckedOutFile object in that collection I then call TakeOverCheckOut() to grab the checked out file so that I can then publish.

I use this enough that I decided to turn it into a cmdlet that is now part of my custom extensions. Like the above script, I return back a custom object that contains the full URLs and other  useful information (such as the List, Site, and Site Collection identifiers). I also exposed a TakeOverCheckOut() and Delete() method which simply calls Microsoft’s implementation of those methods.

I called this cmdlet Get-SPCheckedOutFiles (note that I’d previously released this cmdlet under the name Get-SPFilesCheckedOut but have since reworked and renamed that original implementation).

Here’s the full help for the cmdlet:

PS C:\Users\spadmin> help Get-SPCheckedOutFiles -full

NAME
Get-SPCheckedOutFiles

SYNOPSIS
Retrieves check out details for a given List, Web, or Site.

SYNTAX
Get-SPCheckedOutFiles [-Site] <SPSitePipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

Get-SPCheckedOutFiles [-Web] <SPWebPipeBind> [-ExcludeChildWebs <SwitchParameter>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]

Get-SPCheckedOutFiles [[-Web] <SPWebPipeBind>] [-List] <SPListPipeBind> [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]


DESCRIPTION
Retrieves check out details for a given List, Web, or Site.

Copyright 2010 Falchion Consulting, LLC
> For more information on this cmdlet and others:
>
http://blog.falchionconsulting.com/
> Use of this cmdlet is at your own risk.
> Gary Lapointe assumes no liability.

PARAMETERS
-Site <SPSitePipeBind>
Specifies the URL or GUID of the Site to inspect.

The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form
http://server_name; or an instance of a valid SPSite object.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-Web <SPWebPipeBind>
Specifies the URL or GUID of the Web to inspect.

The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid URL, in the form
http://server_name; or an instance of a valid SPWeb object.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-List <SPListPipeBind>
The list whose checked out files are to be returned.

The value must be a valid URL in the form
http://server_name/lists/listname or /lists/listname. If a server relative URL is provided then the Web parameter must be provided.

Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? false

-ExcludeChildWebs [<SwitchParameter>]
Excludes all child sites and only considers the specified site.

Required? false
Position? named
Default value
Accept pipeline input? false
Accept wildcard characters? false

-AssignmentCollection [<SPAssignmentCollection>]
Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.

When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.

Required? false
Position? named
Default value
Accept pipeline input? true (ByValue)
Accept wildcard characters? false

<CommonParameters>
This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, type, "get-help about_commonparameters".

INPUTS

OUTPUTS

NOTES

For more information, type "Get-Help Get-SPCheckedOutFiles -detailed". For technical information, type "Get-Help Get-SPCheckedOutFiles -full".

------------------EXAMPLE------------------

PS C:\> Get-SPCheckedOutFiles -Site "
http://server_name/"


This example outputs a list of files that are checked out for the given Site Collection


RELATED LINKS
Get-SPFile

 

In the following example I’m retrieving pages from the root Pages library that are checked out:

image

In this example I am running the cmdlet as the aptillon\spadmin user and I’m now able to see the checkout by the user aptillon\glapointe. I ran the cmdlet twice so you could see the default tabular view as well as the more detailed view. Again, you could easily use the Export-Csv cmdlet to dump this information to a file that you can provide your end-users.

I hope you find this cmdlet useful – it personally has proven invaluable to me, particularly when working on anonymous access internet sites as end-users are notorious about creating pages and not getting them checked in.

P.S. With this release the Publish-SPListItems cmdlet has been updated to now consider files that don’t have any existing check-ins.

30Apr/110

Retrieving and Configuring the SharePoint 2010 Developer Dashboard using PowerShell

It’s been almost a year to the day since I’ve released my SharePoint 2010 cmdlets and, despite many good intentions to get them documented on my blog, things have just fallen by the wayside; this was primarily due to me going out on my own and writing my first book – but now that the book is done and I’ve begun to establish myself as an independent consultant, I believe it’s about time I start blogging about all these hidden cmdlets that I’ve created. So, to start I’m going to take a couple of cmdlets that I originally developed for some conference presentations; specifically Get-SPDeveloperDashboard and Set-SPDeveloperDashboard.

Before I show these two new cmdlets, let’s look at what it currently takes to retrieve and manipulate the developer dashboard using Windows PowerShell:

[Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings

As you can see from the preceding figure, you obtain an instance of the SPDeveloperDashboardSettings object via the DeveloperDashboardSettings property of an SPWebService instance (obtained using the static ContentService property of the SPWebService class). Note that there are several properties that we can manipulate beyond just the simple DisplayLevel property that is used to enable or disable the developer dashboard (or to put it into on demand mode). Some people still like to use STSADM to change the DisplayLevel property but doing so doesn’t allow you to manipulate the other properties available; often the reason people use STSADM is because it’s slightly less verbose if all you wish to do is change the DisplayLevel property. Here’s an example of how you would do it with PowerShell:

$dds = [Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings
$dds.DisplayLevel = "On"
$dds.Update()

So, not a whole lot of code but still more than the single STSADM line (that and people have a hard time remembering the full object path to get to the SPDeveloperDashboardSettings object – I personally can remember this easier than the STSADM key names).

Because of this slightly higher level of complexity I decided to create these cmdlets, but I also went ahead and added some PowerShell type extensions so that I could get to the developer dashboard from an SPFarm instance. I’ll examine that before we get into the cmdlets; if you download my source code you should notice a file named Lapointe.SharePoint2010.Automation.Cmdlets.Types.ps1xml in the {Project Root}\PowerShell\Types folder. Here’s the relevant contents of that file:

<?xml version="1.0" encoding="utf-8"?>
<Types>
  <Type>
    <Name>Microsoft.SharePoint.Administration.SPFarm</Name>
    <Members>
      <ScriptProperty>
        <Name>DeveloperDashboard</Name>
        <GetScriptBlock>[Microsoft.SharePoint.Administration.SPWebService]::ContentService.DeveloperDashboardSettings</GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
</Types>

What I’ve done here is essentially create a type extension using XML; the <Name /> element defines the full type name that you want to extend and the <Members /> element contains all the extensions. In this case I’ve added a new property named DeveloperDashboard and I provided the same script we saw previously so that the SPDeveloperDashboardSettings object will be returned. It’s important to understand that you are not limited to just get properties – you can create set properties as well as methods (type help about_types for more information about creating type extensions). With this type extension added we can now access the developer dashboard in a slightly simpler manner:

$dds = (Get-SPFarm).DeveloperDashboard

Using this approach there really isn’t a need for the Get-SPDeveloperDashboard cmdlet that I created, as the cmdlet only saves about seven characters; however, this approach isn’t obvious – what I want is users to be able to type Get-Command *dashboard* so that they can see all the cmdlets related to the developer dashboard. (Plus, I created the cmdlet originally just for demonstration purposes but it does make things a little more obvious). So now that we have the type extension out of the way, let’s take a look at the cmdlet. Here’s a dump of the full help for the Get-SPDeveloperDashboard cmdlet:

PS C:\> help Get-SPDeveloperDashboard -Full

NAME
    Get-SPDeveloperDashboard
    
SYNOPSIS
    Retrieves the Developer Dashboard Settings object.
    
SYNTAX
    Get-SPDeveloperDashboard [-AssignmentCollection <spassignmentcollection>] [<commonparameters>]
    
    
DESCRIPTION
    Retrieves the Developer Dashboard Settings object.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -AssignmentCollection [<spassignmentcollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, 
        can use large amounts of memory and use of these objects in Windows PowerShell scripts requires 
        proper memory management. Using the SPAssignment object, you can assign objects to a variable 
        and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or 
        SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment 
        collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are 
        not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory 
        scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    <commonparameters>
        This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, 
        WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, 
        type, "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    
NOTES
    
        For more information, type "Get-Help Get-SPDeveloperDashboard -detailed". 
        For technical information, type "Get-Help Get-SPDeveloperDashboard -full".
    
    ------------------EXAMPLE------------------
    
    PS C:\> $dash = Get-SPDeveloperDashboard
    
    This example returns back the developer dashboard settings object.
    
RELATED LINKS
    Set-SPDeveloperDashboard 

So obviously the cmdlet is pretty simple as there aren’t any parameters beyond the standard parameters (remember, the -AssignmentCollection parameter is included as part of the cmdlet base class but as the SPDeveloperDashboardSettings object is not disposable there is no reason to use it.

The code for this cmdlet is actually shorter than the help for it:

using System.Collections.Generic;
using System.Management.Automation;
using Lapointe.PowerShell.MamlGenerator.Attributes;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Farm
{
    [Cmdlet(VerbsCommon.Get, "SPDeveloperDashboard", SupportsShouldProcess = false),   SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = false)]
    [CmdletDescription("Retrieves the Developer Dashboard Settings object.")]
    [RelatedCmdlets(typeof(SPCmdletSetDeveloperDashboard))]
    [Example(Code = "PS C:\\> $dash = Get-SPDeveloperDashboard",       Remarks = "This example returns back the developer dashboard settings object.")]
    public class SPCmdletGetDeveloperDashboard : SPGetCmdletBaseCustom<SPDeveloperDashboardSettings>
    {
        protected override IEnumerable<SPDeveloperDashboardSettings> RetrieveDataObjects()
        {
            WriteObject(SPWebService.ContentService.DeveloperDashboardSettings);

            return null;
        }
    }
}

The following figure shows how you can call the cmdlet:

Get-SPDeveloperDashboard

Note that I’ve also added a new view for the SPDeveloperDashboardSettings object type (as shown in the first example – to see all the properties use the Select-Object cmdlet as shown in the second example). The custom views are added just like the custom type extensions – for views, however, you create another XML file which you can see in my source code under the {Project Root}\PowerShell\Format  folder. The following XML snippet illustrates the relevant portion of that file:

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
  <ViewDefinitions>
   <View>
    <Name>SPDeveloperDashboardSettings</Name>
    <ViewSelectedBy>
      <TypeName>Microsoft.SharePoint.Administration.SPDeveloperDashboardSettings</TypeName>
    </ViewSelectedBy>
    <TableControl>
      <TableHeaders>
        <TableColumnHeader>
          <Label>Display Level</Label>
          <Width>14</Width>
          <Alignment>left</Alignment>
        </TableColumnHeader>
        <TableColumnHeader>
          <Label>Trace Enabled</Label>
          <Width>14</Width>
          <Alignment>left</Alignment>
        </TableColumnHeader>
        <TableColumnHeader>
          <Label>Required Permissions</Label>
          <Alignment>left</Alignment>
        </TableColumnHeader>
      </TableHeaders>
      <TableRowEntries>
        <TableRowEntry>
          <TableColumnItems>
            <TableColumnItem>
              <PropertyName>DisplayLevel</PropertyName>
            </TableColumnItem>
            <TableColumnItem>
              <PropertyName>TraceEnabled</PropertyName>
            </TableColumnItem>
            <TableColumnItem>
              <PropertyName>RequiredPermissions</PropertyName>
            </TableColumnItem>
          </TableColumnItems>
        </TableRowEntry>
      </TableRowEntries>
    </TableControl>
  </View>
 </ViewDefinitions>
</Configuration>

Okay, so we’ve made it easier to retrieve the developer dashboard, now I want to change the values in one step (because retrieving the object, changing the value, and calling Update() is just too much work). To do this I created the Set-SPDeveloperDashboard cmdlet. This cmdlet is a bit more complex in that I’ve exposed all the relevant properties of the SPDeveloperDashboardSettings object with an equivalent parameter. Here’s the full help for the cmdlet:

PS C:\> help Set-SPDeveloperDashboard -Full

NAME
    Set-SPDeveloperDashboard
    
SYNOPSIS
    Sets the Developer Dashboard Settings.
    
SYNTAX
    Set-SPDeveloperDashboard [-AutoLaunchEnabled <Boolean>] [-DisplayLevel <Off | OnDemand | On>] [-MaximumCriticalEventsToTrack <Int32>] 
    [-MaximumSQLQueriesToTrack <Int32>] [-RequiredPermissions <EmptyMask | ViewListItems | AddListItems | EditListItems | DeleteListItems |
    ApproveItems | OpenItems | ViewVersions | DeleteVersions | CancelCheckout | ManagePersonalViews | ManageLists | ViewFormPages | Open |
    ViewPages | AddAndCustomizePages | ApplyThemeAndBorder | ApplyStyleSheets | ViewUsageData | CreateSSCSite | ManageSubwebs | CreateGroups
    | ManagePermissions | BrowseDirectories | BrowseUserInfo | AddDelPrivateWebParts | UpdatePersonalWebParts | ManageWeb | 
    UseClientIntegration | UseRemoteAPIs | ManageAlerts | CreateAlerts | EditMyUserInfo | EnumeratePermissions | FullMask>] 
    [-TraceEnabled <Boolean>] [-AdditionalEventsToTrack <String[]>] [-AssignmentCollection <SPAssignmentCollection>] [<CommonParameters>]
    
DESCRIPTION
    Sets the Developer Dashboard Settings.
    
    Copyright 2010 Falchion Consulting, LLC
    > For more information on this cmdlet and others:
    > http://blog.falchionconsulting.com/
    > Use of this cmdlet is at your own risk.
    > Gary Lapointe assumes no liability.

PARAMETERS
    -AutoLaunchEnabled [<Boolean>]
        Indicates whether the developer dashboard can be auto launched.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -DisplayLevel [<SPDeveloperDashboardLevel>]
        Indicates whether the developer dashboard is set to Off, On, or On Demand.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -MaximumCriticalEventsToTrack [<Int32>]
        The maximum number of critical events and asserts that will be recorded in a single transaction (i.e. one request or timer job).
        If a single transaction has more than this number of asserts the remainder will be ignored. This can be set to 0 to disable
        assert tracking.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -MaximumSQLQueriesToTrack [<Int32>]
        The maximum number of SQL queries that will be recorded in a single transaction (i.e. one request or timer job). If a single
        transaction executes more than this number of requests the query will be counted but the query call stack and text will not be kept.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -RequiredPermissions [<SPBasePermissions>]
        A permission mask defining the permissions required to see the developer dashboard. This defaults to SPBasePermissions.AddAndCustomizePages.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -TraceEnabled [<Boolean>]
        Whether a link to display full verbose trace will be available at the bottom of the page when the developer dashboard is launched or not.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -AdditionalEventsToTrack [<String[]>]
        A list of URL tags to track in addition to events with severity above High.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  false
        
    -AssignmentCollection [<SPAssignmentCollection>]
        Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, 
        can use large amounts of memory and use of these objects in Windows PowerShell scripts requires 
        proper memory management. Using the SPAssignment object, you can assign objects to a variable 
        and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or 
        SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment 
        collection or the Global parameter is not used.
        
        When the Global parameter is used, all objects are contained in the global store. If objects are 
        not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory 
        scenario can occur.
        
        Required?                    false
        Position?                    named
        Default value                
        Accept pipeline input?       true (ByValue)
        Accept wildcard characters?  false
        
    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer and OutVariable. For more information, type,
        "get-help about_commonparameters".
    
INPUTS
    
OUTPUTS
    
NOTES
    
        For more information, type "Get-Help Set-SPDeveloperDashboard -detailed". For technical information,
        type "Get-Help Set-SPDeveloperDashboard -full".
    
    ------------------EXAMPLE 1-----------------------
    
    PS C:\> Set-SPDeveloperDashboard -RequiredPermissions "ManageWeb,ManageSubwebs"
    
    This example sets the required permissions to view the developer dashboard.
    
    ------------------EXAMPLE 2-----------------------
    
    PS C:\> Set-SPDeveloperDashboard -DisplayLevel OnDemand -TraceEnabled $true
    
    This example enables the developer dashboard.
    
RELATED LINKS
    Get-SPDeveloperDashboard 

The code for this cmdlet is obviously going to be slightly longer than the Get-SPDeveloperDashboard cmdlet, but again, it’s very simple as most of the code is just for defining the parameters:

using System.Collections.Generic;
using System.Management.Automation;
using Lapointe.PowerShell.MamlGenerator.Attributes;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Farm
{
    [Cmdlet(VerbsCommon.Set, "SPDeveloperDashboard", SupportsShouldProcess = false),
    SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = false)]
    [CmdletDescription("Sets the Developer Dashboard Settings.")]
    [RelatedCmdlets(typeof(SPCmdletGetDeveloperDashboard))]
    [Example(Code = "PS C:\\> Set-SPDeveloperDashboard -DisplayLevel OnDemand -TraceEnabled $true",
        Remarks = "This example enables the developer dashboard.")]
    [Example(Code = "PS C:\\> Set-SPDeveloperDashboard -RequiredPermissions \"ManageWeb,ManageSubwebs\"",
        Remarks = "This example sets the required permissions to view the developer dashboard.")]
    public class SPCmdletSetDeveloperDashboard : SPSetCmdletBaseCustom<SPDeveloperDashboardSettings>
    {
        public SPCmdletSetDeveloperDashboard()
        {
            SPDeveloperDashboardSettings dash = SPWebService.ContentService.DeveloperDashboardSettings;
            AutoLaunchEnabled = dash.AutoLaunchEnabled;
            DisplayLevel = dash.DisplayLevel;
            MaximumCriticalEventsToTrack = dash.MaximumCriticalEventsToTrack;
            MaximumSQLQueriesToTrack = dash.MaximumSQLQueriesToTrack;
            RequiredPermissions = dash.RequiredPermissions;
            TraceEnabled = dash.TraceEnabled;
            AdditionalEventsToTrack = ((List<string>) dash.AdditionalEventsToTrack).ToArray();
        }

        [Parameter(HelpMessage = "Indicates whether the developer dashboard can be auto launched.")]
        public bool AutoLaunchEnabled { get; set; }

        [Parameter(HelpMessage = "Indicates whether the developer dashboard is set to Off, On, or On Demand.")]
        public SPDeveloperDashboardLevel DisplayLevel { get; set; }

        [Parameter(HelpMessage = "The maximum number of critical events and asserts that will be recorded in a single transaction (i.e. one request or timer job). If a single transaction has more than this number of asserts the remainder will be ignored. This can be set to 0 to disable assert tracking.")]
        public int MaximumCriticalEventsToTrack { get; set; }

        [Parameter(HelpMessage = "The maximum number of SQL queries that will be recorded in a single transaction (i.e. one request or timer job). If a single transaction executes more than this number of requests the query will be counted but the query call stack and text will not be kept. ")]
        public int MaximumSQLQueriesToTrack { get; set; }

        [Parameter(HelpMessage = "A permission mask defining the permissions required to see the developer dashboard. This defaults to SPBasePermissions.AddAndCustomizePages.")]
        public SPBasePermissions RequiredPermissions { get; set; }

        [Parameter(HelpMessage = "Whether a link to display full verbose trace will be available at the bottom of the page when the developer dashboard is launched or not.")]
        public bool TraceEnabled { get; set; }

        [Parameter(HelpMessage = "A list of URL tags to track in addition to events with severity above High. ")]
        public string[] AdditionalEventsToTrack { get; set; }

        protected override void UpdateDataObject()
        {
            SPDeveloperDashboardSettings dash = SPWebService.ContentService.DeveloperDashboardSettings;

            dash.AutoLaunchEnabled = AutoLaunchEnabled;
            dash.DisplayLevel = DisplayLevel;
            dash.MaximumCriticalEventsToTrack = MaximumCriticalEventsToTrack;
            dash.MaximumSQLQueriesToTrack = MaximumSQLQueriesToTrack;
            dash.RequiredPermissions = RequiredPermissions;
            dash.TraceEnabled = TraceEnabled;
            dash.AdditionalEventsToTrack.Clear();
            ((List<string>)dash.AdditionalEventsToTrack).AddRange(AdditionalEventsToTrack);

            dash.Update();
        }
    }
}

The following figure shows how you can call the cmdlet:

Set-SPDeveloperDashboard -DisplayLevel On -RequiredPermissions "ManageWeb","ManageSubwebs"

So that wraps up my first (very long overdue) post for my 2010 cmdlets – look for more posts coming soon as well as an update to my index page listing all the available cmdlets.

-Gary