Deploy an Office Online Server Environment using PowerShell DSC

In this article we will cover the process of automating the configuration of an Office Online Server 2016 (OOS) using PowerShell Desired State Configuration. The end result will be a fully working OOS environment that has the French Language Pack installed on. Language pack support is something new I recently added to the OfficeOnlineServerDSC module and which will be available starting with version 1.1.0.0 scheduled to be released on December 20th of 2017.

Prerequisites

In my case, I will be starting with a brand new Windows Server 2016 virtual machine on which I downloaded the installation binaries for OOS, as well as the French language pack. The installation media will be stored in a folder right at the root of my c:\ drive at C:\OOSInstall\:

The Language Packs executables need to be extracted in order for the new OfficeOnlineServerInstallLanguagePack resource to be able to do its job. In my case, I downloaded the French Language Pack from MSDN and stored it into C:\OOSLP\:

Office Online Server Language Pack Binaries

Office Online Server Language Pack Binaries

In order to extract the content of the Language Packs, we need to call the .exe file with /extract:. In my case I will be calling the following:

C:\OOSLP\fr_office_online_server_language_pack_march_2017_x64_10267265.exe /extract:C:\OOSLP

this will result in the following set of files to be extracted in our folder:

Extracted Office Online Server Language Pack

Extracted Office Online Server Language Pack

IMPORTANT!!!At the time of publishing this article, OfficeOnlineServerDSC 1.1.0.0 is not yet available on the PowerShell Gallery, therefore you will need to manually acquire it from https://www.GitHub.com/PowerShell/OfficeOnlineServerDSC.

DSC Script

One we have this in place, we are ready to go ahead and execute our DSC configuration script and let the magic happen:

Install-PackageProvider -Name NuGet -Force -Confirm:$false
Install-Module OfficeOnlineServerDSC -RequiredVersion 1.1.0.0 -Force -Confirm:$false -EA SilentlyContinue

Configuration OOS
{
    Import-DscResource -ModuleName OfficeOnlineServerDSC -ModuleVersion 1.1.0.0

    Node localhost
    {
        WindowsFeature WebServer
        {
            Name = "Web-Server"
            Ensure = "Present"
            IncludeAllSubFeature = $true
        }

        OfficeOnlineServerInstall OOSInstall
        {
            Ensure = "Present"
            Path = "C:\OOSInstall\setup.exe"
            DependsOn = "[WindowsFeature]WebServer"
        }

        OfficeOnlineServerInstallLanguagePack FrenchLP
        {
            Ensure = "Present"
            BinaryDir = "C:\OOSLP\"
            Language = "fr-fr"
            DependsOn = "[OfficeOnlineServerInstall]OOSInstall"
        }

        OfficeOnlineServerFarm FarmConfig
        {
            InternalUrl = "http://localhost"
            AllowHttp = $true
        }
    }
}

OOS

Start-DSCConfiguration OOS -Wait -Force -Verbose

Patching a Multi-Server Farm using PowerShell DSC and Cross-Node Dependency

While I already have an article that covers the details of how you can use PowerShell Desired State Configuration (DSC) to patch a SharePoint Farm, I wanted to write a second article that covers the process of patching a multi-server farm, while respecting a sequential upgrade logic. Starting with PowerShell 5.0 we can now specify cross-node dependencies, which mean a node can wait for a specific resource to be in the desired state on another server before proceeding with the configuration of a given resource on the local node. By combining this new feature with the SharePointDSC module, we can fully automate our sequential upgrade sequence.

Scenario

The scenario is the following: the client has a 4 servers SharePoint 2013 Farm and wishes to upgrade it to the November 2017 Cumulative Update (CU). The client’s maintenance window is on Saturdays and Sundays, between 2AM and 8AM Eastern time. The SharePoint farm had 2 Web Front-End servers (SPWFE1 & SPWFE2), and 2 Application servers (SPAPP1 & SPAPP2). The upgrade sequence should be as follow:

SharePoint Upgrade Sequence

SharePoint Upgrade Sequence

If we were to put this into words, we should be able to install the binaries on all servers in parallel. The installation process may take longer to complete on some servers compared to the others. Once that is completed on all servers, then we should be running PSConfig on one server in the farm, and whenever the process finishes on that first server, then we can go an run it on all other servers in parallel. The orange dotted lines on the diagram represent “logic gates”. The first one, indicates that you should not be running PSConfig on the first server until the binaries installation process has completed on every server in the farm. The second one, specifies that we need to wait for the PSConfig process to finish on the first server before attempting to run it on the another server. These dotted lines will be represented by the Cross-Node Dependency resources WaitForAll in our DSC script.

Script

