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

20Aug/113

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/112

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.

7Jul/112

SharePoint 2010 SP1 Public API Changes

I recently published a post detailing the SharePoint 2010 SP1 PowerShell changes and, in that post, I mentioned that I was probably going to detail the API changes. Well, here they are. Note that the list below is not a comprehensive one in that I’m not showing every assembly (I do have the changes for every assembly but frankly I just got tired of translating the results into a readable format so I kept it to the more prominent assemblies (or at least the ones that I just happened to have done at the time)). In reviewing the list you see that there’s honestly not a whole lot of noteworthy changes, but that’s okay as part of my reasoning for doing this was to discover whether there were any (don’t get me wrong, there are some, in fact, for me there’s 1 very big one that made this whole exercise worth it – I’ll let you figure out which one that is). If you find any I missed please add a comment so that others can see it as well.

  • Microsoft.SharePoint.dll
    • Microsoft.SharePoint.SPRecycleBinItemType
      • New enum value:
        • Web
    • Microsoft.SharePoint.SPWeb
      • New method:
        • public void Recycle()
    • Microsoft.SharePoint.Strings
      • New constants:
        • public const string CannotRecycleRootWeb
        • public const string HealthRule_Explanation_BcsShimsAreEnabled
        • public const string HealthRule_Remedy_BcsShimsAreEnabled
        • public const string RecycleBinWebMissingContainerError
        • public const string SPStorageMetricsProcessingJobDescription
        • public const string SPUsageUserCodeRequestsDescription
        • public const string SPUsageUserCodeRequestsMonitoredDataDescription
        • public const string SiteAlreadyExists
        • public const string StorageMetricsDBObjectsNotFound
        • public const string StorageMetricsFreshnessWarning
        • public const string StorageMetricsNotAvailable
        • public const string TimerJobTitleStorageMetricsProcessing
    • Microsoft.SharePoint.Administration.SPAce<T>
      • New properties:
        • public Byte[] BinaryId() { get; }
        • public Microsoft.SharePoint.Administration.SPIdentifierType BinaryIdType() { get; }
    • Microsoft.SharePoint.Administration.SPAcl<T>
      • New method:
        • public SPAce<T> Add(string principalName, string displayName, SPIdentifierType identifierType, byte[] identifier, T grantRightsMask, T denyRightsMask)
    • Microsoft.SharePoint.Administration.SPContentDatabase
      • New methods:
        • public Microsoft.SharePoint.Administration.SPDeletedSite GetDeletedSite(System.Guid id)
        • public void Move(SPContentDatabase destinationDb, List<SPSite> sitesToMove, Dictionary<string, string> rbsProviderMap, out Dictionary<SPSite, string> failedSites)
    • Microsoft.SharePoint.Administration.SPContentDatabaseCollection
      • New method:
        • public SPContentDatabase Add(string strDatabaseServer, string strDatabaseName, string strDatabaseUsername, string strDatabasePassword, int warningSiteCount, int maximumSiteCount, int status, bool flushChangeLog, bool changeSyncKnowledge)
    • Microsoft.SharePoint.Administration.SPDatabase
      • New method:
        • public void ChangeDatabaseInstance(string databaseServiceInstance)
    • Microsoft.SharePoint.Administration.SPIncomingEmailService
      • New property:
        • public int RetryDeliveryInterval { get; set; }
    • Microsoft.SharePoint.Administration.SPPolicy
      • New method:
        • protected Void OnDeserialization()
    • Microsoft.SharePoint.Administration.SPSiteLookupProvider
      • Changed method (breaking change!)
        • public Void RenameHostHeaderSite(Guid siteId, string newHostHeader) => public Void RenameHostHeaderSite(Guid siteId, Uri newHostHeaderSiteUri)
    • Microsoft.SharePoint.Administration.SPUsageApplication
      • New property:
        • public int UsageInsertionTimeOut { get; set; }
    • Microsoft.SharePoint.Administration.SPUserCodeExecutionTier
      • New property:
        • public int PriorityPerProcess { get; set; }
    • Microsoft.SharePoint.Administration.SPWebApplication
      • New properties:
        • public int StorageMetricsProcessingDuration { get; set; }
        • public uint MaxDiscussionBoardItemsForSiteDataFolderQuery { get; set; }
        • public uint? UserDefinedWorkflowMaximumComplexity { get; set; }
      • New methods:
        • public SPDeletedSiteCollection GetDeletedSites()
        • public SPDeletedSiteCollection GetDeletedSites(SPDeletedSiteQuery query)
        • public SPDeletedSiteCollection GetDeletedSites(Guid siteId)
        • public SPDeletedSiteCollection GetDeletedSites(string sitePath)
        • public void MigrateUsers(IMigrateUserCallback callback)
    • Microsoft.SharePoint.Administration.SPWebService
      • New properties:
        • public int ImagingDownloadSizeLimit { get; set; }
        • public bool EnableHostHeaderSiteBasedSchemeSelection { get; set; }
    • Microsoft.SharePoint.Administration.Claims.SPActiveDirectoryClaimProvider
      • New method:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
    • Microsoft.SharePoint.Administration.Claims.SPAllUserClaimProvider
      • New method:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
    • Microsoft.SharePoint.Administration.Claims.SPClaimHierarchyProvider
      • New methods:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
        • public string GetLocalizedDisplayName()
    • Microsoft.SharePoint.Administration.Claims.SPClaimProvider
      • New property:
        • public virtual bool SupportsUserKey { get; }
      • New methods:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
        • public string GetLocalizedDisplayName()
        • public SPClaim UserKeyForEntity(SPClaim entity)
        • public virtual string GetClaimTypeForUserKey()
        • protected virtual SPClaim GetUserKeyForEntity(SPClaim entity)
    • Microsoft.SharePoint.Administration.Claims.SPClaimProviderDefinition
      • New property:
        • public bool IsVisible { get; set; }
    • Microsoft.SharePoint.Administration.Claims.SPClaimProviderOperationOptions
      • New enum value:
        • OverrideVisibleFlag
    • Microsoft.SharePoint.Administration.Claims.SPFormsClaimProvider
      • New method:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
    • Microsoft.SharePoint.Administration.Claims.SPSystemClaimProvider
      • New method:
        • protected override void FillDefaultLocalizedDisplayName(CultureInfo culture, out string localizedName)
    • Microsoft.SharePoint.BusinessData.Administration.LobSystem
      • New static method:
        • public static LobSystem MergeXml(string xml, out string[] errors, PackageContents packageContents, AdministrationMetadataCatalog metadataCatalog, string settingId)
    • Microsoft.SharePoint.BusinessData.Administration.TypeDescriptor
      • New static method:
        • public static TypeDescriptor MergeXml(string xml, out string[] errors, PackageContents packageContents, Parameter parameter, TypeDescriptor parent, string settingId)
    • Microsoft.SharePoint.BusinessData.SharedService.BdcServiceApplicationProxy
      • New methods:
        • public bool IsSystemTypeEnabled(SystemType systemType)
        • public void EnableSystemType(SystemType systemType, bool value)
    • Microsoft.SharePoint.JSGrid.GridSerializer
      • New method:
        • public void ApplyPostViewIncrementalInsertsAndDeletes(IEnumerable changes, Func> fnGetDefaultValuesForPostViewInserts)
    • Microsoft.SharePoint.JSGrid.HierarchyNode
      • New property:
        • public HierarchyNode Parent { get; set; }
    • Microsoft.SharePoint.Utilities.SPUtility
      • New static method:
        • public static Stream ExecuteCellStorageBinaryRequest(SPFile spfile, Stream request, bool coalesce, ref Guid partitionID, string userName, bool coauthVersioning, string etagMatching, bool fExpectNoFileExists, string contentChangeUnit, string clientFileID, string bypassSchemaID, long nLockType, string lockID, long nTimeout, bool createParentFolder, out string etagReturn, out bool allRequestSucceeded, out int coalesceHRESULT, out string coalesceErrorMessage, out bool containHotboxData, out bool haveOnlyDemotionChanges, ref int binaryReqCountQuota)
    • Microsoft.SharePoint.WebPartPages.ListFormWebPart
      • New method:
        • public bool ShouldSerializeTemplateName()
    • New classes:
      • Microsoft.SharePoint.Administration.SPDeletedSite
      • Microsoft.SharePoint.Administration.SPDeletedSiteCollection
      • Microsoft.SharePoint.Administration.SPDeletedSiteLookupInfo
      • Microsoft.SharePoint.Administration.SPDeletedSiteQuery
      • Microsoft.SharePoint.Administration.SPUsageUserCodeRequests
      • Microsoft.SharePoint.Administration.SPUsageUserCodeRequestsEntry
      • Microsoft.SharePoint.Administration.SPUsageUserCodeRequestsMonitoredData
      • Microsoft.SharePoint.Administration.SPUsageUserCodeRequestsMonitoredDataEntry
      • Microsoft.SharePoint.Administration.Health.SPHealthAnalysisRuleInstance
      • Microsoft.SharePoint.WebControls.IEVersionMetaTag
    • New enum types:
      • Microsoft.SharePoint.Administration.SPIdentifierType
    • New interfaces:
      • Microsoft.SharePoint.Administration.IMigrateUserCallback
      • Microsoft.SharePoint.Administration.ISPSiteLookupProviderRecycleBin
  • Microsoft.SharePoint.Publishing.dll
    • Microsoft.SharePoint.Publishing.Internal.CodeBehind
      • New property:
        • protected bool IsCurrentUserSiteAdmin { get; }
    • Microsoft.SharePoint.Publishing.WebControls.SpellCheckV4Action
      • New method:
        • protected bool ShouldRenderWithoutTabs()
    • Microsoft.SharePoint.Publishing.WebControls.EditingMenuActions.ConsoleAction
      • New method:
        • protected bool ShouldRenderWithoutTabs()
  • Microsoft.SharePoint.Taxonomy.dll
    • Microsoft.SharePoint.Taxonomy.TermStore
      • New method:
        • public Group GetSiteCollectionGroup(SPSite currentSite)
  • Microsoft.SharePoint.Portal.dll
    • Microsoft.Office.Server.UserProfiles.UserProfileService
      • New methods:
        • public void AddLeader(string accountName)
        • public Leader[] GetLeaders()
        • public void RemoveLeader(string accountName)
  • Microsoft.Office.Server.UserProfiles.dll
    • Microsoft.Office.Server.SocialData.PluggableSocialSecurityTrimmerManager
      • New methods:
        • public static string[] GetUrlFoldersRequiringTrim(SPServiceContext serviceContext)
        • public static string[] GetUrlFoldersToAlwaysAllow(SPServiceContext serviceContext)
        • public static void SetTrimmerSettings(SPServiceContext serviceContext, bool enableTrimming)
        • public static void SetTrimmerSettings(SPServiceContext serviceContext, string[] urlFoldersRequiringTrim, string[] urlFoldersToAlwaysAllow)
    • Microsoft.Office.Server.UserProfiles.BusinessDataCatalogConnection
      • New method:
        • public void Delete()
    • Microsoft.Office.Server.UserProfiles.ConnectionManager
      • New method:
        • public DirectoryServiceConnection AddActiveDirectoryConnection(ConnectionType type, string displayName, string server, bool useSSL, string accountDomain, string accountUsername, SecureString accountPassword, List<DirectoryServiceNamingContext> namingContexts, string spsClaimProviderTypeValue, string spsClaimProviderIdValue, string adClaimIDMapAttribute)
    • Microsoft.Office.Server.UserProfiles.UserProfileManager
      • New methods:
        • public void AddLeader(string accountName)
        • public Leader[] GetLeaders()
        • public void RemoveLeader(string accountName)
    • New classes:
      • Microsoft.Office.Server.UserProfiles.Leader
  • Microsoft.Office.Server.Search.dll
    • Microsoft.Office.Server.Search.Administration.CrawlTopologyState
      • New enum values:
        • ActiveToBeRemoved
        • DeactivatingToBeRemoved
    • Microsoft.Office.Server.Search.Administration.SearchServiceApplication
      • New property:
        • public uint CrawlLogCleanupIntervalInDays { get; set; }
    • Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy
      • New property:
        • public LocationConfigurationCollection LocationConfigurations { get; }
    • Microsoft.Office.Server.Search.Query.QueryInfo
      • New property:
        • public string CorrelationId { get; set; }
    • Microsoft.Office.Server.Search.Query.QueryManager
      • New method:
        • public System.Xml.XmlDocument GetResults()
  • Microsoft.SharePoint.PowerShell.dll
    • New classes:
      • Microsoft.SharePoint.PowerShell.SPDeletedSitePipeBind
      • Microsoft.SharePoint.PowerShell.SPHealthAnalysisRuleInstancePipeBind
