Taking advantage of workflows: foreach -parallel, invoke-command, script structuring

1.5k views Asked by At

From a machine running PowerShell v5 April preview:

  • Trying to get a list of (yes, windows 2003, they need to be gone!) servers from AD and note the total servers found
  • Test-Connection to them all and note the total responsive
  • invoke a command (which requires different credentials) on all the servers to run a bunch of commands to gather info about each server and output to a file on them
  • create new PSDrives to map to each server to download the file and note the total files downloaded (i.e. difference between servers that respond to a ping and successfully invoke the command/download results)
  • then run a Get-Printers cmdlet, which won't run on PowerShell v2, and output to a file in the same folder as the downloaded results on the local machine

I've got a working script - it is just very slow and I would like to learn and improve it and make it faster and more elegant. I'm finding it difficult to write this all out and explain haha. If you were trying to tackle this, what way would you set it out?

To begin working out a better way:

Getting a list of the servers and storing in a variable is ok.

$2003s = Get-ADComputer -Filter {OperatingSystem -eq "Windows server 2003"} -Properties  OperatingSystem

Getting a total is ok.

$Total2003s = $2003s.count

Now there might be some 2003 servers that don't exist anymore that weren't removed from AD, so lets ping them all.

$Responsive2003s = Test-Connection $2003s.dnshostname -Delay 1 -Count 1 -ErrorAction SilentlyContinue

This takes forever if there are say 300 objects in $2003s and half don't respond.

Measure-Command {Test-Connection $2003s.dnshostname -Delay 1 -Count 1 -ErrorAction SilentlyContinue}

7 minutes...

So I thought, hey, lets make a Powershell Workflow so I can use foreach -parallel. If I write a quick workflow to get the servers from AD and foreach Test-Connection:

Measure-Command {Workflow-Testconnection}

27 seconds....

Just getting this part alone to work would save time, however I can't work out which way to structure my script/function/workflow or whatever will work best.

I've come up with two roadblocks.

  1. In the small workflow for pinging the servers I can't (work out how to) store the results in a variable to see the total (either with $variable = Test-Connection ..., or Test-Connection | New-Variable, so there's no point in pinging them if I can't see a difference between the servers in ad and the servers that respond. I can use

    $2003sDnsHostname | foreach {
      Get-Printer -ThrottleLimit 500 -ErrorAction SilentlyContinue -ComputerName $_ |
        Sort-Object -Property Portname |
        FT -AutoSize |  Out-File -FilePath "D:\$($_) - Printers.txt"
    }
    

    but it's slow, and if I use the faster workflow, I can't use Format-Table on the output of Get-Printers.

  2. I tried to make the entire thing a workflow using foreach -parallel in the desired spots, but because I've got a $creds = Get-Crendential to work with my Invoke-Command -Credential $Creds the workflow won't even load.

It seems that for every benefit I want with each step of my script, there is a dealbreaker which makes it not worth doing, but I'm sure there is a way :)

The entire working but slow thing, edited to remove sensitive stuff and the like just to get the concept. It could be made into a function with parameters and verbose output etc, but that's also a to-do thing. I want to see if it can be sped up first.

$SVRAcctCreds = Get-Credential

#Enable the ActiveDirectory module as first time users might not have it already on their computer, and it doesn't hurt to enable it again if it's already there
Enable-WindowsOptionalFeature -Online -FeatureName RemoteServerAdministrationTools-Roles-AD-Powershell -NoRestart

$2003s = Get-ADComputer -Filter {OperatingSystem -eq "Windows server 2003"} -Properties operatingsystem
$2003sDnsHostname = $2003s.dnshostname
$Total = $2003s.count
$Responsive2003s = Test-Connection $2003s.dnshostname -Delay 1 -Count 1 -ErrorAction SilentlyContinue
$TotalResponsive2003s = $Responsive2003s.count

Invoke-Command -ComputerName ($2003s.dnshostname) -Credential $SVRAcctCreds -ThrottleLimit 100 -ErrorAction SilentlyContinue -ScriptBlock {
  #Make folder for output
  mkdir Z:\2003Migration | Out-Null

  #Serial number, model number, output to file
  Get-WmiObject win32_computersystem |
    Select-Object Manufacturer, Model | Format-List |
    Out-File -Append -FilePath Z:\2003Migration\"$env:SiteCode $env:SiteName"_MigrationData.txt
  Get-WmiObject win32_bios | Select-Object SerialNumber |
    Out-File -Append -FilePath Z:\2003Migration\"$env:SiteCode $env:SiteName"_MigrationData.txt
  Systeminfo | Select-String "Install Date:" |
    Out-File -Append -FilePath Z:\2003Migration\"$env:SiteCode $env:SiteName"_MigrationData.txt
}

##### Download Gathered data from servers
$2003s | ForEach-Object {
  New-PSDrive -ErrorAction SilentlyContinue -PSProvider FileSystem -Name $_.name -Credential $SVRAcctCreds -Root "\\$($_.dnshostname)\z$\2003Migration"
} | Out-Null
Get-PSDrive | where name -Like "SVR*" | foreach {
  Copy-Item "$($_.Name):" -Recurse -Destination d:\ -ErrorAction SilentlyContinue
} 
$TotalPSDrives = (Get-PSDrive | where name -Like "SVR*").count

# Report other information

"Total number of servers on Windows Server 2003 in AD, matching by OperatingSystem " | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"
"$Total" | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"

"Total Number of servers that responded to a ping command" | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"
"$TotalResponsive2003s" | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"

"Total Number of servers that ran the commands and returned data" | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"
"Total Number of servers that ran the commands and returned data downloaded to D:\2003Migration"
"$TotalPSDrives " | Out-File -Append -FilePath "d:\2003Migration\_Total Server counts.txt"
"$TotalPSDrives "
"Mismatch means server could be pinged but could not run a powershell session to invoke commands, possible hard drive full? Powershell remoting not enabled?"

####Printers
# Didn't have enough time to work out how to only ask responsive servers the printers, so ask all, takes longer, then clean up empty files
$2003sDnsHostname | foreach {
  Get-Printer -ThrottleLimit 500 -ErrorAction SilentlyContinue -ComputerName $_ |
    Sort-Object -Property Portname | FT -AutoSize |
    Out-File -FilePath "D:\2003Migration\$($_) - Printers.txt"
}
Get-ChildItem D:\2003Migration | where Length -EQ 0 | Remove-Item
#clean up text files left on server
Invoke-Command -ComputerName ($2003s.dnshostname) -Credential $SVRAcctCreds -ThrottleLimit 100 -ErrorAction SilentlyContinue -ScriptBlock {
  Remove-Item "Z:\2003migration" -Recurse -ErrorAction SilentlyContinue
}
1

There are 1 answers

0
Ansgar Wiechers On

I always found workflows a little tricky, because they behave subtly different from "regular" PowerShell. You may have less of a hard time using jobs instead. Something like this would give you a list with the names of only those servers that responded to Test-Connection:

$Responsive2003s = $2003s | % {
  Start-Job -ScriptBlock {
    Param($name, $address)
    if (Test-Connection $address -Delay 1 -Count 1 -EA SilentlyContinue) {
      $name
    }
  } -ArgumentList $_.Name, $_.IPv4Address
} | Wait-Job | Receive-Job