Monthly Archive

PowerShell Basics

Scripting Game puzzle – – January 2016

Here’s how I’d solve the puzzle

function get-starttime {
    [CmdletBinding()]
    param(
        [parameter(
                ValueFromPipeline=$true,
                ValueFromPipelineByPropertyName=$true)]
        [Alias('CN', 'Computer')]
        [ValidateNotNullOrEmpty()] 
        [string[]]$computername = $env:COMPUTERNAME
    )
   
    PROCESS {
   
        foreach ($computer in $computername){
            $props = [ordered]@{
                ComputerName = $computer
                StartTime = ''
                'UpTime (Days)' = 0.0
                Status = 'OFFLINE'
            }
   
            if (Test-WSMan -ComputerName $computer -ErrorAction SilentlyContinue) {
                $lbt = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $computer -ErrorAction SilentlyContinue
               
                if ($lbt) {
                
                    $props['StartTime'] = $lbt.LastBootUpTime
           
                    $upt = [math]::round(((Get-Date) - $lbt.LastBootUpTime).TotalDays, 1)
                    $props['UpTime (Days)'] = $upt
               
                    $props['Status'] = 'OK'
                }
                else {
                    $props['Status'] = 'ERROR'
                }
           
            } ## endif
           
            New-Object -TypeName PSObject -Property $props
       
        } ## end foreach
   
    } ## end PROCESS
}

Create an advanced function. Yes I know I’ve used lower case for the function name. I always do to visually separate my code from cmdlets and other functions.

 

Use the [parameter] decorator to enable pipeline input. Only a single parameter so don’t need to bother woth positional parameters. Function is supposed to default to local machien so can’t make parameter mandatory.

 

Requirement to process multiple computers at once presumably means the computername parameter has to take an array – sumultaneous processing implies a work flow which negates the initial requirement to create a function

 

Use the PROCESS block to run a foreach loop that iterates over the collection of computernames.

 

Create a hash table for the results – I’ve used an ordered hash table to preserve the property order. Set the values to a failed connection.

 

use Test-Wsman to see if can reach the computer. If can’t the output object is created. If you can reach the machine then run Get-CimInstance - preferred over Get-WmiObject because it returns the date ready formatted

 

Assuming that works set the start time and status properties. Calculate the uptime in days. I’d prefer to see  just an integer here – tenths of days doesn’t mean anything to most people

 

If the call to Get-CimInstance  fails then set the status to ERROR

Output the object.

 

The requirement to add a proeprty for patching is not clear but I’m assuming it means if the machine has been up for more than 30 days with the 1/10 month as a typo

if you want to add that then

 

Add a property

MightNeedPatching = $false

to the hash table when you create it

 

and add this line

if ($upt -ge 30){$props['MightNeedPatching'] = $true}

after

$upt = [math]::round(((Get-Date) - $lbt.LastBootUpTime).TotalDays, 1)
$props['UpTime (Days)'] = $upt

FQDN

How do you find the FQDN of the machine you’re using. 

The simplest way is to combine a couple of environmental variables:

PS> "$env:COMPUTERNAME.$env:USERDNSDOMAIN"
SERVER02.MANTICORE.ORG

 

If you like using CIM (and who doesn’t) you can try this

PS> Get-CimInstance -ClassName Win32_ComputerSystem |
>> select @{N='FQDN'; E={"$($_.DNSHostName).$($_.Domain)"}}
>>

FQDN
----
server02.Manticore.org

 

This could easily be used for remote machines as well by adding the –ComputerName parameter to Get-CimInstance

 

If you want to go down the .NET route you have:

PS> [System.Net.Dns]::GetHostByName('').HostName
server02.Manticore.org

Testing against an arrays contents

You may need to test if a value is a member of an array. PowerShell provides 2 operators for testing array membership  - -   -in and –contains.

Simple usage is like this

 

PS> $colours = 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'

 

PS> 'blue' -in $colours
True

PS> $colours -contains 'blue'
True

 

