Weekend Scripter: Using PowerShell to Replace STSADM

Summary: Learn about a Windows PowerShell script to replace STSADM –o enumallwebs in SharePoint.

Microsoft Scripting Guy, Ed Wilson, is here. Welcome back today guest blogger, Brian Jackett.

Brian is a senior premier field engineer at Microsoft who has specialized in SharePoint development, Windows PowerShell, and Project Server since 2008. Brian is a steering committee member of the Buckeye SharePoint User Group and coauthor of Microsoft SharePoint 2010: Creating and Implementing Real-World Projects. He enjoys giving back to the community through giving presentations, planning and volunteering at conferences, maintaining a SharePoint/.NET-centric blog (The Frog Pond of Technology), and contributing to the SharePoint Twitterverse (@BrianTJackett).  He also holds several Microsoft Certified Technology Specialist (MCTS) certificates for SharePoint-related technologies.

Just like many other Microsoft server applications, SharePoint has had a fair number of command-line interfaces for administrating the product. STSADM.exe was one of these command line tools. Since SharePoint 2010, Windows PowerShell has become the preferred command-line administration tool.  However, there are certain instances where Windows PowerShell does not provide an exact replacement for tools such as STSADM.exe. In this post, we will explore one such command (enumallwebs) and attempt to replace that functionality with a Windows PowerShell script.

Problem

The SharePoint team that is in charge of Windows PowerShell cmdlets has done a great job of building a replacement for most of the STSADM.exe capabilities (and in many cases, greatly surpassed them). For a list of the cmdlets, see Stsadm to Windows PowerShell mapping in SharePoint 2013.

As you can see, there are a few STSADM.exe capabilities that do not have a 1 to 1 equivalent. One example is the STSADM –o enumallwebs command.  This is a very popular command to generate an XML output of all content databases in the SharePoint farm, site collections, and sites within the databases, the template used, URL, and more. Because there is not a replacement for this command, I started looking for a way to replace portions of the functionality.

Research

Whenever I encounter a large problem or challenge, I find it best to start at the beginning and take one step at a time. To start with, I wanted to see what the Help information for enumallwebs provided.  Knowing what the parameters are and whether they are required vs. optional is very helpful.  Following is the output from STSADM –help enumallwebs:

Image of command output

I do not have the source code for the STSADM executable, so I don’t know how the output is generated.  As such, I will have to rely on a bit of black-box testing, and I’ll run various sample-case executions of the command and compare what output is generated.

I start by looking at the output when running against my SharePoint 2013 lab farm and with no parameters specified. This gives me the basic XML hierarchy that I need to re-create, what properties to pull back, and a sample to compare my Windows PowerShell script results against. The following screenshot shows the output from the enumallwebs command with no parameters.

Image of command output

 I see that the general hierarchy of XML is as follows:

<Databases>

<Database…>

<Site…>

<Webs…>

<Web…/>

</Webs>

</Site>

</Database>

</Databases>

This hierarchy should be fairly easy to replicate. Normally, the Get-SPContentDatabase cmdlet would be sufficient; but unfortunately, it does not include the Central Administration content databases. As a workaround, the Get-SPWebApplication –IncludeCentralAdministration command can be used to gather all web applications including Central Administration. Piping this to Select-Object –ExpandProperty ContentDatabases will then retrieve all the necessary content databases, and we can iterate over each site collection (and later site) contained within each content database.

Get-SPWebApplication -IncludeCentralAdministration | Select-Object -ExpandProperty ContentDatabases

Next, I break down each XML element to find which properties we need to return. There is nothing at the Databases level, so we can continue on to database. The Database XML element has three attributes:

  • SiteCount
  • Name
  • DataSource

I know what I am looking for, but not specifically if or which property on the content database object contains the corresponding data. Therefore, I use a common technique when encountering a new datatype: Format-List –Property *, or fl * if I use the alias for Format-List and don’t include the positional parameter -Property.

Passing the output from Get-SPContentDatabase to fl * lets us see all of the properties and their associated values for the Microsoft.SharePoint.Administration.SPContentDatabase objects that are returned. 

From this I can see that the SPContentDatabase properties for CurrentSiteCount matches SiteCount, Server matches DataSource, and Name matches Name. You can see the initial portion of the output here:

Image of command output

Next up is the Site element. In all Site elements I find the following attributes:

  • Id
  • OwnerLogin
  • InSiteMap

