Parts of ForEach loop in PowerShell being skipped over? - powershell

I have a Powershell script that seems to be functioning fine except for the fact that part of my output is skipped over and I can't figure out why. Here is the script being run:
#Server list provided for the script.
$ServerList = Get-Content $env:USERPROFILE\Documents\Servers.txt
#Counter for first If loop.
$Counter = 0
#Counter for second If loop.
$Counter2 = 0
#ForEach loop going through the server list, picking out OS, Drives, and CPU info and dumping it into an array.
ForEach($Server in ($ServerList))
{
"Collecting server information on $Server, please wait..."
"Collecting Operating System..."
$OS = gwmi Win32_OperatingSystem -ComputerName $Server | select Caption
"Collecting Storage..."
$Drives = gwmi Win32_LogicalDisk -ComputerName $Server | Format-Table DeviceId, #{n="Size in GB";e={[math]::Round($_.Size/1GB,2)}},#{n="Free Space in GB";e={[math]::Round($_.FreeSpace/1GB,2)}}
"Collecting CPU..."
$CPU = gwmi Win32_Processor -ComputerName $Server | select Name, Manufacturer
$ServerInfo = #($OS,$Drives,$CPU)
#$ServerInfo
#Do loop that posts the info stored in the array and ups the first counter. Runs while counter is equal to 0.
Do
{
"All done. Here's all the info we got on $($Server):"
$ServerInfo
$Counter++
}While ($Counter -eq 0)
#If loop that checks if the Counter has been bumped by the Do loop. Sets Counter to 0 and increases Counter2 by 1.
If ($Counter -eq 1)
{
$Counter = 0
$Counter2++
}
#If loop that checks if Coutner2 is equal to a certain number. This is the hard stop to the loop.
If ($Counter2 -eq 2)
{
"Max number of runs met. Stopping."
break
}
}
I know the script is messy and needs a lot of work, but for some reason after the second pass of the ForEach loop, the OS variable is completely skipped over, and doesn't show up in the console. After the first run, it leaves it out entirely and just posts the Drive and CPU information. I thought maybe it was something weird with the Do and If loops, so I commented them out to test but it's the same result.
I've tried posting the variables OS, Drives, and CPU as the loop runs to make sure it's actually saving something to the variable, and it is, and I'm also calling the variables themselves after the loop breaks to see if something weird is happening in the loop/script. I found out that calling the OS variable and the ServerInfo array after the loop finishes causes it to output with the OS information.
Any idea as to why that is?

I think the reason for the strange output is in the way you are using the counters.
If I understand the question correctly, you want (console) output for each server in the Servers.txt file, BUT maximized to a certain number. There could be over one hundred servers in the text file, but you want to limit the output to just a certain number of servers and then break out of the main ForEach loop.
Not only that, but you want to capture and later combine the different pieces of information you obtained using the various Get-WmiObject calls in a way that PowerShell uses to format them.
The trick there is to use | Out-String.
Below is my version of the script.
$ServerList = Get-Content $env:USERPROFILE\Documents\Servers.txt
$Counter = 0 # the running counter; gets incremented for each server
$MaxCount = 2 # the maximum number of servers you want to process
#ForEach loop going through the server list, picking out OS, Drives, and CPU info and dumping it into an array.
ForEach($Server in $ServerList) {
Write-Host "Collecting server information on $Server, please wait..."
Write-Host "Collecting Operating System..." -ForegroundColor Yellow
$OS = (Get-WmiObject Win32_OperatingSystem -ComputerName $Server |
Select-Object Caption |
Out-String).Trim()
Write-Host "Collecting Storage..." -ForegroundColor Yellow
$Drives = (Get-WmiObject Win32_LogicalDisk -ComputerName $Server |
Select-Object DeviceId,
#{n="Size in GB";e={[math]::Round($_.Size/1GB,2)}},
#{n="Free Space in GB";e={[math]::Round($_.FreeSpace/1GB,2)}} |
Out-String).Trim()
Write-Host "Collecting CPU..." -ForegroundColor Yellow
$CPU = (Get-WmiObject Win32_Processor -ComputerName $Server |
Select-Object Name, Manufacturer |
Out-String).Trim()
Write-Host
# join these strings together with newlines so the output will be readable
$ServerInfo = #($OS,$Drives,$CPU) -join "`r`n`r`n"
# output the serverInfo for this server.
$ServerInfo
Write-Host
# instead of the -join above which looks more like the original code you gave,
# you could also do:
# $ServerInfo = "{0}`r`n`r`n{1}`r`n`r`n{2}`r`n" -f $OS, $Drives, $CPU
# or even:
# Write-Host $OS
# Write-Host
# Write-Host $Drives
# Write-Host
# Write-Host $CPU
# Write-Host
# if $Counter has reached the maximum number of servers to process, break out of the ForEach loop
If (++$Counter -ge $MaxCount) {
Write-Host "Max number of runs ($MaxCount) met. Stopping." -ForegroundColor Green
return
}
Write-Host
}

