Querying Remote Servers for Information

While looking through questions in a forum, I found a question about a script to query server information.  The user had a decent start to the script, but also wanted to add some additional properties to include and wasn’t sure how to do that. 

The script that I am including is somewhat similar to an event in this year’s 2011 Scripting Games.  Some of the techniques that I will discuss here I learned by participating.

To allow the script to run against multiple machines, I have set the script up as an Advanced Function or script.  In the first lines of the script, just before the Param section, I have decorated the script with the [CmdletBinding()] attribute.  This attribute signifies that the script can act like a Cmdlet and is available in Powershell v2.  It also allows the script to accept data from the pipeline, as will be shown in the Param block.

[CmdLetBinding()]
Param(
    [Parameter(ValueFromPipeline=$true,ValuefromPipelineByPropertyName=$true)]
    [string]$ComputerName=$env:ComputerName,
    [System.Management.Automation.PSCredential]$credential
)

This script accepts two parameters:  -ComputerName and -Credential.  The attributes above ComputerName are what allows us to pass in values from another cmdlet that we can run this script against.  The Credential parameter takes a PSCredential object just like any other cmdlet would.

Since we are accepting objects through the Pipeline, we have to set up the PROCESS block which takes each item coming in from the pipeline and runs it through the process.  In this case, each computername object that we get.  If needed, I could also set up a BEGIN block and an END block that will handle set-up and tear-down of things at the beginning of the script run and as the script finishes.  Since those are not needed here, I have left them off of the script.

Inside the PROCESS block, the first thing is to create an empty hashtable.  The reason for creating this now is so that all subsequent script blocks (inside If, Foreach, etc) inside the PROCESS block can know about this variable.

$computerObjects = @{}

My next step is to check for two things: 1) Are we running this script against the local computer? or 2) was the script run without providing the PSCredential object?  I need to check this because you cannot provide a Credential parameter to cmdlets that run against the local machine.  As you will soon see, there are some Get-WMIObject calls done that may need to work locally.  Secondly, if you try to run a cmdlet with a Credential parameter and pass in a NULL $credential object, the cmdlet will return an error.  In that case, we will attempt to run the cmdlets against the remote machine using the current user’s context.  If the credentials are not supplied, a warning is displayed to let the user know that there may be a problem connecting without the credentials supplied.