4Oct/1024

Service Accounts and Managed Service Accounts in SharePoint 2010

With SharePoint 2010 we now have the ability to allow SharePoint to manage various service accounts thus foregoing the need to have IT administrators manually manage password changes. This new feature is a great benefit to SharePoint administrators and security conscious admins in general as it allows us to easily enforce our corporate security policies by changing these passwords on a schedule, and the administrators don’t even know what the password is so the likelihood of a compromise due to a disgruntled admin, though not eliminated, is somewhat reduced.

But the introduction of this new feature isn’t all good. The complication comes from the fact that SharePoint 2010 doesn’t implement this capability consistently. So an account that is configured as a Managed Service Account and set to have its password changed automatically could also be used in certain places that don’t understand the managed account concept. When the managed account password is changed the feature that uses that account and only knows the username and password (so it does not use the managed account details) will effectively be broken. As an example, if you configure the Enterprise Search Service to use a managed account whose password is scheduled to be changed every 30 days and you use that same account for the content crawl account then when that password is changed the content crawl will cease to function as it will be unable to authenticate the account. It’s important to note, however, that this issue only comes to light when you configure the managed account to have it’s password changed automatically.

So what things can be managed accounts and what cannot? The following lists what I’ve come across so far (if I’ve missed anything please leave a comment so I can update these lists):

Managed Service Accounts:

  • All Service Application Pool Accounts
    • Access Service Application
    • BCS Service Application
    • Excel Services Service Application
    • Metadata Service Application
    • PerformancePoint Service Application
    • Enterprise Search Service Application
    • Secure Store Service Application
    • Subscription Settings Service Application
    • User Profile Service Application
    • Visio Services Service Application
    • Web Analytics Service Application
    • Word Automation Service Application
    • Word Viewing Service Application
    • PowerPoint Viewing Service Application
    • Security Token Service Application
  • All Content Web Application Pools
  • Service Instances
    • Claims to Windows Token Service
    • Document Conversion Launcher Service
    • Document Conversion Load Balancer Service
    • Microsoft SharePoint Foundation Sandboxed Code Service
    • SharePoint Foundation Help Search
    • SharePoint Server Search (Enterprise Search)
    • Web Analytics Data Processing Service

Service Accounts (should not be managed):

  • Search Crawl Accounts
    • For Foundation Search and Server (Enterprise) Search
  • Unattended User Accounts
    • Excel Services Service Application
    • Visio Services Service Application
    • PerformancePoint Service Application
    • (in general, any Secure Store application credentials)
  • Object Cache Portal Accounts
    • Super User Account
    • Super Reader Account
  • User Profile
    • Synchronization Service Account (listed incorrectly on the FarmCredentialManagement.aspx page)
    • Synchronization Connection Account
  • Server Search Custom Crawl Rule Accounts
    • Any crawl rule that specifies an account other than the default crawl account

Again, these are just the accounts that I’ve personally bumped up against so it may not be a complete listing.

Viewing and Creating Managed Accounts

To see the current list of Managed Service Accounts using Central Admin go to Security –> Configure managed accounts:

Configure Managed Accounts

You can edit the settings for any managed account by simply clicking the edit icon associated with the account you wish to modify. Once on the Manage Account screen you can configure the automatic password change settings:

Configure Managed Account

To perform the same tasks using Windows PowerShell we can use the Get-SPManagedAccount cmdlet to retrieve the list of managed accounts:

Get-SPManagedAccount

