Pimp Your Output: Use Objects

 

Did you ever create a script, and when it was all done, rewrite a large part of it to change the way the output is shown, saved or printed? If you use the power of Powershell in a smart way, you will never have to again.

 

One of the great powers of Windows Powershell, is the way everything you use is an object. The output of every single Powershell cmdlet is an object or a collection of objects. This fact allows you to easily filter, sort or format the output of a command by piping is to Where-Object, Sort-Object or one of the Format cmdlets.

 

Now why don’t you write your scripts in the same way? Think for a minute about what kind of objects your output should be and what properties they should have. Then build these objects in your script. When you run your script, dot-source is (preceed the path to your script by a DOT and a SPACE). This way, all variables set in the script become global variables, which means you can access them after running your script. Either interactively, or by running another script.

 

This is the way to build a collection of objects in a script:

 

$myCol = @()

$collection = …

ForEach ($item in $collection)

{

$myObj = “” | Select Name, Property1, Property2

$myObj.Name = …

$myObj.Property1 = …

$myObj.Property2 = …

$myCol += $myObj

}

$myCol

 

First you create an empty array to hold your collection:

$myCol = @()

 

You grab the collection of object you need to manipulate to get your output values and loop through them (e.g.: Get-QADServer to get the servers in your domain):

$collection = …

ForEach ($item in $collection)

{

}

 

Then you build an empty object to represent your output item and define which properties it should have:

$myObj = “” | Select Name, Property1, Property2

 

You then proceed with your script to get the output values you need (e.g.: do a WMI query to a remote server to get disk space information) and set the values to the appropriate properties of your output object:

$myObj.Name = …

$myObj.Property1 = …

$myObj.Property2 = …

 

When your output object is done, you add it to the output collection and the loop will restart:

$myCol += $myObj

 

After running the script (don’t forget to dot-source it: PS D:>. D:scriptsmyscript.ps1), you can manipulate the output collection to save or print it:

E.g.:

PS D:>$myCol | Where {$_.Property1 -gt 100} | Out-Printer

or

PS D:>$myCol | Sort-Object Name | Out-File D:output.txt

»crosslinked«

Check NIC bind order

Using the wrong bind order for multihomed servers can cause serious problems. Applications not working, network segments flooded with unneccesary traffic and even security leaks are some of the dangers.

That’s why I’ve created a Powershell script that checks all your servers for you. Let me explain how it works:

The binding order is store in the following registry key:

HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Linkage

It contains a multi-string value Bind such as this one:

DeviceNetBT_Tcpip_{A0344E82-4E65-47A2-92FF-B3183A181473}

DeviceNetBT_Tcpip_{C34D5A39-DA8D-4D58-82D4-099098D5501C}

What we are interested in, is to identify each TCP/IP interface in this list. We can identify them by correlating the identifier (or key) between the curly brackets {} to an IP address. We find all relevant information in the following registry key:

SYSTEMCurrentControlSetServicesTcpipParametersInterfaces

It contains subkeys named after the identifiers of the different TCP/IP interfaces, with a multi-string  value IPAddress which contains the IP addresses set on this interface.

So let’s read the second collection of registry keys and values first, and create a collection of objects representing the interfaces, having properties representing the identifier and the IP address:

$registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $serverName)         # Open remote HKLM key

$baseKey2 = $registry.OpenSubKey(“SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces”)

$myCol = @()

$NICS = $baseKey2.GetSubKeyNames()                    # Set of interfaces

ForEach ($NIC in $NICS)

{

$myObj = “” | Select-Object Key, IP         # Create object for relation between identifier and IP address

$key = $baseKey2.OpenSubKey(“$NIC”)

$myObj.Key = $key.name.split(“{“)[1].trimend(“}”)         # Get identifier of interface without brackets

$myObj.IP = $key.GetValue(“IPAddress”)         # Get IP address of interface

$myCol += $myObj         # Create collection of interface objects

}

