SharePoint Maintenance Mode Automation with PowerShell




In an ideal world, downtime (scheduled or otherwise) would be avoided entirely. Unfortunately, there are plenty of reasons why a web site may need to go into a scheduled maintenance mode. It’s important that this is done correctly and performing such as task across a farm manually can be error prone and tedious.

 

Maintenance

In my case, I wanted to automate the activation/deactivation of a maintenance page across a SharePoint farm with multiple Web Front End servers. The same script would be run across a number of different environments with differing topology depending on requirements (development, QA, staging, production, etc).

 

As of .NET 2.0 a very useful feature was introduced which makes putting a single web application (on a single server) into maintenance mode straightforward. If you drop a file named “app_offline.htm” into the IIS web application directory for your web site it will “shut-down the application, unload the application domain from the server, and stop processing any new incoming requests for that application. ASP.NET will also then respond to all requests for dynamic pages in the application by sending back the content of the app_offline.htm file”. [quoted from ScottGu’s Blog].

 

Leveraging this technique I have written a script to provision (or remove) a maintenance page correctly across all the required servers in a SharePoint farm.

 

A few things of note:

  • There are a number of SharePoint specific PowerShell commands being used so, as it is, this script cannot be used for other, non SharePoint, ASP.NET web sites. I would like to hear from anyone who modifies it for another use.
  • The user running the script will need to have enabled PowerShell remoting (Enable-PSRemoting -Force) as well as permission to Write and Delete from the file system of the other servers.
  • As it is, the script drops the app_offline.htm file for every web application zone. In my case, I only wanted to block users from accessing the zone configured to use our custom claims provider. I have left in the check for this in case you find yourself in a similar situation.
  • The maintenance page must be entirely self-contained. By this I mean that all CSS and JS must be embedded and even images must referenced using inline base64 representations. See here for an easy way to achieve this.
  • If want to perform an IISRESET and stop/start the ¬†SharePoint Timer Service at each WFE before taking down the maintenance page then uncomment the relevant lines.

# By Paul Ryan. Free to use and distribute. 
# http://blog.concentra.co.uk/author/paul-ryan/

param (
  [string]$webAppUrl = $(throw "Parameter -webAppUrl is missing!"),
  [bool]$enableMaintenanceMode = $true,
  [string]$pagePath = '.\App_Offline.htm'
)

# support relative paths
if(![string]::IsNullOrEmpty($pagePath) -and $pagePath.StartsWith("."))
{
  $currentPath = Split-Path -parent $MyInvocation.MyCommand.Definition
  $pagePath = $currentPath + $pagePath.TrimStart(".")
}

if($enableMaintenanceMode -eq $true)
{
  Write-Host "Preparing to ENABLE maintenance mode..."
}
else
{
  Write-Host "Preparing to DISABLE maintenance mode..."
}

Add-PSSnapin Microsoft.SharePoint.PowerShell –erroraction SilentlyContinue

# get maintenance page content
if($enableMaintenanceMode -eq $true)
{
  $pageContent = $null
  $pageContent = [System.IO.File]::ReadAllText($pagePath);
  if($pageContent -eq $null)
  {
    Write-Host "Failed to get the maintenance page" -ForegroundColor Red
    Break
  }
}

# get the WFE server names dynamically
$wfeServers = Get-SPServer | Where {($_.ServiceInstances | 
		Where {$_ -is [Microsoft.SharePoint.Administration.SPWebServiceInstance]})} | 
		Select Name

$runLocally = $false
$runRemotelyNamesArray = @()

# print the detected server names for human verification
Write-Host "Detected WFE servers:"
foreach($wfeServer in $wfeServers)
{
  $wfe = $wfeServer.Name
  if($env:COMPUTERNAME -eq $wfe)
  {
    $runLocally = $true
  }
  else
  {
    $runRemotelyNamesArray = $runRemotelyNamesArray + $wfe
  }
  Write-Host $wfe
}

