Expert Commentary: 2012 Scripting Games Advanced Event 2

Expert Commentary: 2012 Scripting Games Advanced Event 2

  • Comments 2
  • Likes

Summary: Microsoft community contributor, Boe Prox, provides expert commentary for 2012 Scripting Games Advanced Event 2.

Microsoft Scripting Guy, Ed Wilson, is here. Boe Prox is the expert commentator for Advanced Event 2.

Photo of Boe Prox

Boe is currently a senior systems administrator with BAE Systems. He has been in the IT industry since 2003, and he has been working with Windows PowerShell since 2009. Boe looks to script whatever he can, whenever he can. He is also a moderator on the Official Scripting Guys Forum. Check out his current projects published on CodePlex: PoshWSUS and PoshPAIG.

Blog: Learn PowerShell | Achieve More
Guest posts on Hey, Scripting Guy! Blog
Twitter: @proxb

This is an event that relates very nicely to something that would be done in the “real world.” Tracking your services on each server is something that should be done daily to ensure that everything is working normally and to make sure that there are no anomalies occurring.

This event requires us to perform a query against local and/or remote servers to pull back all of the services and write out the data to a CSV file. This is definitely a job for Windows PowerShell!

Requirements

This event is not without requirements to give you more of a challenge, so let’s take a look at the key requirements:

  • You need to present your findings in a spreadsheet that includes the following information: the server name, the service name, the start mode of the service, the user account used to start the service, and the current status of the service.
  • You should use the easiest method possible to display the information in a spreadsheet.
  • You must include the ability to run the script on a local computer and on a remote computer.
  • You must include the ability to run the script with alternate credentials when operating against a remote computer, and you should impersonate the logged on user when running locally.
  • You do not need to add comment-based Help for this scenario, but additional points are awarded if you do include appropriate comment-based Help such as the description, examples, and parameters.
  • If the script requires admin rights to run, you should check to ensure that the script is running with admin rights. If those rights are not present, you should display an appropriate message and exit.
  • For the purposes of this exercise, do not write a module. You should make your script completely standalone and have no external dependencies. Therefore, everything needed should be put in this script. Failure to do so will cost you points.

Now that we know what is required for this script, we can begin to dive into the code and see what is going on. I will take the code apart in chunks to explain what I am doing and point out the areas that meet each requirement in addition to other areas that I feel are worth mentioning. I included some extra features for this script, and I will explain my reasoning behind this when I get to them.

The code

