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

22Dec/1140

Updating SharePoint 2010 User Information

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

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

SNAGHTMLa064e71

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

SNAGHTMLa089b49

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

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

 

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

image

And as shown in the People Picker:

SNAGHTMLa14e91b

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

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

SNAGHTMLa17239b

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

image

SNAGHTMLa1a344d

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

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

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

SNAGHTMLa21bc70

SNAGHTMLa236af7

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

SNAGHTMLa265249

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

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

-Gary

19Apr/111

Upgrading User Profile Choice Fields to SharePoint 2010

I’m working on an upgrade (database attach) for a client of mine and I ran into something somewhat unexpected with some User Profile properties – properties that used a choice field in 2007 (fields that allowed the administrator to define a list of values that the user could pick from) were migrated to Managed Term Store based fields when upgraded. At first glance this was pretty cool; the only problem was that, though the field was converted to a Managed Term Store based field (essentially a Taxonomy field), the Terms themselves were not migrated and the user-specific values were lost. Now, I may have done something wrong during the upgrade process and if someone knows what I did wrong please let me know – that said, I decided to just throw together a couple quick scripts that I could use to export the field information (along with the users’ values for those fields) and then import the information in the new Farm.

To accomplish this I created two separate scripts, one for the 2007 Farm and one for the new 2010 Farm. The 2007 script loops through all the User Profile properties and, if the property has choices defined then it creates an XML structure that defines the property name and the associated choices; I put this in a function called Get-SPChoiceProperties. It then loops through all User Profiles and, for each identified property, creates another XML structure which defines the property name and associated values along with the account name; I put this in a function called Get-SPUserProperties. I then merge these two XML structures to create a single structure which is saved to disk.

Here’s the complete code which I store in a file called Export-UserProfileProperties.ps1 (note that the function names and script name are not really important  and the code is by no means perfect – I threw this whole thing together *very* quickly so that I could just get this task done and move on to the next task):

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") | Out-Null
function Get-SPChoiceProperties() {
    $context = [Microsoft.Office.Server.ServerContext]::Default
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
    $props = $upm.GetProperties()
    [xml]$xml = "<Properties></Properties>"
    foreach ($prop in $props) {
        if ($prop.ChoiceList -eq $null) { continue }
        
        $propXml = $xml.CreateElement("Property")
        $xml.DocumentElement.AppendChild($propXml) | out-null
        $propXml.SetAttribute("Name", $prop.Name)
        $choicesXml = $xml.CreateElement("ChoiceList")
        $propXml.AppendChild($choicesXml) | out-null
        foreach ($choice in $Prop.ChoiceList.GetAllTerms($true)) {
            $choiceXml = $xml.CreateElement("Choice")
            $choicesXml.AppendChild($choiceXml) | out-null
            $choiceXml.InnerText = $choice
        }
    }
    $xml
}
function Get-SPUserProperties([string[]]$propertyNames) {
    $context = [Microsoft.Office.Server.ServerContext]::Default
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
    [xml]$xml = "<Users></Users>"
    foreach ($profile in $upm) {
        $userXml = $xml.CreateElement("User")
        $xml.DocumentElement.AppendChild($userXml) | out-null
        $userXml.SetAttribute("Account", $profile["AccountName"].Value)
        $propertyNames | % { 
            $val = ($profile[$_] -as [string[]]) -join ";"
            $userXml.SetAttribute($_, $val) 
        }
    }
    $xml
}
$propertyXml = Get-SPChoiceProperties
[string[]]$properties = $propertyXml.Properties.Property | % {$_.Name}
$userXml = Get-SPUserProperties $properties

[xml]$xml = "<ProfileData>$($propertyXml.OuterXml)$($userXml.OuterXml)</ProfileData>"
$xml.Save("c:\userdata.xml")

The next thing I needed to do was to create the script for the 2010 Farm. This script had three steps for which I created a separate function for each step:

  1. Create the Term Store Group if not present and, for each User Profile field identified in the exported XML create the Term Set and associated Terms. I store the field name and associated Term Set in a hash table that I can then use for the next steps. The function I created for this is called Create-SPProfileTermSets.
  2. For each User Profile property and associated Term Set returned by step 1, update the property with the new Term Set. The function I created for this is called Set-SPUserProfileProperties.
  3. Loop through all the users defined in the exported XML and, for each user, update the User Profile property with the appropriate Term value. The function I created for this is called Set-SPUserProfileValues.