However, I notice some differences in the output from one element to the next—specifically, the inclusion or exclusion of a HostHeader attribute. In the following screenshot, you can see an example of each. This means that I will have to perform an additional check for the HostHeaderIsSiteName property on the site collection to determine whether that attribute needs to be included.

Image of command output

Using the same trick of piping the objects to Format-List –Property *, I find that almost all of the attributes I need are associated with a property on the site collection object. The one exception is InSiteMap

The InSiteMap attribute from the STSADM output refers to the fact that the content database may know about a site collection that exists, but the SharePoint configuration database map not have the site collection listed in its site map. This is otherwise known as an orphaned site collection.

I could not find any documentation about the Microsoft.SharePoint.Administration.SPConfigurationDatabase object (at least none that weren’t marked as obsolete), so I saved the configuration database object into a variable to further inspect it.

$configDB = Get-SPDatabase | where {$_.typename -eq "Configuration Database"}

I then piped the $configDB variable to Get-Member to see what was available. As it turns out, there is a SiteExists() method on the object. After running a few tests, I felt fairly confident that this was what I was looking for. If I passed the site collection, I was iterating into the SiteExists() method on the configuration database, and this would fulfill my InSiteMap attribute.

Image of command output

Next is the Webs element which contains a single attribute:

  • Count

Because the site collection object has a property AllWebs, which returns a SPWebCollection object (similar to an array), I can access the Count of the AllWebs to find how many subsites are in the site collection.

$site.AllWebs.Count

The last element is the Web element, and it contains a number of attributes:

  • Id
  • Url
  • LanguageId
  • TemplateName
  • TemplateId

Once again, piping the SPWeb objects to Format-List –Property * reveals many of the properties that I will need. The one troublesome property will be TemplateName, but I can see that it is a combination of the WebTemplate and Configuration properties.

Solution

After researching all of the hierarchy components, I put them together into the following script.  You can download it from the Script Center Repository: PowerShell replacement for "STSADM -o enumallwebs" in SharePoint 2010 or 2013. I recommend redirecting the output to a file to avoid line-wrapping issues with the faux XML output that is generated.

Param([string]$DatabaseName,

        [string]$DatabaseServer,

        [switch]$IncludeFeatures,

        [switch]$IncludeEventReceivers)

############################################################

#SP_Enum-AllWebs_vHSG.ps1

#

#Author: Brian T. Jackett

#Last Modified Date: 2013-11-08

#

#Script to replace functionality from STSADM -o enumallwebs

############################################################

# some references can be used from the below article

#http://www.craigtothepoint.com/Lists/Posts/Post.aspx?ID=11

function ProcessContentDatabase([Microsoft.SharePoint.Administration.SPContentDatabase]$SPContentDatabase)

{

        Write-Verbose "Begin - Processing content database: $($db.Name)"

        Write-Output "<Database SiteCount=`"$($db.CurrentSiteCount)`" Name=`"$($db.Name)`" DataSource=`"$($db.Server)`">"

 

        foreach($site in $db.Sites)

        {

            ProcessSiteCollection $site

        }

        Write-Output "</Database>"

        Write-Verbose "End - Processing content database: $($db.Name)"

}

 

function ProcessSiteCollection([Microsoft.SharePoint.SPSite]$SPSite)