Or we can retrieve a specific account using the -Identity parameter or by passing in a Web Application or Service:

Get-SPManagedAccount -Identity "localdev\spfarm"

clTo change the settings for a Managed Account we can use the Set-SPManagedAccount cmdlet:

Set-SPManagedAccount

To create a new Managed Account we use the New-SPManagedAccount cmdlet. In the example below I’m manually creating a PSCredential object so that I can specify my password (pa$$w0rd) in script (very useful for building out dev or test environments – otherwise you should use Get-Credential to prompt for the password so that it is not hard coded anywhere):

New-SPManagedAccount

Applying Managed Accounts

Once you have your Managed Accounts created you can begin to use them for things such as Service Instances and Service and Content Application Pools. To associate a managed account with a specific Service Instance using Central Admin you can go to Security –> Configure service accounts. On the Service Accounts page you can set the account used for the Farm Account, Service Instances, Web Content Application Pools, and Service Application Pools. The Service Instances are highlighted in the following image:

Configure Service Accounts

Service Instances

To set the account associated with a particular Service Instance using Windows PowerShell we simply get the ProcessIdentity property of the Service Instance and set its Username property. Once set we call Update() to update the Configuration Database and then Deploy() to push the change out to all Service Instances. To make this easier I put this code in a function that I can call by passing in the Service Instance and credentials to use:

function Set-ServiceIdentity($svc, $username)
{
  
$pi = $svc.Service.ProcessIdentity
  
if ($pi.Username-ne $username) {
      
$pi.Username= $username
      
$pi.Update()
      
$pi.Deploy()
    }
}

Here’s an example of how you can call this function:

Set-ServiceIdentity

Service Application Pools

To create a new Service Application pool we use the New-SPServiceApplicationPool cmdlet and pass in the name of the Application Pool to create and the Managed Account to assign as the Application Pool identity:

New-SPServiceApplicationPool

It’s extremely important to note that the application pool that you create using the New-ServiceApplicationPool cmdlet cannot be used for your content Web Applications. Unfortunately there is no out-of-the-box equivalent for creating Application Pools for Web Applications.

Web Application Pools

As previously noted there is no cmdlet for creating Application Pools for Web Applications. Instead what you need to do is first check if the Application Pool you need already exists by using the SPWebService’s ContentService static property. If it exists then pass in just the name of the Application Pool to the New-SPWebApplication cmdlet, otherwise pass in the name and the Managed Account to use as the Application Pool’s identity:

New-SPWebApplication

Applying Service Accounts

When it comes to applying non-managed accounts to the various features things get a little more complicated. Let’s start with the Crawl Accounts.

SharePoint Foundation Search Service

For SharePoint Foundation Search we can set the crawl account (or content access account) using Central Admin by navigating to the Services on Server page and clicking the SharePoint Foundation [Help] Search link which takes you to the settings page where we can set the crawl account:

SharePoint Foundation Search Settings

To set the same information using Windows PowerShell we actually have to go old-school and use STSADM as there’s no PowerShell equivalent cmdlet. Here’s a snippet of PowerShell code that I use to accomplish this:

function ConvertTo-UnsecureString([System.Security.SecureString]$string)
{
    $unmanagedString = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($string)
    $unsecureString = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($unmanagedString)
    [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($unmanagedString)

    return $unsecureString
}

$searchSvcAccount = Get-Credential "localdev\spsearchsvc"
$crawlAccount = Get-Credential "localdev\spcrawl"

$stsadmArgs = "-o spsearch -action start " + `
    "-farmserviceaccount `"$($searchSvcAccount.Username)`" " + `
    "-farmservicepassword `"$(ConvertTo-UnsecureString $searchSvcAccount.Password)`" " + `
    "-farmcontentaccessaccount `"$($crawlAccount.Username)`" " + `
    "-farmcontentaccesspassword `"$(ConvertTo-UnsecureString $crawlAccount.Password)`" " + `
    "-databaseserver `"spsql1`" " +  `
    "-databasename `"SharePoint_FoundationSearch`""

Write-Host "Running: stsadm $stsadmArgs"
$stsadmoutput = cmd /c "stsadm $stsadmArgs" 2>&1
if ($lastexitcode -ne 0) {
    throw "Unable to start Foundation Search Service.`n$stsadmoutput"
}

Note that I’m using a helper function to convert the secure password to a static string which I can then pass to the STSADM spsearch command.

SharePoint Server Search Service

To manage the crawl account for the SharePoint Server Search Service (also known as the Enterprise Search Service) using Central Admin we simply need to navigate to the Search Administration page of the Service Application that we wish to modify and click the link for the Default content access account. This will bring up the following screen:

Default content access account

Note that by default this account will be set to be the same account you used for the Search Service Instance which is a Managed Account. If you do not change this account and you have configured SharePoint to manage the account password then your crawls will fail when the password changes. To make this change using Windows PowerShell we use the Set-SPEnterpriseSearchServiceApplication cmdlet:

$crawlAccount = Get-Credential "localdev\spcrawl"
$searchApp | Set-SPEnterpriseSearchServiceApplication -DefaultContentAccessAccountPassword $crawlAccount.Password -DefaultContentAccessAccountName $crawlAccount.Username

Remember not to do this step until after you have provisioned the Administration Component.

Object Cache Accounts

Many administrators when they first configure SharePoint 2010 and hit a Web Application for the first time are likely to see a recurring event in the event log stating that the object cache has not been configured correctly. The specific error is as follows:

Object Cache: The super user account utilized by the cache is not configured. This can increase the number of cache misses, which causes the page requests to consume unneccesary system resources.

This is essentially telling you that you have missed a manual configuration step in which you need to run some PowerShell to set two accounts for SharePoint to use to access the object cache:

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()
}
$webApp = Get-SPWebApplication "http://content"
$portalSuperUserAccount = Get-Credential "localdev\SPSuperUser"
$webApp.Properties["portalsuperuseraccount"] = $portalSuperUserAccount.UserName
Set-WebAppUserPolicy $webApp $portalSuperUserAccount.UserName $portalSuperUserAccount.UserName "Full Control"

$portalSuperReaderAccount = Get-Credential "localdev\SPSuperReader"
$webApp.Properties["portalsuperreaderaccount"] = $portalSuperReaderAccount.UserName
Set-WebAppUserPolicy $webApp $portalSuperReaderAccount.UserName $portalSuperReaderAccount.UserName "Full Read"

Make sure that you do not use the same account for both the super user and super reader. (And of course make sure you change the URL and account names to match your environment). For more information about these settings see the following TechNet article: http://technet.microsoft.com/en-us/library/ff758656.aspx

Unattended Accounts

There are some services, specifically the Visio Services Service Application, the Excel Services Service Application, and the PerformancePoint Service Application, that allow us to set an account that we can use for access data sources behind the scenes. These are called unattended access accounts. To set these accounts we must create a new target application in the Secure Store Service Application and associate the target application’s ID with the appropriate Service Application. The following PowerShell code demonstrates how to do this for the Visio Services Service Application (the Excel Services Service Application is virtually identical and just uses cmdlets specific to Excel rather than Visio; PerformancePoint is a lot simpler):