I then load the exported XML and call these three functions in order, passing in the appropriate information as required. Here’s the completed code (again, it’s rough but it works – well, it met my needs):

function Create-SPProfileTermSets($session, [string]$groupName, [xml]$data) {
    
    $group = $session.DefaultSiteCollectionTermStore.Groups[$groupName]
    if ($group -eq $null) {
        $group = $ts.DefaultSiteCollectionTermStore.CreateGroup($groupName)
        $group.TermStore.CommitAll()
    }
    $termSets = @{}
    $data.ProfileData.Properties.Property | % {
        $name = $_.Name
        $termSet = $group.TermSets[$name]
        if ($termSet -eq $null) {
            $termSet = $group.CreateTermSet($name)
        }
        $termSets += @{$name=$termSet}
        $_.ChoiceList.Choice | % {
            $termValue = $_.Replace("&", "")
            $term = $termSet.Terms[$termValue]
            if ($term -eq $null) {
                Write-Host "Adding $termValue"
                $termSet.CreateTerm($termValue, 1033) | Out-Null
            }
        }
        $group.TermStore.CommitAll()        
    }
    $termSets
}
function Set-SPUserProfileProperties($termSets) {
    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context
    $properties = $upm.ProfilePropertyManager.GetCoreProperties()
    foreach ($key in $termSets.Keys) {
        $prop = $properties.GetPropertyByName($key)
        $prop.TermSet = $termSets[$key]
        $prop.Commit()
    }
}
function Set-SPUserProfileValues($termSets, [xml]$data) {
    $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"}
    $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)
    $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context
    
    $data.ProfileData.Users.User | % {
        if ($upm.UserExists($_.Account)) {
            $up = $upm.GetUserProfile($_.Account)
            Write-Host "Evaluating $($_.Account)..."
            foreach ($key in $termSets.Keys) {
                Write-Host "    Setting $key..."
                $prop = $up[$key]
                $prop.Clear()
                [string[]]$term = $_.GetAttribute($key).Split(';')
                $term | % {
                    if (![string]::IsNullOrEmpty($_)) {
                        $prop.Add($termSets[$key].Terms[$_].Name)
                    }
                }
            }
            $up.Commit()
        }
    }
}

$ts = New-Object Microsoft.SharePoint.Taxonomy.TaxonomySession (Get-SPSite "http://<ENTER YOUR SITE URL HERE>"),$true
[xml]$xml = Get-Content C:\userdata.xml
$termSets = Create-SPProfileTermSets $ts "Profile Properties" $xml
Set-SPUserProfileProperties $termSets
Set-SPUserProfileValues $termSets $xml

One note about the Taxonomy Session – notice that I don’t use the Get-SPTaxonomySession cmdlet – this is because I can’t stand how this cmdlet works; when you create a TaxonomySession object using the API you have the ability to force a reload from the database, thereby ignoring locally cached data, but when you use the cmdlet you don’t have this option. This can cause all kinds of issues when you are running scripts/functions like this repeatedly as what is cached may not reflect what is current and this will inevitably cause you headaches. (If you decide to use this code don’t forget to set the URL for the Taxonomy Session).

So that was my adventure for the day – as I said in the beginning, if there’s a way to do the upgrade that negates the need to do any of this please let me know; otherwise, perhaps others will benefit from today’s upgrade headaches :) .

18Apr/1132

Deploying SharePoint 2010 Solution Package Using PowerShell (Revisited)

If you were at my PowerShell for developers talk at the European SharePoint Best Practices Conference last week then you’ll know that I’ve never been all that happy with how I was approaching Farm Solution deployment, as detailed in an earlier post from sometime last year (Deploying SharePoint 2010 Solution Packages Using PowerShell). So what are some of the issues I have with what I created? Here’s a quick list:

  • There are two functions – I really just want one function to call and let the function figure out what to do based on the parameters provided.
  • I want to be able to provide a directory containing WSP files to deploy (sure, I could use Get-ChildItem to grab all my files, iterate through them, and then call the function, but that means I have to type more each time I want to execute and I’m way too lazy for that).
  • There’s no consideration for simply updating Solution Packages rather than retracting and redeploying.
  • I was using the Start-SPAdminJob cmdlet and stopping and starting the admin service – something that we shouldn’t be doing and is really just an old throwback to 2007. It’s just a bad idea – don’t do it.
  • I was forcing information such as GAC and CAS settings in the XML when I could easily get the information via the SPSolution object once added.
  • And finally, there was no real help available so you had to really know what was going on to understand how to construct the XML file and to then call the file.