# define the script to run at each WFE
$remoteScript = 
{
  param([string]$webAppUrl, [string]$pageContent, [bool]$enableMaintenanceMode)
  Add-PSSnapin Microsoft.SharePoint.PowerShell –erroraction SilentlyContinue
	
  # get the web application
  $webApp = Get-SPWebApplication $webAppUrl

  # iterate the iis zone settings
  foreach ($zone in $webApp.IisSettings.Values)
  {
    try
    {
      $displayPrefix = "Maintenance mode for zone '$($zone.ServerComment)' on $($env:COMPUTERNAME):"
			
      # in our case we only want to prevent access to the form authentication zone.
      # eg if the crawl wants to run that should be fine
      #$actForThisZone = ($zone.ClaimsAuthenticationProviders | 
      #			Where {$_.MembershipProvider -eq "xxxClaimsProviderxxx"} | 
      #			measure).Count -gt 0
      $actForThisZone = $true
      if($actForThisZone)
      {
        $vDirPath = $zone.Path.ToString() + '\app_offline.htm'
        $templatePath = [Microsoft.SharePoint.Utilities.SPUtility]::`
		GetGenericSetupPath('TEMPLATE\ADMIN\SPOffline\app_offline.htm')

        # if the application should be offline, copy the file
        if ($enableMaintenanceMode -eq $true)
        {								
          # save file
          [System.IO.File]::WriteAllText($vDirPath, $pageContent)
					
          Write-Host "$displayPrefix ENABLED"
        }
        else
        {
          # delete the file if it exists
          if ([System.IO.File]::Exists($vDirPath))
          {
            [System.IO.File]::Delete($vDirPath)
          }
  					
          # perform IISRESET
          #$supressOutput = iisreset
  					
          # restart the sharepoint timer service
          #$supressOutput = net stop sptimerv4
          #$supressOutput = net start sptimerv4
  					
          Write-Host "$displayPrefix DISABLED"
        }
      }
    }
    catch [System.Exception]
    {
      $errorMessage = $Error[0]
      Write-Host "ERROR: $displayPrefix $errorMessage " -ForegroundColor Red
    }
  }
}

# run remotely on all other WFEs
if($runRemotelyNamesArray.Length -gt 0)
{
  # get the credentials required to login to the other WFEs
  $cred = $host.ui.PromptForCredential("Enter Credentials", `
		"Enter credentials of the SharePoint administrator", `
		$env:userdomain + "\" + $env:username, "")
  if($cred -eq $null)
  {
    Write-Host "Credentials are required" -ForegroundColor Red
    Break
  }
  Invoke-Command -ComputerName $runRemotelyNamesArray `
	-Authentication CredSSP -Credential $cred -ScriptBlock $remoteScript `
	-ArgumentList $webAppUrl, $pageContent, $enableMaintenanceMode
}

# run on the current WFE
if($runLocally)
{
  Invoke-Command -ScriptBlock $remoteScript `
	-ArgumentList $webAppUrl, $pageContent, $enableMaintenanceMode
}

Write-Host "DONE" -ForegroundColor Green

If you find this helpful please subscribe to our feed and feel free to leave a comment with your thoughts.




2 thoughts on “SharePoint Maintenance Mode Automation with PowerShell”

  1. Hi thanks for Script

    I am getting below error while running it
    Missing property name after reference operator.
    At C:\Users\TEMP.Desktop\down.ps1:93 char:65
    + $templatePath = [Microsoft.SharePoint.Utilities.SPUtility]:: <<<< `
    + CategoryInfo : ParserError: (:::String) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingPropertyName

    1. Try writing the statement across a single line. As so:
      $templatePath = [Microsoft.SharePoint.Utilities.SPUtility]::GetGenericSetupPath(‘TEMPLATE\ADMIN\SPOffline\app_offline.htm’)

Leave a Reply

Your email address will not be published.