PowerShell using hashtable - powershell

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.

Related

Parts of ForEach loop in PowerShell being skipped over?

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
}

powershell cmdlet Get-Vm not displaying at the correct time

This is the script in it's most current form, on line 6 of the code I am trying to get it to display the current condition of the virtuals on the host.
when I run the code, it doesn't display until the very end.
It runs properly everywhere else, and it proforms the actions like I want it to. But Get-VM doesn't run until right before the script exits.
Is there some sort of forcing command that makes commands execute that I don't know of? I know it's probably just some stupid mistake I am not seeing.
Help is appreciated, if you need further clarification, just let me know.
#new-alias -Name rh -Value read-host
Get-VM
$AA = rh "Would you like to preform an action?[y/n]"
While($AA -eq "y"){
$A = rh "What would you like to do? `n save?[sa] `n start?[st] `n stop?[x]
`n restart?[re]`n "
# blank-vm -name blank
If($A -eq "sa"){
$s = "Save"
$B = rh "Which VM would you like to $s ?[a for all]"
if($B -eq "a"){
Get-VM | Save-VM
}
else{save-vm -name "$B"}
}
If($A -eq "st"){
$s = "Start"
$B = rh "Which VM would you like to $s ?[a for all]"
if($B -eq "a"){
Get-VM | Start-VM
}
else{Start-vm -name "$B"}
}
If($A -eq "x"){
$s = "Stop"
$B = rh "Which VM would you like to $s ?[a for all]"
if($B -eq "a"){
Get-VM | stop-VM
}
else{Stop-vm -name "$B"}
}
If($A -eq "re"){
$s = "Restart"
$B = rh "Which VM would you like to $s ?[a for all]"
if($B -eq "a"){
Get-VM | restart-VM
}
else{restart-vm -name "$B"}
}
$AA = rh "Would you like to preform another action?[y/n]"
}
This seems to be a problem with the Hyper-V cmdlets and the Read/Write-Host cmdlets, e. g.
Get-VM
Write-Host "Test"
will result in first writing the string Test to the host and then outputting the result of Get-VM. Get-VMHost, Get-VMHardDisk and Get-VMSwitch have the same issue. The "standard" cmdlets like Get-Service and Get-Process are working normally, e. g.
Get-Service
Write-Host "Test"
will first outputting a list of services and then the string Test. Using the Write-Output instead of Write-Host will result in normal behavior too.
To avoid this you can convert your Get-VM output to string, e. g.:
Get-VM | Out-String
Write-Host "Test"
Other cmdlets could have the same issue

Is it possible to output subsequent information to same line

