Compare values with previous loop - arrays

I made this script to monitor free space of server partitions. Now I'm trying to compare latest loop with previous one with storing free space values to arrays, but I've never done anything like this.
I don't know how exactly to work with arrays - one has to be erased at the beginning of the loop and second has to store the previous values, then they have to be compared.
Could you please give me at least a hint?
#play star-wars imperial march
function play-alarm {
Start-Job {
[Console]::Beep(440,500)
}
}
$cred = Get-Credential domain\username
$hostname = "server1"
#if free space lower, then play alarm
$low_level = "10"
#get drives letters
$partitions = Get-WmiObject Win32_LogicalDisk -Computername $hostname -Credential $cred | foreach DeviceID
#create arrays
$old_values = New-Object System.Collections.ArrayList
$new_values = New-Object System.Collections.ArrayList
#noob loop
$repeat = "2"
while ($i -lt $repeat) {
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host "Free space at server:" -BackgroundColor Black
#backup previouse values and clear array for new ones
$old_values = $new_values
$new_values.Clear()
foreach ($partition in $partitions) {
$device = "DeviceID='" + $partition + "'"
$size = Get-WmiObject Win32_LogicalDisk -Credential $cred -ComputerName $hostname -Filter $device |
ForEach-Object {$_.Size}
$size = [Math]::Round($size/1GB)
$free = Get-WmiObject Win32_LogicalDisk -Credential $cred -ComputerName $hostname -Filter $device |
ForEach-Object {$_.FreeSpace}
$free = [Math]::Round($free/1GB)
Write-Host $free
#add value rounded to GB
$new_values.Add($free)
#if device is CD-ROM, the size or space is zero - this cause error when calculating percentage of free space
if ($size -eq "0") {
Write-Host "disk " $partition "is CD-ROM" -ForegroundColor Yellow
} else {
$perc = ($free/$size)*100
$perc = [Math]::Round($perc, 3)
if ($perc -le $low_level) {
Write-Host "Not enough free space at partition " $partition "!!!" $perc "%" -BackgroundColor Red #| play-alarm
Start-Sleep -s 15
} else {
Write-Host "disk " $partition "is OK - " $perc "%" -ForegroundColor Green
}
}
}
if ($old_values -eq $new_values) {
Write-Host "no change..."
} else {
Write-Host "Attention - change!!!" -BackgroundColor Red
}
$time = $((Get-Date).ToString())
Write-Host "Loop finished..." $time -BackgroundColor Black
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Start-Sleep -s 300
}

Copy previous values
$old_values = $new_values
$new_values.Clear()
The problem with this is $old_values is now referring to the same array as $new_values. Use
$old_values = $new_values.Clone()
to create a copy.
Compare
You don't want to use -eq to compare the contents of container objects. You can loop through the arrays and compare each element
for ($index=0;$index -lt $old_values.count;$index+=1) {
if ($old_values[$index] -ne $new_values[$index]) {
write-host "Changed!"
break
}
}
Or you can use Compare-Object
if (Compare-Object $old_values $new_values) {
write-host "No Change!"
} else {
write-host "Changed!"
}
Compare-Object returns a list of differences. If the contents you compare are the same, then it returns an empty (and boolean false) value.

If you want to compare current values to those of a previous run you need to store the current values somewhere at the end of the script (in a CSV file for instance), and read that file (if it exists) at the beginning of the script.
$csv = 'C:\path\to\data.csv'
if (Test-Path -LiteralPath $csv) {
$old_values = Import-Csv $csv
} else {
# initialize $old_values as an empty array if the file doesn't exist (yet)
$old_values = #()
}
...
$new_values | Export-Csv $csv -NoType
Build $new_values as a list of custom objects by selecting the relevant properties of the WMI data. Avoid formatting the diskspace data unless it's for output to the user.
$new_values = Get-WmiObject Win32_LogicalDisk -Computer $hostname -Credential $cred |
Select-Object DeviceID, Size, FreeSpace
You can use calculated properties for adding derived information (like the free disk space ratio):
... | Select-Object DeviceID, Size, FreeSpace, #{n='Ratio';e={$_.FreeSpace/$_.Size}}
Use Compare-Object for comparing old and new data:
Compare-Object -ReferenceObject $old_values -DifferenceObject $new_values -Property DeviceID, Size, FreeSpace