{

    Write-Verbose "Begin - Processing site collection: $($site.URL)"

 

    if($site.HostHeaderIsSiteName -eq $false)

    {

        Write-Output "<Site Id=`"$($site.Id)`" OwnerLogin=`"$($site.Owner.UserLogin)`" InSiteMap=`"$($configDB.SiteExists($($site.Id)))`">"

    }

    else

    {

        Write-Output "<Site Id=`"$($site.Id)`" OwnerLogin=`"$($site.Owner.UserLogin)`" InSiteMap=`"$($configDB.SiteExists($($site.Id)))`" HostHeader=`"$($site.HostName)`">"

    }

 

    if($IncludeEventReceivers)

    {

        Write-Output "<EventReceiverAssemblies>"

        foreach($eventReceiver in $site.EventReceivers)

        {

            Write-Output "<EventReceiverAssembly Name=`"$($eventReceiver.Assembly)`" />"

        }

        Write-Output "</EventReceiverAssemblies>"

    }

    if($IncludeFeatures)

    {

        Write-Output "<Features>"

        foreach($feature in $site.Features)

        {

            Write-Output "<Feature Id=`"$($feature.DefinitionId)`" Count=`"1`" DisplayName=`"$($feature.Definition.DisplayName)`" InstallPath=`"$($feature.Definition.RootDirectory)`" Status=`"$($feature.Definition.Status)`" />"

        }

        Write-Output "</Features>"

    }

 

    Write-Output "<Webs Count=`"$($site.AllWebs.Count)`">"

 

    foreach($web in $site.AllWebs)

    {

        ProcessSite $web

    }

    Write-Output "</Webs>"

 

    Write-Output "</Site>"

    Write-Verbose "End - Processing site collection: $($site.URL)"

 

    $site.Dispose()

}

 

function ProcessSite([Microsoft.SharePoint.SPWeb]$SPWeb)

{

    Write-Verbose "Begin - Processing site: $($web.URL)"

    if(!($IncludeFeatures -or $IncludeEventReceivers))

    {

        Write-Output "<Web Id=`"$($web.Id)`" Url=`"$($web.Url)`" Languauge=`"$($web.Language)`" TemplateName=`"$($web.WebTemplate)#$($web.Configuration)`" TemplateId=`"$($web.WebTemplateId)`" />"

    }

    else

    {

        Write-Output "<Web Id=`"$($web.Id)`" Url=`"$($web.Url)`" Languauge=`"$($web.Language)`" TemplateName=`"$($web.WebTemplate)#$($web.Configuration)`" TemplateId=`"$($web.WebTemplateId)`">"

 

        if($IncludeEventReceivers)

        {

            Write-Output "<EventReceiverAssemblies>"

            foreach($eventReceiver in $web.EventReceivers)

            {

                Write-Output "<EventReceiverAssembly Name=`"$($eventReceiver.Assembly)`" />"

            }

            Write-Output "</EventReceiverAssemblies>"

        }

        if($IncludeFeatures)

        {

            Write-Output "<Features>"

            foreach($feature in $web.Features)

            {

                Write-Output "<Feature Id=`"$($feature.DefinitionId)`" Count=`"1`" DisplayName=`"$($feature.Definition.DisplayName)`" InstallPath=`"$($feature.Definition.RootDirectory)`" Status=`"$($feature.Definition.Status)`" />"

            }

            Write-Output "</Features>"

        }

        Write-Output "</Web>"

    }

    Write-Verbose "End - Processing site: $($web.URL)"

 

    $web.Dispose()

}

 

# start of main portion of script

if((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell) -eq $null)

{

    Add-PSSnapin Microsoft.SharePoint.PowerShell

}

 

$farm = [Microsoft.SharePoint.Administration.SPFarm]::Local

 

# check if farm was able to be referenced

if($farm -eq $null)

{

    Write-Error "Unable to access farm, exiting script"

    exit

}

else

{

    # get reference to configuration database to be used in site collection processing

    $configDB = Get-SPDatabase | where {$_.typename -eq "Configuration Database"}

    [Microsoft.SharePoint.Administration.SPContentDatabase[]]$contentDB = Get-SPWebApplication -IncludeCentralAdministration | Select-Object -ExpandProperty ContentDatabases

 

    # filter content databases based on user input (if supplied)

    if(-not [string]::IsNullOrEmpty($DatabaseName))

    {

        $contentDB = $contentDB | Where-Object {$_.Name -eq $DatabaseName}

    }

    if(-not [string]::IsNullOrEmpty($DatabaseServer))

    {

        $contentDB = $contentDB | Where-Object {$_.Server -eq $DatabaseServer}

    }

    Write-Output "<Databases>"

    foreach($db in $contentDB)

    {

        ProcessContentDatabase $db

    }

    Write-Output "</Databases>"

}

Note  I have added some additional parameters (IncludeFeatures and IncludeEventReceivers), which I omitted from this post due to length considerations. I attempted to research the IncludeWebParts, IncludeSetupFiles, and IncludeCustomListView parameters, but I ran into issues getting sample output or finding the corresponding properties necessary. I also included Write-Verbose calls for additional troubleshooting if necessary.

In this post, I walked through attempting to replace the functionality from STSADM –o enumallwebs with a Windows PowerShell script. In the process of examining the output and researching corresponding Windows PowerShell substitutes, I learned quite a bit about the SharePoint object model. Specifically, the Microsoft.SharePoint.Administration.SPConfigurationDatabase datatype was entirely new to me. Although this script is not meant to be an official replacement for STSADM, you got to see one way to tackle an issue such as this.

~Brian

Thank you, Brian!

Join me tomorrow when we begin a series of posts by guest blogger, Rohn Edwards. It is a really cool series that you will not want to miss. I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

No Comments