We will start off by defining our Node structure in our configuration script. Since SPWFE2, SPAPP1 & SPApp2 all follow the same “logic”, we will put them all 3 in the same basket. The other server, SPWFE1 has unique logic and therefore needs to have its own Node definition. The skeleton for this will look like the following:

Configuration SPFarmUpdate
{
    $CredsSPFarm = Get-Credential -UserName "contoso\sp_farm" -Message "Farm Account"
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 1.9.0.0
    Node "SPWFE1"
    {
        # First Node
    }

    Node @("SPWFE2", "SPAPP1", "SPAPP2")
    {
        # Other Nodes
    }
}

Since we can go ahead and install the binaries on all 4 servers at the same time, we can put the same logic block in both Node sections. The DSC resource used to install binaries update is SPProductUpdate. Because my client’s maintenance window is on Saturdays and Sundays between 2 and 8 AM, I will make sure to specify that window in my resource block.I will also specify that I wish to shutdown all services on my server to speed up the installation process. This will cause and outage of the SharePoint server. Just to keep things clear however, I will give them different names:

Configuration SPFarmUpdate
{
    $CredsSPFarm = Get-Credential -UserName "contoso\sp_farm" -Message "Farm Account"
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 1.9.0.0
    Node "SPWFE1"
    {
        SPProductUpdate November2017CUFirstNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
    }

    Node @("SPWFE2", "SPAPP1", "SPAPP2")
    {
        SPProductUpdate November2017CUSecondaryNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
    }
}

Once the binaries have been installed on the servers, we need to use the SPConfigWizard DSC resource to execute PSConfig and actually apply the update to the servers and databases. Once again, I will be specifying my maintenance window to ensure DSC doesn’t trigger an upgrade during business hours, and I will give the two DSC resource block different names to better illustrate the components involved. Our script now looks like the following at this stage:

Configuration SPFarmUpdate
{
    $CredsSPFarm = Get-Credential -UserName "contoso\sp_farm" -Message "Farm Account"
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 1.9.0.0
    Node "SPWFE1"
    {
        SPProductUpdate November2017CUFirstNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }

        SPConfigWizard PSConfigFirstNode
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
    }

    Node @("SPWFE2", "SPAPP1", "SPAPP2")
    {
        SPProductUpdate November2017CUSecondaryNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }

        SPConfigWizard PSConfigSecondaryNode
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }
    }
}

Since by default, everything in DSC is executed sequentially at the node level, there is a risk that whatever server finishes installing its binaries automatically calls PSConfig even if the other servers are still in the middle of installing their binaries, or that multiple servers try calling PSConfig at the same time. To prevent this from happening, we will use the WaitForAll DSC resource which allows us to tell a node to wait for something else to complete on another node. In our case, we will tell the First server to wait for the binaries to be done installing on all servers in the farm and we will also tell the Secondary servers not to start their PSConfig process until it is first completed on the first node. In order for us to tell a DSC resource not to execute until something else on a different server has completed, we will use the DependsOn clause, and will “point” it to the associated WaitForAll instance. I will also tell the cross-node dependency to retry and check every minutes, for a maximum of 30 minutes to see if the dependency on the other server has completed. By doing so, we now have a complete script that looks like the following:

Configuration SPFarmUpdate
{
    $CredsSPFarm = Get-Credential -UserName "contoso\sp_farm" -Message "Farm Account"
    Import-DscResource -ModuleName SharePointDSC -ModuleVersion 1.9.0.0
    Node "SPWFE1"
    {
        SPProductUpdate November2017CUFirstNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }

        WaitForAll ServersToHaveBinariesInstalled
        {
            ResourceName = "[SPProductUpdate]November2017CUSecondaryNode"
            NodeName = @("SPWFE2", "SPAPP1", "SPAPP2")
            RetryIntervalSec = 60
            RetryCount = 30
            DependsOn = "[SPProductUpdate]November2017CUFirstNode"
        }

        SPConfigWizard PSConfigFirstNode
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn = "[WaitForAll]ServersToHaveBinariesInstalled"
        }
    }

    Node @("SPWFE2", "SPAPP1", "SPAPP2")
    {
        SPProductUpdate November2017CUSecondaryNode
        {
            SetupFile = "C:\Patch\November2017CU.exe"
            ShutdownServices = $true
            BinaryInstallDays = @("sat", "sun")
            BinaryInstallTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
        }

        WaitForAll PSConfigToBeFinishedOnFirstServer
        {
            ResourceName = "[SPConfigWizard]PSConfig"
            NodeName = @("SPWFE1")
            RetryIntervalSec = 60
            RetryCount = 30
            DependsOn = "[SPProductUpdate]November2017CUSecondaryNode"
        }

        SPConfigWizard PSConfigSecondaryNode
        {
            Ensure = "Present"
            DatabaseUpgradeDays = @("sat", "sun")
            DatabaseUpgradeTime = "2:00am to 8:00am"
            PsDscRunAsCredential = $CredsSPFarm
            DependsOn = "[WaitForAll]PSConfigToBeFinishedOnFirstServer"
        }
    }
}
$ConfigData = @{
    AllNodes = @(
        @{
            NodeName = "SPWFE1"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        },
        @{
            NodeName = "SPWFE2"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        },
        @{
            NodeName = "SPAPP1"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        },
        @{
            NodeName = "SPAPP2"
            PSDscAllowPlainTextPassword = $true;
            PSDscAllowDomainUser = $true;
        }
    )
}

