The power of PowerShell: An intro for Windows Server admins

In this increasingly devops-minded world, automation is king. Here’s how to get started with PowerShell.

The power of PowerShell: An intro for Windows Server admins

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

PowerShell: Get-WinEvent -FilterHashtable

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.

PowerShell: Get-Content

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.

PowerShell: Get-WinEvent

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 *

PowerShell: Get-WinEvent -FilterHashtable

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


PowerShell: 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}}


PowerShell: 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.

  1. Operating system
  2. Total memory
  3. Processor name and speed
  4. 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.

PowerShell: Get-ADforest

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:

  1. Operating system → Win32_OperatingSystem
  2. Total memory → Win32_PhysicalMemory
  3. Processor name and speed → Win32_Processor
  4. 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.

PowerShell: Get-CimInstance

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.

PowerShell: Caption

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.

PowerShell: Capacity

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




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.

PowerShell: Putting it all together
1 2 Page 1
Page 1 of 2
The 10 most powerful companies in enterprise networking 2022