Note the order of value and array changes between the operators.

 

You  can also use these operators in Where-Object

 

PS> $testcolours = 'blue', 'pink', 'yellow'

 

PS> $testcolours | where {$_ -in $colours}
blue
yellow

PS> $testcolours | where {$colours -contains $_}
blue
yellow

 

Often the value we want is a property of an object

 

PS> $to = New-Object -TypeName PSObject -Property @{Colour = 'green'}
PS> $to1 = New-Object -TypeName PSObject -Property @{Colour = 'pink'}

 

PS> $to, $to1 | where Colour -in $colours

Colour
------
green

PS> $to, $to1 | where {$colours -contains $_.Colour}

Colour
------
green

 

The –in operator can be used in the simplified Where-Object syntax (it was introduced for that purpose) but –contains has to use the full, original syntax

 

For testing non-membership you also get –notin and –notcontains

 

PS> $to, $to1 | where Colour -notin $colours

Colour
------
pink

PS> $to, $to1 | where {$colours -notcontains $_.Colour}

Colour
------
pink

Starting to use PowerShell

A common question goes along the lines of  “I’ve leant PowerShell (from class, book etc) but what do I do next?”

 

The usual answer is to pick a problem in your organisation and solve it.

 

I’ve provided an example in this UK TechNet blog post

http://blogs.technet.com/b/uktechnet/archive/2016/01/04/starting-your-powershell-journey.aspx

 

Its the second one of a series on PowerShell

2015 December Scripting Games Puzzle

The December 2015 puzzle was based on the 12 Days of Christmas song.  Starting with this here-string.

$list = @"
1 Partridge in a pear tree
2 Turtle Doves
3 French Hens
4 Calling Birds
5 Golden Rings
6 Geese a laying
7 Swans a swimming
8 Maids a milking
9 Ladies dancing
10 Lords a leaping
11 Pipers piping
12 Drummers drumming
"@

 

A here-string is a multiline string. It is NOT an array – its a single string. The first task is to  split into an array of individual lines:

 

## split into individual lines
$s = $list -split '\n'

Each line of the multiline here string ends with a new line – so that becomes the split point.

 

The array can be sorted by length of line. Ascending or descending wasn’t specified so here’s both options:

'Sort by length ascending'
$s | Sort-Object -Property Length

 

"`n "
'sort by length descending'
$s | Sort-Object -Property Length -Descending

 

Removing the numbers to give justthe text and sorting by length of text. I trmmed the strings as some empty spaces had appeared in the arrays. I think because I copied the here-string

"`n "
'remove numbers sort by length ascending'
$s.Trim() -replace '\d{1,2}\s', '' | Sort-Object -Property Length #| group length | ft -a -wrap

 

"`n "
'remove numbers sort by length descending'
$s.Trim() -replace '\d{1,2}\s', '' | Sort-Object -Property Length -Descending

 

Create objects. Split on white space and restrict output to 2 elements – number and text in this case. Create object using New-object

"`n "
#'create objects'
$items = @()
$s.Trim() | foreach {
    $item =  $psitem -split '\s',2
    $items += New-Object -TypeName PSObject -Property @{
        Count = $item[0] -as [int]
        Item = $item[1]
    }
}

 

Count the number of birds

"`n "
'count of birds'
$birdcount = ($items -match '(Partridge|Doves|Hens|Birds|Geese|Swans)' | Measure-Object -Property Count -Sum).Sum
$birdcount

 

Count all items

"`n "
'count of items'
$itemcount = ($items | Measure-Object -Property Count -Sum).Sum
$itemcount

 

If you treat the song as stating the gifts are cumulative then how many gifts are given in total.  Each item is given (13 – the day on which its given) times i.e. 12 to 1 times respectively.  The total number of items can be calculated like this

"`n "
'cumulative count of items'
$total = 0
$items | foreach {$total += $psitem.Count * (13-$psitem.Count) }
$total

 

As a bonus here’s how you calculate the cumulative number of each type of item.