SPFarmUpdate -ConfigurationData $ConfigData

Remove Item from the Search Index in SharePoint using PowerShell

While working at a client site this week, we encountered an issue where they had duplicate entries for the same Document ID, meaning that when they tried to access that document via its DocId Url, they were presented with a search page showing the two items instead. Without going into the details as to what was actually causing this issue to surface, the easy fix for this was to go and remove one of the duplicate from the Search Index. Through this interface, this can be done as follow:

1 – Navigate to the Search Service Application Page in Central Administration.

Search Service Application page in Central Administration

Search Service Application page in Central Administration

2 – Click on Crawl Log in the left navigation.

SharePoint Search Crawl Log

SharePoint Search Crawl Log

3 – In the top navigation, click on URL View.

URL View in Search Crawl Log

URL View in Search Crawl Log

4 – Search for the URL (or pattern) you want to remove.

Search Crawl Log for URL

Search Crawl Log for URL

5 – Find the item in the list and click on it to expand the contextual menu.

Crawl Log Entry Options

Crawl Log Entry Options

6 – From the list of options, select Remove the item from the Index.

The PowerShell Equivalent

In order for my clients to automate this process for the URLs they want to remove from the Search Index, I created a quick cmdlet called Remove-SPEnterpriseSearchURLFromIndex which simply takes in a URL pattern. Upon detecting URL entries in the Crawl Log that match the provided URL, the cmdlet will prompt the user to remove the item from the index or not.

[CmdletBinding]
function Remove-SPEnterpriseSearchURLFromIndex
{
    param( 
        [System.String]
        $Url = "Default"
    )

    $ssas = Get-SPEnterpriseSearchServiceApplication

    foreach($ssa in $ssas)
    {
        $cl = New-Object Microsoft.Office.Server.Search.Administration.CrawlLog $ssa
        $logEntries = $cl.GetCrawledUrls($false,100,$Url,$true,-1,-1,-1,[System.DateTime]::MinValue, [System.DateTime]::MaxValue)

        foreach($logEntry in $logEntries.Rows)
        {
            Write-Host "You are about to remove " -NoNewline
            Write-Host $logEntry.FullUrl -ForegroundColor Green

            do{
                $deletionAnswer = Read-Host "Do you confirm the deletion (y/n)"
            }while($deletionAnswer.ToLower() -ne 'n' -and $deletionAnswer.ToLower() -ne 'y')

            switch($deletionAnswer)
            {
                'y'
                {
                    $catch = $cl.RemoveDocumentFromSearchResults($logEntry.FullUrl)
                    if($catch)
                    {
                        Write-Host "Deleted" -ForegroundColor Yellow
                    }
                    else
                    {
                        Write-Host "Could not delete the item" -ForegroundColor Red
                    }
                }
                'n'
                {
                    break
                }
            }
        }
    }
}

Easily Retrieve IIS ApplicationPool Credentials

This is going to be a very short Blog Post, not to say a brain dump, but if you ever need to retrieve the credentials used in a Specific ApplicationPool in IIS, you can use the following snippet of PowerShell code to do so:

$appPools = Get-WebConfiguration -Filter '/system.applicationHost/applicationPools/add'

foreach($appPool in $appPools)
{
    if($appPool.ProcessModel.identityType -eq "SpecificUser")
    {
        Write-Host $appPool.Name -ForegroundColor Green -NoNewline
        Write-Host " -"$appPool.ProcessModel.UserName"="$appPool.ProcessModel.Password
    }
}

This will output something similar to the following:

Now, I am sure some of you are probably freaking out by now, realizing that people with access to the server can easily retrieve the credentials from IIS app pools that are running as a specific user. Let me assure you that there is nothing magic about the PowerShell code above. When you specify credentials for an IIS application pool, after verifying against Active Directory that the provided credentials are valid, IIS will go and actually encrypt and store those credentials locally. Using the Get-WebConfiguration cmdlet allows you to retrieve and decrypt those.
So yes, the moment a user has access to run the PowerShell cmdlet on the server, he is also able to retrieved stored credentials for users running the app pool.