Related

Optimizing Powershell Script to Query Remote Server OS Version

I want optimize a simple task: pull server OS version into a neat table. However, some servers in our environment have Powershell disabled. Below you fill find my script, which works! However, it takes about 20 seconds or so per server, since it waits for the server to return the results of the invoked command before moving onto the next server in the list. I know there's a way to asynchronously pull the results from a PS command, but is this possible when I need to resort to cmd line syntax for servers that can't handle PS, as shown in the catch statement?
$referencefile = "ps_servers_to_query.csv"
$export_location = "ps_server_os_export.csv"
$Array = #()
$servers = get-content $referencefile
foreach ($server in $servers){
#attempt to query the server with Powershell.
try{
$os_version = invoke-command -ComputerName $server -ScriptBlock {Get-ComputerInfo -Property WindowsProductName} -ErrorAction stop
$os_version = $os_version.WindowsProductName
} # If server doesnt have PS installed/or is disabled, then we will resort to CMD Prompt, this takes longer however.. also we will need to convert a string to an object.
catch {
$os_version = invoke-command -ComputerName $server -ScriptBlock {systeminfo | find "OS Name:"} # this returns a string that represents the datetime of reboot
$os_version = $os_version.replace('OS Name: ', '') # Remove the leading text
$os_version = $os_version.replace(' ','') # Remove leading spaces
$os_version = $os_version.replace('Microsoft ','') # Removes Microsoft for data standardization
}
# Output each iteration of the loop into an array
$Row = "" | Select ServerName, OSVersion
$Row.ServerName = $Server
$Row.OSVersion = $os_version
$Array += $Row
}
# Export results to csv.
$Array | Export-Csv -Path $export_location -Force
Edit: Here's what I'd like to accomplish. Send the command out to all the servers (less than 30) at once, and have them all process the command at the same time rather than doing it one-by-one. I know I can do this if they all could take PowerShell commands, but since they can't I'm struggling. This script takes about 6 minutes to run in total.
Thank you in advance!
If I got it right something like this should be all you need:
$referencefile = "ps_servers_to_query.csv"
$export_location = "ps_server_os_export.csv"
$ComputerName = Get-Content -Path $referencefile
$Result =
Get-CimInstance -ClassName CIM_OperatingSystem -ComputerName $ComputerName |
Select-Object -Property Caption,PSComputerName
$Result
| Export-Csv -Path $export_location -NoTypeInformation

How can I stop iterating through entire list multiple times in ForEach?

How can I apply 1 IP address in a list to 1 server in another list?
Then move onto the next IP and apply it to the next server.
servers.txt looks like:
server1
server2
server3
ip.txt looks like:
10.1.140.80
10.1.140.81
10.1.140.83
I just want to go through the list and apply
10.1.140.80 to server1
10.1.140.81 to server2
10.1.140.83 to server3
Instead, my script is applying all 3 IP addresses to each server.
I dont want to cycle through all IP addresses over and over.
How can I iterate through the lists properly and correct this?
$computers = "$PSScriptRoot\servers.txt"
$iplist = gc "$PSScriptRoot\ip.txt"
function changeip {
get-content $computers | % {
ForEach($ip in $iplist) {
# Set IP address
$remotecmd1 = 'New-NetIPAddress -InterfaceIndex 2 -IPAddress $ip -PrefixLength 24 -DefaultGateway 10.1.140.1'
# Set DNS Servers - Make sure you specify the server's network adapter name at -InterfaceAlias
$remotecmd2 = 'Set-DnsClientServerAddress -InterfaceAlias "EthernetName" -ServerAddresses 10.1.140.5, 10.1.140.6'
Invoke-VMScript -VM $_ -ScriptText $remotecmd1 -GuestUser Administrator -GuestPassword PASSWORD -ScriptType PowerShell
Invoke-VMScript -VM $_ -ScriptText $remotecmd2 -GuestUser Administrator -GuestPassword PASSWORD -ScriptType PowerShell
}
}
}
changeip
Use your the Get-Content cmdlt to place both file contents into an array then pull the individual values by array position. You'll probably want some logic to check if the array size matches and custom handling if it does not. In your above example, you are basically putting a for each loop inside another foreach loop which is giving you the behavior you are seeing.
$computers = GC "C:\server.txt"
$iplist = GC "C:\ip.txt"
for ($i = 0; $i -lt $iplist.Count ; $i++) {
Write-host ("{0} - {1}" -f $computers[$i],$iplist[$i])
}
Or if you are married to using the foreach logic for one list to be all fancy like instead of basic iterating with a for loop then you can add a counter in your foreach loop. Then you can look up your array index of your already parsed iplist array. It's basically doing the same thing though..
$computers = "C:\server.txt"
$iplist = GC "C:\ip.txt"
get-content $computers | % {$counter = 0} {
Write-host ("{0} - {1}" -f $_,$iplist[$counter])
$counter++
}
Just for clarity purposes as well, please note in this line:
"get-content $computers | %"
The % is actually an alias for ForEach-Object which is why you are getting the foreach inside a foreach output that you are seeing.

