Blog Stats
  • Posts - 24
  • Articles - 0
  • Comments - 16
  • Trackbacks - 0

 

Creating large SharePoint structures from Powershell and xml, part ii

(this is the second part; for the first part, see Creating large SharePoint structures from Powershell and xml)

The SharePoint structure needs to be created somehow.

We can let our junior consultant write the xml structure manually - after all, he should have plenty of time on his hands now; better find him some work before he gets himself into too much trouble.

We can write a rich client where the user can create a structure, pick template etc etc.

Or we can use the product itself. Create the structure in SharePoint, and read it using a powerful scripting language, resulting in a rich xml. Like, say, Powershell.

In our last post, we saw how powerful hashtables were when creating SharePoint stuff from Powershell. Can we use these the other way as well?

It would be mighty nice if we could do something like:

$splat = Get-SPWeb http://server

$xml = Detransmogrificator $splat

But life is rarely so simple.

Although the arguments to New-SPWeb are simple, it is possible to do much more with a web than that. And some arguments are shortcuts to 'real' objects. For example, 'owners' become proper SPUser objects.

What I'm trying to say, is that we need to read the structure the hard way, collecting properties and creating the xml structure along the way. It is not difficult. A few things might be worth pointing out.

Portnumber

When creating a new webapp, we specify which port number we should use. This is a typical example of how something is easy to create, but difficult to read.

Or, if not difficult, at least tedious:

$spi = New-Object Microsoft.SharePoint.Administration.SPIisSettings
$webapp.IisSettings.TryGetValue([Microsoft.SharePoint.Administration.SPUrlZone]::Default, [ref]$spi)
$path = $spi.Path
$portNum = $spi.ServerBindings[0].Port

Template

Web templates is a new feature in SharePoint 2010. For an excellent and through introduction visit Vesa Juvonen's blog.

Web templates supposedly inherit a site definition. I say supposedly; the only thing it seems to inherit is the site definition ID. That is, if you 'get' the template ID of a web created using a web template, you'll typically get "STS#0". Which isn't particularily useful, as you typically want the web template ID - which has the format of {guid}#templatename.

Vesa's blog strongly suggests saving the web template name to the web's property bag. The Get-Template method supports this, and also the 'full' name.

function GetTemplate($web)
{
    # if it is a web template which was made following Vesa's recipe, the webtemplateid is stored in the property bag.
    $webId = $web.AllProperties["WebTemplateId"]
    if ($webId -ne $null)
    {
        if ($webId.Contains('#')) { # presumably GUID#Name - which is what we want
            return $webId
        }
        # sigh. Try to find the template itself. (it may not work; just because
        # a web is of type A, that may (no longer) be an allowed webtemplate)
        # we're looking for a webtemplate which contains the name
        $wt = $web.GetAvailableWebTemplates([int]$web.Language) | Where-Object { $_.Name.Contains($webId) }
        return $wt.Name
    }
    $wtName = $web.WebTemplate
    $wtId = $web.WebTemplateId
    return "$($wtName)#$($wtId)"
}

XML

Creating and saving the xml structure is very simple.

function SaveWebApplication($webapp)
{
    $webAppElem = $xml.CreateElement("WebApplication")
    $spi = New-Object Microsoft.SharePoint.Administration.SPIisSettings
    $webapp.IisSettings.TryGetValue([Microsoft.SharePoint.Administration.SPUrlZone]::Default, [ref]$spi)
    $path = $spi.Path
    $portNum = $spi.ServerBindings[0].Port
    $webAppElem.SetAttribute("Name", $webapp.Name)
    $webAppElem.SetAttribute("ApplicationPool", $webapp.ApplicationPool.DisplayName)
    $webAppElem.SetAttribute("DatabaseServer", $webapp.ContentDatabases[0].Server)
    $webAppElem.SetAttribute("DatabaseName", $webapp.ContentDatabases[0].Name)
    $webAppElem.SetAttribute("Port", $portNum)
    $webAppElem.SetAttribute("Path", $path)
    $siteCollsElem = $xml.CreateElement("SiteCollections")
    foreach ($sitecoll in $webapp.Sites)
    {
        SaveSiteCollection $sitecoll $siteCollsElem
    }
    $ignored = $webAppElem.AppendChild($siteCollsElem)
    $ignored = $webAppsElem.AppendChild($webAppElem)
}

$xml = New-Object System.Xml.XmlDocument
$doc = $xml.CreateElement("Structure")
$webAppsElem = $xml.CreateElement("WebApplications")
if (($saveWebApps -eq $null) -or ($saveWebApps.Count -eq 0))
{
    $webapps = Get-SPWebApplication
    foreach ($webapp in $webapps)
    {
        SaveWebApplication $webapp
    }
} else {
    foreach ($webAppName in $saveWebApps) # savewebapps is an array which specify which webapps to save
    {
        $webapp = Get-SPWebApplication $webAppName
        SaveWebApplication $webapp
    }
}

$doc.AppendChild($webAppsElem)
$xml.AppendChild($doc)

$xml.Save($filename)

Url

As mentioned in the previous post, I want to use the hierarchical structure of xml to help create urls. If an element specifies “news,” rather than “http://server/dept1/news”, I can easily copy-paste-move it between branches and levels.

function SaveWebs($websElement, $web)
{
    $somethingAdded = $false
    if ($web.Exists -eq $true)
    {
        $webElem = $xml.CreateElement("Web")
        $webElem.SetAttribute("Title", $web.Title)
        $url = $web.Url   # find the relative URL
        $lix = $url.LastIndexOf('/')
        $url = $url.Substring($lix + 1)
        $webElem.SetAttribute("Url", $url)
        if ($web.Name.Length > 0) {
            $webElem.SetAttribute("Name", $web.Name)
        }
        if ($web.Name.Length > 0) {
            $webElem.SetAttribute("Description", $web.Description)
        }
        $templateId = GetTemplate $web
        $webElem.SetAttribute("Template", $templateId)

        $language = $web.Language
        if ($language -ne 1033) { # don't bother saving the default (keep xml as simple as possible)
            $webElem.SetAttribute("Language", $language)
        }

        $added = $false
        $subWebsElement = $xml.CreateElement("Webs")
        foreach ($subweb in $web.Webs)
        {
            $added = SaveWebs $subWebsElement $subweb #$subweb.Url
        }
        if ($added -eq $true)
        {
            $webElem.AppendChild($subWebsElement)
        }
        $websElement.AppendChild($webElem)
        $somethingAdded = $true
    }
    $somethingAdded
}

But how do we know what to save?

One huge problem with this part of the solution, is that it is tightly coupled to the farm we’re analyzing, in that we only read the things we know we’re interested in. And that varies a lot.

One of the servers where I tried this mechanism had a mix of English and Norwegian sites. When collecting this information, I only ‘save’ the setting if it is different from English.

Perhaps a similar approach may be taken for other things that language? Only save if different from default.

But two problems remain: What are the defaults? And what are the parameters we need to query? (even as I write this, I realize that the second question may be solved through some creative use of “Get-Member”) (Hm)

Once the xml-structure has been created, it may be edited manually.


Feedback

# re: Creating large SharePoint structures from Powershell and xml, part ii

Gravatar please send me the document to my mail.... 7/4/2012 12:00 PM | yoga

Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 
 

 

 

Copyright © Norgean