Patching your SharePoint Farm with PowerShell DSC

Patching a SharePoint 2013/2016 farm with the help of PowerShell Desired State Configuration (DSC) is a common ask I get from customers almost every single time I deliver a DSC engagement. As part of the SharePointDSC module, we offer two main resources to help you automate the patching process for your farm: SPProductUpdate and SPConfigWizard.

  • SPProductUpdate resource is responsible for installing the patch’s bits onto a server in the farm. It is the equivalent of manually running the installer for a Cummulative/Public update onto the given server. It is very important to note that declaring a resource block of this type in your DSC configuration ONLY installs it on the given node. You need to make sure that this resource block gets defined on every server in your farm to make sure all servers have the bits installed on them. This resource allows you to speed up the installation process on the various nodes by automatically shutting down the various Search Services that normally slow down the installation process. In order to shutdown those services during the installation, you need to specify the ShutdownServices parameter to $true
  • SPConfigWizard on the other hand, is the equivalent of running PSConfig on a given server. It is responsible for committing the installed bits into the configuration database to finalize the farm’s upgrade process. Just like the SPProductUpdate resource, this one needs to be defined against every server in the farm.

Patching Process

In this article, I will demo the process of patching a SharePoint 2016 farm, however the process is the same if you wish to patch a SharePoint 2013 farm. To properly demonstrate the patching process, I will be using a SharePoint 2016 RTM farm, and will be patching it to the September 2017 Public Update, which includes the Feature Pack 2 bits.

  1. The first step is to go an download the SharePoint 2016 – September 2017 Public Update from the web. Decide where you wish to save it. My recommendation is to put it on a Shared Network Location that all servers will be able to access. However, you need to understand the implications of running the Update installer from a Network location using DSC, because your installation process may get stuck due to the User Account Control protection. I’ve put together a short article that lists the most common gotchas for when using DSC and solutions to them. In my case, the file will be put under \\DSC-Share\SP16-Sept16PU\sts2016-kb4011127-fullfile-x64-glb.exe
  2. The second step is to add the DSC Resource blocks into your PowerShell configuration script. The recommendation here is for you to put them right after the SharePoint binaries have been installed via SPInstall, and right before your are actually attempting to have the server join the farm via SPFarm. This would also be the recommendation as far as location within the script for where to install the Language Packs. That is if you are using DSC to install your farm from the ground up.

    For this article however, I am going to demonstrate the case where you already have a SharePoint 2016 Farm built and all you are trying to do in apply a Public Update on it via DSC. The following is the complete script I will be using to achieve this:

    Configuration SP2016September2017PU
    {
        Import-DscResource -ModuleName "SharePointDSC" -ModuleVersion "1.9.0.0"
        $CredsspFarm = Get-Credential -Message "Farm Account"
    
        Node $AllNodes.NodeName
        {
            SPProductUpdate Sept2017PU
            {
                SetupFile = "\\DSC-Share\SP16-Sept16PU\sts2016-kb4011127-fullfile-x64-glb.exe"
                ShutdownServices = $true
                Ensure = "Present"
                PsDscRunAscredential = $CredsspFarm
            }
    
            SPConfigWizard PSConfig
            {
                Ensure = "Present"
                PsDscRunAscredential = $CredsspFarm
            }
        }
    }
    
    $ConfigurationData = @{
        AllNodes = @(
            @{
                NodeName = "SPWFE1"
                PSDscAllowPlainTextPassword = $true;
                PSDscAllowDomainUser = $true;
            },
            @{
                NodeName = "SPWFE2"
                PSDscAllowPlainTextPassword = $true;
                PSDscAllowDomainUser = $true;
            },
            @{
                NodeName = "SPAPP1"
                PSDscAllowPlainTextPassword = $true;
                PSDscAllowDomainUser = $true;
            }
        )
     }
    
    SP2016September2017PU -ConfigurationData $ConfigurationData
    
  3. Initiate the Start-DSCConfiguration SP2016September2017PU -Wait -Verbose -Force cmdlet to initiate the configuration of the servers in the farm.

That was easy enough wasn’t it? Now, whenever a new update comes in that you wish to apply to your farm, simply update the SetupFile parameter’s value to the new PU file. DO NOT ever include more than one SPProduct update block for a given server in your DSC configuration. Updates in SharePoint are cumulative, meaning that if your goal is to update a farm to the October 2017 PU, you don’t need to install the September 2017 PU first.