PowerShell using hashtable

The below scripts works, but it takes a long time to complete the job. Can somebody help me to convert this script to faster way.
$servers = Get-Content Servers.txt
$TCount = $servers.Count
$count = 1
foreach ($server in $servers) {
Write-Host "$Count / $Tcount - $Server" -NoNewline
$Result = Get-VM -Name $server | Set-Annotation -CustomAttribute "SNAP" -Value "True"
if ($Result.Value -eq "true") {
Write-Host "`t - Success" -fore "green"
} else {
Write-Host "`t - Failed" -fore "Red"
}
$count = $Count +1
}
How is it possible for something like Get-HardDisk -VM "myVM" to work at all? After all, “myVM” is a string, not a VirtualMachine, so shouldn’t this fail?
The reason this works is because VI Toolkit takes advantage of a feature of PowerShell that lets you transform the arguments you receive on the command line. This is the basis of what we call the VI Toolkit’s “Object By Name” feature: If you specify a string where an object should be, VI Toolkit works behind the scenes to replace this string with the object that the string represents.
Inevitably this lookup has a cost, the question is how much is that cost? This brings us to a rather unfortunate property of VI Toolkit, which is that when you get a VM, all filtering is done on the client side. On one hand this is good because it allows us to support wildcards and case-insensitivity. However there is one very unfortunate consequence, which is that it takes just as long to load one VM as it takes to load all of them (more on how we are improving this below). This is the basic reason that the second example is so slow: every time Get-HardDisk is called, VI Toolkit looks up that one machine object behind the scenes.
http://blogs.vmware.com/PowerCLI/2009/03/why-is-my-code-so-slow.html
try:
$servers = Get-Content Servers.txt
$VMs = Get-VM | Where-Object {$_.Name -in $servers}
$TCount = $servers.Count
$count = 1
foreach ($vm in $VMs)
{
Write-Host "$Count / $Tcount - $Server" -NoNewline
$Result = $vm | Set-Annotation -CustomAttribute "SNAP" -Value "True"
if ($Result.Value -eq "true") {
Write-Host "`t - Success" -fore "green"
} else {
Write-Host "`t - Failed" -fore "Red"
}
$count = $Count +1
}
to load the VMs once, then loop over the $VMs to add the annotation.

Local Groups and Members

I have a requirement to report the local groups and members from a specific list of servers. I have the following script that I have pieced together from other scripts. When run the script it writes the name of the server it is querying and the server's local group names and the members of those groups. I would like to output the text to a file, but where ever I add the | Out-File command I get an error "An empty pipe element is not allowed". My secondary concern with this script is, will the method I've chosen the report the server being queried work when outputting to a file. Will you please help correct this newbies script errors please?
$server=Get-Content "C:\Powershell\Local Groups\Test.txt"
Foreach ($server in $server)
{
$computer = [ADSI]"WinNT://$server,computer"
"
"
write-host "==========================="
write-host "Server: $server"
write-host "==========================="
"
"
$computer.psbase.children | where { $_.psbase.schemaClassName -eq 'group' } | foreach {
write-host $_.name
write-host "------"
$group =[ADSI]$_.psbase.Path
$group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
write-host **
write-host
}
}
Thanks,
Kevin
You say that you are using Out-File and getting that error. You don't show_where_ in your code that is being called from.
Given the code you have my best guess is that you were trying something like this
Foreach ($server in $server){
# All the code in this block
} | Out-File c:\pathto.txt
I wish I had a technical reference for this interpretation but alas I have not found one (Think it has to do with older PowerShell versions). In my experience there is not standard output passed from that construct. As an aside ($server in $server) is misleading even if it works. Might I suggest this small change an let me know if that works.
$servers=Get-Content "C:\Powershell\Local Groups\Test.txt"
$servers | ForEach-Object{
$server = $_
# Rest of code inside block stays the same
} | Out-File c:\pathto.txt
If that is not your speed then I would also consider building an empty array outside the block and populate is for each loop pass.
# Declare empty array to hold results
$results = #()
Foreach ($server in $server){
# Code before this line
$results += $group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
# Code after this line
}
$results | Set-Content c:\pathto.txt
Worthy Note
You are mixing Console output with standard output. depending on what you want to do with the script you will not get the same output you expect. If you want the lines like write-host "Server: $server" to be in the output file then you need to use Write-Output