"`n "
'cumulative number of each item'
$totalitems =@()
$items | foreach {
    $totalitems += New-Object -TypeName PSObject -Property @{
        Count = $psitem.Count * (13-$psitem.Count)
        Item = $psitem.Item
    }
}
$totalitems

Objects, properties and values

One thing that seems to keep causing confusion is using Select-Object to pick off one or more properties from a set of objects:

PS> Get-CimInstance -ClassName Win32_Share | select Path

Path
----
C:\WINDOWS
C:\

 

C:\windows\system32\spool\drivers
C:\Users

 

The gap in the output is because the IPC$ share doesn’t have a path defined.

 

What you have is a ‘Selected’ version of the original object

PS> Get-CimInstance -ClassName Win32_Share | select Path | Get-Member

   TypeName: Selected.Microsoft.Management.Infrastructure.CimInstance

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Path        NoteProperty string Path=C:\WINDOWS

 

Very often you’ll only want the value of the property. In which case you need to use the –ExpandProperty parameter on Select-Object

PS> Get-CimInstance -ClassName Win32_Share | select -ExpandProperty Path
C:\WINDOWS
C:\

 

C:\windows\system32\spool\drivers
C:\Users

Inputting computer names

Somehting I was writing yesterday started me thinking about the way you input a list of computer names to a cmdlet. Many cmdlets have a ComputerName parameter so knowing how to deal with this sort of input will help. There are a range of techniques.

 

One of the simplest approaches is to create your list as a variable and use the variable:

$servers = 'SERVER02', 'W12R2SCDC01', 'W12R2SUS', 'W12R2DSC', 'W12R2TGT', 'W12R2WEB01', 'W12R2WEB02', 'W12R2OD01'
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $servers

 

If you don’t need the server list as a variable then input it directly to the cmdlet

Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName 'SERVER02', 'W12R2SCDC01', 'W12R2SUS', 'W12R2DSC', 'W12R2TGT', 'W12R2WEB01', 'W12R2WEB02', 'W12R2OD01'

 

The drawback is that the list of machine names is embedded in your code. The variable approach above is easier to maintain

 

A common approach is to use Foreach-Object

$servers = 'SERVER02', 'W12R2SCDC01', 'W12R2SUS', 'W12R2DSC', 'W12R2TGT', 'W12R2WEB01', 'W12R2WEB02', 'W12R2OD01'
$servers | foreach {Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $psitem}

 

This works but adds unnecessary code. If you are performing other tasks in the foreach processing it may be worthwhile

Likewise using a foreach loop

$servers = 'SERVER02', 'W12R2SCDC01', 'W12R2SUS', 'W12R2DSC', 'W12R2TGT', 'W12R2WEB01', 'W12R2WEB02', 'W12R2OD01'
foreach ($server in $servers) {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $server
}

 

The foreach loop will be quicker but use more memory.

If you create a CSV file make sure you use ComputerName as the field header – then you can do this

Import-Csv .\computers.csv |
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3'

if the header is something else – eg computer – then you need to use foreach-object

Import-Csv .\computers.csv |
foreach {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $_.Computer
}

 

Import-Csv .\computers.csv |
foreach {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $psitem.Computer
}

 

If you have the names in a text file you may be tempted to do this

Get-Content .\computers.txt |
foreach {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $_
}

 

You can also use $psitem instead of $_

 

A neater way is to do this

Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName (Get-Content .\computers.txt)

 

which gets us back to the PowerShell one liner solution.

 

If you’re reading the computernames from Active Directory you have to do a bit of work because the AD cmdlets don’t return a computerName property – they return name.

 

You can either

Get-ADComputer -Filter * |
foreach {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName $psitem.Name
}

 

or if you want to be a bit cleverer

Get-ADComputer -Filter * |
select @{N='ComputerName'; E = {$_.Name}} |
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3'

 

Use select-object to create a computername property on the pipeline object.

 

