European SharePoint Best Practices Conference Wrap Up
I just got back from London and all I can say is, “Wow!” This was the first time that myself and my wife and daughter have ever been out of the US and we had an absolute blast – we did so much walking that we literally wore our shoes thin – it’s truly an amazing place with incredible history everywhere you turn.
As for the conference itself, first off I want to thank Steve Smith and all those that were involved with organizing such a wonderful conference – as a speaker they definitely set the bar for other conference organizers and I hope that attendees saw the same attention to detail and overall quality that the speakers saw (this conference is truly unique amongst all the ones I’ve spoken at).
In regards to my two sessions – I totally blew my timing on both of them (more so on the developer one) and was unable to show everything that I planned but I do hope that those that attended got some useful information for the time spent (virtually nobody left when I ran late so I’m going to take that as a good sign that people were getting value out of the presentations). I plan to post some of the sample scripts that I demonstrated during the presentations but that will come over the next few weeks; for now I’ve posted my slide decks (saved with notes so you can see some of my examples) for you to download:
| Windows PowerShell for SharePoint 2010 Administrators | |
| Windows PowerShell for SharePoint 2010 Developers |
Don’t forget that you can also download the PowerShell cheat sheet that I provided during the sessions (see my earlier post).
Thanks again to everyone that attended my sessions and for all the great tweets that came out during and after the sessions – I know I still have lots to learn when it comes to public speaking so any kind of feedback is very much appreciated!
Windows PowerShell Cheat Sheet
While preparing for my two PowerShell talks that I presented at the European SharePoint Best Practices Conference last week in London, I soon discovered that I had way too much content to present. Specifically, I wanted to begin both talks with a “PowerShell 101” piece where I walked through some of the more basic concepts relevant to each audience (mainly those that I always got tripped up on); unfortunately there’s just too many things to choose from and I couldn’t possibly show everything I wanted. Well, I happened to be in Orlando the week before I left for the conference and I had a chance to talk with Rob Bogue about what my plans were – well, to make a long story short, Rob had stated that the one thing he’d love to have is a simple, one-page cheat sheet showing some common syntactical examples. I thought this was not only a great idea in general but could also help solve my problem for my presentations – now I didn’t have to show everything during my talk, I could just show those bits that warranted more explanation and then provide everyone the cheat sheet as a handout for later review. And of course, I never planned on limiting the audience to just the attendees of the conference, though I did want them to be the first recipients. So, feel free to download my Windows PowerShell Cheat Sheet and good luck with all your PowerShell endeavors!
Creating PowerShell Help Files Dynamically
It’s been a while since I’ve blogged anything useful due to all my writing efforts going to the book that I’ve been working on. However, I’ve recently wrapped up my last chapter for the book so I figured what better time to start focusing on my blog again.
The first thing I wanted to cover was something that I should have documented back in May of 2010 when I released my SharePoint 2010 cmdlets and it is something that I just finished referencing in my last book chapter: creating help files for custom cmdlets.
In the book I emphasize that creating these help files is not a trivial task and there are virtually no good tools available to help you with the effort. When I created my SharePoint 2007 cmdlets, I had first tried using the Cmdlet Help Editor v1.0 that the Windows PowerShell team released back in May of 2007; I quickly found that this wasn’t very practical for long term use for numerous reasons, particularly when looking at SharePoint cmdlets where I don’t have a snap-in that I can point the tool to. But the biggest issue I had with using this tool, or any other tool that required me to add descriptions manually, was that I just couldn’t keep up with the changes. I was making new cmdlets and extending existing ones so fast that it was taking me longer to update the help file than it was to create/update the cmdlet – that was just unacceptable to me.
As you may know, I’m all about automation; so I figured, how hard could it possibly be to automate the creation of the help file? All I needed to do was to decorate my cmdlets with some metadata that I could then use with a little reflection to spit out the required XML. I could then generate this XML on the Post Build event of my assembly so that the help file would be automatically updated just prior to the SharePoint Solution Package (WSP) being generated. So, back during the beta days of SharePoint 2010, while migrating my 2007 STSADM extensions to cmdlets, I decided to go ahead and implement my idea. I released it along with my SharePoint 2010 source code with little fanfare (not very many people are generating PowerShell cmdlets and even fewer are creating help files for them). However, with my book having a reference to this code I figured I would write a short post about it and, along the way, break the code out of the SharePoint project and into it’s own project, thereby making it useful for anyone who wishes to dynamically develop Windows PowerShell help files for their cmdlets.
| Downloads You can download the .NET Assembly (Lapointe.PowerShell.MamlGenerator.dll) alone or the source code to the assembly from my downloads page. I thought about providing a separate download for the source but I’m lazy and really didn’t want to have to manage two different download packages; so, if you wish to download the source, just download the SharePoint 2010 source code and you’ll find the Lapointe.PowerShell.MamlGenerator project. |
So how does it work? It’s actually quite simple; I’ve created a series of custom Attribute classes which you can use to decorate your cmdlet class. I then use reflection to interrogate the Assembly for any classes that are based on PSCmdlet; for each class I look for these attributes. The rest is just simple XML generation. The Attributes are defined in the following table:
| Name | Example |
|---|---|
| CmdletDescriptionAttribute This Attribute is assigned to the class and contains the main, verbose description, as well as the synopsis for the cmdlet (or shorter description). You can only have one CmdletDescriptionAttribute assigned to the class. | [CmdletDescription("Delete a list from a web site.")] |
| ExampleAttribute This Attribute is assigned to the class and contains any example code and corresponding descriptions. You may assign multiple instances of the ExampleAttribute to the class (one for each example). | [Example( Code = "PS C:\\> Get-SPList \"http://server_name/lists/mylist\" | Remove-SPList -BackupDirectory \"c:\\backups\\mylist\"", Remarks = "This example deletes the list mylist and creates a backup of the list in the c:\\backups\\mylist folder.")] |
| RelatedCmdletsAttribute This Attribute is assigned to the class and contains the listing of related cmdlets. The related cmdlets can be added using the cmdlet's type or any applicable string representation of the cmdlet (appropriate for related cmdlets that are external to the current project). You can only have one RelatedCmdletsAttribute assigned to the class. | [RelatedCmdlets( typeof(SPCmdletGetList), ExternalCmdlets = new[] {"Get-SPWeb"})] |
| SupportsWildcardsAttribute This Attribute is assigned to the class and indicates whether the cmdlet supports wildcards. This Attribute is a marker only and contains no properties. You can only have one SupportsWildcardsAttribute assigned to the class. | [SupportsWildcards] |
These are all the custom Attributes that I’m using; the rest are are native to PowerShell and can be found in the System.Management.Automation namespace. Primarily I’m using CmdletAttribute and ParameterAttribute.
The example is taken from my SharePoint 2010 cmdlets and demonstrates how most of these Attributes are used:
using System.Collections.Generic; using Microsoft.SharePoint; using Microsoft.SharePoint.PowerShell; using System.Management.Automation; using Lapointe.SharePoint2010.Automation.Cmdlets.PipeBindObjects; using Lapointe.PowerShell.MamlGenerator.Attributes; namespace Lapointe.SharePoint2010.Automation.Cmdlets.Lists { [Cmdlet(VerbsCommon.Get, "SPList", SupportsShouldProcess = false), SPCmdlet(RequireLocalFarmExist = true)] [CmdletDescription("Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.")] [RelatedCmdlets(typeof(SPCmdletDeleteList), typeof(SPCmdletCopyList), typeof(SPCmdletCopyListSecurity), typeof(SPCmdletExportListSecurity), ExternalCmdlets = new[] {"Get-SPWeb", "Start-SPAssignment", "Stop-SPAssignment"})] [Example(Code = "PS C:\\> $list = Get-SPList \"http://server_name/lists/mylist\"", Remarks = "This example retrieves the list at http://server_name/lists/mylist.")] public class SPCmdletGetList : SPGetCmdletBaseCustom<SPList> { #region Parameters [Parameter(Mandatory = false, ValueFromPipeline = true, Position = 0, ParameterSetName = "AllListsInIdentity")] public SPListPipeBind Identity { get; set; } [Parameter(Mandatory = false, ValueFromPipeline = false, ParameterSetName = "AllListsByType")] public SPBaseType ListType { get; set; } [Parameter(Mandatory = false, ValueFromPipeline = true, HelpMessage = "Specifies the URL or GUID of the Web containing the list to be retrieved.\r\n\r\nThe 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.")] public SPWebPipeBind Web { get; set; } #endregion protected override IEnumerable<SPList> RetrieveDataObjects() { List<SPList> lists = new List<SPList>(); SPWeb web = null; if (this.Web != null) web = this.Web.Read(); if (Identity == null && ParameterSetName != "AllListsByType") { foreach (SPList list in web.Lists) lists.Add(list); } else if (Identity == null && ParameterSetName == "AllListsByType") { foreach (SPList list in web.GetListsOfType(ListType)) lists.Add(list); } else { SPList list = this.Identity.Read(web); if (list != null) lists.Add(list); } AssignmentCollection.Add(web); foreach (SPList list1 in lists) { AssignmentCollection.Add(list1.ParentWeb); AssignmentCollection.Add(list1.ParentWeb.Site); } return lists; } } }
With the cmdlet properly decorated I can make quick changes to my code and update the help documentation as I update the cmdlet. I don’t have to remember to go to another class file and remember this extremely cryptic MAML based XML format. This makes me considerably more efficient and it keeps my help files more up to date and inline with the actual cmdlet (I wish Microsoft would do something like this as much of the help for the SharePoint 2010 cmdlets is inaccurate, incomplete, or missing entirely).
The following shows a snippet of the help file that gets generated based on the attributes defined (I also use the Assembly’s CopyrightAttribute and DescriptionAttribute to add the copyright details that you see in the XML below):
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/1" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10"> <command:details> <command:name>Get-SPList</command:name> <maml:description> <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para> </maml:description> <maml:copyright> <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para> <maml:para> > For more information on this cmdlet and others:</maml:para> <maml:para> > http://blog.falchionconsulting.com/</maml:para> <maml:para> > Use of this cmdlet is at your own risk.</maml:para> <maml:para> > Gary Lapointe assumes no liability.</maml:para> </maml:copyright> <command:verb>Get</command:verb> <command:noun>SPList</command:noun> <dev:version>1.0.0.0</dev:version> </command:details> <maml:description> <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para> <maml:para /> <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para> <maml:para> > For more information on this cmdlet and others:</maml:para> <maml:para> > http://blog.falchionconsulting.com/</maml:para> <maml:para> > Use of this cmdlet is at your own risk.</maml:para> <maml:para> > Gary Lapointe assumes no liability.</maml:para> </maml:description> <command:syntax> <command:syntaxItem> <maml:name>Get-SPList</maml:name> <command:parameter required="false" position="1"> <maml:name>Identity</maml:name> <command:parameterValue required="true">SPListPipeBind</command:parameterValue> </command:parameter> <command:parameter required="false" position="named"> <maml:name>Web</maml:name> <command:parameterValue required="true">SPWebPipeBind</command:parameterValue> </command:parameter> <command:parameter required="false" position="named"> <maml:name>AssignmentCollection</maml:name> <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue> </command:parameter> </command:syntaxItem> <command:syntaxItem> <maml:name>Get-SPList</maml:name> <command:parameter required="false" position="named"> <maml:name>ListType</maml:name> <command:parameterValue required="true">GenericList | DocumentLibrary | Unused | DiscussionBoard | Survey | Issue | UnspecifiedBaseType</command:parameterValue> </command:parameter> <command:parameter required="false" position="named"> <maml:name>Web</maml:name> <command:parameterValue required="true">SPWebPipeBind</command:parameterValue> </command:parameter> <command:parameter required="false" position="named"> <maml:name>AssignmentCollection</maml:name> <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue> </command:parameter> </command:syntaxItem> </command:syntax> <command:parameters> <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="1" variableLength="false"> <maml:name>Identity</maml:name> <maml:description> <maml:para /> </maml:description> <command:parameterValue required="false" variableLength="false">SPListPipeBind</command:parameterValue> <dev:type> <maml:name>SPListPipeBind</maml:name> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:parameter> <command:parameter required="false" globbing="false" pipelineInput="false" position="named" variableLength="false"> <maml:name>ListType</maml:name> <maml:description> <maml:para /> </maml:description> <command:parameterValue required="false" variableLength="false">SPBaseType</command:parameterValue> <dev:type> <maml:name>SPBaseType</maml:name> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:parameter> <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false"> <maml:name>Web</maml:name> <maml:description> <maml:para>Specifies the URL or GUID of the Web containing the list to be retrieved.</maml:para> <maml:para /> <maml:para>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.</maml:para> </maml:description> <command:parameterValue required="false" variableLength="false">SPWebPipeBind</command:parameterValue> <dev:type> <maml:name>SPWebPipeBind</maml:name> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:parameter> <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false"> <maml:name>AssignmentCollection</maml:name> <maml:description> <maml:para>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.</maml:para> <maml:para /> <maml:para>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.</maml:para> </maml:description> <command:parameterValue required="false" variableLength="false">SPAssignmentCollection</command:parameterValue> <dev:type> <maml:name>SPAssignmentCollection</maml:name> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:parameter> </command:parameters> <command:inputTypes> <command:inputType> <dev:type> <maml:name /> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:inputType> </command:inputTypes> <command:returnValues> <command:returnValue> <dev:type> <maml:name /> <maml:uri /> <maml:description> <maml:para /> </maml:description> </dev:type> </command:returnValue> </command:returnValues> <command:terminatingErrors /> <command:nonTerminatingErrors /> <maml:alertSet> <maml:title /> <maml:alert> <maml:para>For more information, type "Get-Help Get-SPList -detailed". For technical information, type "Get-Help Get-SPList -full".</maml:para> </maml:alert> </maml:alertSet> <command:examples> <command:example> <maml:title>------------------EXAMPLE------------------</maml:title> <dev:code>PS C:\> $list = Get-SPList "http://server_name/lists/mylist"</dev:code> <dev:remarks> <maml:para>This example retrieves the list at http://server_name/lists/mylist.</maml:para> </dev:remarks> </command:example> </command:examples> <maml:relatedLinks> <maml:navigationLink> <maml:linkText>Remove-SPList</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Copy-SPList</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Copy-SPListSecurity</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Export-SPListSecurity</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Get-SPWeb</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Start-SPAssignment</maml:linkText> <maml:uri /> </maml:navigationLink> <maml:navigationLink> <maml:linkText>Stop-SPAssignment</maml:linkText> <maml:uri /> </maml:navigationLink> </maml:relatedLinks> </command:command>
As you can see, there is a lot of XML that you would have to manually generate (with over 50 cmdlets at the time of writing this and close to a hundred additional planned, you can hopefully understand why I wouldn't want to generate this manually).
So how do you call out to the assembly to generate the help file? Pretty easily! In the Lapointe.PowerShell.MamlGenerator Assembly there is a static class called CmdletHelpGenerator. This class has three static methods that you can call based on how you want to pass in the cmdlet's Assembly information:
public static void GenerateHelp(string outputPath, bool oneFile); public static void GenerateHelp(string inputFile, string outputPath, bool oneFile); public static void GenerateHelp(Assembly asm, string outputPath, bool oneFile);
If you want to run this code from PowerShell you can do so using the following syntax:
$asm = "W:\Lapointe.SharePoint2010.Automation.dll" $cmdletAsm = "W:\Lapointe.PowerShell.MamlGenerator.dll" $targetPath = "W:\MyProject\POWERSHELL\Help" [System.Reflection.Assembly]::LoadFrom($asm) | Out-Null [Lapointe.PowerShell.MamlGenerator.CmdletHelpGenerator]::GenerateHelp($asm, $targetPath, $true)
And again, this works with any cmdlet development, not just SharePoint cmdlet development. Hopefully all you ambitious PowerShell developers out there who appreciate the need to provide your IT Administrators with proper help documentation will find this code as useful as I have.
Good luck and happy PowerShelling!
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
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.
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:
- SharePoint 2010 PowerShell Cmdlets Source Code
- SharePoint 2010 Server Cmdlets WSP
- SharePoint 2010 Foundation Cmdlets WSP
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!
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:

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.
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.
4th Grade Math and PowerShell
My 10 year old daughter came home from school today with a simple math problem that she had to solve and she asked if there was an easier way to solve the problem. Basically the teacher explained that the number six was a perfect number because the sum of it's factors (excluding itself) is equal to the number and she wanted to know what the next perfect number was. The kids basically had to go through the numbers and manually add up the factors until they found a match. So naturally, the programmer in me said, sure we can write a quick algorithm to find the next few perfect numbers.
After writing the code, using PowerShell of course, I realized that this little math algorithm demonstrated a few key PowerShell concepts that would be good for anyone starting out with PowerShell to know. Here's the code I wrote for her along with the output:
So in this really simple math example you can see how to use a basic for loop construct, dynamic arrays, dynamic typing, static method calls, and variable replacement within strings.
As you can see, for loops in PowerShell are identical to those in C# so there's nothing new there (unless you're new to C# that is). Dynamic arrays on the other hand are kind of cool - you can declare an empty array by simply using @(). Don't confuse this with declaring empty hash tables which use curly braces instead of parenthesis, @{}. Adding elements to these arrays is as easy as using the += operator: $factors += $i.
You can see the dynamic typing where I'm dividing $i by $j - if it divides evenly then the type returned would be an integer, otherwise it would be a float. So a really easy way to check if it divided evenly is to see if the returned type is equal to [int] (we can work with a type by wrapping a type name in brackets).
Calling static methods is a little different than what you may be used to in C#. As mentioned above types are defined by wrapping the type name in brackets - if we want to call a static method (or access a static property) of a type then we simply separate the method or property name and the type name with double colons. So in this example I'm finding the square root of the number by using the static Sqrt method on the System.Math class: [Math]::Sqrt($i).
The last little bit, dynamic variable replacement, just demonstrates how we can use $() to force the contents of the parentheses to be evaluated before they are used within the string for the Write-Host command. In this case I wanted to show the number of factors but if I did not wrap the $factors.Length bit in parentheses my output would look like this: 6 (1 2 3.Length factors)= 1 2 3.
So, as you can see, PowerShell is a really great tool and can be used for things other than SharePoint, even helping your 4th grader with her math homework
Creating a SharePoint 2010 Enterprise Search Service Application using PowerShell
The information in this post is specific to SharePoint 2010 Beta 2 and may need adjusting for the RTM version.
In an effort to continue with my previous posts where I demonstrated how to build a basic farm and it's site structure using XML configuration files and PowerShell for SharePoint 2010 I would like to now share how to create a search service application. An automated install of the service applications is, without a doubt, the most difficult PowerShell task you'll undertake when scripting your SharePoint 2010 install, specifically the search application is the most difficult which is why I've chosen to explain it first as I expect it to be one of the most needed and one of the least understood. Note that I'm not planning on giving any depth to what the various components are, there's plenty of other resources that will explain what the admin component is, for example.
To start off let's look at the XML file that will drive our setup. Like my previous examples I have a fairly simplistic XML structure that drives all my configurations. This structure allows me to create as many service application instances as needed, each with their own configurations:
<Services>
<EnterpriseSearchService ContactEmail="no-reply@sp2010.com"
ConnectionTimeout="60"
AcknowledgementTimeout="60"
ProxyType="Default"
IgnoreSSLWarnings="false"
InternetIdentity="Mozilla/4.0 (compatible; MSIE 4.01; Windows NT; MS Search 6.0 Robot)"
IndexLocation="c:\sharepoint\indexes"
PerformanceLevel="PartlyReduced"
Account="sp2010\spsearch">
<EnterpriseSearchServiceApplications>
<EnterpriseSearchServiceApplication Name="Enterprise Search Service Application"
DatabaseServer="spsql1"
DatabaseName="SharePoint_Search"
FailoverDatabaseServer=""
Partitioned="false"
Partitions="1"
SearchServiceApplicationType="Regular">
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearch" />
<CrawlServers>
<Server Name="sp2010b2" />
</CrawlServers>
<QueryServers>
<Server Name="sp2010b2" />
</QueryServers>
<SearchQueryAndSiteSettingsServers>
<Server Name="sp2010b2" />
</SearchQueryAndSiteSettingsServers>
<AdminComponent>
<Server Name="sp2010b2" />
<ApplicationPool Name="SharePoint Enterprise Search Application Pool" Account="sp2010\spsearchsvc" />
</AdminComponent>
<Proxy Name="Enterprise Search Service Application Proxy" Partitioned="false">
<ProxyGroup Name="Default" />
</Proxy>
</EnterpriseSearchServiceApplication>
</EnterpriseSearchServiceApplications>
</EnterpriseSearchService>
</Services>
Examining the structure above you can see that I chose to put the <EnterpriseSearchService /> element under a <Services /> element - this will allow me to have all my service configurations in one file rather than a separate file for each service (note that there can be only one <EnterpriseSearchService /> element). Under the <EnterpriseSearchService /> element I have a container element for the applications - there should be only one <EnterpriseSearchServiceApplications /> elements but you can have as many <EnterpriseSearchServiceApplication /> elements under it. The application element is where all the meat of the configurations are. Within this element you define the application pool to use, the crawl and query servers to use, and the server for the administrative component, and finally the proxy definition and it's proxy group memberships. The <CrawlServers /> and <QueryServers /> elements can have as many <Server /> child elements as needed but the <AdminComponent /> element can have only one <Server /> child element. And finally the <Proxy /> element can have as many <ProxyGroup /> child elements as desired.
Okay, so that's the easy part - hopefully you can begin to see the power and flexibility of this simple XML file. No for the scripts - first we need to look at a couple of helper functions, one to get/create our application pools and another for the proxy group memberships. Let's take a look at the application pool function which I called Get-ApplicationPool:
function Get-ApplicationPool([System.Xml.XmlElement]$appPoolConfig) {
#Try and get the application pool if it already exists
$pool = Get-SPIisWebServiceApplicationPool -Identity $appPoolConfig.Name -ErrorVariable err -ErrorAction SilentlyContinue
if ($err) {
#The application pool does not exist so create.
Write-Host "Getting $($appPoolConfig.Account) account for application pool..."
$managedAccount = (Get-SPManagedAccount -Identity $appPoolConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
if ($err) {
$accountCred = Get-Credential $appPoolConfig.Account
$managedAccount = New-SPManagedAccount -Credential $accountCred
}
Write-Host "Creating application pool $($appPoolConfig.Name)..."
$pool = New-SPIisWebServiceApplicationPool -Name $appPoolConfig.Name -Account $managedAccount
}
return $pool
}
In this function I'm attempting to get the application pool if it already exists and if it doesn't then I proceed to attempt to get the managed account that will be associated with the application pool. If the managed account doesn't exist then I prompt for credentials and then create the managed account which I then use to create the application pool which gets returned to the calling function.
The next function, which I've named Set-ProxyGroupMembership associates my service application proxy with one or more proxy groups:
function Set-ProxyGroupsMembership([System.Xml.XmlElement[]]$groups, [Microsoft.SharePoint.Administration.SPServiceApplicationProxy[]]$InputObject)
{
begin {}
process {
$proxy = $_
#Clear any existing proxy group assignments
Get-SPServiceApplicationProxyGroup | where {$_.Proxies -contains $proxy} | ForEach-Object {
$proxyGroupName = $_.Name
if ([string]::IsNullOrEmpty($proxyGroupName)) { $proxyGroupName = "Default" }
$group = $null
[bool]$matchFound = $false
foreach ($g in $groups) {
$group = $g.Name
if ($group -eq $proxyGroupName) {
$matchFound = $true
break
}
}
if (!$matchFound) {
Write-Host "Removing ""$($proxy.DisplayName)"" from ""$proxyGroupName"""
$_ | Remove-SPServiceApplicationProxyGroupMember -Member $proxy -Confirm:$false -ErrorAction SilentlyContinue
}
}
foreach ($g in $groups) {
$group = $g.Name
$pg = $null
if ($group -eq "Default" -or [string]::IsNullOrEmpty($group)) {
$pg = [Microsoft.SharePoint.Administration.SPServiceApplicationProxyGroup]::Default
} else {
$pg = Get-SPServiceApplicationProxyGroup $group -ErrorAction SilentlyContinue -ErrorVariable err
if ($pg -eq $null) {
$pg = New-SPServiceApplicationProxyGroup -Name $name
}
}
$pg = $pg | where {$_.Proxies -notcontains $proxy}
if ($pg -ne $null) {
Write-Host "Adding ""$($proxy.DisplayName)"" to ""$($pg.DisplayName)"""
$pg | Add-SPServiceApplicationProxyGroupMember -Member $proxy
}
}
}
end {}
}
This function is probably a bit more complicated than it needs to be but I'm going to use it with every service application script so I'll explain it briefly here and just reference this post in my future posts. For this function I wanted to be able to pass the proxy object that I created into the function using the pipeline rather than a parameter (it just flowed better that way and allowed me to pass more than one proxy if I desired without having to write a loop within the function). The first thing I'm doing in this function is clearing out any existing proxy group assignments that may have been set automatically but are not what I want per the XML file. Once I've cleared undesired assignments then I add any missing assignments. Some service applications will automatically add the proxy to the default proxy group which may not be what you want.
Now that we have our two helper functions out of the way we can start looking at the core function. I'll talk about it in chunks and then at the end of this post provide the complete function.
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.EnterpriseSearchService
Line 1 loads the file into a System.Xml.XmlDocument typed variable and then I grab the <EnterpriseSearchService /> element and set that to the $svcConfig variable. Next I need to get the search service itself and set that to a variable which I'll use throughout the function as well. I pass the -Local switch in to get the service instance on the current machien. If I'm unable to find a service instance then something is wrong and I throw an error:
1: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
2: if ($searchSvc -eq $null) {
3: throw "Unable to retrieve search service."
4: }
Next I need to get the managed account that will be used for the search service. I first try to retrieve the account in case it already exists and if it doesn't exist then I create after asking the user for the password:
1: Write-Host "Getting $($svcConfig.Account) account for search service..."
2: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
3: if ($err) {
4: $searchSvcAccount = Get-Credential $svcConfig.Account
5: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
6: }
Now that we have a managed account and service instance we can set the core properties for the search service. I end up doing this on every machine but it only needs to be done once - just easier to set it every time rather than try and figure out if it's been set yet and doing so has no negative repercussions:
1: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
2: -ServiceAccount $searchSvcManagedAccount.Username `
3: -ServicePassword $searchSvcManagedAccount.SecurePassword `
4: -ContactEmail $svcConfig.ContactEmail `
5: -ConnectionTimeout $svcConfig.ConnectionTimeout `
6: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
7: -ProxyType $svcConfig.ProxyType `
8: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
9: -InternetIdentity $svcConfig.InternetIdentity `
10: -PerformanceLevel $svcConfig.PerformanceLevel
11:
12: Write-Host "Setting default index location on search service..."
13: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
The core service settings are in place, now it's time to create all the service applications. In the example XML we have just one but we could have more so I use the ForEach-Object cmdlet to loop through all the definitions:
1: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
The first thing we need to do to create our app is to create the application pool for the service application itself and the administration component:
1: $appConfig = $_
2:
3: #Try and get the application pool if it already exists
4: $pool = Get-ApplicationPool $appConfig.ApplicationPool
5: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
Before creating the application pools I store the current XML element in the $appConfig node for easier reference and to avoid conflicts with sub-loops. I then call the helper function I showed earlier to create the two application pools which I'll use later. Next I check to see if the service application has already been created (line 1 below) by calling Get-SPEnterpriseSearchServiceApplication and if it does not exist then I create a new one. This helps when you have to run the script again due to possible errors that may occur later in the script (I've often seen update conflict errors occur randomly, running the script again is usually all that's necessary):
1: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
2: if ($searchApp -eq $null) {
3: Write-Host "Creating enterprise search service application..."
4: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
5: -DatabaseServer $appConfig.DatabaseServer `
6: -DatabaseName $appConfig.DatabaseName `
7: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
8: -ApplicationPool $pool `
9: -AdminApplicationPool $adminPool `
10: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
11: -SearchApplicationType $appConfig.SearchServiceApplicationType
12: } else {
13: Write-Host "Enterprise search service application already exists, skipping creation."
14: }
Now that the service application exists we can go ahead and create the proxy and set the proxy group memberships:
1: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
2: if ($proxy -eq $null) {
3: Write-Host "Creating enterprise search service application proxy..."
4: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
5: } else {
6: Write-Host "Enterprise search service application proxy already exists, skipping creation."
7: }
8: if ($proxy.Status -ne "Online") {
9: $proxy.Status = "Online"
10: $proxy.Update()
11: }
12: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
Like with the service application I first try and get the proxy in case it has already been created and if I don't find it then I create it. Once I have a reference to the proxy object I check to see if it's online and if not then I set it online and call Update() to commit the change. And finally I call the Set-ProxyGroupsMembership function that I previously defined.
The intent of the script is to allow it to be run on multiple servers to support a multi-server scripted deployment. That's where this next bit comes in:
1: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
2: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
3: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
4: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
For both the crawl servers, query servers, and admin component I get the name of the current computer ($env:computername) and then check to see if an <Server /> element has been declared with a matching name for the specific component. The variables declared are then used throughout the rest of the script.
Before I can create the crawl or query component I need start search service instance that we previously acquired:
1: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
2: $searchSvc | Start-SPEnterpriseSearchServiceInstance
3: }
If the service isn't already online and if we're on an appropriate server then I start the service by passing the service instance to the Start-SPEnterpriseSearchServiceInstance cmdlet. Next I need to set the administration component:
1: if ($installAdminCmpnt) {
2: Write-Host "Setting administration component..."
3: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
4: }
The trick with this bit is that you have to set the administration component before you can set the query or crawl components so the first time you run this script it must be on the sever that is to run the administration component - short of having the user run the script multiple times on the same server and adding appropriate code to handle that I've not come up with any way around this - frankly, it sucks, big time - so be careful with this one!
Okay, we're about halfway through, still with me?
Now it's time to create the crawl topology:
1: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($crawlTopology -eq $null) {
3: Write-Host "Creating new crawl topology..."
4: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
5: } else {
6: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
7: }
8:
9: if ($installCrawlSvc) {
10: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
11: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
12: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
13: Write-Host "Creating new crawl component..."
14: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
15: } else {
16: Write-Host "Crawl component already exist, skipping crawl component creation."
17: }
18: }
On line 1 I'm getting all existing crawl topologies for the service application (Get-SPEnterpriseSearchCrawlTopology) and filtering on whether or not the crawl topology has components and is active or not. I do this because when the search application is created it automatically creates a crawl topology for us but that topology is not configured correctly (there are no crawl components) but once the topology has been made active it doesn't let us change it in order to add crawl components. When I create our new topology it will be inactive so I will use this fact when I run the script on the next server. Once I have the crawl topology I can then add the crawl components using the New-SPEnterpriseSearchCrawlComponent cmdlet (note that you have to pass in the crawl store ID so I have to get that ID as shown in line 12).
After we create crawl topology and components we do essentially the exact same thing for the query topology and components:
1: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
2: if ($queryTopology -eq $null) {
3: Write-Host "Creating new query topology..."
4: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
5: } else {
6: Write-Host "A query topology with query components already exists, skipping query topology creation."
7: }
8: if ($installQuerySvc) {
9: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
10: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
11: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
12: Write-Host "Creating new query component..."
13: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
14: Write-Host "Setting index partition and property store database..."
15: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
16: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
17: } else {
18: Write-Host "Query component already exist, skipping query component creation."
19: }
20: }
Great! We have our admin component created, our crawl topology and components created, and our query topology and components created. Now we just need to make things active. There's nothing more to do with the admin component so we'll first start the "Search Query and Site Settings Service" and then continue with the crawl topology:
1: if ($installSyncSvc) {
2: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
3: }
So starting the query and site settings service was easy, now lets move on to the hard stuff:
1: #Don't activate until we've added all components
2: $allCrawlServersDone = $true
3: $appConfig.CrawlServers.Server | ForEach-Object {
4: $server = $_.Name
5: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
6: if ($top -eq $null) { $allCrawlServersDone = $false }
7: }
8:
9: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
10: Write-Host "Setting new crawl topology to active..."
11: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
14: while ($true) {
15: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
16: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
17: if ($ct.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original crawl topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
25: }
The first thing I do is set a variable to indicate whether I've gotten all designated crawl servers configured - we don't want to set the crawl topology active until all the servers have been configured because once we make it active we can't change it (this is critical if you are planning on doing a phased server roll-out - you will need to rebuild your topology if you need to add additional crawl or query components). On line 11 I set the topology as active using the Set-SPEnterpriseSearchCrawlTopology cmdlet. Problem is not quite that simple - you see, this cmdlet runs asynchronously, meaning that it returns immediately and does not wait until the service is made active - this is critical because we can't proceed to the query piece until the crawl topology is active so all I'm doing in lines 14 through 22 is checking the status and if it's not "Ready" then I sleep for 2 seconds and try again.
Only one more thing - now that the crawl topology is active we do, once again, the same thing for the query topology:
1: $allQueryServersDone = $true
2: $appConfig.QueryServers.Server | ForEach-Object {
3: $server = $_.Name
4: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
5: if ($top -eq $null) { $allQueryServersDone = $false }
6: }
7:
8: #Make sure we have a crawl component added and started before trying to enable the query component
9: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
10: Write-Host "Setting query topology as active..."
11: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
12:
13: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
14: while ($true) {
15: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
16: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
17: if ($qt.State -eq "Active" -and $state -eq $null) {
18: break
19: }
20: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
21: Start-Sleep 2
22: }
23: # Need to delete the original query topology that was created by default
24: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
25: }
This code is identical to that of the crawl topology but uses the query specific cmdlets.
And, finally, after about 236 lines of code, we're done! Makes me miss the days of MOSS 2007 where I could start search with one line of STSADM (maybe I need to create a Start-OSearch cmdlet
). So, putting it all together, here's the complete function:
1: function Start-EnterpriseSearch([string]$settingsFile = "Configurations.xml") {
2: [xml]$config = Get-Content $settingsFile
3: $svcConfig = $config.Services.EnterpriseSearchService
4:
5: $searchSvc = Get-SPEnterpriseSearchServiceInstance -Local
6: if ($searchSvc -eq $null) {
7: throw "Unable to retrieve search service."
8: }
9:
10: Write-Host "Getting $($svcConfig.Account) account for search service..."
11: $searchSvcManagedAccount = (Get-SPManagedAccount -Identity $svcConfig.Account -ErrorVariable err -ErrorAction SilentlyContinue)
12: if ($err) {
13: $searchSvcAccount = Get-Credential $svcConfig.Account
14: $searchSvcManagedAccount = New-SPManagedAccount -Credential $searchSvcAccount
15: }
16:
17: Get-SPEnterpriseSearchService | Set-SPEnterpriseSearchService `
18: -ServiceAccount $searchSvcManagedAccount.Username `
19: -ServicePassword $searchSvcManagedAccount.SecurePassword `
20: -ContactEmail $svcConfig.ContactEmail `
21: -ConnectionTimeout $svcConfig.ConnectionTimeout `
22: -AcknowledgementTimeout $svcConfig.AcknowledgementTimeout `
23: -ProxyType $svcConfig.ProxyType `
24: -IgnoreSSLWarnings $svcConfig.IgnoreSSLWarnings `
25: -InternetIdentity $svcConfig.InternetIdentity `
26: -PerformanceLevel $svcConfig.PerformanceLevel
27:
28: Write-Host "Setting default index location on search service..."
29: $searchSvc | Set-SPEnterpriseSearchServiceInstance -DefaultIndexLocation $svcConfig.IndexLocation -ErrorAction SilentlyContinue -ErrorVariable err
30:
31: $svcConfig.EnterpriseSearchServiceApplications.EnterpriseSearchServiceApplication | ForEach-Object {
32: $appConfig = $_
33:
34: #Try and get the application pool if it already exists
35: $pool = Get-ApplicationPool $appConfig.ApplicationPool
36: $adminPool = Get-ApplicationPool $appConfig.AdminComponent.ApplicationPool
37:
38: $searchApp = Get-SPEnterpriseSearchServiceApplication -Identity $appConfig.Name -ErrorAction SilentlyContinue
39: if ($searchApp -eq $null) {
40: Write-Host "Creating enterprise search service application..."
41: $searchApp = New-SPEnterpriseSearchServiceApplication -Name $appConfig.Name `
42: -DatabaseServer $appConfig.DatabaseServer `
43: -DatabaseName $appConfig.DatabaseName `
44: -FailoverDatabaseServer $appConfig.FailoverDatabaseServer `
45: -ApplicationPool $pool `
46: -AdminApplicationPool $adminPool `
47: -Partitioned:([bool]::Parse($appConfig.Partitioned)) `
48: -SearchApplicationType $appConfig.SearchServiceApplicationType
49: } else {
50: Write-Host "Enterprise search service application already exists, skipping creation."
51: }
52:
53: $installCrawlSvc = (($appConfig.CrawlServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
54: $installQuerySvc = (($appConfig.QueryServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
55: $installAdminCmpnt = (($appConfig.AdminComponent.Server | where {$_.Name -eq $env:computername}) -ne $null)
56: $installSyncSvc = (($appConfig.SearchQueryAndSiteSettingsServers.Server | where {$_.Name -eq $env:computername}) -ne $null)
57:
58: if ($searchSvc.Status -ne "Online" -and ($installCrawlSvc -or $installQuerySvc)) {
59: $searchSvc | Start-SPEnterpriseSearchServiceInstance
60: }
61:
62: if ($installAdminCmpnt) {
63: Write-Host "Setting administration component..."
64: Set-SPEnterpriseSearchAdministrationComponent -SearchApplication $searchApp -SearchServiceInstance $searchSvc
65: }
66:
67: $crawlTopology = Get-SPEnterpriseSearchCrawlTopology -SearchApplication $searchApp | where {$_.CrawlComponents.Count -gt 0 -or $_.State -eq "Inactive"}
68: if ($crawlTopology -eq $null) {
69: Write-Host "Creating new crawl topology..."
70: $crawlTopology = $searchApp | New-SPEnterpriseSearchCrawlTopology
71: } else {
72: Write-Host "A crawl topology with crawl components already exists, skipping crawl topology creation."
73: }
74:
75: if ($installCrawlSvc) {
76: $crawlComponent = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $env:ComputerName}
77: if ($crawlTopology.CrawlComponents.Count -eq 0 -and $crawlComponent -eq $null) {
78: $crawlStore = $searchApp.CrawlStores | where {$_.Name -eq "$($appConfig.DatabaseName)_CrawlStore"}
79: Write-Host "Creating new crawl component..."
80: $crawlComponent = New-SPEnterpriseSearchCrawlComponent -SearchServiceInstance $searchSvc -SearchApplication $searchApp -CrawlTopology $crawlTopology -CrawlDatabase $crawlStore.Id.ToString() -IndexLocation $appConfig.IndexLocation
81: } else {
82: Write-Host "Crawl component already exist, skipping crawl component creation."
83: }
84: }
85:
86: $queryTopology = Get-SPEnterpriseSearchQueryTopology -SearchApplication $searchApp | where {$_.QueryComponents.Count -gt 0 -or $_.State -eq "Inactive"}
87: if ($queryTopology -eq $null) {
88: Write-Host "Creating new query topology..."
89: $queryTopology = $searchApp | New-SPEnterpriseSearchQueryTopology -Partitions $appConfig.Partitions
90: } else {
91: Write-Host "A query topology with query components already exists, skipping query topology creation."
92: }
93: if ($installQuerySvc) {
94: $queryComponent = $queryTopology.QueryComponents | where {$_.ServerName -eq $env:ComputerName}
95: if ($queryTopology.QueryComponents.Count -eq 0 -and $queryComponent -eq $null) {
96: $partition = ($queryTopology | Get-SPEnterpriseSearchIndexPartition)
97: Write-Host "Creating new query component..."
98: $queryComponent = New-SPEnterpriseSearchQueryComponent -IndexPartition $partition -QueryTopology $queryTopology -SearchServiceInstance $searchSvc
99: Write-Host "Setting index partition and property store database..."
100: $propertyStore = $searchApp.PropertyStores | where {$_.Name -eq "$($appConfig.DatabaseName)_PropertyStore"}
101: $partition | Set-SPEnterpriseSearchIndexPartition -PropertyDatabase $propertyStore.Id.ToString()
102: } else {
103: Write-Host "Query component already exist, skipping query component creation."
104: }
105: }
106:
107: if ($installSyncSvc) {
108: Start-SPServiceInstance -Identity "Search Query and Site Settings Service"
109: }
110:
111: #Don't activate until we've added all components
112: $allCrawlServersDone = $true
113: $appConfig.CrawlServers.Server | ForEach-Object {
114: $server = $_.Name
115: $top = $crawlTopology.CrawlComponents | where {$_.ServerName -eq $server}
116: if ($top -eq $null) { $allCrawlServersDone = $false }
117: }
118:
119: if ($allCrawlServersDone -and $crawlTopology.State -ne "Active") {
120: Write-Host "Setting new crawl topology to active..."
121: $crawlTopology | Set-SPEnterpriseSearchCrawlTopology -Active -Confirm:$false
122:
123: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
124: while ($true) {
125: $ct = Get-SPEnterpriseSearchCrawlTopology -Identity $crawlTopology -SearchApplication $searchApp
126: $state = $ct.CrawlComponents | where {$_.State -ne "Ready"}
127: if ($ct.State -eq "Active" -and $state -eq $null) {
128: break
129: }
130: Write-Host -ForegroundColor Yellow "Waiting on Crawl Components to provision..."
131: Start-Sleep 2
132: }
133: # Need to delete the original crawl topology that was created by default
134: $searchApp | Get-SPEnterpriseSearchCrawlTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchCrawlTopology -Confirm:$false
135: }
136:
137: $allQueryServersDone = $true
138: $appConfig.QueryServers.Server | ForEach-Object {
139: $server = $_.Name
140: $top = $queryTopology.QueryComponents | where {$_.ServerName -eq $server}
141: if ($top -eq $null) { $allQueryServersDone = $false }
142: }
143:
144: #Make sure we have a crawl component added and started before trying to enable the query component
145: if ($allCrawlServersDone -and $allQueryServersDone -and $queryTopology.State -ne "Active") {
146: Write-Host "Setting query topology as active..."
147: $queryTopology | Set-SPEnterpriseSearchQueryTopology -Active -Confirm:$false -ErrorAction SilentlyContinue -ErrorVariable err
148:
149: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
150: while ($true) {
151: $qt = Get-SPEnterpriseSearchQueryTopology -Identity $queryTopology -SearchApplication $searchApp
152: $state = $qt.QueryComponents | where {$_.State -ne "Ready"}
153: if ($qt.State -eq "Active" -and $state -eq $null) {
154: break
155: }
156: Write-Host -ForegroundColor Yellow "Waiting on Query Components to provision..."
157: Start-Sleep 2
158: }
159: # Need to delete the original query topology that was created by default
160: $searchApp | Get-SPEnterpriseSearchQueryTopology | where {$_.State -eq "Inactive"} | Remove-SPEnterpriseSearchQueryTopology -Confirm:$false
161: }
162:
163: $proxy = Get-SPEnterpriseSearchServiceApplicationProxy -Identity $appConfig.Proxy.Name -ErrorAction SilentlyContinue
164: if ($proxy -eq $null) {
165: Write-Host "Creating enterprise search service application proxy..."
166: $proxy = New-SPEnterpriseSearchServiceApplicationProxy -Name $appConfig.Proxy.Name -SearchApplication $searchApp -Partitioned:([bool]::Parse($appConfig.Proxy.Partitioned))
167: } else {
168: Write-Host "Enterprise search service application proxy already exists, skipping creation."
169: }
170: if ($proxy.Status -ne "Online") {
171: $proxy.Status = "Online"
172: $proxy.Update()
173: }
174: $proxy | Set-ProxyGroupsMembership $appConfig.Proxy.ProxyGroup
175:
176: }
177: }
178:
This script took me an incredible amount of time to figure out and I really hope others are able to benefit from it. If you find areas of improvement or anything that requires correction please, please, please post a comment so that I and others can benefit from your experiences with it.
Also, this script is a derivative of a slightly more complex one that I use for all my stuff and though that more complex script has gone through many rounds of testing this one has not - mainly I've not had a chance to test in a multi-server environment and have only had time to do a single server deploy (though the changes related to the servers were very small and, if they were to fail, would likely have failed on the single server). Mainly try to remember that the product is still in beta so you should expect that things may either change between now and RTM or things may just not work from one environment to the next.
Good luck and happy scripting!