#Get the Visio Service App
$svcApp = Get-SPServiceApplication | where {$_.TypeName -like "*Visio*"}
#Get the existing unattended account app ID
$unattendedServiceAccountApplicationID = ($svcApp | Get-SPVisioExternalData).UnattendedServiceAccountApplicationID
#If the account isn't already set then set it
if ([string]::IsNullOrEmpty($unattendedServiceAccountApplicationID)) {
    #Get our credentials
    $unattendedAccount = Get-Credential "localdev\SPUnattended"

    #Set the Target App Name and create the Target App
    $name = "$($svcApp.ID)-VisioUnattendedAccount"
    Write-Host "Creating Secure Store Target Application $name..."
    $secureStoreTargetApp = New-SPSecureStoreTargetApplication -Name $name `
        -FriendlyName "Visio Services Unattended Account Target App" `
        -ApplicationType Group `
        -TimeoutInMinutes 3

    #Set the group claim and admin principals
    $groupClaim = New-SPClaimsPrincipal -Identity "nt authority\authenticated users" -IdentityType WindowsSamAccountName
    $adminPrincipal = New-SPClaimsPrincipal -Identity "$($env:userdomain)\$($env:username)" -IdentityType WindowsSamAccountName

    #Set the account fields
    $usernameField = New-SPSecureStoreApplicationField -Name "User Name" -Type WindowsUserName -Masked:$false
    $passwordField = New-SPSecureStoreApplicationField -Name "Password" -Type WindowsPassword -Masked:$false
    $fields = $usernameField, $passwordField

    #Set the field values
    $secureUserName = ConvertTo-SecureString $unattendedAccount.UserName -AsPlainText -Force
    $securePassword = $unattendedAccount.Password
    $credentialValues = $secureUserName, $securePassword

    #Get the service context
    $subId = [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default
    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($svcApp.ServiceApplicationProxyGroup, $subId)

    #Check to see if the Secure Store App already exists
    $secureStoreApp = Get-SPSecureStoreApplication -ServiceContext $context -Name $name -ErrorAction SilentlyContinue
    if ($secureStoreApp -eq $null) {
        #Doesn't exist so create.
        Write-Host "Creating Secure Store Application..."
        $secureStoreApp = New-SPSecureStoreApplication -ServiceContext $context `
            -TargetApplication $secureStoreTargetApp `
            -Administrator $adminPrincipal `
            -CredentialsOwnerGroup $groupClaim `
            -Fields $fields
    }
    #Update the field values
    Write-Host "Updating Secure Store Group Credential Mapping..."
    Update-SPSecureStoreGroupCredentialMapping -Identity $secureStoreApp -Values $credentialValues

    #Set the unattended service account application ID
    $svcApp | Set-SPVisioExternalData -UnattendedServiceAccountApplicationID $name
}

When it comes to PerformancePoint we have a lot less work we need to do as the product team was nice enough to make it so that the Set-SPPerformancePointSecureDataValues does all the work of setting up the target application for us (note though that they did screw up how the Service Application is passed into the cmdlet requiring you to pass in the ID of the Service Application rather than the actual Service Application object):

$unattendedAccount = Get-Credential "localdev\SPUnattended"
$secureValues = Get-SPPerformancePointSecureDataValues -ServiceApplication $svcApp.Id
if ($secureValues.DataSourceUnattendedServiceAccount -ne $unattendedServiceAccount.UserName) {
    Write-Host "Setting unattended service account $($unattendedServiceAccount.UserName)..."
    $svcApp.Id | Set-SPPerformancePointSecureDataValues -DataSourceUnattendedServiceAccount $unattendedServiceAccount
}

User Profile Synchronization Service Identity

One thing to watch out for is when setting the account for the User Profile Synchronization Service. This service wants you to use the Farm Account as the identity. This means that your Farm Admin account cannot have it’s password managed by SharePoint if you intend to use this service (or at least, it shouldn’t be unless you don’t mind manually fixing this service every time your password changes – good luck with that BTW). Your Farm Admin account will always be a Managed Account (you can’t change that) so be extra careful when changing this accounts password (either manually or automatically). To set this account using Central Admin you can click Start next to the User Profile Synchronization Service entry on the Services on Server page.

User Profile Synchronization Service

To accomplish the same thing using PowerShell we need to get an instance of the Synchronization Service and set a few properties and call the SetSynchronizationMachine method passing in the username and password of the Farm Admin account (note that it requires the password be passed in as a standard string and not a secure string so I use my previously defined ConvertTo-UnsecureString function):

$syncMachine = Get-SPServer "sp2010dev"
$profApp = Get-SPServiceApplication | where {$_.Name -eq "User Profile Service Application 1"}
$account = Get-Credential "localdev\spfarm"
if ($syncMachine.Address -eq $env:ComputerName) {
    $syncSvc = Get-SPServiceInstance -Server $env:ComputerName | where {$_.TypeName -eq "User Profile Synchronization Service"}
    $syncSvc.Status = [Microsoft.SharePoint.Administration.SPObjectStatus]::Provisioning
    $syncSvc.IsProvisioned = $false
    $syncSvc.UserProfileApplicationGuid = $profApp.Id
    $syncSvc.Update()
    $profApp.SetSynchronizationMachine($syncMachine.Address, $syncSvc.Id, $account.UserName, (ConvertTo-UnsecureString $account.Password))
}

if ($syncSvc.Status -ne "Online") {
    Write-Host "Starting User Profile Synchronization Service..."
    Start-SPServiceInstance $syncSvc
}
do {Start-Sleep 2} while ((Get-SPServiceInstance -Server $env:ComputerName | where {$_.TypeName -eq "User Profile Synchronization Service"}).Status -ne "Online")

Summary

As you can see setting the accounts that are used throughout SharePoint 2010 is anything but consistent and in some cases a real pain in the a$$. I know I didn’t cover how to set every account (custom crawl rule accounts, user profile sync connection accounts, others?) but hopefully someone out there has already documented these, or if not perhaps they’d be nice enough to post a comment here for others benefit from (maybe one day I’ll add them myself but for now I think this post is quite long enough). As always, please let me know if I’ve missed something or otherwise got something wrong as I certainly don’t claim to have all the answers.

Happy PowerShelling Smile

12Aug/1049

Getting an Inventory of All SharePoint Documents Using Windows PowerShell

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

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

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

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

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

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

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

12Jun/1017

Deploying SharePoint 2010 Solution Packages Using PowerShell

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

9Jun/109

SharePoint 2010 Service Application Charts

I, along with Paul Stork, recently gave a SharePoint 2010 deployment webcast where we discussed, among other things, Service Applications and some of the considerations that must be taken into account when planning your deployment strategy. We also presented a first look at SharePoint Composer and SharePoint Maestro, the two core products that ShareSquared has been developing for close to a year now.

During the presentation we mentioned that there were some great charts available to help you in planning your Service Applications but that they weren’t the easiest thing to find as they are buried in a series of technical diagrams on TechNet. You can find two of the three charts we referenced at this link, http://technet.microsoft.com/en-us/library/cc263199.aspx. I’ve also added another chart which shows the dependencies of one Service Application to another (note that this particular chart is a work in progress as we are still discovering odd dependency cases that only occur in certain situations).

Chart 1: Service Applications per SKU

The first chart identifies all the core Service Applications and whether they store data, can be used cross-farm, and to which SharePoint SKU they belong. This chart is particularly useful in planning your initial licensing requirements:

Service applications

Description

Stores data?

Cross-farm?

SharePoint Foundation 2010

SharePoint Server 2010 Standard

SharePoint Server 2010 Enterprise

Access Services

View, edit, and interact with Microsoft® Access® 2010 databases in a browser.

Cache

X

Business Data Connectivity

Access line-of-business (LOB) data systems.

DB

X

X

X

X

Excel Services Application

Viewing and interact with Excel files in a browser.

Cache

X

Managed Metadata Service

Access managed taxonomy hierarchies, keywords and social tagging infrastructure as well as Content Type publishing across site collections.

DB

X

X

X

PerformancePoint

Provides the capabilities of PerformancePoint Services.

Cache

X

Search

Crawls content, produces index partitions, and serves search queries.

DB

X

X

X

Secure Store Service

Provides single sign-on authentication to access multiple applications or services.

DB

X

X

X

State Service

Provides temporary storage of user session data for SharePoint Server components.

DB

X

X

Usage and Health Data Collection

Collects farm wide usage and health data and provides the ability to view various usage and health reports.

DB

X

X

X

User Profile

Adds support for My Sites, Profiles pages, Social Tagging and other social computing features.

DB

X

X

X

Visio Graphics Service

Viewing and refresh of published Microsoft® Visio® diagrams in a Web browser.

Blob cache

X

Web Analytics

Provides Web Service interfaces.

X

X

X

Word Automation Services

Performs automated bulk document conversions.

Cache

 

X

X

Microsoft SharePoint Foundation Subscription Settings Service

Tracks subscription IDs and settings for services that are deployed in partitioned mode. Windows PowerShell only.

DB

X

X

X

Source:

Visio (http://go.microsoft.com/fwlink/?LinkID=167090)

PDF (http://go.microsoft.com/fwlink/?LinkID=167092)

XPS (http://go.microsoft.com/fwlink/?LinkID=167091)

Chart 2: Databases That Support SharePoint 2010 Products

This next chart takes what was in diagram form in the original TechNet diagram and displays it in a chart so that it’s a bit easier to read. Use this chart when planning your SQL Server storage requirements:

Service Application Database

Database

Relative Size

Size Guidance

Usage and Health Data Collection Service Application

Usage

Extra-large

Scale up. Only one database service application per farm. Place on separate spindle.

Business Data Connectivity Service Application

Business Data Connectivity

Small

Scale up.

Application Registry Service Application

Application Registry (used during upgrade only)

Small

Scale up.

Microsoft SharePoint Foundation Subscription Settings Service

Subscription Settings

Small

Scale up. You can scale out by creating additional service applications.

Search Service Application

Search Administration

Medium

Scale up. You can scale out by creating additional service applications.

Search Service Application

Crawl

Extra-large

Scale out. For large environments, put on a server that does not contain the Property databases.

Search Service Application

Property

Large to Extra-large

Scale out. For large environments, put on its own server for faster query results.

Web Analytics Service Application

Reporting

Extra-large

Scale up.

Web Analytics Service Application

Staging

Medium

Scale out.

State Service Application, Visio Service Application, InfoPath Forms Services

State

Medium-large

Scale out.

User Profile Service Application

Profile

Medium-large

Scale up. You can scale out by creating additional service applications.

User Profile Service Application

Synchronization

Medium-large

Scale up. You can scale out by creating additional service applications.

User Profile Service Application

Social Tagging Small to Extra-large

Scale up. You can scale out by creating additional service applications.

Managed Metadata Service Application

Managed Metadata

Medium

Scale up. You can scale out by creating additional service applications.

Secure Store Service Application

Secure Store

Small

Scale up. You can scale out by creating additional service applications.

Word Automation Service Application

Word Automation Services

Small

Scale up.

PerformancePoint Service Application

PerformancePoint

Small

Scale up. You can scale out by creating additional service applications.

Source:

Visio (http://go.microsoft.com/fwlink/?L
inkId=187970)

PDF (http://go.microsoft.com/fwlink/?LinkId=187969)

XPS (http://go.microsoft.com/fwlink/?LinkId=187971)

Chart 3: Service Application Dependencies

This last chart is one that we’ve been manually constructing based on our experiences with automating the setup of Service Applications. When doing a scripted install (or even when you use the FCW or manually configure Service Applications) it’s critical to know which Service Applications are dependents for other Service Applications. For example, if you are configuring the User Profile Service Application you must also configure the Managed Metadata Service Application. If you don’t do this you will get errors stating that certain fields cannot be edited when editing a user’s profile – these errors don’t give any indication that what’s missing is the Managed Metadata Service Application – you just have know.

The following chart is an attempt to help users with this hurdle – note that it is still a work in progress as it is very difficult to detect all dependencies as some are only a dependency under certain usage scenarios. Anything with an asterisks (*) next to the “X” indicates that the dependency is conditional based on usage scenarios:

Service Applications

Access

Business Data Connectivity Excel Services Managed Metadata PerformancePoint Foundation Search Enterprise Search Secure Store State Usage and Health Data User Profile Visio Graphics Web Analytics Word Automation Subscription Settings

Access

X

Business Data Connectivity X* X*
Excel Services X*
Managed Metadata X*
PerformancePoint X*
Foundation Search
Enterprise Search X* X X* X*
Secure Store X*
State
Usage and Health Data Collection
User Profile X* X* X* X*
Visio Graphics X* X
Web Analytics X
Word Automation X*
Subscription Settings                              

The way you read this chart is to find the Service Application of interest on the left and follow it to the right to see what Service Application it depends on. As you can see there’s not a lot of dependencies and most of the ones that do exist are conditional (for example, all the ones that depend on the Subscription Settings Service Application only depend on it if using Partitioning Mode, or basically a multi-tenant configuration).

As this chart is a work in progress I appreciate any feedback on it’s accuracy. If anyone notices anything that is incorrect with the chart please add a comment and I will be sure to update it accordingly.

14May/1016

Announcing My SharePoint 2010 PowerShell Cmdlets & STSADM Commands Now Available for Download

I’ve been wanting to release the SharePoint 2010 version of my STSADM extensions for quite some time but honestly just haven’t had the time to migrate as many as I would have liked. With over 145 STSADM extensions for SharePoint 2007 it was a challenge determining which ones I should focus on initially for the migration.

But today I’m happy to announce my initial release which contains 46 PowerShell cmdlets and 56 STSADM commands specific to SharePoint 2010. Yup, you read right, I’ve decided to maintain support for my STSADM commands and have been migrating them over as I create the equivalent replacement PowerShell cmdlet (though I recommend you don’t use them and suck it up and get used to PowerShell). You should note that there are more STSADM commands than PowerShell cmdlets – that’s because some of the things I was doing with STSADM can now easily be done with out of the box PowerShell cmdlets (I also have new PowerShell cmdlets that do not have an STSADM equivalent – everything new I create will be a cmdlet and I’ll create no new STSADM commands).

It’s going to take me a while to create all the posts needed to explain each cmdlet (assuming I create one at all) so for now I’ve created this simple table which lists all the STSADM commands and PowerShell cmdlets that are available in this initial release (I’ll eventually update my command index page but for now let this serve as the main reference for what is available as of 5/14/2010):

STSADM Commands

PowerShell Cmdlets

Notes

gl-activatefeature

Enable-SPFeature2

There’s an OOTB Enable-SPFeature cmdlet, this one simple adds some capabilities which were present in my existing STSADM command.

gl-addaudiencerule

New-SPAudienceRule

 

gl-addavailablesitetemplate

  I’ll eventually create a cmdlet for this.

gl-adduser2

  Use the OOTB New-SPUser cmdlet.

gl-adduserpolicyforwebapp

Add-SPWebApplicationUserPolicy

 

gl-applytheme

  This can be done pretty easily using Get-SPWeb and the ApplyTheme() method of the SPWeb object.

gl-backup

  Use the OOTB Backup-SPFarm and Backup-SPSite cmdlets.

gl-backupsites

Backup-SPSite2

Extends Backup-SPSite by including IIS settings.

gl-convertsubsitetositecollection

ConvertTo-SPSite

 

gl-copycontenttypes

Copy-SPContentType

 

gl-copylist

Copy-SPList

 

gl-copylistsecurity

Copy-SPListSecurity

 

gl-createaudience

New-SPAudience

 

gl-createcontentdb

  Use the OOTB New-SPContentDatabase cmdlet.

gl-createpublishingpage

New-SPPublishingPage

 

gl-createquotatemplate

New-SPQuotaTemplate

 

gl-createwebapp

  Use the OOTB New-SPWebApplication cmdlet

gl-deactivatefeature

Disable-SPFeature2

Extends the OOTB Disable-SPFeature cmdlet.

gl-deleteallusers

  I probably won’t replicate this as it is easily done using the OOTB Remove-SPUser cmdlet.

gl-deleteaudience

Remove-SPAudience

 

gl-deletelist

Remove-SPList

 

gl-deletewebapp

  Use the OOTB Remove-SPWebApplication cmdlet.

gl-disableuserpermissionforwebapp

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-editquotatemplate

Set-SPQuotaTemplate

 

gl-enableuserpermissionforwebapp

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-enumaudiencerules

Export-SPAudienceRules

 

gl-enumavailablepagelayouts

Get-SPPublishingPageLayout

 

gl-enumavailablesitetemplates

Get-SPAvailableWebTemplates

 

gl-enumeffectivebaseperms

  This is fairly easy to do OOTB so I may not create a cmdlet for it.

gl-enumfeatures

  Use the OOTB Get-SPFeature cmdlet.

gl-enuminstalledsitetemplates

  Use the OOTB Get-SPWebTemplate cmdlet.

gl-enumpagewebparts

Get-SPWebPartList

 

gl-enumunghostedfiles

Get-SPCustomizedPages

 

gl-execadmsvcjobs

Start-SPAdminJob2

I honestly need to research this a bit more as I’m not sure it’s necessary anymore but I’ve replicated the functionality in case someone finds it useful.

gl-exportaudiences

Export-SPAudiences

 

gl-exportcontenttypes

Export-SPContentType

 

gl-exportlist

Export-SPWeb2

I’ve just extended the Export-SPWeb2 cmdlet to add additional parameters.

gl-exportlistsecurity

Export-SPListSecurity

 

gl-extendwebapp

  Use the OOTB New-SPWebApplicationExtension cmdlet.

gl-fixpublishingpagespagelayouturl

Repair-SPPageLayoutUrl

 

gl-importaudiences

Import-SPAudiences

 

gl-importlist

Import-SPWeb2

I’ve just extended the Import-SPWeb2 cmdlet to add additional parameters.

gl-importlistsecurity

Import-SPListSecurity

 

gl-listaudiencetargeting

Set-SPListAudienceTargeting

 

gl-managecontentdbsettings

  Use the OOTB Set-SPContentDatabase cmdlet.

gl-propagatecontenttype

Propagate-SPContentType

 

gl-publishitems

Publish-SPListItems

 

gl-reghostfile

Reset-SPCustomizedPages

 

gl-removeavailablesitetemplate

  I’ll eventually create a cmdlet for this (maybe).

gl-repairsitecollectionimportedfromsubsite

Repair-SPSite

 

gl-replacewebpartcontent

Replace-SPWebPartContent

 

gl-setbackconnectionhostnames

Set-SPBackConnectionHostNames

 

gl-setselfservicesitecreation

  Not sure if I’ll migrate this or not.

gl-syncquotas

Set-SPQuota

 

gl-tracelog

  Use the OOTB Set-SPDiagnosticConfig cmdlet.

gl-unextendwebapp

  Use the OOTB Remove-SPWebApplication cmdlet.
 

Get-SPAudience

 
 

Get-SPAudienceManager

 
 

Get-SPContentType

 
 

Get-SPFile

 
 

Get-SPLimitedWebPartManager

 
 

Get-SPList

 
 

Get-SPPublishingPage

 
 

Get-SPQuotaTemplate

 
 

Set-SPAudience

 

For those that know a thing or two about cmdlet development you might be interested in knowing that I am dynamically generating the help XML file for the cmdlets. If you download the source you’ll find a class which uses reflection to interrogate the assembly and dynamically build the help file just prior to building the WSP package. This saved me literally days of hand editing XML.

You can download the source and WSP files here or from the Downloads page:

After you deploy the package you can type “help <cmdlet name>” to get detailed help about each cmdlet, including parameter descriptions and example usage. If you want to see the list of cmdlets installed type the following:

gcm | where {$_.DLL –like "*lapointe*"}

As always, your use of these cmdlets/stsadm commands is at your own risk – I do as much testing as I can but every environment is different and there’s simply not enough time in a day. If you have any suggestions or feedback please don’t hesitate to leave a comment – I appreciate all of them!

29Apr/1011

Discovering Who Has Access to SharePoint 2010 Securable Objects

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

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

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

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

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

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

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

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

C:\ PS> . .\SecurityReport.ps1

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

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

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

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Now lets see the lists:

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

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

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

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

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

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

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

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

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

4Apr/106

Starting the SharePoint 2010 Foundation Search Service using PowerShell

It's been a while since my last real SharePoint 2010 scripting post but we're getting close to RTM so I figured I need to buckle down and play some catch up and get some long overdue posts published. So, continuing my series of posts on scripting the various services and service applications within SharePoint 2010 I decided that I would share something that I know a lot of people have been struggling with recently - scripting the SharePoint Foundation Search Service.

This one threw me for a bit of a loop because all the other services and service applications can be configured almost exclusively using PowerShell cmdlets - this one though has to be configured almost exclusively using the object model. We basically have four cmdlets available to help with the configuration and unfortunately they're not much help at all:

  • Get-SPSearchService - Returns back an object representing the actual service
  • Get-SPSearchServiceInstance - Returns an object representing a service configuration for the service
  • Set-SPSearchService - Updates a few select properties associated with the service
  • Set-SPSearchServiceInstance - Updates the ProxyType for the service

The main failing with these cmdlets is that you can't set the services process identity, the database name and server or failover server, and you can't trigger the provisioning of the service instances which is required for the service to be considered fully "started". All of these things I can do through Central Admin but there's no way to do it using any provided cmdlets - so how do we solve the problem? By getting our hands dirty and writing a boat load of code against the object model.

So let's get started. As before we'll use an XML file to drive the setup process:

<Services>
<FoundationSearchService Enable="true"
AddStartAddressForNonNTZone="false"
MaxBackupDuration="2880"
PerformanceLevel="PartlyReduced"
DatabaseServer="SPSQL1"
DatabaseName="SharePoint_Search_Help"
FailoverDatabaseServer="">
<SvcAccount Name="sp2010\spsearch" />
<CrawlAccount Name="sp2010\spcrawl" />
<Servers>
<Server Name="sp2010svr" ProxyType="Default" />
</Servers>
</FoundationSearchService>
</Services>

As you can see the configuration file is pretty simple. We define two accounts that we'll use, one for the process identity of the service and the other for the crawl account. There's a few simple attributes for the database and some miscellaneous configurations and a list of all the servers in which the service should be started on.

Okay, let's start digging into the actual script. The first thing I do is load the XML file to a variable, $svcConfig, which I use throughout the function:

   1: [xml]$config = Get-Content $settingsFile
   2: $svcConfig = $config.Services.FoundationSearchService

Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <FoundationSearchService /> element and set that to the $svcConfig variable. Next I need to determine if the script should continue on this server by checking the <Servers /> element to see if there's a match for the current machine:

   1: #See if we want to start the svc on the current server.
   2: $install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
   3: if (!$install) { 
   4:     Write-Host "Machine not specified in Servers element, service will not be started on this server."
   5:     return
   6: }

So at this point we know that we're on a target machine so the first thing we want to do is use the Start-SPServiceInstance to start the Foundation Search Service:

   1: #Start the service instance
   2: $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
   3: if ($svc -eq $null) {
   4:     $svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
   5: }
   6: Start-SPServiceInstance -Identity $svc

The trick with this is that if we're not using SharePoint Foundation then once the service is initially started it renames itself to "SharePoint Foundation Help Search" so I had to put a provision to look for one name or the other to allow this script to be run multiple times and from multiple machines. Now that the service is started lets set a few variables that we'll use throughout the rest of the script:

   1: #Get the service and service instance
   2: $searchSvc = Get-SPSearchService
   3: $searchSvcInstance = Get-SPSearchServiceInstance -Local
   4:  
   5: $dbServer = $svcConfig.DatabaseServer
   6: $failoverDbServer = $svcConfig.FailoverDatabaseServer

We'll use the $searchSvc and $searchSvcInstance variables extensively. Note that we'll also need to repeat lines one and two at least a couple of times to avoid update conflicts as a result of timer jobs modifying those objects.

The next step will be to set the process identity for the service. We'll go ahead and also get the crawl account information while we're at it to avoid prompting for passwords in more than one location:

   1: #Get the service account details
   2: Write-Host "Provide the username and password for the search crawl account..."
   3: $crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
   4: Write-Host "Provide the username and password for the search service account..."
   5:   $searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name
   6:  
   7: #Get or Create a managed account for the search service account.
   8: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
   9: if ($err) {
  10:     $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
  11: }
  12:  
  13: #Set the account details if different than what is current.
  14: $processIdentity = $searchSvc.ProcessIdentity
  15: if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
  16:     $processIdentity = $searchSvc.ProcessIdentity
  17:     $processIdentity.CurrentIdentityType = "SpecificUser"
  18:     $processIdentity.ManagedAccount = $searchSvcManagedAccount
  19:     Write-Host "Updating the service process identity..."
  20:     $processIdentity.Update()
  21:     $searchSvc.Update()
  22: }    

This is where things start to get interesting. I use the Get-Credential cmdlet to return back the credentials of the user to use for the service but once I have that there's no parameter on any cmdlet that will allow me to set the credential so I have to do it using the object model. I use the $searchSvc variable from earlier and edit the object returned by the ProcessIdentity property (after confirming that the value needs to be changed).

Once we have the process set we can go ahead and set the other simple properties on the service - fortunately the cmdlet Set-SPSearchService can actually help us out with this one:

   1: #It doesn't hurt if this runs more than once so we don't bother checking before running.
   2: Write-Host "Updating the search service properties..."
   3: $searchSvc | Set-SPSearchService `
   4:     -CrawlAccount $crawlAccount.Username `
   5:     -CrawlPassword $crawlAccount.Password `
   6:     -AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
   7:     -MaxBackupDuration $svcConfig.MaxBackupDuration `
   8:     -PerformanceLevel $svcConfig.PerformanceLevel `
   9:     -ErrorVariable err `
  10:     -ErrorAction SilentlyContinue
  11: if ($err) {
  12:     throw $err
  13: }

Alright, that was the easy stuff - now we have to deal with the database. The first step is to see if there's already a database defined for the service and if it matches what we want. This is important as we want to be able to run the script more than once so we don't want to just blindly delete and recreate the database. The first bit of code builds a connection string using the SqlConnectionStringBuilder object (note that in PowerShell you have to use the PSBase property to access the properties on this object) and then compares that to what is currently set. If a match is not found then the existing database is deleted and the search service updated:

   1: #Build the connection string to the new database.
   2: [System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
   3: $builder1.psbase.DataSource = $dbServer
   4: $builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
   5: $builder1.psbase.IntegratedSecurity = $true
   6: Write-Host "Proposed database connection: {$builder1}"
   7:  
   8: [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
   9: $dbMatch = $false
  10: if ($searchDb -ne $null) {
  11:     #A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
  12:     [System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
  13:     Write-Host "Existing database connection: {$builder2}"
  14:     if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
  15:         $dbMatch = $true
  16:     }
  17:     if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
  18:         $dbMatch = $true
  19:     }
  20:     if (!$dbMatch) {
  21:         #The database does not match the configuration provided so delete it.
  22:         Write-Host "The specified database details do not match existing details. Clearing existing."
  23:         $searchSvcInstance.SearchDatabase = $null
  24:         $searchSvcInstance.Update()
  25:         Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
  26:         $searchDb.Delete()
  27:         Write-Host "Finished deleting search DB."
  28:         $searchDb = $null
  29:     } else {
  30:         Write-Host "Existing Database details match provided details ($($builder2))"
  31:     }
  32: }

At this point if the $searchDb variable is null then we want to go ahead and create a new search database:

   1: #If we don't have a DB go ahead and create one.
   2: if ($searchDb -eq $null) {
   3:     $dbCreated = $false
   4:     try
   5:     {
   6:         Write-Host "Creating new search database {$builder1}..."
   7:         $searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
   8:         Write-Host "Provisioning new search database..."
   9:         $searchDb.Provision()
  10:         Write-Host "Provisioning search database complete."
  11:         $dbCreated = $true
  12:  
  13:         #Re-get the service to avoid update conflicts
  14:         $searchSvc = Get-SPSearchService
  15:         $searchSvcInstance = Get-SPSearchServiceInstance -Local
  16:         
  17:         Write-Host "Associating new database with search service instance..."
  18:         $searchSvcInstance.SearchDatabase = $searchDb
  19:         Write-Host "Updating search service instance..."
  20:         $searchSvcInstance.Update()
  21:         
  22:         #Re-get the service to avoid update conflicts
  23:         $searchSvc = Get-SPSearchService
  24:         $searchSvcInstance = Get-SPSearchServiceInstance -Local
  25:     }
  26:     catch
  27:     {
  28:         if ($searchDb -ne $null -and $dbCreated) {
  29:             Write-Warning "An error occurred updating the search service instance, deleting search database..."
  30:             try
  31:             {
  32:                 #Clean up
  33:                 $searchDb.Delete()
  34:             }
  35:             catch
  36:             {
  37:                 Write-Warning "Unable to delete search database."
  38:                 Write-Error $_
  39:             }
  40:         }
  41:         throw $_
  42:     }        
  43: }

I first create a new SPSearchDatabase object by calling the static Create() method and passing in the SqlConnectionStringBuilder object that was previously created. I then call the Provision() method to actually create the database on the SQL server instance. Once it's created we can associate the database with the service by setting the SearchDatabase property on the $searchSvcInstance variable. If an error occurs then I attempt to delete the database from SQL Server if it's not yet associated with the service.

Now that we have our database provisioned we can go ahead and set the failover server:

   1: #Set the database failover server
   2: if (![string]::IsNullOrEmpty($failoverDbServer)) {
   3:     if (($searchDb.FailoverServiceInstance -eq $null) -or `
   4:         ![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
   5:     {
   6:         try
   7:         {
   8:             Write-Host "Adding failover database instance..."
   9:             $searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
  10:             Write-Host "Updating search service instance..."
  11:             $searchSvcInstance.Update()
  12:         }
  13:         catch
  14:         {
  15:             Write-Warning "Unable to set failover database server. $_"
  16:         }
  17:     }
  18: }

Most of the logic here is just in determining whether or not to set the failover server. Basically you just call the AddFailoverServiceInstance() method of the SearchDatabase property (SPSearchDatabase) and then update the service instance.

We're almost there - we've set all the properties we can now we need to complete the provisioning process:

   1: $status = $searchSvcInstance.Status
   2: #Provision the service instance on the current server
   3: if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
   4:     if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
   5:         try
   6:         {
   7:             Write-Host "Provisioning search service instance..."
   8:             $searchSvcInstance.Provision()
   9:         }
  10:         catch
  11:         {
  12:             Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
  13:             if ($status -ne $searchSvcInstance.Status) {
  14:                 try
  15:                 {
  16:                     $searchSvcInstance.Status = $status
  17:                     $searchSvcInstance.Update()
  18:                 }
  19:                 catch
  20:                 {
  21:                     Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
  22:                 }
  23:             }
  24:             throw $_
  25:         }
  26:     }
  27: }