Function Get-ServiceData {

    <#

        .SYNOPSIS

            Command used to find all services with option to write to a CSV File

       

        .DESCRIPTION

            Command used to find all services with option to write to a CSV File

       

        .PARAMETER Computername

            A single or collection of systems to perform the query against

       

        .PARAMETER Credential

            Alternate credentials to use for query of services

       

        .PARAMETER ToCSV

            Name of CSV to write the results of the query to

       

        .PARAMETER Throttle

            Number of asynchronous jobs that will run at a time

           

        .PARAMETER ShowProgress

            Displays the progress of the services query

       

        .NOTES

            Author: Boe Prox

            Created: 14March2012

       

        .EXAMPLE

            Get-ServiceData

           

            Description

            -----------

            Retrieves all services from the local system

           

        .EXAMPLE

            $Servers = 'Server1','Server2','Server3'

            Get-ServiceData -Computername $Servers -ShowProgress

           

            Description

            -----------

            Retrieves all services from the remote servers and displays a progress bar     

           

        .EXAMPLE

            $Servers = Get-Content Servers.txt

            $Servers | Get-ServiceData -ShowProgress -Throttle 10

           

            Description

            -----------

            Retrieves all services from the remote servers while running 10 runspace jobs at a time

            and displays a progress bar to show the status of each runspace job.            

           

        .EXAMPLE

            $Servers = Get-Content Servers.txt

            $Servers | Get-ServiceData -Credential (Get-Credential) -ToCSV (Join-Path $pwd report.csv)

           

            Description

            -----------

            Retrieves all services for each system in the Servers.txt file while using the supplied

            alternate credentials and outputs the data to a CSV file.           

    #>

    #Requires -Version 2.0

    [cmdletbinding(

        DefaultParameterSetName = 'NonCSV'

    )]

    Param (

        [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]

        [Alias('CN','__Server')]

        [string[]]$Computername = $Env:Computername,

       

        [parameter()]

        [System.Management.Automation.PSCredential]$Credential,

       

        [parameter(ParameterSetName = 'CSV')]

        [ValidateNotNullOrEmpty()]

        [String]$ToCSV,

       

        [parameter()]

        [int]$Throttle = 5,

       

        [parameter()]

        [switch]$ShowProgress

    )

If you are going to create an advanced function, you definitely have to have inline Help within that function. This does not mean that you write your own Help function and offer that. It means that you use the Help in Windows PowerShell to accomplish this task. I included examples about how to use this function so others can reference it when trying to run the command.

Also included is [cmdletbinding()], which makes this advanced function advanced by adding extra parameters such as –Verbose and –WhatIf. I also make sure that I add a default parameter value for my ComputerName parameter to point to the local machine if nothing is given. Also, if no file name is given with ToCSV, it will throw an error until a file name is specified.

    Begin {

        #Function that will be used to process runspace jobs

        Function Get-RunspaceData {

            [cmdletbinding()]

            param(

                [switch]$Wait,

                [switch]$ShowProgress

            )

            Do {

                $more = $false        

                Foreach($runspace in $runspaces) {

                    If ($runspace.Runspace.isCompleted) {

                        $Script:Report += $runspace.powershell.EndInvoke($runspace.Runspace) |

                            Select SystemName,Name,State,StartMode,StartName

                        $runspace.powershell.dispose()

                        $runspace.Runspace = $null

                        $runspace.powershell = $null

                        $Script:i++ 

                        If ($PSBoundParameters['ShowProgress']) {

                            Write-Progress -Activity 'Services Query' -Status ("Processing Runspace: {0}" -f $runspace.computer) `

                            -PercentComplete (($i/$totalcount)*100)             

                        }                  

                    } ElseIf ($runspace.Runspace -ne $null) {

                        $more = $true

                    }

                }

                If ($more -AND $PSBoundParameters['Wait']) {

                    Start-Sleep -Milliseconds 100

                }  

                #Clean out unused runspace jobs

                $temphash = $runspaces.clone()

                $temphash | Where {

                    $_.runspace -eq $Null

                } | ForEach {

                    Write-Verbose ("Removing {0}" -f $_.computer)

                    $Runspaces.remove($_)

                }            

            } while ($more -AND $PSBoundParameters['Wait'])

        }

           

        Write-Verbose ("Performing inital Administrator check")

        $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()

        $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")    

          

        #Counter for Write-Progress

        $Script:i = 0     

       

        #Main collection to hold all data returned from runspace jobs

        $Script:report = @()   

       

        Write-Verbose ("Building hash table for WMI parameters")

        $wmihash = @{

            Query = "SELECT SystemName,Name,StartMode,State,Startname FROM Win32_Service"

            ErrorAction = "Stop"

        }

       

        #Supplied Alternate Credentials?

        If ($PSBoundParameters['Credential']) {

            $wmihash.credential = $Credential

        }

       

        #Define hash table for Get-RunspaceData function

        $runspacehash = @{}

        If ($PSBoundParameters['ShowProgress']) {

            $runspacehash.ShowProgress = $True

        }

        #Define Scriptblock for runspaces

        $scriptblock = {

            Param (

                $Computer,

                $wmihash

            )          

            Write-Verbose ("{0}: Checking network connection" -f $Computer)

            If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {

                #Check if running against local system and perform necessary actions

                Write-Verbose ("Checking for local system")

                If ($Computer -eq $Env:Computername) {

                    $wmihash.remove('Credential')

                } Else {

                    $wmihash.Computername = $Computer

                }

                Try {

                    Get-WmiObject @wmihash

                    } Catch {

                        Write-Warning ("{0}: {1}" -f $Computer,$_.Exception.Message)

                        Break

                }

            } Else {

                Write-Warning ("{0}: Unavailable!" -f $Computer)

                Break

            }       

        }

       

        Write-Verbose ("Creating runspace pool and session states")

        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()

        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)

        $runspacepool.Open() 

       

        Write-Verbose ("Creating empty collection to hold runspace jobs")

        $Script:runspaces = New-Object System.Collections.ArrayList

        $totalcount = $computername.count

    }

The Begin block is pretty meaty but it sets up a lot of the code that will be processed later in the function.

A few of the requirements are covered here with an initial check for Administrator rights and allowing the code to run on a local or a remote system. A gotcha is also presented here because the use of alternate credentials will not work against a local system.

Image of command output

If a local system is included with other remote systems, the alternate credentials are simply removed from the runspace.

Although I do not have time to dive into everything here, I will say that this is where I set up a script block that handles each client computer, use two hash tables for splatting parameters (one for the Get-WMIObject cmdlet and another for a function that will be defined later). I also make use of the [runspacefactory] accelerator that allows me to set up some background runspace jobs and also allows throttling those runspaces, which allows for quicker processing of the queries. I chose this over using the *-Job cmdlets because there is not an easy way to throttle jobs, and this is more of a lightweight method to run jobs in a background runspace.

To quickly jump into an example, the following screenshots show the amount of time it took to run this function by using the background runspaces versus using a more synchronous command, ForEach, against 132 systems.

Get-ServiceData Function

Image of command output

Using ForEach

Image of command output

As you can see, the difference is about 220 seconds. While not a massive amount of time, it is still a nice improvement over the synchronous way using ForEach. Credit for the throttling technique goes to Windows PowerShell MVP, Dr. Tobias Weltner, for his webcast, Speeding Up PowerShell: Multithreading, which explained the use of [runspacefactory] to provide throttling.

        Process {       

        Write-Verbose ("Validating that current user is Administrator or supplied alternate credentials")       

        If (-Not ($Computername.count -eq 1 -AND $Computername[0] -eq $Env:Computername)) {

            #Now check that user is either an Administrator or supplied Alternate Credentials

            If (-Not ($IsAdmin -OR $PSBoundParameters['Credential'])) {

                Write-Warning ("You must be an Administrator to perform this action against remote systems!")

                Break

            }

        }

        ForEach ($Computer in $Computername) {

           #Create the powershell instance and supply the scriptblock with the other parameters

           $powershell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($computer).AddArgument($wmihash)

          

           #Add the runspace into the powershell instance

           $powershell.RunspacePool = $runspacepool

          

           #Create a temporary collection for each runspace

           $temp = "" | Select-Object PowerShell,Runspace,Computer

           $Temp.Computer = $Computer

           $temp.PowerShell = $powershell

          

           #Save the handle output when calling BeginInvoke() that will be used later to end the runspace

           $temp.Runspace = $powershell.BeginInvoke()

           Write-Verbose ("Adding {0} collection" -f $temp.Computer)

           $runspaces.Add($temp) | Out-Null

          

           Write-Verbose ("Checking status of runspace jobs")

           Get-RunspaceData @runspacehash

        }                        

    }

The Process block does just that: processes all of the computers and assigns each computer as a background runspace. More requirements are met here, including the finishing check of Administrator rights if running against remote systems. If there is only a local system and it is the only system in the collection, alternate credentials are not required nor are Administrator rights because any local user can perform a query against WMI. 

    End {                    

        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))

        $runspacehash.Wait = $true

        Get-RunspaceData @runspacehash

       

        Write-Verbose ("Closing the runspace pool")

        $runspacepool.close()       

        If ($PSBoundParameters['ShowProgress']) {

            #Close the Write-Progress bar so it does not affect the displaying of data when completed.       

            Write-Progress -Activity 'Services Query' -Status 'Completed' -Completed           

        }

       

        If ($PSBoundParameters['ToCSV']) {

            Write-Verbose ("Writing report to CSV: {0}" -f $ToCSV)

            $Report | Export-Csv -Path $ToCSV -NoTypeInformation

        } Else {

            Write-Verbose ("Displaying Report")

            Write-Output $Report

        }

    }

}

The End block finishes up the remaining runspace jobs, and depending on what the user chose for output (CSV or no CSV), the function will output the data to the console or write all of the data to a specified CSV file (meeting an important requirement). I chose to use Export-CSV because it meets the requirement for the easiest method possible to create a CSV file. If I was using Windows PowerShell 3.0, I could have thrown this in with the runspace script block and used the –Append parameter to further increase the performance of this function instead of holding all of the data until the end and writing out to the CSV file.

Let’s give it a quick run to see it in action and view the final output.

$Servers = Get-Content .\servers.txt

Get-ServiceStartMode -Computername $servers -ShowProgress -ToCSV "C:\users\boe\desktop\report.csv"

 Image of command output

 

As you can see, all of the data that was required is on the spreadsheet to view.

The script is available to download from the Script Center Repository.

~Boe

2012 Scripting Games Guest Commentator Week will continue tomorrow when we will present the scenario for Event 3.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • <p>Consider this typical scenario:</p> <p>* I am logged in to my local computer (LAPTOP1) as DOMAIN\Jason which has no administrator rights locally.</p> <p>* I want to query the services on a remote computer (SERVER1) and my user account (still DOMAIN\Jason) does have administrator rights on that server.</p> <p>* I execute your script on my laptop using: Get-ServiceData -ComputerName SERVER1</p> <p>Result: it fails unnecessarily with &quot;You must be an Administrator to perform this action against remote systems!&quot; when it would have worked just fine.</p>

  • <p>Boe, this explaination was awesome. &nbsp;I&#39;m not a &quot;developer&quot; but I&#39;ve written .NET code before and created threads to handle foreground/background tasks. &nbsp;I also watched that video you referenced last night and it was eye opening. &nbsp;It breaks some concepts I thought I was sure of!! &nbsp;I&#39;ve been contricting myself to using Powershell cmdlets whenver there is a cmdlet to handle a task and now I know that isn&#39;t always the best way to go.</p> <p>Thanks again for the enlightenment!</p>