Nice. Now we have the $myCol variable holding a collection of objects like so:

PS D:Scripts> $myCol

Key                                                                               IP

—                                                                                  –

1B68A2DD-B093-4E12-A347-CED975434F12      {0.0.0.0}

313D4DA6-B0A6-41B6-B34D-97C89A3E0D65     {10.2.0.112}

A0344E82-4E65-47A2-92FF-B3183A181473        {10.1.0.112}

C34D5A39-DA8D-4D58-82D4-099098D5501C     {0.0.0.0}

Now let’s read the binding order:

$baseKey1 = $registry.OpenSubKey(“SYSTEM\CurrentControlSet\Services\Tcpip\linkage”)

$bindorder = $baseKey1.GetValue(“Bind”)

That’s easy! Now to match the correct IP address to the second object in the $bindorder array:

$wantedboundkey = $bindorder[$index].split(“{“)[1].trimend(“}”)         # Get identifier of …th TCP/IP interface in binding order

$wantedbound = $myCol | Where {$_.Key -eq $firstboundkey}         # Find matching interface in collection

$wantedbound.IP        # Return matching IP address

And there you have it!

PS D:Scripts>$wantedbound.IP

10.2.0.112

Below is the entire script that queries Active Directory for computers and runs the above script (defined as a function).

You should be able to figure out how it works.

Enjoy!

$outputFile = “D:Scriptsoutput.txt”                        # Default output path

function Get-BoundIP

{

param([string]$serverName, [Int]$index)

$registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $serverName)        # Open remote HKLM key

If ($registry)

{

$baseKey1 = $registry.OpenSubKey(“SYSTEM\CurrentControlSet\Services\Tcpip\linkage”)

$bindorder = $baseKey1.GetValue(“Bind”)

$baseKey2 = $registry.OpenSubKey(“SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces”)

$myCol = @()

$NICS = $baseKey2.GetSubKeyNames()        # Set of interfaces

ForEach ($NIC in $NICS)

{

$myObj = “” | Select-Object Key, IP        # Create object for relation between identifier and IP address

$key = $baseKey2.OpenSubKey(“$NIC”)

$myObj.Key = $key.name.split(“{“)[1].trimend(“}”)        # Get identifier of interface

$myObj.IP = $key.GetValue(“IPAddress”)        # Get IP address of interface

$myCol += $myObj        # Create collection of interface objects

}

$wantedboundkey = $bindorder[$index].split(“{“)[1].trimend(“}”)        # Get identifier of …th interface in binding order

$wantedbound = $myCol | Where {$_.Key -eq $wantedboundkey}        # Find matching interface in collection

$wantedbound.IP        # Return matching IP address

}

}

Write-Host “Grabbing server names …”

$servers = Get-QADComputer -SizeLimit 0 -SearchRoot “OU=Servers,DC=example,DC=com” | Sort-Object Name        # Get all servers from AD

$myCollection = @()

ForEach ($server in $servers)

{

Write-Host “Processing server” $server.name

$myObject = “” | Select-Object Name, FirstIP, SecondIP        # Create object for output organization

$myObject.Name = $server.name        # Set server name

$myObject.FirstIP = Get-BoundIP $server.name 0       # Run function to get first bound IP address

$myObject.SecondIP = Get-BoundIP $server.name 1

$myCollection += $myObject        # Build collection of objects

}

$output = $myCollection | Format-Table -AutoSize        # Format output

# Create output

If ((Test-Path $outputFile) -eq $False)                # If the output file does not exist

{

Write-Host “Creating output file …”

$creatingOutputFile = New-Item $outputFile -ItemType File                # Create output file

}

Write-Host “Writing output to file …”

$output | Out-File -FilePath $outputFile -Force                # Output file is overwritten with new output

Write-Host “Launching output file ($outputFile) …”

Invoke-Item $outputFile