If (($ComputerName -eq $env:ComputerName) -or (-not $credential))
 {
  If (-not $credential) {Write-Warning "You have not provided credentials.  Remote connections may fail."}

If either of the two conditions are true, we can now attempt to gather the information.  I have set up a try/catch area so that if there are any problems trying to connect to the server (i.e. Access denied because credentials weren’t supplied), then the object will be created blank with the words “Access Denied” appended to the Computer Name.

try
  {
   $computerProperties = @{
    ComputerName=$ComputerName;
    FreeSpaceC="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Filter "DeviceID='C:'").Size / 1MB);
    FreeSpaceD="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Filter "DeviceID='D:'").Size / 1MB);
    TimeZone=(Get-WMIObject -Class Win32_TimeZone -ComputerName $ComputerName).Caption;
    IPAddress=Get-WMIObject -Class Win32_NetworkAdapterConfiguration -ComputerName $ComputerName | Where-Object {$_.IPAddress} | select-object -expand IPAddress;
    Domain=([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name;
    OU=(Get-QADComputer -Identity $ComputerName).ParentContainer;
    OS=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName).Caption;
    ServicePack=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName).CSDVersion
   }
  }
  catch
  {
   $computerProperties = @{
    ComputerName="$ComputerName - ACCESS DENIED";
    FreeSpaceC="";
    FreeSpaceD="";
    TimeZone="";
    IPAddress="";
    Domain="";
    OU="";
    OS="";
    ServicePack=""
   }
  }

I have highlighted line 23 to indicate the method I used to get the OU information.  Since I am not running a Windows 2008 Domain farm, I don’t have the infrastructure set up to use the Active Directory Module and the included cmdlets.  The syntax for these cmdlets is similar.  Instead I have the Quest Active Directory Cmdlets installed and used them.  This information can also be gathered by using .NET calls to the System.DirectoryServices namespace, but that does require a bit more code.  Also notice that the cmdlets in the above sample do not contain the “-Credential $Credential” parameters included. 

In this section, the script has determined that we are connecting to a remote machine and that we also have a PSCredential object to use. 

If (test-connection $ComputerName -quiet -count 1)
  {
   $computerProperties = @{
    ComputerName=$ComputerName;
    FreeSpaceC="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Credential $Credential -Filter "DeviceID='C:'").Size / 1MB);
    FreeSpaceD="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Credential $Credential -Filter "DeviceID='D:'").Size / 1MB);
    TimeZone=(Get-WMIObject -Class Win32_TimeZone -ComputerName $ComputerName -Credential $Credential).Caption;
    IPAddress=Get-WMIObject -Class Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Credential $Credential | Where-Object {$_.IPAddress} | select-object -expand IPAddress;
    Domain=([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name;
    OU=(Get-QADComputer -Identity $ComputerName -Credential $Credential).ParentContainer;
    OS=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $Credential).Caption;
    ServicePack=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $Credential).CSDVersion
   }
  }
  else
  {
   $computerProperties = @{
    ComputerName="$ComputerName - OFFLINE";
    FreeSpaceC="";
    FreeSpaceD="";
    TimeZone="";
    IPAddress="";
    Domain="";
    OU="";
    OS="";
    ServicePack=""
   }
  }

This code sample is very similar to the one above it.  The difference here is that we test to make sure that the server is online.  The Test-Connection cmdlet will ping the computer one time (-count 1) and return True or False if it gets a reply.  If the computer does not respond, another blank object will be created with the word “OFFLINE” appended to the computer name.

In the above code blocks, I have used the hashtable that I created back in line 10.  In earlier versions of Powershell, to add additional NoteProperty items to an object, you had to use the Add-Member cmdlet.  In the current version, you can create a hashtable of all the properties and their values.  This makes the code cleaner and easier to read.  Now when the final lines of the script come together, no matter what scenario produced our object properties, we have one line to create the object and then output it.

The final operation is to create the PSObject and add all the properties to it.

$outputObject = new-object PSObject -property $computerProperties
 
 Write-Output $outputObject

While it is not necessarily wrong to write the output to the host as you get it, it does not follow the guidelines of Powershell best practices.  For instance, now that the data is coming out of the script as an object, you can do interesting things to gather the data. 

Let’s say I named this script “Get-ServerInfo.ps1”.  (I know, not very imaginative, but just work with me, here.)  If I want to gather all the servers that I have in an excel spreadsheet and list only the servers that I can get data from and say, group them by OS Version, I could write the following:

PS C:\MyScripts\> Import-CSV .\MyServers.csv | .\Get-ServerInfo.ps1 | Where-Object {$_.OS} | Format-Table * -GroupBy OS

In this example, we grab the list of server names from the CSV file (assuming they have a heading of “ComputerName”) and pipe each one to our script (also assuming that the current user context has rights to the remote servers…if you don’t, you need to add the Credential parameter).  We filter only objects that have data in the OS field, and then format a table to list these computers grouped by the Operating System.  If the script only writes the data out to the host, you can’t slice and dice your data.

Here is the complete sourcecode from the examples above.  Hope this helps

[CmdLetBinding()]
Param(
    [Parameter(ValueFromPipeline=$true)]
    [string]$ComputerName=$env:ComputerName,
 [System.Management.Automation.PSCredential]$credential
)

PROCESS
{
 $computerProperties = @{}
 If (($ComputerName -eq $env:ComputerName) -or (-not $credential))
 {
  If (-not $credential) {Write-Warning "You have not provided credentials.  Remote connections may fail."}
  try
  {
   $computerProperties = @{
    ComputerName=$ComputerName;
    FreeSpaceC="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Filter "DeviceID='C:'").Size / 1MB);
    FreeSpaceD="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Filter "DeviceID='D:'").Size / 1MB);
    TimeZone=(Get-WMIObject -Class Win32_TimeZone -ComputerName $ComputerName).Caption;
    IPAddress=Get-WMIObject -Class Win32_NetworkAdapterConfiguration -ComputerName $ComputerName | Where-Object {$_.IPAddress} | select-object -expand IPAddress;
    Domain=([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name;
    OU=(Get-QADComputer -Identity $ComputerName).ParentContainer;
    OS=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName).Caption;
    ServicePack=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName).CSDVersion
   }
  }
  catch
  {
   $computerProperties = @{
    ComputerName="$ComputerName - ACCESS DENIED";
    FreeSpaceC="";
    FreeSpaceD="";
    TimeZone="";
    IPAddress="";
    Domain="";
    OU="";
    OS="";
    ServicePack=""
   }
  }
 }
 else
 {
  If (test-connection $ComputerName -quiet -count 1)
  {
   $computerProperties = @{
    ComputerName=$ComputerName;
    FreeSpaceC="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Credential $Credential -Filter "DeviceID='C:'").Size / 1MB);
    FreeSpaceD="{0:N2}" -f ((Get-WMIObject -Class Win32_LogicalDisk -ComputerName $ComputerName -Credential $Credential -Filter "DeviceID='D:'").Size / 1MB);
    TimeZone=(Get-WMIObject -Class Win32_TimeZone -ComputerName $ComputerName -Credential $Credential).Caption;
    IPAddress=Get-WMIObject -Class Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Credential $Credential | Where-Object {$_.IPAddress} | select-object -expand IPAddress;
    Domain=([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name;
    OU=(Get-QADComputer -Identity $ComputerName -Credential $Credential).ParentContainer;
    OS=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $Credential).Caption;
    ServicePack=(Get-WMIObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $Credential).CSDVersion
   }
  }
  else
  {
   $computerProperties = @{
    ComputerName="$ComputerName - OFFLINE";
    FreeSpaceC="";
    FreeSpaceD="";
    TimeZone="";
    IPAddress="";
    Domain="";
    OU="";
    OS="";
    ServicePack=""
   }
  }
 }
 $outputObject = new-object PSObject -property $computerProperties
 
 Write-Output $outputObject
}
Advertisements
This entry was posted in Advanced Function, PSObjects, Scripting, WMI and tagged , , , , . Bookmark the permalink.