Related

PowerShell Get Windows OS Version Fast and Do Different Things

Is there a faster way to get a specific registry value from a list of servers? I'm selecting a text file of computers with different flavors of windows and getting the OS product name. I'm finding that it's taking a couple seconds per computer to retrieve.
Current script:
Clear-Host
# Prompt for file containing list of target
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$myDialog = New-Object System.Windows.Forms.OpenFileDialog
$myDialog.Title = "Select File of Target Systems"
$myDialog.InitialDirectory = $PSScriptRoot
$myDialog.Filter = "TXT (*.txt) | *.txt"
$result = $myDialog.ShowDialog()
If ($result -eq "OK") {
$Computers = Get-Content $myDialog.FileName
}
Else {
Write-Host "`nCancelled by User`n"
}
$Array = #()
# Loop Through Computers
ForEach ($Computer in $Computers) {
Write-Warning "Processing $Computer"
# Get Registry Values
Try {
$OSVersion = Invoke-Command -ComputerName $Computer -ScriptBlock { (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName }
# Create a custom object
$ComplexObject = New-Object PSCustomObject
$ComplexObject | Add-Member -MemberType NoteProperty -Name "Server name" -Value $Computer
$ComplexObject | Add-Member -MemberType NoteProperty -Name "OS Version" -Value $OSVersion
# Add custom object to our array
$Array += $ComplexObject
}
Catch {
$_.Exception.Message
Break
}
}
# Results
If ($Array) {
# Display results in new window
$Array | Out-GridView -Title "OS Version Results"
# Display results in PS console
$Array
}
My end goal later on in the script is to do different things based on the OS version so I want to separate them into independent lists:
If (We have Win2008 servers) {
"Do This"
}
If (We have Win2012R2 servers) {
"Do This"
}
Clear-Host
# Prompt for file containing list of target
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$myDialog = [System.Windows.Forms.OpenFileDialog]::new()
$myDialog.Title = "Select File of Target Systems"
$myDialog.InitialDirectory = $PSScriptRoot
$myDialog.Filter = "TXT (*.txt) | *.txt"
$result = $myDialog.ShowDialog()
If ($result -eq "OK") {
$Computers = Get-Content $myDialog.FileName
}
Else {
Write-Host "`nCancelled by User`n"
}
# Get Registry Values
$Array = Try {
Invoke-Command -ComputerName $Computers -ScriptBlock {
(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName
} -ErrorAction stop | Select-Object #{n="Server Name";e={$_.pscomputername}},
#{n="OS Version";e={$_}}
}
Catch {
write-warning $_.Exception.Message
break
}
# Results
If ($Array) {
# Display results in new window
$Array | Out-GridView -Title "OS Version Results"
# Display results in PS console
$Array
}
You can use Get-AdComputer like:
Get-ADComputer -Filter {(OperatingSystem -like "*windows*server*") -and (Enabled -eq "True")} -Properties OperatingSystem | Select -ExpandProperty OperatingSystem | ForEach {
If($_ -match "Windows Server 2008.*"){
# Server 2008
}
If($_ -match "Windows Server 2012.*"){
# Server 2012
}
# Add more like 2016,2019
}

Get-WMIObject Win32_NetworkAdapterConfiguration - Looping through X number of DNS Servers for each NIC

For a group of computers, I want to connect to each one, enumerate each NIC that has a real IP, and for each NIC I want to loop through each DNS server (first, second, third, fourth, fifth etc) and if it contains a specific IP (e.g. 8.8.8.8) I want to replace it (with 7.7.7.7) without affecting any other DNS Servers. How do I do this for X number of DNS Servers without doing each server entry one at a time (like I have done below for the first 3) and change it at the same time.
$computer ='wrk-01'
$NICs = Get-WMIObject Win32_NetworkAdapterConfiguration -computername $computer |
where{$_.IPEnabled -eq "TRUE"}
Foreach($NIC in $NICs) {
# $DNSServerCount = $Nic.DNSServerSearchOrder.Count
# $DNSServerArray = #($nic.DNSServerSearchOrder[$DNSServerCount - $DNSServerCount,$DNSServerCount - $DNSServerCount + 1,$DNSServerCount - $DNSServerCount + 2])
# write-host $nic.description
# write-host $nic.PSComputerName $DNSServerArray
if($nic.dnsserversearchorder[0] -like "8.8.8.8"){
write-host "matched 8.8.8.8 at position 0"
$NIC.SetDNSServerSearchOrder(#("7.7.7.7", $nic.dnsserversearchorder[1], $nic.dnsserversearchorder[2]))
}
if($nic.dnsserversearchorder[1] -like "8.8.8.8"){
write-host "matched 8.8.8.8 at position 1"
$NIC.SetDNSServerSearchOrder(#($nic.dnsserversearchorder[0],"7.7.7.7", $nic.dnsserversearchorder[2]))
}
if($nic.dnsserversearchorder[2] -like "8.8.8.8"){
write-host "matched 8.8.8.8 at position 2"
$NIC.SetDNSServerSearchOrder(#($nic.dnsserversearchorder[0], $nic.dnsserversearchorder[1],"7.7.7.7"))
}
}
Here's one way to check and update if there is a match. Once a match is found, it will replace any matching line with the desired replacement otherwise output, the collected list is then set as the new DNS servers. If none match, it will just continue to the next nic.
$computer ='wrk-01'
$NICs = Get-WMIObject Win32_NetworkAdapterConfiguration -computername $computer |
where{$_.IPEnabled -eq "TRUE"}
Foreach($NIC in $NICs)
{
$DNSIPs = $nics.DNSServerSearchOrder
if($DNSIPs -contains '8.8.8.8')
{
Write-Host "Match found... updating" -ForegroundColor Green
$NewDNS = $DNSIPs | foreach {
$_ -replace '8.8.8.8','7.7.7.7'
}
$null = $NICs.SetDNSServerSearchOrder($NewDNS)
}
}
One recommendation that may save you some headache is to not use plural variable names, but instead use a descriptive name like this.
$computer ='wrk-01'
$NICList = Get-WMIObject Win32_NetworkAdapterConfiguration -computername $computer |
where{$_.IPEnabled -eq "TRUE"}
Foreach($NIC in $NICList)
{
$DNSIPList = $nics.DNSServerSearchOrder
if($DNSIPList -contains '8.8.8.8')
{
Write-Host "Match found... updating" -ForegroundColor Green
$NewDNS = $DNSIPList | foreach {
$_ -replace '8.8.8.8','7.7.7.7'
}
$null = $NICs.SetDNSServerSearchOrder($NewDNS)
}
}

Expand property in hashtable

I'm having a simple issue with a script, where I want to run a GCI against a remote server, issue is, the value is combined with another hashtable property, so the GCI fails.
The script reads entries from a two-column .csv, the headers are "server" and "platform"
Here's what I've got:
$ShortDate = (Get-Date).ToString('MM/dd/yyyy')
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI \\$_.Server\c$\log\Completed_Summary_*.html -EA 0 | where {$.LastWriteTime -ge "$ShortDate"}) {
Write-Host "FOUND"
} # end of IF GCI
} # end of For-Each
$serverObjects += New-Object -TypeName PSObject -Property $CheckServer
The problem is that the entry for $_.Server should be SERVER1, SERVER2, SERVER3, etc, all the entries in the servers.csv, instead, the values for both $_.Server and $_.Platform are combined. Such as:
Write-Host "Checking" \\#{Server=SERVER1; Platform=PLATFORM_1}.Server\c$\log\Completed_Summary_*.html
it should show as follows:
Write-Host "Checking" \\SERVER1\log\Completed_Summary_*.html
How do I un-combine them so that the GCI command works?
PowerShell only does simple variable expansion inside strings. For more complex expressions like index operations or accessing object properties/methods it would insert the stringified value of the array or object variable and leave the rest of the operation untouched.
Demonstration:
PS C:\> $array = 23, 42
PS C:\> Write-Host "some $array[1] or other"
some 23 42[1] or other
PS C:\> $object = New-Object -Type PSObject -Property #{Foo=23; Bar=42}
PS C:\> Write-Host "some $object.Foo or other"
some #{Bar=42; Foo=23}.Foo or other
To avoid this you need to either:
assign the resulting value to a variable first and use that variable in the string:
$value = $array[5]
Write-Host "some $value or other"
$value = $object.Foo
Write-Host "some $value or other"
use a subexpression ($(...)):
Write-Host "some $($array[5]) or other"
Write-Host "some $($object.Foo) or other"
use the format operator (-f):
Write-Host "some {0} or other" -f $array[5]
Write-Host "some {0} or other" -f $object.Foo
modify like it
$ShortDate = Get-Date -Hour 0 -Minute 0 -Second 0
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
$Dir="C:\temp"
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI "\\$($_.Server)\c$\log\Completed_Summary_*.html" -EA 0 | where {$_.LastWriteTime -ge $ShortDate})
{
Write-Host "FOUND"
}
}

Ping a list of host names and output the results to a csv in powershell

I have a large list of hostnames I need to ping to see if they are up or down. I'm not really that great at scripting but I managed to figure this much out:
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name is up" -ForegroundColor Green
}
else{
Write-Host "$name is down" -ForegroundColor Red
}
}
This gets me what I need but i now need to write out these results to a csv file and i have no idea how to do that.
Please Help!
You can use the following code instead (I simply altered the write-host calls to CSV formatting) and execute it with "PowerShell.exe script.ps > output.csv"
Note that you must execute it from the folder that contains hnames.txt, or simply change the "hnames.txt" to a full path.
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name,up"
}
else{
Write-Host "$name,down"
}
}
P.S. You can also use the Out-File Cmdlet to create the CSV file
I am a complete newbie to Powershell, so I took this on as a learning task, as I needed a quick and simple way to check a list of PC's for up/down status. These tweaks were needed to get it to output cleanly to the screen and to a txt file
$Output= #()
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
$Output+= "$name,up"
Write-Host "$Name,up"
}
else{
$Output+= "$name,down"
Write-Host "$Name,down"
}
}
$Output | Out-file "C:\support\result.csv"
$Output= #()
$names = Get-Content ".\input\Servers.txt"
foreach ($name in $names){
if (Test-Connection -Delay 15 -ComputerName $name -Count 1 -ErrorAction SilentlyContinue -quiet){
$Output+= "$name,up"
Write-Host "$Name,up" -ForegroundColor Green
}
else{
$Output+= "$name,down"
Write-Host "$Name,down" -ForegroundColor Red
}
}
$Output | Out-file ".\output\result.csv"
This is a tad cleaner, and includes the original foreground options but, BTW, the 'delay' switch seems to be ignored -PB
I would do it this way. Using a list of computers and -asjob works very well. The Responsetime property (confusingly the header is "Time(ms)") will be non-null if the host is up.
$names = Get-content hnames.txt
test-connection $names -asjob -count 1 | receive-job -wait -auto
Source Destination IPV4Address IPV6Address Bytes Time(ms)
------ ----------- ----------- ----------- ----- --------
COMP001 yahoo.com 74.6.231.21 32 39
COMP001 microsoft.com 40.113.200.201 32
Lately I do it this way. It requires threadjobs installed in powershell 5.1. Or just use get-port. I stick it in a mymod\mymod.psm1 module file somewhere in $env:psmodulepath. I can check a classroom in under 10 seconds.
function get-pport { # multi-threaded
param($list)
$list |
% { $_ | start-threadjob { get-port $input } -throttlelimit 20 } |
receive-job -wait -auto
}
function Get-Port {
Param (
[parameter(ValueFromPipeline)]
[string[]]$Hostname='yahoo.com'
)
begin {
$ports = 22,5988,3389,5985
$ping = New-Object System.Net.Networkinformation.ping
$Timeout = 200 # ms
}
process {
$hostname | foreach {
$openPorts = #()
foreach ($port in $ports) {
$client = New-Object System.Net.Sockets.TcpClient
$beginConnect = $client.BeginConnect($_,$port,$null,$null)
Start-Sleep -Milli $TimeOut
if($client.Connected) { $openPorts += $port }
$client.Close()
}
$result = $Ping.Send($_, $timeout)
if (! $result) { write-error "hostname $_ not found" }
$pingstatus = ($result.status -eq 'Success')
New-Object -typename PSObject -Property #{
HostName = $_
Port = $openPorts
Ping = $pingstatus
} | select hostname,port,ping
} # end foreach
} # end process
}
Example:
$avid = cat afile.txt
pport $avid
HostName Port Ping
-------- ---- ----
A006 {3389, 5985} True
A011 {3389, 5985} True
A015 {3389} True

