The Admin’s First Steps: Testing Service Health

The Admin’s First Steps: Testing Service Health

  • Comments 4
  • Likes

Summary: Richard Siddaway talks about using Windows PowerShell to automate health tests across your server estate.

Hey, Scripting Guy! Question Hey, Scripting Guy! I’ve just starting using Windows PowerShell to administer my systems, and I’ve been asked to test the service health on my servers. How can I do that?

—GK

Hey, Scripting Guy! Answer Hello GK,

Honorary Scripting Guy, Richard Siddaway, here today—filling in for my good friend, The Scripting Guy. You’re in luck. Today, I’ve got a solution to that issue as part of my series on how an administrator can start making productive use of Windows PowerShell.

One thing you will see with this series is that I’m using a wide range of techniques: scripts, workflows, .csv files for data, hash tables for data. The idea is threefold:

  1. To show you as many different techniques, tips, and tricks as I can.
  2. To emphasize that there are nearly always multiple ways of achieving the end result with Windows PowerShell. Am I claiming that the way I show is the best? Very definitely not. It just happens to be the one I thought was best when I sat down to solve the issue.
  3. To walk you through the development process from concept to a usable administration tool.

Exchange Server 2007 was the first major product with management that was based on Windows PowerShell. Among the raft of cmdlets that Exchange Server 2007 delivered was Test-ServiceHealth. This little gem has been overlooked in the mass of the Exchange literature, but the idea behind it is great: It tests an Exchange server to determine if the required services are running. It doesn’t supply a full health check, but if you know the services are running, it removes a lot of issues from consideration when you are troubleshooting. If you run the test remotely, you get a bonus because you’ve implicitly tested the network connectivity to the machine.

I’m going to show you how to follow in the tradition of all great administrators and steal that concept to use as the basis of a tool that you can implement in your environment to test other server types, and their services, in the same way. You can add Exchange Server into the mix yourself if you so desire, but I’m going to use a domain controller, a SQL Server system, and a web server as my examples.

I suspect that you’ve used Get-Service and you are familiar with its output (truncated here for brevity):

Get-Service

 

Status   Name               DisplayName                          

------   ----               -----------                          

Running  AdobeARMservice    Adobe Acrobat Update Service 

What you may not have done is this:

Get-Service -Name BITS,W32Time, WinRM

 

Status   Name               DisplayName                          

------   ----               -----------                          

Running  BITS               Background Intelligent Transfer Ser...

Stopped  W32Time            Windows Time                         