For all these reasons I’ve decided to completely rewrite the script. As a result it’s a bit more complicated at first blush but that’s mainly due to some additional error handling, progress reporting, and blocking code that I’ve added; as well as the additional parameter related code and associated help. I’ve essentially followed the pattern that I described with my earlier post on Feature activation and have made the function work more like a cmdlet (with full help, parameter sets, and use of PipeBind objects). Before I share the code, I’d like to show the complete help that is available for the function:

NAME
    Deploy-SPSolutions
    
SYNOPSIS
    Deploys one or more Farm Solution Packages to the Farm.
    
SYNTAX
    Deploy-SPSolutions [-Identity] <String> [[-UpgradeExisting]] [[-AllWebApplications]] [[-WebApplication] <SPWebApplicationPipeBind[]>] [<CommonParameters>]
    
    Deploy-SPSolutions [-Config] <XmlDocument> [<CommonParameters>]
    
DESCRIPTION
    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
    If using an XML configuration file, the format of the file must match the following:
      <Solutions>    
        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
          <WebApplications>            
         <WebApplication>http://example.com/</WebApplication>        
          </WebApplications>    
        </Solution>
      </Solutions>
    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.

PARAMETERS
    -Config <XmlDocument>
        The XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -Identity <String>
        The directory, WSP file, or XML configuration file containing the WSP files to deploy.
        
        Required?                    true
        Position?                    1
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -UpgradeExisting [<SwitchParameter>]
        If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
        
        Required?                    false
        Position?                    2
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -AllWebApplications [<SwitchParameter>]
        If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
        
        Required?                    false
        Position?                    3
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    -WebApplication <SPWebApplicationPipeBind[]>
        Specifies the Web Application(s) to deploy the WSP file to.
        
        Required?                    false
        Position?                    4
        Default value                
        Accept pipeline input?       false
        Accept wildcard characters?  
        
    <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
    -------------------------- EXAMPLE 1 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
    
    -------------------------- EXAMPLE 2 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
    
    -------------------------- EXAMPLE 3 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 4 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
    
    -------------------------- EXAMPLE 5 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
    
    -------------------------- EXAMPLE 6 --------------------------
    
    PS C:\>. .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions C:\Solutions.xml
    
    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
    
RELATED LINKS
    Get-Content
    Get-SPSolution
    Add-SPSolution
    Install-SPSolution
    Update-SPSolution
    Uninstall-SPSolution
    Remove-SPSolution 

As you can see, this is a lot more useful for someone wishing to execute this script as not only does it provide information about the XML structure but it also provides several usage examples.

So, without further delay, here’s the new version of the deployment script (note that I changed the function name to Deploy-SPSolutions so it won’t impact environments that depend on the old function):

