Thursday, November 26, 2015

PowerShell: Adding Elements to an Array

If you've been using PowerShell for a while, you may have stumbled across this syntax:
$Array += $Item
Or this:
$Array = $Array + $Item
This is inefficient and slow, and here is why. The += operator, instead of simple adding a new element to the array, creates a second copy of the array with the new element. To visualize:
$Array = 'A'
#New array = A
#Sum of array sizes = 1
$Array += 'B'
#Old array = A
#New array = A,B
#Sum of array sizes = 3
$Array += 'C'
#Old array = A,B
#New array = A,B,C
#Sum of array sizes = 5
...
$Array += 'Z'
#Old array = A,B,C,...,Y
#New array = A,B,C,...,Y,Z
#Sum of array sizes = 51
This may seem innocuous, but when the array is large, the issue is exacerbated. Say the array has millions of elements. Every time an element is added, another completely new array containing millions of items is created. So the memory usage is basically double (plus one) the size of the array.

The more efficient way to add to an array is to move it back a level. So instead of:
foreach ($Item in $Items) {
    $Array += $Item
}
Use this:
$Array = foreach ($Item in $Items) {
    $Item
}
This creates the array once. Much quicker, much more efficient. I'm not a mathematician, but I've run my own tests and have seen the difference.

This took 1.2 seconds:
$Array = 1..50000 | ForEach-Object { "$_" }
This took 143 seconds:
1..50000 | ForEach-Object { $Array += "$_" }
I didn't have the patience to test higher than 50000. Your mileage may vary, but += is not going to win this race.

PowerShell: Get Windows Firewall Status for Remote Systems

Recently I needed to check the status of Windows Firewall on several remote systems. No guarantee of PowerShell v4+, so I had to improvise a bit.

There are multiple methods of pulling the Windows Firewall status, some of them easier than others. I settled on using PowerShell remoting and the netsh command. Since I first learned about PowerShell remoting, I have fallen in love with it. Makes non-interactive (local or RDP logon) administration so much easier. With no guarantee of PowerShell 4 (or even 3) I chose to use netsh to actually pull the firewall status.

First, the netsh command:
netsh advfirewall show domain state
Since these systems are on a domain, I check the domain profile and don't bother checking the others. For systems not on a domain, I would recommend checking the standard and/or public profiles.

The netsh command will return something like this:

Domain Profile Settings:
----------------------------------------------------------------------
State                                 ON
Ok.

What I want is the state, and don't care about the other lines. The line with the firewall state is element number 3.
@(netsh advfirewall show domain state)[3]
I don't want the extra spaces, so I used regex with -replace, which worked very well.
@(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' 
Now I just have to execute that script block on each computer in our list (which I pulled with a filter through Get-ADComputer).
$Computer = Get-ADComputer -Identity "SampleServer1"
$ScriptBlock = { @(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' }
$Status = Invoke-Command -ComputerName $Computer.Name -ScriptBlock $ScriptBlock
If you have multiple systems you need to check, build a custom object to hold the array. This will also allow you to output to a CSV rather than a plain text file.
$Object = [PSCustomObject]@{
     Computer = $Computer.Name
     Status = $Status
}
Full code below. I've added error handling for the system being unreachable.
#############################################################
##
## PowerShell script - Benjamin Hubbard
##
## This script outputs to display and CSV the status of the 
## Windows Firewall on the inputted computer list.
##
#############################################################

$ComputerListFilter = "Name -like 'Serv*'"

$ComputerList = Get-ADComputer -Filter $ComputerListFilter
$ScriptBlock = { @(netsh advfirewall show domain state)[3] -replace 'State' -replace '\s' }

foreach ($Computer in $ComputerList) {
    if (Test-Connection -ComputerName $Computer.Name -Quiet -Count 1) {
        try {
            $Status = Invoke-Command -ComputerName $Computer.Name -ErrorAction Stop -ScriptBlock $ScriptBlock
        }
        catch {
            $Status = "Unable to retrieve firewall status"
        }
    }
    else {
        $Status = "Unreachable"
    }
    $Object = [PSCustomObject]@{
        Computer = $Computer.Name
        Status = $Status
    }

    Write-Output $Object
    $Object | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation

}

Tuesday, May 5, 2015

PowerShell: Error Handling When Accessing Remote Systems


Let me just get this out of the way: I love PowerShell.

Today I’d like to cover issues when accessing remote systems. Server unreachable. Firewall port not open. PowerShell Remoting not enabled. These are all issues that can throw wrenches into a script.

Server Unreachable

To check if a server is reachable, you can ping it. Or you can use the PowerShell cmdlet Test-Connection. In these examples, $Hostname contains the DNS name of the system.

Test-Connection -ComputerName $Hostname -Count 1 -Quiet

I recommend using full parameter names in a script to decrease any ambiguity and to increase the chances that the script will be compatible with later versions of PowerShell. Here we are testing the connection to system $Hostname, sending a single ping (-Count 1), and suppressing output to just either $true or $false (-Quiet). This allows for evaluation in an if-else block.

if (Test-Connection -ComputerName $Hostname -Quiet -Count 1) {
   Write-Output "$Hostname reachable"
}
else {
   Write-Output "$Hostname unreachable"
}

Firewall Port Not Open or PowerShell Remoting Not Enabled

What if the system is reachable and pinging, but the script is still throwing errors when trying to connect? Could be a firewall port issue. Or possibly PowerShell remoting is disabled. Regardless of why the script is throwing errors when connecting to the system, accounting for the problem will make life easier when it does happen.

This one is going to be a bit more complex.

First, the cmdlet: Invoke-Command. While it may sound like something out of a text-based role-playing game, Invoke-Command in reality allows remote execution of code and programs. You can run things on one system from another system. Again, $Hostname is simply the DNS name of the system. $MyBlockOfCode is whatever code that needs to be executed on the remote machine.

Invoke-Command -ComputerName $Hostname -ScriptBlock $MyBlockOfCode

But after executing the cmdlet, PowerShell throws an error about being unable to connect to the destination. You know that the server is online from using Test-Connection, so Test-Connection wasn’t able to catch this error. What you need is a try-catch block. You “try” this or that command, and if it works, great! If not, it will “catch” the error for you.

try {
   Invoke-Command -ComputerName $Hostname `
       -ScriptBlock $MyBlockOfCode -ErrorAction Stop
   Write-Output "$Hostname connection successful"
}
catch {
   Write-Output "$Hostname connection unsuccessful"
}

-ErrorAction Stop is so that any errors with Invoke-Command are treated as terminating errors rather than non-terminating errors. You don’t really need to know the why. Just that try-catch only will catch terminating errors.

Altogether we have the following code:
if (Test-Connection -ComputerName $Hostname -Quiet -Count 1) {
   try {
       Invoke-Command -ComputerName $Hostname `
           -ScriptBlock $MyBlockOfCode -ErrorAction Stop
       Write-Output "$Hostname connection successful"
   }
   catch {
       Write-Output "$Hostname connection unsuccessful"
   }
}
else {
   Write-Output "$Hostname unreachable"
}

I took out the line Write-Output "$Hostname connection successful" since it is redundant. If it doesn’t say unreachable, it is reachable and you’ll get “connection successful” or “connection unsuccessful” anyway.

And there you have some basic error handling for when you run into issues accessing remote systems.