Running  WinRM              Windows Remote Management (WS-Manag...

The names of the services you want to see are used as a filter, so you restrict the data returned. Notice that the actual name is used, not the display name. The same result could be achieved by using Where-Object:

$services = 'BITS', 'W32Time', 'WinRM'

Get-Service | where Name -in $services

Filtering after the fact isn’t as efficient, especially when dealing with remote machines. You can take this a step further and put the services into a list before you call Get-Service:

$services = 'BITS', 'W32Time', 'WinRM'

Get-Service -Name  $services

The only difference is that you need to put quotation marks around the names.  At this stage, you have the basis of the service health test.

Let’s define the services that we want to use for each type of server:

Domain controller

  • Active Directory Web Services (adws)
  • Active Directory Domain Services (ntds)
  • Key Distribution Center Service (kdc)
  • Windows Time Service (w32time)

SQL Server

  • Distributed Transaction Coordinator (MSDTC)
  • SQL Server (MSSQLSERVER)
  • SQL Server Agent (SQLSERVERAGENT)
  • SQL Server Integration Services (MsDtsServer100)

Web server

  • IIS Admin Service (IISADMIN)
  • Windows Process Activation Service (was)
  • WinHTTP Web Proxy Auto-Discovery Service (WinHttpAutoProxySvc)
  • World Wide Web Publishing Service (w3svc)

Note  The names and types of services may vary depending on your version of Windows.

One of the first questions you need to answer is, “How am I going to store the server names and the list of associated services?”

You could use a .csv file, but then you have to compromise by looping through the service fields or by keeping the services in one field and splitting them into an array. XML is another alternative, but it can be awkward to work with.

I saw the following technique on the PowerShell.org forum, and thought it could be useful for this scenario:

$servers = @{}

$servers += @{'Server02' = 'ADWS', 'NTDS', 'Kdc', 'W32Time'}

$servers += @{'W08R2SQL08' = 'msdtc', 'mssqlserver', 'sqlserveragent', 'msdtsserver100'}

$servers += @{'WebR201' = 'iisadmin', 'was', 'WinHttpAutoProxySvc', 'w3svc'}

 

foreach ($key in $servers.keys){

  Get-Service -ComputerName $key -Name $servers[$key]

}

Create an empty hash table, and then add a series of hash tables to it where the key is the server name and the value is a comma-separated list of services. You can then iterate the hash table keys and use the key and value in Get-Service to fetch the data.

If you don’t like the idea of putting your data with the script, you can create a text file that holds the commands to create the hash table (I will call it servers.txt):

$servers = @{}

$servers += @{'Server02' = 'ADWS', 'NTDS', 'Kdc', 'W32Time'}

$servers += @{'W08R2SQL08' = 'msdtc', 'mssqlserver', 'sqlserveragent', 'msdtsserver100'}

$servers += @{'WebR201' = 'iisadmin', 'was', 'WinHttpAutoProxySvc', 'w3svc'}

You can then modify your script so it looks like this:

Invoke-Expression (Get-Content servers.txt -raw)

 

foreach ($key in $servers.keys){

  Get-Service -ComputerName $key -Name $servers[$key]

}

The trick is in this line:

Invoke-Expression (Get-Content servers.txt -raw)

It uses Get-Content to read servers.txt. The –Raw parameter is new in Windows PowerShell 3.0, and it is a dynamic parameter for the file system provider. It’s only available for file system drives. The Help file states “
The –Raw parameter ignores new line characters and returns the entire content of a file in one string. By default, the content of a file is returned as an array of strings that is delimited by the newline character.” 

This means that you get a single string that Invoke-Expression can run. If you leave off the –Raw parameter, you will get an error message.

That handles data coming in, but the output leaves a bit to be desired because it doesn’t include the computer name. The knee-jerk reaction is to use Add-Member to append the computer name to the output. But before we do that, let’s test what actually comes from Get-Service:

PS> Get-Service | select -f 1 | fl *

 

Name                : ADWS

RequiredServices    : {}

CanPauseAndContinue : False

CanShutdown         : True

CanStop             : True

DisplayName         : Active Directory Web Services

DependentServices   : {}

MachineName         : .

ServiceName         : ADWS

ServicesDependedOn  : {}

ServiceHandle       : SafeServiceHandle

Status              : Running

ServiceType         : Win32OwnProcess

Site                :

Container           :

We have a property called MachineName. In this case, it as a value of “.”, which indicates the local machine. All we have to do use Select-Object to override the default output:

Invoke-Expression (Get-Content servers.txt -raw)

 

foreach ($key in $servers.keys){

  Get-Service -ComputerName $key -Name $servers[$key] |

  select @{N='ComputerName'; E={$_.MachineName}},

  Name, DisplayName, Status

}

I’ve used a calculated field to rename the MachineName property to ComputerName. You may want to output this data for future use, or even pipe it into another cmdlet. In both cases, ComputerName is the common parameter name, so the property will work better if it is renamed.

You now have a usable tool to test the service health on your remote servers. The drawback is that the file name is hard coded. As a last step, let’s make that a parameter:

function test-servicehealth {

[CmdletBinding()]

param(

[parameter(Mandatory=$true,

      ValueFromPipeline=$true,

      ValueFromPipelineByPropertyName=$true)]

[ValidateScript({Test-Path -Path $_ })]

[string]$path

)

PROCESS{

Invoke-Expression (Get-Content -Path $path -Raw)

 

foreach ($key in $servers.keys){

  Get-Service -ComputerName $key -Name $servers[$key] |

  select @{N='ComputerName'; E={$_.MachineName}},

  Name, DisplayName, Status

}

} # End process

}

Your script has been turned into an advanced function. Save the code as testservicehealth.ps1 and dot source it to load the function, or incorporate it in your Admin toolkit module. This means that you can use the function like a cmdlet. It will accept input via the –Path parameter or pipeline:

test-servicehealth -path .\servers.txt

'servers.txt' | test-servicehealth

The function takes a single parameter (–Path), which is mandatory. When the path to the file is tested, an error message is generated if the file can’t be found. Get-Content uses the value of the –Path parameter for the file to open. The working code needs to be in a Process{} block because you’re accepting pipeline input. (I capitalize the BEGIN, PROCESS, and END blocks to make the code easier to read.) You can now have a set of files that contains different sets of servers or services, and chose the one you need.

You could extend this concept in a number of ways:

  • Add additional server types, such as file server, DFS, DNS, or DHCP.
  • Create a scheduled task to run the job.
  • Put the output into an HTML, Word, or Excel document.
  • Email the results to one or more administrators, especially if there was a failure.
  • If the services aren’t running, restart the service or even the machine.

GK, that is all there is to using Windows PowerShell to test the service health on your servers. Next time, I’ll have another idea for you to try as you bring more automation into your environment. In the meantime, for a very special message try this:

(72,97,112,112,121,32,66,105,114,116,104,100,97,121,32,116,111,32,82,105,99,104,97,114,100 | foreach {[char][byte]$psitem}) -join ""

Bye for now.

~Richard

Richard Siddaway is based out of the UK, and he spends his time automating anything and everything for Kelway, Ltd. A six-year Windows PowerShell MVP, Richard is a prolific blogger, mainly about Windows PowerShell. He is a frequent speaker at user groups and Windows PowerShell conferences. He has written a number of books: PowerShell in Practice, PowerShell and WMI, PowerShell in Depth (co-author), PowerShell Dive (co-editor), and he is currently finishing Learn Active Directory Management in a Month of Lunches, which features a lot of Windows PowerShell. All of the books are available from Manning Publications.

Contact information: Richard Siddaway's Blog

Thanks, Richard.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Good stuff! (and Happy Birthday!)

    I wish I knew where this practice came from; it drives me bonkers every time I see it:

    $servers = @{}

    $servers += @{'Server02' = 'ADWS', 'NTDS', 'Kdc', 'W32Time'}

    Not only does it kill your performance to use the += operator on a hashtable that way (though you may not notice it unless you're working with large data sets), it also requires more typing than the way a hashtable is intended to be used.  I just don't see the benefit.

    These methods should be preferred:

    $servers = @{}

    $servers['Server02'] = 'ADWS', 'NTDS', 'Kdc', 'W32Time'

    #Or:

    $servers.Add('Server03', ('ADWS', 'NTDS', 'Kdc', 'W32Time'))

    The difference being that the Add method will throw an exception if the key already exists, and the index notation will just overwrite the old value; either one might be more appropriate, depending on your needs.

  • The reasons for doing that way

    1)  It works

    2)  The series is about introducing ideas for admins new to PowerShell.

    3)  For normal organisations the method presented is more than adequate.  And as I point out in the post if you split the has table creation across a number of files you can run a number of processes each hitting a different batch of servers.  Parallel processing either forced as in this example, using workflows or jobs is the way to overcome performance issues when faced with large datasets.  It also minimises the impact if a particular process doesn't complete - you only lose a fraction of your result set not everything

  • To get the secret message I had to change $psitem to $_ ...  At least it wasn't "Drink More Ovaltine". ;)

  • Runs everyday in my environment