If the service instance is not currently marked as Online (again, accounting for multiple runs) and the service instance we're working with is for the current machine then we call the Provision() method on the service instance. If an error occurs provisioning the service then I try to set the status back to its previous value.

Only two steps left; First we need to create a timer job to trigger the search service instance to be provisioned on the other servers in the farm:

   1: #Re-get the service to avoid update conflicts
   2: $searchSvc = Get-SPSearchService
   3:  
   4: #Create the timer job to update the instances for the other servers.
   5: foreach ($serviceInstance in $searchSvc.Instances) {
   6:     if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
   7:         -and $serviceInstance -ne $searchSvcInstance `
   8:         -and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
   9:         $definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
  10:         if ($definition -ne $null) {
  11:             Write-Host  "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
  12:         } else {
  13:             Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
  14:             $job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
  15:             $job.Update($true)
  16:         }
  17:     }
  18: }

And finally, we need to set the ProxyType for the service instances so I loop through the <Server /> elements and call the Set-SPSearchServiceInstance cmdlet, providing the ProxyType attribute as defined in the XML:

   1: #Set the proxy type for all the service instances.
   2: $svcConfig.Servers.Server | ForEach-Object {
   3:     $server = $_
   4:     $instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
   5:     if ($instance -ne $null `
   6:         -and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
   7:         Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
   8:         $instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType   
   9:     }
  10: }

