Until recently, a clear delineation existed between Windows system administrators and developers. You’d never catch a system administrator writing a single line of code, and you’d never catch a developer bringing up a server. Neither party dared to cross this line in Windows environments. Nowadays, with the devops movement spreading like wildfire, that line is fading away.
A basic premise of devops is automation, which allows us to maintain consistent, repeatable processes while removing the error-prone ways of our being human. The only way to automate is through the command line. If you’re in a Windows environment, the command line to use is PowerShell. Once considered an inferior command-line experience to Linux, Windows now touts a very powerful and functional command line through PowerShell.
If you’re a Windows system administrator you’ve probably been clicking buttons, dragging windows, and scrolling scroll bars for a long time. Using the GUI -- even on a server -- has been common in the Windows world. We thought nothing of firing up a remote desktop client and logging into a server to do our bidding. This might be OK for very small businesses with only a couple servers, but enterprises soon realized this approach doesn’t scale. Something else had to be done. The solution was to turn to scripts to automate as much of this management as possible. With the introduction of PowerShell, sys admins now had a tool to make it happen.
Here we provide a hands-on introduction to what Windows system administrators can accomplish using PowerShell in an increasingly devops-minded world. So roll up your sleeves, fire up PowerShell, and follow along.
Event log querying across servers
One task I struggled with as an IT pro was automating the reading of event logs. Windows event logs contain a ton of useful information. The problem is getting to all the data and collating it into a meaningful form. Using PowerShell cmdlets like Get-WinEvent
and Get-EventLog
, I could finally gather information from one to 1,000 servers with relative ease.
Before we start writing our code, we need to figure out what events we want to retrieve. For the purposes of this introduction, I’ll be searching for event ID 6005 in my servers’ System event log. Why that ID? Because the ID is used to indicate when a server has started. If you want to search for a different event, you can easily switch out this ID for one that represents your desired event.
Before I can start pulling these events from all my servers, I first need to know how to do it on a single server. There are a few cmdlets that can do this, such as Get-EventLog
and Get-WinEvent
. I’m going to use Get-WinEvent
. This cmdlet is typically faster and allows you to perform more advanced filtering. It is a little harder to get a handle on than Get-EventLog
, but I believe a more thorough understanding of Get-WinEvent
pays off in the long run.
Use Get-WinEvent
To retrieve information about events on remote computers we need to use the -ComputerName
parameter. Because servers produce multiple event logs, we’ll then need to narrow that down by the System event log and finally the event ID. Get-WinEvent
offers a few options for filtering, but the easiest to use is the -FilterHashTable
parameter. By using the LogName
and ID
as hashtable keys we can easily narrow down what kind of events will be retrieved.
Get-WinEvent -FilterHashtable @{LogName = 'System'; ID = 6005} -ComputerName labdc.lab.local
You can see in the screenshot above that I’m querying the labdc.lab.local
computer and I can see three events with the ID
of 6005
.
Expand the search to more servers
As you can see, finding all instances of an event ID
on a single server is easy, but what about multiple servers? One way is to use a simple text file listing all of your server names. If you have Active Directory up and running, you can easily use Get-AdComputer
to pull necessary server names as well. For this introduction, I’ll use a text file, from which the Get-Content
cmdlet can pull out all of my server names into a variable.
You can see I have three servers in the text file. I’d like to store all of these server names in a variable to reference in a minute, so I’ll create one called $Servers
.
$Servers = Get-Content -Path C:\Servers.txt
Now I can check each one of those servers by placing that Get-WinEvent
line we defined earlier inside of a foreach
loop where we iterate over each line in the C:\Servers.txt
file. Although this time, instead of specifying the name of an individual server, I’m using the variable $s
. This represents each server name as it’s processed in $Servers
.
foreach ($s in $Servers) {
Get-WinEvent -FilterHashtable @{LogName = 'System'; ID = 6005} -ComputerName $s
}
When you run this you’ll get something that looks like this. Not every helpful, right? Which servers did these events come from? We’ll need to add a bit more code to get the output just right.
By default, PowerShell tries to be helpful by showing you only what it thinks are the essentials. However, sometimes you need to see more, as is the case here, because what you’re seeing from Get-WinEvent
is not the true output. You’re only seeing what PowerShell is configured to output to the console. To see all of the properties that come out of Get-WinEvent
you’ll need to use the Select-Object
cmdlet or select
as it is aliased.
Be selective on properties with Select-Object
Get-WinEvent -FilterHashtable @{LogName = 'System'; ID = 6005} -ComputerName labdc.lab.local | select -First 1 *
Using the -First
parameter to Select-Object
(here as the alias select
) returns the first event record on the System event log of computer labdc.lab.local
, and the asterisk at the end of the command allows me to see all the properties, not only the ones PowerShell displays to the console by default. Notice that there’s a property called MachineName
? This is exactly what we need in our output.
foreach ($s in $Servers) {
$getWinEventParams = @{
'FilterHashTable' = @{LogName = 'System'; ID = 6005}
'ComputerName' = $s
}
Get-WinEvent @getWinEventParams | Select-Object TimeCreated,MachineName
}
By using the Select-Object
cmdlet to manipulate the output from Get-WinEvent
I can now limit our output to the TimeCreated
and MachineName
properties. Notice in my screenshot some odd-looking machine names? That was when the server I’m querying was named something else. I don’t really care what the server was named a long time ago. It looks like I might not be able to use the MachineName
property. Instead, I’ll use the value of $s
to ensure I get a consistent server name.
foreach ($s in $Servers) {
$getWinEventParams = @{
'FilterHashTable' = @{LogName = 'System'; ID = 6005}
'ComputerName' = $s
}
Get-WinEvent @getWinEventParams | Select TimeCreated,@{n='MachineName';e = {$s}}
}
Yay! Now I don’t see those old names in there. I only have to change the MachineName
property by using a calculated property. This allowed me to create a new MachineName
property with a value of my choosing.
Also, you’ll notice that I created a $getWinEventParams
variable and passed that to Get-WinEvent
instead of passing those parameters individually. This is a method called splatting that allows you to pass parameters to cmdlets, rather than having to pass all of them on a single line. It’s a clean way of passing parameters to commands in PowerShell.
Build a server inventory report: A lesson in CIM
Dozens of tools on the market today can inventory your servers. These range from expensive, full-blown suites like Microsoft System Center Configuration Manager, Altiris, and the rest to absolutely free tools. These work, of course, but what if you don’t want to spend time learning a new piece of software or simply need to quickly query something from a few servers? Maybe you have specific requirements and the tool you usually use can’t meet that requirement. You can use PowerShell instead.
Before we begin coding, let’s determine what we need pulled from each of the servers. For this example, I’ll pull the following information from each server.
- Operating system
- Total memory
- Processor name and speed
- Total disk space on the C: drive
The Active Directory module
Also, because the Active Directory cmdlets do not come with PowerShell out of the box I’ll need to download and install Remote Server Administration Tools (RSAT). This will give me the Active Directory module.
Again, as with the event log report we gathered, we’ll need a way to get a list of server names. Rather than use a text file, let’s get these names from Active Directory (AD). For today, I want to gather some information on all of my global catalog servers in my AD forest.
This is a walk in the park using the Get-ADforest
cmdlet.
Let’s assign those servers to a variable again.
$Servers = (Get-ADForest).GlobalCatalogs
Introducing CIM
Now that we have our list of servers, how do we pull our target information? For this, we’ll need to understand a little about Common Information Model (CIM). Every Windows machine has a CIM repository. This repository holds hundreds of classes. Each of these classes contains object properties. One way to query these classes is through the Get-CimInstance
cmdlet. This is a newer cmdlet that uses PSRP (PowerShell remoting protocol). This means you must have WinRM enabled and available on all of your servers. I’m using all Windows Server 2012 R2 servers configured to have WinRM available in my demonstrations, so your mileage may vary.
The four attributes I am looking for exist in different CIM classes on each of my servers. To save time in tracking these down, here’s the breakdown:
- Operating system →
Win32_OperatingSystem
- Total memory →
Win32_PhysicalMemory
- Processor name and speed →
Win32_Processor
- Total disk space on C: drive →
Win32_LogicalDisk
To query each of these classes I’ll use the Get-CimInstance
cmdlet. This command has two parameters we’ll need to use: -ClassName
and -ComputerName
. You can use Get-CimInstance
as demonstrated below.
Let’s see if we can get the operating system name. Notice how I queried the Win32_OperatingSystem
class above, but you don’t see the operating system name? What gives? As with the Get-WinEvent
cmdlet we went over earlier, Get-CimInstance
also does not show the “real” output. However, in this instance we won’t use the Select-Object
cmdlet. Instead, Get-CimInstance
has a -Property
parameter where we can specify an asterisk to see all of the properties.
Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName labdc.lab.local -Property *
Once you do this, if all goes well, you should see an output with lots of different property names, including one called Caption
, which contains the name of the operating system.
Now that I know the property name, I can limit our output to displaying the Caption
property alone, with no need for the -Property
parameter anymore. That was only needed to check out the values.
(Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName labdc.lab.local).Caption
This same methodology applies to all of the other attributes we need to retrieve. To find the processor we’ll use the Name property on the Win32_Processor
class.
(Get-CimInstance -ClassName Win32_Processor -ComputerName labdc.lab.local).Name
The same goes for Win32_PhysicalMemory
and Win32_LogicalDisk
with one caveat. If you try to retrieve the total memory on a server using the Capacity
property you’ll get the total bytes back. For example, if I have 1GB in a demo server, it will show up as 1073741824. That’s not very intuitive. I’d like to get that back in gigabytes. Luckily, this is easy to do in PowerShell. Simply divide total bytes by 1GB.
Put it all together
Now that you have an understanding of how to retrieve this information on a single server, let’s put it all together in a foreach
loop to query each of our servers.
$Servers = (Get-ADForest).GlobalCatalogs
foreach ($Server in $Servers) {
$Output = @{‘Name’ = $Server }
$Session = New-CimSession -Computername $Server
if ($Session) {
$Output.OperatingSystem = (Get-CimInstance -CimSession $Session -ClassName Win32_OperatingSystem).Caption
$Output.Memory = (Get-CimInstance -CimSession $Session -ClassName Win32_PhysicalMemory).Capacity / 1GB
$Output.CPU = (Get-CimInstance -CimSession $Session -ClassName Win32_Processor).Name
$Output.FreeDiskSpace = (Get-CimInstance -CimSession $Session -ClassName Win32_LogicalDisk -Filter "DeviceID = ‘C:’”).FreeDiskSpace / 1GB
Remove-CimSession $Session
[pscustomobject]$Output
}
}
Notice that I used the New-CimSession
cmdlet. Instead of the -ComputerName
parameter on each Get-CimInstance
line, I use the -CimSession
parameter instead. This is for performance reasons. Every time Get-CimInstance
runs with the -ComputerName
parameter it creates a temporary new CIM session on the remote computer. Since I need to make multiple calls to CIM, I can simply create a single CIM session for each server and repeatedly use that single session. It’s more efficient and faster.
Also notice the $Output
hashtable variable. Since it’s not possible for me to gather all of these attributes with a single command I have to store the results in a variable for each server. Then, once I am done with the server I convert the $Output
hashtable to a custom object.
If the stars align, you should get an output similar to this.