function global:Deploy-SPSolutions() {
  <#
  .Synopsis
    Deploys one or more Farm Solution Packages to the Farm.
  .Description
    Specify either a directory containing WSP files, a single WSP file, or an XML configuration file containing the WSP files to deploy.
    If using an XML configuration file, the format of the file must match the following:
      <Solutions>    
        <Solution Path="<full path and filename to WSP>" UpgradeExisting="false">
          <WebApplications>            
            <WebApplication>http://example.com/</WebApplication>        
          </WebApplications>    
        </Solution>
      </Solutions>
    Multiple <Solution> and <WebApplication> nodes can be added. The UpgradeExisting attribute is optional and should be specified if the WSP should be udpated and not retracted and redeployed.
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo Web Application (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -WebApplication http://demo,http://mysites
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to the http://demo and http://mysites Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs -AllWebApplications
    
    This example loads the function into memory and then deploys all the WSP files in the specified directory to all Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable).
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions -Identity C:\WSPs\MyCustomSolution.wsp -AllWebApplications -UpgradeExisting
    
    This example loads the function into memory and then deploys the specified WSP to all Web Applications (if applicable); existing deployments will be upgraded and not retracted and redeployed.
  .Example
    PS C:\> . .\Deploy-SPSolutions.ps1
    PS C:\> Deploy-SPSolutions C:\Solutions.xml
    
    This example loads the function into memory and then deploys all the WSP files specified by the Solutions.xml configuration file.
  .Parameter Config
    The XML configuration file containing the WSP files to deploy.
  .Parameter Identity
    The directory, WSP file, or XML configuration file containing the WSP files to deploy.
  .Parameter UpgradeExisting
    If specified, the WSP file(s) will be updated and not retracted and redeployed (if the WSP does not exist in the Farm then this parameter has no effect).
  .Parameter AllWebApplications
    If specified, the WSP file(s) will be deployed to all Web Applications in the Farm (if applicable).
  .Parameter WebApplication
    Specifies the Web Application(s) to deploy the WSP file to.
  .Link
    Get-Content
    Get-SPSolution
    Add-SPSolution
    Install-SPSolution
    Update-SPSolution
    Uninstall-SPSolution
    Remove-SPSolution
  #>
  [CmdletBinding(DefaultParameterSetName="FileOrDirectory")]
  param (
    [Parameter(Mandatory=$true, Position=0, ParameterSetName="Xml")]
    [ValidateNotNullOrEmpty()]
    [xml]$Config,

    [Parameter(Mandatory=$true, Position=0, ParameterSetName="FileOrDirectory")]
    [ValidateNotNullOrEmpty()]
    [string]$Identity,
    
    [Parameter(Mandatory=$false, Position=1, ParameterSetName="FileOrDirectory")]
    [switch]$UpgradeExisting,
    
    [Parameter(Mandatory=$false, Position=2, ParameterSetName="FileOrDirectory")]
    [switch]$AllWebApplications,
    
    [Parameter(Mandatory=$false, Position=3, ParameterSetName="FileOrDirectory")]
    [Microsoft.SharePoint.PowerShell.SPWebApplicationPipeBind[]]$WebApplication
  )
  function Block-SPDeployment($solution, [bool]$deploying, [string]$status, [int]$percentComplete) {
    do { 
      Start-Sleep 2
      Write-Progress -Activity "Deploying solution $($solution.Name)" -Status $status -PercentComplete $percentComplete
      $solution = Get-SPSolution $solution
      if ($solution.LastOperationResult -like "*Failed*") { throw "An error occurred during the solution retraction, deployment, or update." }
      if (!$solution.JobExists -and (($deploying -and $solution.Deployed) -or (!$deploying -and !$solution.Deployed))) { break }
    } while ($true)
    sleep 5  
  }
  switch ($PsCmdlet.ParameterSetName) { 
    "Xml" { 
      # An XML document was provided so iterate through all the defined solutions and call the other parameter set version of the function
      $Config.Solutions.Solution | ForEach-Object {
        [string]$path = $_.Path
        [bool]$upgrade = $false
        if (![string]::IsNullOrEmpty($_.UpgradeExisting)) {
          $upgrade = [bool]::Parse($_.UpgradeExisting)
        }
        $webApps = $_.WebApplications.WebApplication
        Deploy-SPSolutions -Identity $path -UpgradeExisting:$upgrade -WebApplication $webApps -AllWebApplications:$(($webApps -eq $null) -or ($webApps.Length -eq 0))
      }
      break
    }
    "FileOrDirectory" {
      $item = Get-Item (Resolve-Path $Identity)
      if ($item -is [System.IO.DirectoryInfo]) {
        # A directory was provided so iterate through all files in the directory and deploy if the file is a WSP (based on the extension)
        Get-ChildItem $item | ForEach-Object {
          if ($_.Name.ToLower().EndsWith(".wsp")) {
            Deploy-SPSolutions -Identity $_.FullName -UpgradeExisting:$UpgradeExisting -WebApplication $WebApplication
          }
        }
      } elseif ($item -is [System.IO.FileInfo]) {
        # A specific file was provided so assume that the file is a WSP if it does not have an XML extension.
        [string]$name = $item.Name
        
        if ($name.ToLower().EndsWith(".xml")) {
          Deploy-SPSolutions -Config ([xml](Get-Content $item.FullName))
          return
        }
        $solution = Get-SPSolution $name -ErrorAction SilentlyContinue
        
        if ($solution -ne $null -and $UpgradeExisting) {
          # Just update the solution, don't retract and redeploy.
          Write-Progress -Activity "Deploying solution $name" -Status "Updating $name" -PercentComplete -1
          $solution | Update-SPSolution -CASPolicies:$($solution.ContainsCasPolicy) `
            -GACDeployment:$($solution.ContainsGlobalAssembly) `
            -LiteralPath $item.FullName
        
          Block-SPDeployment $solution $true "Updating $name" -1
          Write-Progress -Activity "Deploying solution $name" -Status "Updated" -Completed
          
          return
        }

        if ($solution -ne $null) {
          #Retract the solution
          if ($solution.Deployed) {
            Write-Progress -Activity "Deploying solution $name" -Status "Retracting $name" -PercentComplete 0
            if ($solution.ContainsWebApplicationResource) {
              $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
            } else {
              $solution | Uninstall-SPSolution -Confirm:$false
            }
            #Block until we're sure the solution is no longer deployed.
            Block-SPDeployment $solution $false "Retracting $name" 12
            Write-Progress -Activity "Deploying solution $name" -Status "Solution retracted" -PercentComplete 25
          }

          #Delete the solution
          Write-Progress -Activity "Deploying solution $name" -Status "Removing $name" -PercentComplete 30
          Get-SPSolution $name | Remove-SPSolution -Confirm:$false
          Write-Progress -Activity "Deploying solution $name" -Status "Solution removed" -PercentComplete 50
        }

        #Add the solution
        Write-Progress -Activity "Deploying solution $name" -Status "Adding $name" -PercentComplete 50
        $solution = Add-SPSolution $item.FullName
        Write-Progress -Activity "Deploying solution $name" -Status "Solution added" -PercentComplete 75

        #Deploy the solution
        
        if (!$solution.ContainsWebApplicationResource) {
          Write-Progress -Activity "Deploying solution $name" -Status "Installing $name" -PercentComplete 75
          $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -Confirm:$false
          Block-SPDeployment $solution $true "Installing $name" 85
        } else {
          if ($WebApplication -eq $null -or $WebApplication.Length -eq 0) {
            Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to all Web Applications" -PercentComplete 75
            $solution | Install-SPSolution -GACDeployment:$($solution.ContainsGlobalAssembly) -CASPolicies:$($solution.ContainsCasPolicy) -AllWebApplications -Confirm:$false
            Block-SPDeployment $solution $true "Installing $name to all Web Applications" 85
          } else {
            $WebApplication | ForEach-Object {
              $webApp = $_.Read()
              Write-Progress -Activity "Deploying solution $name" -Status "Installing $name to $($webApp.Url)" -PercentComplete 75
              $solution | Install-SPSolution -GACDeployment:$gac -CASPolicies:$cas -WebApplication $webApp -Confirm:$false
              Block-SPDeployment $solution $true "Installing $name to $($webApp.Url)" 85
            }
          }
        }
        Write-Progress -Activity "Deploying solution $name" -Status "Deployed" -Completed
      }
      break 
    }
  } 
}