Phew - we're done! Let's put it all together now - here's the complete script:


function Start-FoundationSearch([string]$settingsFile = "Configurations.xml") {
[xml]$config = Get-Content $settingsFile
$svcConfig = $config.Services.FoundationSearchService

#See if we want to start the svc on the current server.
$install = (($svcConfig.Servers.Server | where {$_.Name -eq $env:computername}) -ne $null)
if (!$install) {
Write-Host "Machine not specified in Servers element, service will not be started on this server."
return
}

#Start the service instance
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Search" -and $_.Parent.Name -eq $env:ComputerName}
if ($svc -eq $null) {
$svc = Get-SPServiceInstance | where {$_.TypeName -eq "SharePoint Foundation Help Search" -and $_.Parent.Name -eq $env:ComputerName}
}
Start-SPServiceInstance -Identity $svc

#Get the service and service instance
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local

$dbServer = $svcConfig.DatabaseServer
$failoverDbServer = $svcConfig.FailoverDatabaseServer

#Get the service account details
Write-Host "Provide the username and password for the search crawl account..."
$crawlAccount = Get-Credential $svcConfig.CrawlAccount.Name
Write-Host "Provide the username and password for the search service account..."
$searchSvcAccount = Get-Credential $svcConfig.SvcAccount.Name

#Get or Create a managed account for the search service account.
$searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.SvcAccount.Name -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
}