You can of course revert to the one liner solution

Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ComputerName (Get-ADComputer -Filter * | select -ExpandProperty name)

 

Use ExpandProperty on select-object to strip out the Name value and pass that to your cmdlet.

 

As you can see there are many ways to achieve the same goal – some easier than others. These examples aren’t necessarily complete.

Next time you need to pass a list of values to a cmdlet stop and think about the best way to do it. You might save some typing and more efficient code.

Formating multiple outputs

Using Get-WmiObject and Get-Service you can do this:

PS> Get-WmiObject -Class Win32_Service -Property StartMode -Filter “Name='BITS'” | select StartMode

StartMode
---------
Auto

PS> Get-Service -Name BITS | select Status

Status
------
Running

 

If you try running the two commands in a script, in ISE or even like this:

PS> Get-WmiObject -Class Win32_Service -Property StartMode -Filter “Name='BITS'” | select StartMode; Get-Service -Name BITS | select Status

StartMode
---------
Auto

 

You only get the first result.  If you reverse the order of the commands:

PS> Get-Service -Name BITS | select Status; Get-WmiObject -Class Win32_Service -Property StartMode -Filter “Name='BITS'” | select StartMode

Status
------
Running

 

You still get the first result.

 

When you run a pipeline the results are automatically piped to Out-Default. The formatting system then decides to use a table format because you have less than 5 properties.

 

When the commands are run individually each calls Out-Default individuallly. When the 2 commands are run in a script (or other option that causes them to execuate together) the formatting takes its direction from the first object it receives and because the second object doresn’t have matching properties nothing is displayed.

 

The answer is to manually force each command to pipe to out-default

PS> Get-WmiObject -Class Win32_Service -Property StartMode -Filter “Name='BITS'” | select StartMode | Out-Default; Get-Service -N
ame BITS | select Status | Out-Default

StartMode
---------
Auto

 

Status
------
Running

 

OR

PS> Get-Service -Name BITS | select Status | Out-Default; Get-WmiObject -Class Win32_Service -Property StartMode -Filter “Name='B
ITS'” | select StartMode | Out-Default

Status
------
Running

 

StartMode
---------
Auto

 

This is a common issue that users trip over as they learn PowerShell.

Splatting and Default parameters

One thing you don’t hear much about is default parameters.

Consider this

Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceId = 'C:'"

 

A pretty standard use of CIM.

 

Now think if you have to do this across a number of machines on a regular basis.  Typing could get a bit tedious.

You could use splatting:

$params = @{
  ClassName = 'Win32_LogicalDisk'
  Filter = "DeviceId = 'C:'"
}

Get-CimInstance @params

 

Create a hash table of parameter names and values and use that to reduce your typing. Because its a hash table you can modify as required to use other classes or Filters

 

An alternative is to use default parameters

$PSDefaultParameterValues = @{
'Get-CimInstance:ClassName' = 'Win32_LogicalDisk'
'Get-CimInstance:Filter' = "DeviceId = 'C:'"
}

Get-CimInstance

 

Use the $PSDefaultParameterValues variable to hold your default values. Note how the cmdlet and parameter are defined. You can then call the cmdlet and the default parameters and their values are applied.

 

If you want to override the default values you may have to do it for all of the default values for a cmdlet – in the above case the Filter is nonsensical if applied to Win32_OperatingSystem so you’d have to do this

Get-CimInstance -ClassName Win32_OperatingSystem -Filter "Manufacturer LIKE '%'"

 

Used with a bit of care splatting and default parameters are a good way to save typing

Out of Process

One thing I’ve been seeing come up a lot recently is the problem of modules and cmdlets cot being available when jobs and workflows are executed even though the module has been specifically loaded into PowerShell.

 

This is because workflows and Jobs run in a separate process when you execute them – NOT your current PowerShell process.  The worflow or job process doesn’t run your profile and doesn’t auto load modules.  

 

You need to specifically perform the module import. Remember you can’t use Import-Module in a workflow so you have to wrap that part in  an InlineScript block.