PowerShell: Set-Content having issues with "file already in use"

I'm working on a PowerShell script that finds all the files with PATTERN within a given DIRECTORY, prints out the relevant lines of the document with the PATTERN highlighted, and then replaces the PATTERN with a provided REPLACE word, then saves the file back. So it actually edits the file.
Except I can't get it to alter the file, because Windows complains about the file already being open. I tried several methods to solve this, but keep running into the issue. Perhaps someone can help:
param(
[string] $pattern = ""
,[string] $replace = ""
,[string] $directory ="."
,[switch] $recurse = $false
,[switch] $caseSensitive = $false)
if($pattern -eq $null -or $pattern -eq "")
{
Write-Error "Please provide a search pattern." ; return
}
if($directory -eq $null -or $directory -eq "")
{
Write-Error "Please provide a directory." ; return
}
if($replace -eq $null -or $replace -eq "")
{
Write-Error "Please provide a string to replace." ; return
}
$regexPattern = $pattern
if($caseSensitive -eq $false) { $regexPattern = "(?i)$regexPattern" }
$regex = New-Object System.Text.RegularExpressions.Regex $regexPattern
function Write-HostAndHighlightPattern([string] $inputText)
{
$index = 0
$length = $inputText.Length
while($index -lt $length)
{
$match = $regex.Match($inputText, $index)
if($match.Success -and $match.Length -gt 0)
{
Write-Host $inputText.SubString($index, $match.Index) -nonewline
Write-Host $match.Value.ToString() -ForegroundColor Red -nonewline
$index = $match.Index + $match.Length
}
else
{
Write-Host $inputText.SubString($index) -nonewline
$index = $inputText.Length
}
}
}
Get-ChildItem $directory -recurse:$recurse |
Select-String -caseSensitive:$caseSensitive -pattern:$pattern |
foreach {
$file = ($directory + $_.FileName)
Write-Host "$($_.FileName)($($_.LineNumber)): " -nonewline
Write-HostAndHighlightPattern $_.Line
%{ Set-Content $file ((Get-Content $file) -replace ([Regex]::Escape("[$pattern]")),"[$replace]")}
Write-Host "`n"
Write-Host "Processed: $($file)"
}
The issue is located within the final block of code, right at the Get-ChildItem call. Of course, some of the code in that block is now a bit mangled due to me trying to fix the problem then stopping, but keep in mind the intent of that part of the script. I want to get the content, replace the words, then save the altered text back to the file I got it from.
Any help at all would be greatly appreciated.
Removed my previous answer, replacing it with this:
Get-ChildItem $directory -recurse:$recurse
foreach {
$file = ($directory + $_.FileName)
(Get-Content $file) | Foreach-object {
$_ -replace ([Regex]::Escape("[$pattern]")),"[$replace]")
} | Set-Content $file
}
Note:
The parentheses around Get-Content to ensure the file is slurped in one go (and therefore closed).
The piping to subsequent commands rather than inlining.
Some of your commands have been removed to ensure it's a simple test.
Just a suggestion but you might try looking at the documentation for the parameters code block. There is a more efficient way to ensure that a parameter is entered if you require it and to throw an error message if the user doesn't.
About_throw: http://technet.microsoft.com/en-us/library/dd819510.aspx
About_functions_advanced_parameters: http://technet.microsoft.com/en-us/library/dd347600.aspx
And then about using Write-Host all the time: http://powershell.com/cs/blogs/donjones/archive/2012/04/06/2012-scripting-games-commentary-stop-using-write-host.aspx
Alright, I finally sat down and just typed everything sequentially in PowerShell, then used that to make my script.
It was actually really simple;
$items = Get-ChildItem $directory -recurse:$recurse
$items |
foreach {
$file = $_.FullName
$content = get-content $file
$newContent = $content -replace $pattern, $replace
Set-Content $file $newcontent
}
Thanks for all your help guys.

Resources