#Set the account details if different than what is current.
$processIdentity = $searchSvc.ProcessIdentity
if ($processIdentity.ManagedAccount.Username -ne $searchSvcManagedAccount.Username) {
$processIdentity = $searchSvc.ProcessIdentity
$processIdentity.CurrentIdentityType = "SpecificUser"
$processIdentity.ManagedAccount = $searchSvcManagedAccount
Write-Host "Updating the service process identity..."
$processIdentity.Update()
$searchSvc.Update()
}

#It doesn't hurt if this runs more than once so we don't bother checking before running.
Write-Host "Updating the search service properties..."
$searchSvc | Set-SPSearchService `
-CrawlAccount $crawlAccount.Username `
-CrawlPassword $crawlAccount.Password `
-AddStartAddressForNonNTZone $svcConfig.AddStartAddressForNonNTZone `
-MaxBackupDuration $svcConfig.MaxBackupDuration `
-PerformanceLevel $svcConfig.PerformanceLevel `
-ErrorVariable err `
-ErrorAction SilentlyContinue
if ($err) {
throw $err
}

#Build the connection string to the new database.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder1 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder1.psbase.DataSource = $dbServer
$builder1.psbase.InitialCatalog = $svcConfig.DatabaseName
$builder1.psbase.IntegratedSecurity = $true
Write-Host "Proposed database connection: {$builder1}"

[Microsoft.SharePoint.Search.Administration.SPSearchDatabase]$searchDb = $searchSvcInstance.SearchDatabase
$dbMatch = $false
if ($searchDb -ne $null) {
#A database is already set - if it's the one we expect then keep it, otherwise we want to delete it.
[System.Data.SqlClient.SqlConnectionStringBuilder]$builder2 = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($searchDb.DatabaseConnectionString)
Write-Host "Existing database connection: {$builder2}"
if ($builder2.ConnectionString.StartsWith($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch -and $searchDb.DatabaseConnectionString.Equals($builder1.ConnectionString, [StringComparison]::OrdinalIgnoreCase)) {
$dbMatch = $true
}
if (!$dbMatch) {
#The database does not match the configuration provided so delete it.
Write-Host "The specified database details do not match existing details. Clearing existing."
$searchSvcInstance.SearchDatabase = $null
$searchSvcInstance.Update()
Write-Host "Deleting {$($searchDb.DatabaseConnectionString)}..."
$searchDb.Delete()
Write-Host "Finished deleting search DB."
$searchDb = $null
} else {
Write-Host "Existing Database details match provided details ($($builder2))"
}
}

#If we don't have a DB go ahead and create one.
if ($searchDb -eq $null) {
$dbCreated = $false
try
{
Write-Host "Creating new search database {$builder1}..."
$searchDb = [Microsoft.SharePoint.Search.Administration.SPSearchDatabase]::Create($builder1)
Write-Host "Provisioning new search database..."
$searchDb.Provision()
Write-Host "Provisioning search database complete."
$dbCreated = $true

#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local

Write-Host "Associating new database with search service instance..."
$searchSvcInstance.SearchDatabase = $searchDb
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()

#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService
$searchSvcInstance = Get-SPSearchServiceInstance -Local
}
catch
{
if ($searchDb -ne $null -and $dbCreated) {
Write-Warning "An error occurred updating the search service instance, deleting search database..."
try
{
#Clean up
$searchDb.Delete()
}
catch
{
Write-Warning "Unable to delete search database."
Write-Error $_
}
}
throw $_
}
}

#Set the database failover server
if (![string]::IsNullOrEmpty($failoverDbServer)) {
if (($searchDb.FailoverServiceInstance -eq $null) -or `
![string]::Equals($searchDb.FailoverServiceInstance.NormalizedDataSource, $failoverDbServer, [StringComparison]::OrdinalIgnoreCase))
{
try
{
Write-Host "Adding failover database instance..."
$searchSvcInstance.SearchDatabase.AddFailoverServiceInstance($failoverDbServer)
Write-Host "Updating search service instance..."
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Unable to set failover database server. $_"
}
}
}