I have a script that pings an ip address and send that information to a console window. In the case of high ping times or missed pings, it is also written to a log. I would like to keep only the high ping times and missed pings in the console window and allow the good pings to overwrite each other. Is that possible?
For high ping times, this is the output (similar code is used for missed pings).
$out = ("{0}ms at $(get-date -format G)" -f $ping.ResponseTime)
write-host $out -foregroundcolor "yellow"
$out >> .\runningPing$ipAddress.txt
For normal ping times, the output is this.
$out ("{0}ms" -f $ping.ResponseTime)
write-host $out -foregroundcolor "green"
I'd like to make that last line just overwrite itself for normal pings, but let the high and missed pings push up the screen as the program runs. Is that something I can do in PS?
SOLUTION
Thanks to #Mathias R. Jensen, I came up with this solution:
if ($ping.statuscode -eq 0) {
if ($ping.responsetime -gt $waitTime) {
$highPings = $highPings + 1
$out = ("{0}ms at $(get-date -format G)" -f $ping.ResponseTime)
[console]::SetCursorPosition(0,$highPings + $droppedPings + 1)
write-host $out -foregroundcolor "yellow"
$out >> $outFile
}
else {
$out = ("{0}ms $i of $pingCount" -f $ping.ResponseTime)
[console]::SetCursorPosition(0,$highPings + $droppedPings + 2)
write-host $out -foregroundcolor "green"
}
}
else {
$droppedPings = $droppedPings + 1
$out = ("missed ping at $(get-date -format G)")
[console]::SetCursorPosition(0,$highPings + $droppedPings + 1)
write-host $out -foregroundcolor "red"
$out >> $outFile
}
I think you should use Write-Progress for the good pings. You don't need to give a percentage, and you can use the -Status parameter to show just the last good one.
Here's a small example I wrote that might demonstrate how it would look/operate (you can execute this yourself to see, it doesn't ping anything it's just a simulation):
$goods = 0
0..100 | % {
if ((Get-Random -Minimum 0 -Maximum 100) -ge 50) {
$goods += 1
Write-Progress -Activity Test -Status "Last good ping: $_ ($goods total good pings)"
} else {
Write-Warning "Bad ping"
}
Start-Sleep -Seconds 1
}
In this case you could have even calculated, for example a percentage of good pings and used that in Write-Progress but I wanted to show that you don't need to use it as a progress bar for it to be useful.
As I mentioned in the comments, the cursor position can be controlled by this method:
[control]::SetCursorPosition([int]$x,[int]$y)
The [console] type accelerator points to the same Console class that enables you to WriteLine() to the console in a C# console application. You can also control colors and other console behavior if you feel like it:
Clear-Host
[console]::ForegroundColor = "Red"
1..10|%{
[console]::SetCursorPosition(2+$_,$_-1)
[console]::WriteLine("Hello World!")
}
[console]::ForegroundColor = "Green"
briantist has the better approach to this but I was playing around and came up with this as well. It will not work in ISE but should do it's job on the PowerShell console. It uses "`b" which is the backspace character so that the text will overwrite itself on the console host write. Might not help you but could be useful to others.
switch($ping.ResponseTime){
{$_ -ge 0 -and $_ -le 100}{
$out = "{0}ms" -f $_
$options = #{ForegroundColor = "Green"; NoNewline = $true}
$backup = "`b" * $out.Length
}
{$_ -ge 500 -and $_ -le 900}{
$out = "{0}ms at $(get-date -format G)" -f $_
$options = #{ForegroundColor = "Yellow"; NoNewline = $false}
$backup = "`n"
}
}
Write-Host "$backup$out" #options
Uses a switch to set the options based on the the range of ping times. Sets a small hash table which is splatted to the write-host. Not perfect but it shows another way to do it.
Again this mostly done for fun.

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

Hash table Get_Item returning blank line - Powershell

$searchterm = read-host “Enter search term for uninstallers”
$uninstallers = get-childitem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
$founditems = $uninstallers | ? {(Get-ItemProperty -path (“HKLM:\”+$_.name) -name Displayname -erroraction silentlycontinue) -match $searchterm}
write-host “Searched registry for uninstall information on $searchterm”
write-host “——————————————”
$x = 0
$uninstallcommandtable = #{}
$uninstalldisplaytable = #{}
if ($founditems -eq $null) {“None found”} else {
write-host “Found “($founditems | measure-object).count” item(s):`n”
$founditems | % {
$x = $x + 1
Write-host "Item: $x"
Write-host “Displayname: “$_.getvalue(“Displayname”)
Write-host “Displayversion: “$_.getvalue(“Displayversion”)
Write-host “InstallDate: “$_.getvalue(“InstallDate”)
Write-host “InstallSource: “$_.getvalue(“InstallSource”)
Write-host “UninstallString: “$_.getvalue(“UninstallString”)
$uninstallcommandtable.Add($x, $_.getvalue(“UninstallString”))
$uninstalldisplaytable.Add($x, $_.getvalue(“Displayname”))
Write-host “`n”
}
}
Write-host ($uninstalldisplaytable | Out-String)
$whichprogram = read-host "Which program do you want to uninstall?"
Write-host ($uninstallcommandtable.Get_Item($whichprogram) | Out-String)
For some reason the last Write-host is returning a blank line. I verified with a test output just before the last read-host, so I know the $uninstallcommandtable is proper. Any ideas would be great.
Because your hashtable Names are type System.Int32. This will show you that:
$uninstallcommandtable.Keys | % {$_.GetType().FullName}
Read-Host is setting a variable of type System.String. So you will need to convert the string to an System.Int32 like this:
Write-host $uninstallcommandtable.Get_Item([Int32] $whichprogram)
You can also use:
Write-host $uninstallcommandtable.Item([Int32] $whichprogram)
Alternatively, you can make the key a string when you create the hash entry:
$uninstallcommandtable.Add("$x", $_.getvalue(“UninstallString”))