When it comes to using the function I believe the help documentation speaks for itself so I won’t reiterate it here.

As always, I’m open to suggestions as to how to improve this function so please leave a comment if you find something wrong or have a suggestion for making it better.

-Gary

17Apr/111

Retrieving SharePoint 2010 Feature Activations Using Windows PowerShell

During my PowerShell for Developers presentation in London last week I promised to show and demonstrate a script for retrieving Feature activations; unfortunately I ran out of time and was not able to show this script to the degree that I’d intended so I decided to throw together this blog post.

When developing custom Features it is very common to expect that there will need to be some level of update required for those Features. Typically this means that, after deploying the Feature via a Solution Package, you will need to re-activate that Feature in order to trigger any additional code to run (or, if you are using the new SharePoint 2010 Feature upgrade capabilities you will need to run the Upgrade(Boolean) method of the SPFeature object). The problem is knowing where the Feature is activated throughout the Farm. Using PowerShell there are two ways to do this – you can use the Get-SPFeature cmdlet and test the results against the appropriate scope or you can use the various “Query” methods that have been provided for each scope. I don’t recommend that you use the Get-SPFeature cmdlet as it is very inefficient, and as such, I won’t bother showing an example of that here. Instead I’ll focus on the “Query” methods approach.

Whether your Feature is scoped to the Farm, Web Application, Site Collection, or Site, there is a method that you can call to get an SPFeature object which effectively corresponds to a Feature activation. For Farm scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebService class, obtainable via the SPWebService class’ static AdministrationService property; for Web Application scoped Features you use the static QueryFeaturesInAllWebServices(Guid, Boolean) method of the SPWebService class; for Site Collection scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebApplication class; and for Site scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPSite class.