$status = $searchSvcInstance.Status
#Provision the service instance on the current server
if ($status -ne [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
if ([Microsoft.SharePoint.Administration.SPServer]::Local -eq $searchSvcInstance.Server) {
try
{
Write-Host "Provisioning search service instance..."
$searchSvcInstance.Provision()
}
catch
{
Write-Warning "The call to SPSearchServiceInstance.Provision (server '$($searchSvcInstance.Server.Name)') failed. Setting back to previous status '$status'. $($_.Exception)"
if ($status -ne $searchSvcInstance.Status) {
try
{
$searchSvcInstance.Status = $status
$searchSvcInstance.Update()
}
catch
{
Write-Warning "Failed to restore previous status on the SPSearchServiceInstance (server '$($searchSvcInstance.Server.Name)'). $($_.Exception)"
}
}
throw $_
}
}
}

#Re-get the service to avoid update conflicts
$searchSvc = Get-SPSearchService

#Create the timer job to update the instances for the other servers.
foreach ($serviceInstance in $searchSvc.Instances) {
if ($serviceInstance -is [Microsoft.SharePoint.Search.Administration.SPSearchServiceInstance] `
-and $serviceInstance -ne $searchSvcInstance `
-and $serviceInstance.Status -eq [Microsoft.SharePoint.Administration.SPObjectStatus]::Online) {
$definition = $serviceInstance.Farm.GetObject("job-service-instance-$($serviceInstance.Id.ToString())", $serviceInstance.Farm.TimerService.Id, [Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition])
if ($definition -ne $null) {
Write-Host "A provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)' already exists; skipping."
} else {
Write-Host "Creating provisioning job for the SPSearchServiceInstance on server '$($serviceInstance.Server.Name)'..."
$job = New-Object Microsoft.SharePoint.Administration.SPServiceInstanceJobDefinition($serviceInstance, $true)
$job.Update($true)
}
}
}

#Set the proxy type for all the service instances.
$svcConfig.Servers.Server | ForEach-Object {
$server = $_
$instance = Get-SPSearchServiceInstance | where {$_.Server.Name -eq $server.Name}
if ($instance -ne $null `
-and $server.ProxyType.ToLowerInvariant() -ne $instance.ProxyType.ToLowerInvariant) {
Write-Host "Setting proxy type for $($instance.Server.Name) to $($server.ProxyType)..."
$instance | Set-SPSearchServiceInstance -ProxyType $server.ProxyType
}
}
}

One thing you should note is that I'm not setting the schedule for the service. This is because the timer job class that I'd need to use to set the schedule is marked internal thus making it impossible for me to set the schedule without using reflection.

As you can see we're in a bit of a conundrum with SharePoint 2010 - scripting your installations is considered to be a best practice and you should strive to do so whenever possible but the level of complexity involved with scripting such simple things has made it prohibitively complex for the average administrator to do.

I recognized this issue the very first day I started working with SharePoint 2010 and to solve the problem I've been working on a product for ShareSquared called SharePoint Composer which will allow administrators, architects, and developers to visually design their SharePoint configurations and then build out the entire Farm using the model they create in the design tool. This tool will allow you to enforce your corporate standards by clearly documenting every configuration and building the farm based on those configurations in a single-click, automated way - all without having to know any PowerShell at all! Keep a watch here for more information about SharePoint Composer.

Note - I've not had a chance to test this in a multi-server farm so if anyone can give me some feedback about their experiences with it I'd greatly appreciate it.