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.

SharePoint DSC 1.9.0.0 is Released

The latest version of the SharePoint DSC module has been released this last Wednesday, October 5th. You can now obtain the latest bits of the module by running:

Install-Module SharePointDSC

If you wish to update the configurations on a given SharePoint farm to run these latest bits, make sure each .MOF file is first recompiled using the latest package and that the new module bits are copied to every server in the farm.

Here are some of the major changes that were introduced in this version:

  • SPServiceIdentity: A new DSC Resource has been introduced in the module to represent the managed account that is assigned to a service instance.
  • SPWebAppSiteUseAndDeletion: Fixed an issue with the scheduling where is was incorrectly setting the delays in weeks instead of in days.
  • SPWebAppGeneralSettings: Fixed an issue where web applications that weren’t assigned a TimeZone were incorrectly returning -1 instead of $null.
  • SPProductUpdate: Fixed an issue for updating a SharePoint 2013 farm were the resource was always complaining that the farm was missing the OSearch16 (SharePoint 2016 version).
  • Several other fixes related to the Get-TargetResource methods not properly returning values.

Along with the SharePointDSC 1.9.0.0 release, the SharePoint Orchestrator script for ReverseDSC, which allows you to extract the configuration out of an existing SharePoint 2013 or 2016 farm, has also been updated. To leanr more about what is new in the ReverseDSC Orchestrator Script, take a look at my previous blog post What’s New With SharePointDSC.Reverse 1.9.0.0