Saturday, April 28, 2012

Using Powershell for IIS Administration

We have a relatively complex integration and test environment to house a clustered web application. As would be expected, things occasionally go awry, and remedial action is required. Until recently, we'd be doing this sort of activity/support by hand - we normally know what to look for, and how to fix it, so it didn't seem like a real problem.

However, such activity is repetitious, tedious and attracts the odd error or two in interpretation or the absence of some step. Additionally, IP gained from such activities is not often captured formally and hence may be lost over time.

So I wrote a 'menu' based powershell script to automate a number of the more common scenarios.To do this, I made particular use of the web administration powershell module (see http://technet.microsoft.com/en-us/library/ee790599.aspx for details). This post just describes a few simple tasks that might be of use.

One of the initial issues I had was the platform differences in using the web administration module. On IIS 7.5, web administration (WM) is a module, otherwise a snap in. As the script needed to work on Windows 7 as well as Windows 2008 (not R2), an adaptive way to load WM is required.

Stack overflow to the rescue! See this excellent answer from a stack overflow user, which I morphed into the following function:

 # Ensure that the correct mechanism is used to load the web administration module   
 function ImportWebAdministration() {  
   $iisVersion = Get-ItemProperty "HKLM:\software\microsoft\InetStp";  
   if ($iisVersion.MajorVersion -eq 7)  
   {  
     if ($iisVersion.MinorVersion -ge 5)  
     {  
       Import-Module WebAdministration;  
     }        
     else  
     {  
       if (-not (Get-PSSnapIn | Where {$_.Name -eq "WebAdministration";})) {  
         Add-PSSnapIn WebAdministration;  
       }  
     }  
   }  
 }  

So this is a snippet from one of the scripts, showing an excerpted 'status' set of behaviours, that expects the name of a web site name to be supplied to it, and that a sub site with a specific relative URL exists.

Just a few very minor points of interest, focusing on IIS cmdlet use:

Line 2: Search a web config file (obtained using get-webconfigfile) for some pattern
Line 16: Get a web site object that matches some parameter name we have passed in
Line 28: Output some details of the application pool associated with the main web site
Lines 33-35: Use a regex from the Expresso library to strip out just a URL
Lines 42-46: Since we believe that a URL should be reachable as part of the overall health of the site, use some  low level .NET objects to verify this using a GET call

1:  function GetWebConfigEntry([string] $url, [string] $searchString) {   
2:    $m = select-string (get-webconfigfile IIS:\Sites\$url) -pattern $searchString |  
3:       select-object Line  
4:    $m.Line.ToString().Trim()  
5:  }  
6:  function CompareDSCURLs([string] $lhs, [string] $rhs) {  
7:    $same = $lhs -eq $rhs  
8:    if (-not $same) {  
9:      Write-Error "DSC mismatch"    
10:    }  
11:    $same  
12:  }  
13:  function WebServerRoleStatus() {  
14:    write-Host "`nWeb Server Role Status`n"   
15:    write-Host `n"Check DSC configuration"   
16:    $ws=get-website | where-object { $_.Name -match $webSiteName }  
17:    if ($ws -eq $null) {   
18:      Throw "There is no web site like/called $webSiteName"  
19:    }  
20:    $wsname=$ws.name  
21:    $sitedsc = MatchURL (GetWebConfigEntry $wsname "dsc.svc")   
22:    $subsitedsc = MatchURL (GetWebConfigEntry "$wsname/subsite" "dsc.svc")  
23:    $same = CompareDSCURLs $sitedsc $subsitedsc  
24:    if ($same) {   
25:      ValidateGETOnURL $sitedsc  
26:    }  
27:    write-Host "`nApplication pool for $wsname"   
28:    Get-ChildItem -Path IIS:\AppPools |   
29:          Where-object { $_.Name -eq $ws.applicationPool } |   
30:          select-object -property Name,state,managedPipelineMode  
31:  }  
32:  function MatchURL([string] $url, [bool] $outputMatch = $true) {  
33:    $url -match   
34:      '(?<Protocol>\w+):\/\/(?<Domain>[\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*' |   
35:      out-null  
36:    if ($outputMatch) {   
37:      Write-Host $matches[0]  
38:    }  
39:    $matches[0]  
40:  }  
41:  function ValidateGETOnURL([string] $url) {   
42:    write-Host "Check URL $url is reachable"  
43:    [net.httpWebRequest] $req = [net.webRequest]::create($url)  
44:    $req.method = "GET"  
45:    $req.TimeOut = 10000  
46:    [net.httpWebResponse] $res = $req.getResponse()  
47:    $result = $res.StatusCode  
48:    write-host "HTTP Response is " $result "`n"  
49:  }  

Of course, the use of a pattern match across a web.config file is clumsier than it should be, but the script is not meant to represent 'gold plated' powershell development - it's deliberately utilitarian. We could of course use  XPath over an XmlDocument to do a more targeted action, as below:

1:  $xml = [xml] (get-content (get-webconfigfile IIS:\Sites\Site))  
2:  $xml.SelectNodes("/Configuration/applicationsettings/Org.Internet.Infrastructure.Site.WebServices.  
3:  Properties.Settings/setting[@name='Org_Internet_Infrastructure_  
4:  Site_WebServices_DSC']/value")  

This assumes that the node we are interested in has the XPath as indicated. Powershell script writing is almost cathartic - but I still prefer the Korn shell!

No comments: