Trouble with foreach logic - powershell

I'm trying to get a list of all virtual machines in all of my Veeam backup jobs. I wrote this
#Add Veeam snapin
Add-PSSnapin VeeamPSSnapin
#variables
$Masterlist = #()
$jobs = Get-VBRJob
foreach($job in $jobs) {
$backupJobObjects = Get-VBRJobObject -Job $job
foreach($backupJobObject in $backupJobObjects) {
$MyObject = New-Object PSObject -Property #{ Name = $backupJobObject.Name }
}
$Masterlist += $MyObject
}
$Masterlist | sort-object -Property Name
but it only spits out data from one job (there are 5). I assume this is because of some logic error in the foreach loop but I'm not seeing it. Can anyone help?

As per the per the comment from 4c74356b41
foreach($backupJobObject in $backupJobObjects) {
$MyObject = New-Object PSObject -Property #{ Name = $backupJobObject.Name }
$Masterlist += $MyObject
}
Is how the last foreach loop should look

Related

dynamically creating key/value of an object and exporting to CSV

After getting a search result from an LDAP Server, i need to create a pscustomobject dynamically.
The Problem here is that some of the attributes are not set for all users.
this is why i cannot create the pscustomobject the traditional way.
Name = $($item.Attributes['givenname'].GetValues('string'))
Surname = $($item.Attributes['sn'].GetValues('string'))
The Attribute Name does not exist for all users and doing this throws an error.
How can i create the pscustomobject in this case where i need to add both key and value dynamically.
Here is what i have so far:
$vals="cn","tel","email","sn","givenname","ou"
$c.Bind()
$r = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList $baseDN,$Filter,$scope,$attrlist
$re = $c.SendRequest($r)
foreach ($item in $re.Entries) {
foreach($attr in $vals){
if($item.Attributes.Keys -contains $attr){
$pskeys += $attr
}}
foreach($pskey in $pskeys){
$data += [pscustomobject]#{
$($pskey) = $($item.Attributes[$pskey].GetValues('string'))
}}
$pskeys = #()
}
givenname does not exist for all the users and this is why the pscustombject must be created dynamically.
I cannot use a HashTable or some kind of a List as duplicate values must be allowed. There are cases where the attributes sn and givenname are equal.
After hours of trying and failing i can only hope for the Wizards of Stackoverflow to show me how this can be achieved.
I need a pscustomobject where i can save the available attributes and skip the missing attributes dynamically. Is there a way to do this?
Regards
Try following :
$table = [System.Collections.ArrayList]::new()
foreach ($item in $re.Entries) {
$newRow = New-Object -TypeName psobject
foreach($attr in $vals){
if($item.Attributes.Keys -contains $attr){
$pskeys += $attr
}}
foreach($pskey in $pskeys){
foreach($item in $item[$pskey].Attributes.GetValues('string'))
{
$newRow | Add-Member -NotePropertyName $item.Name -NotePropertyValue $item.Value
}
}
$table.Add($newRow) | Out-Null
}
$table | Format-Table
Finally!
I have gotten it to work!
The Trick was to enclose $pskey and $item.Attributes[$pskey].GetValues('string') in $()
Without $() Add-Member was adding the properties as Arrays and not as Strings.
Here is the working Code:
$table = New-Object System.Collections.ArrayList
$c.Bind()
$r = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList $baseDN,$Filter,$scope,$attrlist
$re = $c.SendRequest($r)
foreach ($item in $re.Entries) {
$newRow = New-Object -TypeName psobject
foreach($attr in $vals){
if($item.Attributes.Keys -contains $attr){
$pskeys += $attr
}}
foreach($pskey in $pskeys){
$newRow | Add-Member -NotePropertyName $($pskey) -NotePropertyValue $($item.Attributes[$pskey].GetValues('string'))
}
$table.Add($newRow) | Out-Null
$pskeys = #()
}
$table | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 -Append -Delimiter ";"
Thank You jdweng for pointing me in the right direction.
$table | Format-Table on the console and the resulting CSV after the Export look flawless now.
My Problem is solved.

PowerShell create a new object and add the values to an array

What I am trying to achieve here is add the servers and the updates that are not installed on the server to an array and create a new object that is going to display the names of the servers in one column and the missing updates on another column, but at the end I am getting an empty Grid-View table.
The values for the servers and updates are read from a file.
Write-Host
#Read the password from stdin and store it in a variable
$password = Read-Host -AsSecureString -Prompt "Enter your password"
Write-Host
#Get credentials and password for later user
$cred = New-Object System.Management.Automation.PSCredential ("Administrator#testing.local", $password )
#Get the list of available servers to test
$servers = Get-Content -Path $HOME\Desktop\servers.txt
#Get the list of available updates that need to be installed on the server
$available_updates = Get-Content $HOME\Desktop\update.txt
$add_updates = #()
$add_updates_and_servers = #()
#Get each server name from the list and execute the following commands
foreach ($server in $servers) {
#Test if the server is reponding
$ping = Test-Connection $server -Count 1 -Quiet
#If the above command returns True continue
if ($ping -eq "True") {
#Write a message saying Testing server_name
Write-Host "Testing $server"
foreach ($update in $available_updates) {
#Check if update is installed
$updates_from_os = Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock { Get-HotFix | Select-Object -Property HotFixID | Where-Object -Property HotFixID -EQ $Using:update } -HideComputerName | Select-Object -ExpandProperty HotFixID
if (!$updates_from_os) {
$add_updates += $update
}
}
New-Object -TypeName PSObject -Property $updates -OutVariable final
$updates = #{
"Server" = $server
"Updates" = $add_updates
}
}
$add_updates_and_servers += $final
}
$add_updates_and_servers | Out-GridView
For what is probably happening with your script:
I suspect that each time you calling the statement New-Object -TypeName PSObject -Property $updates -OutVariable final You overwriting any previous created $final object which references to the same objects as your $add_updates_and_servers collection.
Anyways, try to avoid using the increase assignment operator (+=) to create a collection, instead stream the results to a variable (or even better, directly to next/final cmdlet: ... }| Out-GridView).
Something like:
$add_updates_and_servers = foreach ($server in $servers) {
$ping = Test-Connection $server -Count 1 -Quiet
if ($ping -eq "True") {
Write-Host "Testing $server"
$add_updates = #(
foreach ($update in $available_updates) {
$updates_from_os = Invoke-Command -ComputerName $server -Credential $cred -ScriptBlock { Get-HotFix | Select-Object -Property HotFixID | Where-Object -Property HotFixID -EQ $Using:update } -HideComputerName | Select-Object -ExpandProperty HotFixID
if (!$updates_from_os) { $update }
}
)
[PSCustomObject]#{
"Server" = $server
"Updates" = $add_updates
}
}
}
Note: in case you want each $update in a separate column, also have a look at: Not all properties displayed

Hashtables and RunSpaces in powershell

I am having some problems with updating data in a list from the results of a hash table. I am 99% sure it is due to the lack of understanding of what i am doing.
I am generating a $list of servers from a CSV. the CSV contains Servername, domain, description, plus some additional blank columns for use later.
what i am trying to do in a nutshell: i need to pull the down processes from a list of remote servers. to do this i am throwing each server from the list and function into its own runspace, the Hashtable is updating as expected. But i can not update the original $list i have.
here is my code:
Function OpenFile ($FilePath) {
$OFDiag = new-object system.windows.forms.openfiledialog
$OFDiag.filter = 'CSV (*.csv) | *.csv'
$OFDiag.ShowDialog() | out-null
$OFDiag.filename
}
# Create Primary Variables
$FilePath = OpenFile
$list = (get-content $FilePath) -replace '\(',' -' #ALL Servers and Groups need to remove parenthesis
$list = $list -replace '\)' #finish up removing the parenthesis
$list = $list -replace ' Or WorkGroup'
$list = convertFrom-CSV $list | select 'Name', 'Computer_Description', 'Domain' #Need to convert the list into a CSV formatted table.
$list = $list | sort Name
$list | Add-Member -NotePropertyName 'LastReboot' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'LastDeployment' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'RebootStatus' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'DownProcess' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'EnabledStatus' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'RDP' -NotePropertyValue $null
$list | Add-Member -NotePropertyName 'SchedTask' -NotePropertyValue $null
$servers = $list | %{$_.Name} | sort #ALL SERVERS - ONLY Servernames
$ServProSel = {
#
# Checks for Running Services and Processes.
# This Makes a determination as to what service/process groups should be checked.
# The Information as to what processes to look for are sent to the ProSer_Check function
# information from there is sent to the ServerStatus Tab
#
#Write-Host 'starting ServerProSel'
Param ($computer,$cred,$grpName,$hash)
#$cred = $(get-Variable "$Domain" -valueOnly)
$ck =#{} #$(Get-Variable -name "SCP_$serName" -ValueOnly)
Function ProSer_Check {
# This is the actual function that is run on the remote system to check
# for processes and services.
param ( [array] $Prcs,
[string] $Computer )
$script:chkres =#()
foreach ($p in $Prcs){
$script:res = Get-Process -name $p -ErrorAction SilentlyContinue
if (!$res) {
$chk = "$p -DOWN`r`n"
$chkres += $chk
}
}
if ($chkres.count -eq 0){
$chkres = "All Processes Up"}
Return $chkres
}
switch -Regex ($grpName){
'Demonstration' {
$Prcs = #('Process.Service'); break}
'Historian' {
$Prcs =#('Process.Service'); break}
'Models' {
$Prcs =#('UpdaterServer'); break}
'Inflictor' {
$Prcs =#('Automation.EngineService','Automation.Manager.Service','Automation.SmfLauncher','postgres','Redundancy.Server','WatchDog.Service'); break}
'Simulator' {
$Prcs =#('proc','moni','server','serve','clerk','web'); break}
'WebServer' {
$Prcs =#('w3wp','VShell'); break}
default {
$Prcs =#('svchost'); break}
}
$R = invoke-command -credential $cred -computername $Computer -scriptblock ${function:ProSer_Check} -ArgumentList $Prcs,$Computer
$hash[$Computer]=([string]$R)
}
$Script:runspaces = New-Object System.Collections.ArrayList
$Global:hash = [hashtable]::Synchronized(#{})
$global:sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$global:runspacepool = [runspacefactory]::CreateRunspacePool(1, 10, $sessionstate, $Host)
$global:runspacepool.Open()
Function SendToRunSpace {
$function = $args[0]
#$function
$powershell = [powershell]::Create().AddScript($function).AddArgument($computer.name).AddArgument($cred).AddArgument($grpName).AddArgument($hash)
$powershell.RunspacePool = $global:runspacepool
#$hash = #{Name=$computer.name;DownProcess = "Waiting.."}
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
}
ForEach ($Computer in $list) {
$domain = $computer.Domain
$grpName = $computer.'Computer_Description'
$cred = $(get-Variable "$Domain" -valueOnly)
#Create the powershell instance and supply the scriptblock with the other parameters
if(!$(Get-Variable "TEST_$domain" -ValueOnly)){
CredCheck $computer.name $cred
}
#SendToRunSpace $scriptBlock $computer $domain $global:hash
SendToRunSpace $ServProSel $computer $cred $grpName $global:hash
}
I am running this in PowerShell ISE so i can edit on the fly and test things. When i run this code i generate the $list and $hash items. Ultimately i would like to grab the value out of the $hash for the server and update the corresponding server information in the $list object.
or is there a better way to do this? is the Hashtable the only way to Synchronize data from the runspaces to the current process?

Loop Confusion Comparing Objects

I am trying to create my own service comparison script. I see some online but want to do this myself. I've only gotten so far. I keep getting confused.
The desired output is with the following format. It doesn't even have to show what's different. I just want to see what the previous state was compared to the current state. I did a compare-object and it didn't give me the format I desired. I then thought maybe I should just do two nested loops and create a new object with the states I want in it. It didn't work out correctly, it returns an array. So then I thought, maybe a for loop in the foreach loop... I keep confusing myself and it's so close.
You have to provide a csv with some services to compare to to make this work as part of it's paramaters.
Usage
Inspect-ServiceSnapshot -SnapshotPath "C:\YourPath"
Desired output
Name CurrentState PreviousState
app1 Running Stopped
Code So Far
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidatePattern("C:")]
[string]
$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]
$TimeToWait
)
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
} else {
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
}
}
This should do it because it is actually checking the value for the old service to be the same as the one Get-Service provides at a certain time.
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Leaf})]
[string]$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]$TimeToWait = 0
)
if($TimeToWait) { Start-Sleep -Seconds $TimeToWait }
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (Get-Service)) {
# make sure we are dealing with the SAME service
$oldItem = $old | Where-Object { $_.Name -eq $entry.Name }
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = if ($oldItem) { $oldItem.status } else { 'Unknown' }
}
$list += $object
}
$list
}
This answer is assuming there are no new services added on a regular basis.
You almost got it! You can forgo the nested loops, get rid of the else block (it is redundant), and use an index loop. When you're using nested loops like that, you are iterating through all of the array elements in the $old array every time you iterate through one of the (get-service) objects. This can cause issues when your arrays include thousands of objects.
You can easily get to what you want by using a for loop.
e.g.
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
}
$list = #();
$old = Import-Csv -Path $SnapshotPath | Sort-Object Name;
$new = Get-Service | Sort-Object Name;
for ($i -eq 0; $i -lt $old.length -or $i -lt $new.length; $i++) {
$object = New-Object -TypeName psobject -Property #{
Name = $new[$i].Name
CurrentStatus = $new[$i].status
DisplayName = $new[$i].displayname
PreviousStatus = $old[$i].status
}
$list += $object;
}
$list;
A lot of your code is redundant and you can just do this all in one go. Since you're not technically comparing any objects, you can just populate the fields as they go.

Resolve-DnsName inside Test-Connection

I was wondering how I could return the Resolve-DnsName output from my Test-Connection script and add it to the CSV I created.
I like to capture the Name, Type, TTL, Section from that please.
Only invoke the Resolve-DnsName when the ping is not successful.
$servers = Get-Content "servers.txt"
$collection = $()
foreach ($server in $servers)
{
$status = #{ "ServerName" = $server; "TimeStamp" = (Get-Date -f s) }
$result = Test-Connection $server -Count 1 -ErrorAction SilentlyContinue
if ($result)
{
$status.Results = "Up"
$status.IP = ($result.IPV4Address).IPAddressToString
}
else
{
$status.Results = "Down"
$status.IP = "N/A"
$status.DNS = if (-not(Resolve-DnsName -Name $server -ErrorAction SilentlyContinue))
{
Write-Output -Verbose "$server -- Not Resolving"
}
else
{
"$server resolving"
}
}
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus
$collection += $serverStatus
}
$collection | Export-Csv -LiteralPath .\ServerStatus3.csv -NoTypeInformation
but nothing new is added to the CSV.
You ran into a PowerShell gotcha. PowerShell determines the columns displayed in tabular/CSV output from the first object processed. If that object doesn't have a property DNS that column won't be shown in the output, even if other objects in the list do have it. If other objects don't have properties that were present in the first object they will be displayed as empty values.
Demonstration:
PS C:\> $a = (New-Object -Type PSObject -Property #{'a'=1; 'b'=2}),
>> (New-Object -Type PSObject -Property #{'a'=3; 'b'=4; 'c'=5}),
>> (New-Object -Type PSObject -Property #{'b'=6; 'c'=7})
>>
PS C:\> $a | Format-Table -AutoSize
a b
- -
1 2
3 4
6
PS C:\> $a[1..2] | Format-Table -AutoSize
c b a
- - -
5 4 3
7 6
If you want to generate tabular output always create your objects uniformly with the same set of properties. Choosing sensible defaults even allows you to reduce your total codebase.
$collection = foreach ($server in $servers) {
$status = New-Object -Type PSObject -Property #{
'ServerName' = $server
'TimeStamp' = Get-Date -f s
'Results' = 'Down'
'IP' = 'N/A'
'HasDNS' = [bool](Resolve-DnsName -Name $server -EA SilentlyContinue)
}
$result = Test-Connection $server -Count 1 -EA SilentlyContinue
if ($result) {
$status.Results = 'Up'
$status.IP = ($result.IPV4Address).IPAddressToString
}
$status
}