How to implement a parallel jobs and queues system in Powershell [duplicate]

I spent days trying to implement a parallel jobs and queues system, but... I tried but I can't make it. Here is the code without implementing nothing, and CSV example from where looks.
I'm sure this post can help other users in their projects.
Each user have his pc, so the CSV file look like:
pc1,user1
pc2,user2
pc800,user800
CODE:
#Source File:
$inputCSV = '~\desktop\report.csv'
$csv = import-csv $inputCSV -Header PCName, User
echo $csv #debug
#Output File:
$report = "~\desktop\output.csv"
#---------------------------------------------------------------
#Define search:
$findSize = 40GB
Write-Host "Lonking for $findSize GB sized Outlook files"
#count issues:
$issues = 0
#---------------------------------------------------------------
foreach($item in $csv){
if (Test-Connection -Quiet -count 1 -computer $($item.PCname)){
$w7path = "\\$($item.PCname)\c$\users\$($item.User)\appdata\Local\microsoft\outlook"
$xpPath = "\\$($item.PCname)\c$\Documents and Settings\$($item.User)\Local Settings\Application Data\Microsoft\Outlook"
if(Test-Path $W7path){
if(Get-ChildItem $w7path -Recurse -force -Include *.ost -ErrorAction "SilentlyContinue" | Where-Object {$_.Length -gt $findSize}){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),$w7path
$newLine | add-content $report
$issues ++
Write-Host "Issue detected" #debug
}
}
elseif(Test-Path $xpPath){
if(Get-ChildItem $w7path -Recurse -force -Include *.ost -ErrorAction "SilentlyContinue" | Where-Object {$_.Length -gt $findSize}){
$newLine = "{0},{1},{2}" -f $($item.PCname),$($item.User),$xpPath
$newLine | add-content $report
$issues ++
Write-Host "Issue detected" #debug
}
}
else{
write-host "Error! - bad path"
}
}
else{
write-host "Error! - no ping"
}
}
Write-Host "All done! detected $issues issues"
Parallel data processing in PowerShell is not quite simple, especially with
queueing. Try to use some existing tools which have this already done.
You may take look at the module
SplitPipeline. The cmdlet
Split-Pipeline is designed for parallel input data processing and supports
queueing of input (see the parameter Load). For example, for 4 parallel
pipelines with 10 input items each at a time the code will look like this:
$csv | Split-Pipeline -Count 4 -Load 10, 10 {process{
<operate on input item $_>
}} | Out-File $outputReport
All you have to do is to implement the code <operate on input item $_>.
Parallel processing and queueing is done by this command.
UPDATE for the updated question code. Here is the prototype code with some
remarks. They are important. Doing work in parallel is not the same as
directly, there are some rules to follow.
$csv | Split-Pipeline -Count 4 -Load 10, 10 -Variable findSize {process{
# Tips
# - Operate on input object $_, i.e $_.PCname and $_.User
# - Use imported variable $findSize
# - Do not use Write-Host, use (for now) Write-Warning
# - Do not count issues (for now). This is possible but make it working
# without this at first.
# - Do not write data to a file, from several parallel pipelines this
# is not so trivial, just output data, they will be piped further to
# the log file
...
}} | Set-Content $report
# output from all jobs is joined and written to the report file
UPDATE: How to write progress information
SplitPipeline handled pretty well a 800 targets csv, amazing. Is there anyway
to let the user know if the script is alive...? Scan a big csv can take about
20 mins. Something like "in progress 25%","50%","75%"...
There are several options. The simplest is just to invoke Split-Pipeline with
the switch -Verbose. So you will get verbose messages about the progress and
see that the script is alive.
Another simple option is to write and watch verbose messages from the jobs,
e.g. Write-Verbose ... -Verbose which will write messages even if
Split-Pipeline is invoked without Verbose.
And another option is to use proper progress messages with Write-Progress.
See the scripts:
Test-ProgressJobs.ps1
Test-ProgressTotal.ps1
Test-ProgressTotal.ps1 also shows how to use a collector updated from jobs
concurrently. You can use the similar technique for counting issues (the
original question code does this). When all is done show the total number of
issues to a user.