To create our PowerShell function we’ll simply take in a SPFeatureDefinition object and use a switch statement to call the appropriate method based on the scope of the Feature. To make the function more versatile we can use the Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind type which will allow the caller to pass in either the name of the Feature, its ID, or an actual SPFeatureDefinition object; additionally, we can use parameter attributes to easily allow the value to be passed in via the object pipeline. And finally, we’ll add an additional parameter stating that we wish to retrieve only those activations that require upgrading and we’ll add some basic help for the function.

The following code listing represents the completed function – I recommend that you save this to a file named Get-SPFeatureActivations.ps1. Note that I plan on adding this as a cmdlet to my downloadable extensions thereby making the need for this script unnecessary, however, I believe that this example provides a great template to use for creating professional looking, production ready scripts that both IT administrators and developers can use.

function Get-SPFeatureActivations() {
  <#
  .Synopsis
    Retrieves Feature activations for the given Feature Definition.
  .Description
    Retrieves the SPFeature object for each activation of the SPFeatureDefinition object.
  .Example
    Get-SPFeatureActivations TeamCollab
  .Parameter Identity
    The Feature name, ID, or SPFeatureDefinition object whose activations will be retrieved.
  .Parameter NeedsUpgrade
    If specified, only Feature activations needing upgrading will be retrieved.
  .Link
    Get-SPFeature
  #>
  [CmdletBinding()]
  param (
    [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
    [Alias("Feature")]
    [ValidateNotNullOrEmpty()]
    [Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind]$Identity,
    
    [Parameter(Mandatory=$false, Position=1)]
    [switch]$NeedsUpgrade
  )
  begin { }
  process {
    $fd = $Identity.Read()
    switch ($fd.Scope) {
      "Farm" {
        [Microsoft.SharePoint.Administration.SPWebService]::AdministrationService.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
        break
      }
      "WebApplication" {
        [Microsoft.SharePoint.Administration.SPWebService]::QueryFeaturesInAllWebServices($fd.ID, $NeedsUpgrade.IsPresent)
        break
      }
      "Site" {
        foreach ($webApp in Get-SPWebApplication) {
          $webApp.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
        }
        break
      }
      "Web" {
        foreach ($site in Get-SPSite -Limit All) {
          $site.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent)
          $site.Dispose()
        }
        break
      }
    }
  }
  end { }
}

Assuming you’ve saved the file to the root of the C drive (not recommended but its what I do when I’m doing demos) then you can load the function into memory using dot sourcing as shown in the following example (note that the help for the function shows the help information specified by the block comment help):

SNAGHTML45555695

Once the function is loaded into memory you can start using it. In the following example I’m returning back all the locations where the MyCustomFeature Feature is activated; I then use the Select-Object cmdlet to return just the URL for each activation:

Get-SPFeatureActivations MyCustomFeature | select @{Expression={$_.Parent.Url};Label="Url"}

In this next example, instead of simply outputting the URL of each activation, I’m forcing the Feature to be reactivated using the Enable-SPFeature cmdlet (use the -Force parameter to force the Feature to be reactivated – you could also change the code to deactivate the Feature using the Disable-SPFeature cmdlet and then activate using the Enable-SPFeature cmdlet):

Get-SPFeatureActivations MyCustomFeature | ForEach-Object {
  Enable-SPFeature -Identity MyCustomFeature -Url $_.Parent.Url -Force

}

Similarly you can retrieve only those Features needing upgrade and then call the Upgrade() method, as shown in this next example:

Get-SPFeatureActivations MyCustomFeature -NeedsUpgrade | ForEach-Object {
  $_.Upgrade($false
)
}

I strongly recommend that, before you re-deploy a Feature that may be activated at an unknown number of scopes, you run this function (or something similar to it) so that you fully understand the impact of upgrading your Feature. One more thing to watch out for, if your environment is very large you may wish to modify this function so that it does not return the SPFeature object but instead just returns the URL corresponding to the activation – you can then use the Get-SPFeature cmdlet to retrieve the SPFeature object; the benefit of this is that you can immediately dispose of the parent object and prevent potential out of memory errors (I’m particularly concerned with Site Collection and Site scoped Features here where the Parent property of the SPFeature object corresponds to an SPSite or SPWeb object which must be disposed).

That’s all I’ve got for now; hopefully you’ve found this useful!

-Gary