Tag Archives: icm

Keep PowerShell Cmdlets Powerful

I’ve seen what I’m about to show you done repeatedly in Windows PowerShell. With that thought in my mind, I’ve decided it’s time to officially write about it. The first time I noticed someone doing it, well, it was me. I did, however, write a correction to my team so they knew I was aware of my error, and as a hope they wouldn’t repeat it.

Those doing this may be new to PowerShell, but if you aren’t careful and don’t fully understand the capabilities of the cmdlets you use, then it’s possible you might make this mistake, too. What am I talking about? I’m talking about Invoke-Command and an improper way that it’s sometimes used.

Invoke-Command‘s purpose is to run a command, or commands, on remote computers and return the results back to the local computer. That said, it can be used on the local computer, as well; however, this is usually only necessary to check if PowerShell Remoting is working on the local computer. While I don’t normally do this in my examples, I’m piping my result of this example to Format-Table -AutoSize once they’re returned from the remote computer. This is so it’ll display better on this webpage.

Invoke-Command -Computer DC01 -ScriptBlock {Get-PSDrive -PSProvider FileSystem} | Format-Table -AutoSize
Name Used (GB) Free (GB) Provider Root              CurrentLocation PSComputerName
---- --------- --------- -------- ----              --------------- --------------
A                                 A:\                               DC01
C        12.91     26.75          C:\  Users\tommymaynard\Documents DC01
D                                 D:\                               DC01

This example of Invoke-Command connected to the remote computer DC01, ran a filtered Get-PSDrive command, returned the results to my computer, and then were formatted by the Format-Table cmdlet.

Now, let’s say we have… 23 computer names stored in the variable $Computers and we want to run the command we used above, against each computer. What so many people seem to do is wrap Invoke-Command in a Foreach construct (think: loop). I’m guessing, because I’ve made the error myself, that it’s because someone learned about Foreach first. Like I said, I discovered my error on my own, and only moments later. It didn’t wake me at 2 a.m. in a cold sweat after two months, which could’ve been possible.

Here’s how not to do this, and again, $Computers is holding the names of 23 computers.

Foreach ($Computer in $Computers) {
    Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     --------- --------      ----                                --------------- --------------
C                  20.47         59.18               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  50.36         29.30               C:\                                 ...rd\Documents WEB01
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  53.59         26.06               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

What we’ve done here is force Invoke-Command to run against only one computer at a time. During each iteration of the Foreach loop, Invoke-Command runs against the computer name currently stored in $Computer. This variable is updated to the next computer name in the variable at the start of each loop. It’s like this: connect to DC02 and run the command. Now, connect to WEB01 and run the command. Are you done, Invoke-Command? Okay then, now Foreach says to run the same command against the computer SQL01. That’s not how this cmdlet was designed to be used.

Before we go any further, I wrapped this command inside the Measure-Command cmdlet to determine how long it took my system to run this against the computers in $Computers. It took nearly 30 seconds. While that’s not an eternity, we’re only talking about 23 computers, and we’re only talking about a single command.

Measure-Command -Expression {
    Foreach ($Computer in $Computers) {
        Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
    }
}
Days : 0
Hours : 0
Minutes : 0
Seconds : 27
Milliseconds : 513
Ticks : 275132689
TotalDays : 0.000318440612268519
TotalHours : 0.00764257469444444
TotalMinutes : 0.458554481666667
TotalSeconds : 27.5132689
TotalMilliseconds : 27513.2689

Now, let’s set this command up the proper way. The ComputerName parameter of Invoke-Command can take a collection of computers. It should be said that many cmdlets can take more than one computer as a value to their ComputerName parameter. The difference is that Invoke-Command can run against all of the 23 computers at the same time. In fact, the default throttle limit—the number of computers in which the command will run against simultaneously—is 32. It can be changed by including the ThrottleLimit parameter, such as -ThrottleLimit 50 or -ThrottleLimit 15.

Now, here’s how this command should’ve been written.

Invoke-Command -ComputerName $Computers -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     -------u-- --------      ----                                --------------- --------------
A                                                    A:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  41.72         37.93               C:\                                 ...rd\Documents WEB01
C                  41.13         38.52               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  46.75         32.91               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

I measured this command five times, and the average of those five runs was only 2.8 seconds. Remember, the Foreach loop took almost 30 seconds to get the same results. This is due to the fact that, again, Invoke-Command will work with the remote computers simultaneously (when it’s not inside a Foreach). There’s never any waiting to run the command on any additional computers unless there’s more computers than the value of ThrottleLimit.

Please keep the topic in this post in mind, as it’s possible to momentarily forget this feature just long enough to wrap the Invoke-Command cmdlet in a Foreach, or other looping construct.

 

Getting Local Variables into Remote Sessions

There are at least three ways to get your local variables into your remote sessions. I’ve written these in preferential order beginning with the best option. Don’t forget to read about_Remote_Variables (Get-Help -Name about_Remote_Variables) to learn more, and see more examples.

$Word1 = 'PowerShell'
$Word2 = 'on'
$Computer = 'Server1'

# Using Scope Modifier
# (1st Option -- works in PowerShell 3.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Write-Verbose -Message "$Using:Word1 $Using:Word2 $env:COMPUTERNAME (Using Scope Modifer)" -Verbose
}
VERBOSE: PowerShell on Server1 (Using Scope Modifier)


# Param
# (2nd Option -- works in PowerShell 2.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Param($Word1InParam,$Word2InParam)
    Write-Verbose -Message "$Word1InParam $Word2InParam $env:COMPUTERNAME (Param)" -Verbose
} -ArgumentList $Word1, $Word2
VERBOSE: PowerShell on Server1 (Param)


# Args
# (3rd Option -- works in PowerShell 2.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Write-Verbose -Message "$($args[0]) $($args[1]) $env:COMPUTERNAME (Args)" -Verbose
} -ArgumentList $Word1, $Word2
VERBOSE: PowerShell on Server1 (Args)