Home Scripting Games Scripting Games Task #3 Commentary
 

Scripting Games Task #3 Commentary

In task #3, you were asked to create a HTML report to inventory disk parameters. We’ve seen plenty of great scripts, and I’d like to comment on some of the techniques used.

Getting Disk Information

To get to the actual disk information remotely, Dr. Scripto promised that all machines have WMI and CIM enabled and accessible. Which raises the question: what is the difference between WMI and CIM?

WMI (Windows Management Instrumentation) is a standard way of asking a machine to return information on logical and physical entities. It has been around for decades and works pretty well. It has one design problem, though: to access remote machines, WMI always uses a technique called “DCOM”. DCOM is hard to manage across firewalls and not very robust, so if the connection is unstable etc. WMI may fail.

This is why beginning with PowerShell 3.0, there is an alternate way of remotely accessing machines called CIM. With CIM, you can actually decide which protocol to use. You can still use the old DCOM to address older machines, but by default CIM uses WinRM, a webservice-based techique that is also used by PowerShell Remoting (and all newer Microsoft remote management tools).

Since WMI is available on all machines, you can get away with using Get-WMIObject which a lot of you did. Some of you have also used CIM (which I liked), and you may want to look at the solutions to find out more about this. In the task given, it was not necessary to use CIM though because WMI was available everywhere. That’s why I am not commenting on CIM usage at this point.

Create a versatile function body

Here is sample code that illustrates how you can easily create a function that works against local and remote computers using WMI and optionally allows to submit a credential either as string or as credential object:

function Get-DiskInfo
{
    [CmdletBinding(DefaultParameterSetName='integrated')]
    param
    (
        [Parameter(Mandatory=$false,ParameterSetName='integrated')]
        [Parameter(Mandatory=$true,ParameterSetName='credential')]
        $ComputerName,

        [Parameter(ParameterSetName='credential')]
        $Credential
    )

    if ($PSBoundParameters.ContainsKey('Credential') -and $Credential -is [String])
    {
        $PSBoundParameters.Credential = Get-Credential -UserName $Credential -Message 'Enter Password'
    }

    Get-WmiObject -Class Win32_LogicalDisk -Filter 'DriveType=3' @PSBoundParameters -ErrorAction Stop
}

Note the creative use of ParameterSets: the parameter ComputerName can be either optional (ParameterSet “integrated”) or mandatory (ParameterSet “credential”). The parameter Credential is optional, but since it belongs to ParameterSet “credential”, when you use it, you must also submit a computer name. PowerShell does all the validation work for you, as you can see.

The user now can (a) call the function without any parameters, (b) just with a computer name using his current credentials, or (c) with alternate credentials if he targets a remote machine.

The function also illustrates how you submit the parameters gathered by your function to Get-WMIObject. $PSBoundParameters basically is a hash table with all the parameters the user submitted.

By using splatting (@PSBoundParameters), PowerShell automatically submits the parameters to Get-WMIObject. They just need to have the same name as the parameters they are supposed to be submitted to. So if the user submitted a credential, it will be handed over to Get-WMIObject, else not.

Note also how the function makes sure Credential really is a PowerShell credential object. It first checks whether the user has submitted that parameter and if the parameter contains a string. If so, the string is converted to a credential object using Get-Credential. Note that the result must be assigned to $PSBoundParameters, else splatting would still hand over the initial value.

Note: you may have seen the use of this attribute with a parameter:

[System.Management.Automation.Credential()]
$Credential

This would ensure that the parameter Credential is always automatically converted into a credential object, and if the user submitted just a string, it would open a dialog to ask for the password. Unfortunately, this technique does not work with optional parameters because PowerShell always executes the attribute. That’s why the solution presented is not using the attribute and instead converts the parameter manually if needed.

Get-WMIObject does all the querying part. In this case, it looks for instances of Win32_LogicalDisk and uses a filter to look only for DriveType=3 (fixed disks). Pretty awesome to see what PowerShell can do for you!

Creating an HTML report

To actually create an HTML report, the information must be in object form. Each object property is then turned into an HTML table column if you submit the object to ConvertTo-HTML.

I have talked about the various techniques you can use to create your own objects in my last commentary on task #2. In this scenario, one easy way is to use Select-Object and simply restrict the results returned by Get-WMIObject:

Get-WmiObject -Class Win32_LogicalDisk -Filter 'DriveType=3' |
 Select-Object -Property DeviceID, Size, FreeSpace

This would, of course, keep the existing property names and property values. To prettify the output, you can always use hashtables and calculated properties:

$name = @{
  Name = 'Drive'
  Expression = { $_.DeviceID }
}

$size = @{
  Name = 'Size'
  Expression = { '{0:#,##0.0} GB' -f ($_.Size / 1GB) }
}

$free = @{
  Name = 'Free'
  Expression = { '{0:#,##0.0} MB' -f ($_.FreeSpace / 1MB) }
}

Get-WmiObject -Class Win32_LogicalDisk -Filter 'DriveType=3' |
 Select-Object -Property $name, $size, $free

There is really no need to use New-Object and add multiple calls to Add-Member if all you want to do is create a simple object with a subset of (optionally calculated and renamed) properties. Use New-Object when you want to create completely new objects, and use Add-Member when you want to add sophisticated members like methods or script properties.

Making it work in a pipeline

One last challenge was for your function to accept pipeline input so you can pipe in computer names and process them one by one.

Key here is to declare the ComputerName parameter as ValueFromPipeline and use a process block in your script. The code in your process block will then be executed for each incoming computer name.

Since pipeline-awareness is standard procedure, I am not explicitly covering it here. You all did a great job in implementing this, and if you are reading this and are wondering what this is about, please take a look at the scripting games submissions. In a nutshell, it works like this:

function Get-DiskInfo
{
  param
  (
    [Parameter(ValueFromPipeline=$true)]
    $ComputerName
  )

  process
  {
    "Processing $ComputerName" 
  }

}

When you now submit one or more computer names to Get-DiskInfo, each computer will be automatically processed:

'server1', 'server2' | Get-DiskInfo

However, there is still a challenge to write really cool and versatile functions: you cannot submit a string array as a parameter directly this way:

Get-DiskInfo -ComputerName 'server1', 'server2'

This time, the function will always process the entire list of computers which leads to errors.

To make your function compatible to both string arrays provided via pipeline and parameter, it must be designed like this:

function Get-DiskInfo
{
  param
  (
    [Parameter(ValueFromPipeline=$true)]
    [String[]]
    $ComputerName
  )

  process
  {
    $ComputerName | Foreach-Object { "Processing $_" }
  }

}

Note how the process block “unrolls” the content of $ComputerName and processes each element separately. If the computer names come from the pipeline, the process block will be repeated. If they come from a parameter, the process block will only execute once, but now the foreach-object loop will repeat the code for each computer name submitted to the parameter.

Thanks for your time, have a great week, and hey, if you live in Europe and are looking for a first class PowerShell training, then simply rent me!

I do PowerShell inhouse trainings all the time and would be honored to help you and your team as well. Just drop a line to tobias.weltner(AT)email.de.

 

 

 

 

 

Facebooktwittergoogle_pluspinterestlinkedinFacebooktwittergoogle_pluspinterestlinkedin  rssrss
The following two tabs change content below.

Tobias

PowerShell Wizard at scriptinternals
I love to create things and master challlenges. That's why I love PowerShell so much. I am a 10+ year Microsoft MVP, have written more than a 100 IT-related books with Microsoft Press and others, and like to expain complex things in a simple and understandable way. I am giving PowerShell trainings throughout Europe and organize the annual German PS Community Conference.