ForEach loop over string in powershell - powershell

I am trying to find all last job statuses fro a list of VMs backed up with veeam backup.
Strangely the loop do not go to the next vm. Here is what I do:
Add-PSSnapin VeeamPSSnapin
$VMlist = "vm1, vm2"
$VMlist = $VMlist.split(",");
Foreach ($i in $VMlist) {
foreach($Job in (Get-VBRJob))
{
$Session = $Job.FindLastSession()
if(!$Session){continue;}
$Tasks = $Session.GetTaskSessions()
$Tasks | ?{$_.Name -eq $VMlist} | %{write-host $_.Name ":" $_.Status}
It seems I have a problem in the for each loop, since it stuck and I do not get any output. What is thebest way to iterate over the slit of VMs?
Thanks in advance!

You're looking for the $VMList array in $Tasks not the individual VM $i, just change: {$_.Name -eq $i}
Also your VM names will include leading spaces, either remove the spaces from your input string "vm1,vm2", or use Trim() after Split()
Add-PSSnapin VeeamPSSnapin
$VMlist = "vm1,vm2"
$VMlist = $VMlist.split(",");
foreach ($i in $VMlist) {
foreach ($Job in (Get-VBRJob)) {
$Session = $Job.FindLastSession()
if (!$Session) {continue; }
$Tasks = $Session.GetTaskSessions()
$Tasks | Where-Object {$_.Name -eq $i} | ForEach-Object {Write-Host $_.Name ":" $_.Status}
}
}

Related

Trying to add PSCustomObjects to an ArrayList on a remote machine

I have some experience with PowerShell, and usually Google or searching forums like these yields the answers when I have questions - but not this time.
I'm trying to collect the number of .log files in a directory on a remote server, then I'd like to store the location (drive letter and folder path) and the count in an array list for later. So far everything is working as I'd expect, but I'm running into trouble adding my PSCustomObjects to the array list. I'm not sure if it's because I'm executing on a remote server or if something else is causing the problem. Here is my code:
$server = Read-Host -Prompt 'Please enter the server name'
[System.Collections.ArrayList]$returnObj = #()
Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
Write-Host $obj
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
}
}
}
$returnObj
In this format I get a syntax error on the line
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
If I change the above line to $returnObj.Add($obj) I avoid the syntax error, but instead I get an error saying I cannot call a method on a null valued expression.
I've tried creating the ArrayList inside the Invoke-Command and I've tried using New-Object instead of PSCustomObject to no avail.
I think your mixing stuff a bit up, this will do:
$returnObj = Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
[PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
}
}
}
$returnObj
The problem is this line:
[System.Collections.ArrayList]$returnObj = #()
is declared outside of the Invoke-Command -ScriptBlock. This means it's not available within the session on the remote machine, and as such can not be used there.
On a side note, you cannot fill an array like you fill a Hashtable with data.
Arrays are filled like $MyArray = #(); $MyArray += 'MyValue'
Hashtables like $MyHash=#{}; $MyHash.SomeKey = 'SomeValue' or as you indicated $MyHash.Add('SomeKey', 'SomeValue')
ArrayLists are filled like [System.Collections.ArrayList]$MyArrayList = #(); $MyArrayList.Add('SomeValue')
I hope this makes it a bit more clear. The return values can always be catched before the Invoke-Command or even before a simple foreach (). For example $result = 0..3 | ForEach-Object {$_} is perfectly valid too.
You need to actually return your object from the remote system to your local system since you cannot use your $returnObj within the remote session.
As an example:
$returnValue = Invoke-Command -ComputerName $server {
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
#return the object via the pipline
$obj
}
$returnObj.Add($returnValue | Select-Object DatabaseFolder,LogFileCount)
The above example is lacking of proper error handling, therefore you would get an error if the remote system is not reachable but it's a start.

No method found "op_addition"

I want to add a specific selection of data in a variable like this:
$a = Get-Service | select -First 10 | ? Name -Like "App*"
$collection = $null
foreach($item in $a){
if($item.Status -like "Running"){
$collection = $collection + $item
}
}
When trying to run I got an error like:
No method found "op_addition"
What can I do to save my selection in a separate variable?
At first, $collection is $null.
Then, after the first loop iteration, $collection is a single ServiceController object, since $null + $object is just $object.
On the second loop iteration, it fails because ServiceController don't have any overloads for +, just as the error informs you.
You'll need to declare $collection an actual collection (you can use the #() array subexpression operator) for + to work they way you expect:
$a = Get-Service | select -First 10 | ? Name -Like "App*"
$collection = #()
foreach($item in $a){
if($item.Status -like "Running"){
$collection = $collection + $item
}
}
Alternatively, assign all the output from the loop directly to $collection:
$a = Get-Service | select -First 10 | ? Name -Like "App*"
$collection = foreach($item in $a){
if($item.Status -like "Running"){
$item
}
}
Which could of course be simplified with a single Where-Object statement in the pipeline:
$collection = Get-Service App* |Where-Object {$_.Status -eq 'Running'}

ForEach-Object piped to CSV is only showing last element

I'm trying to export to csv the scheduled tasks for multiple remote machines. I'm using a modified version of this script. I'm trying to export one csv per machine that lists all of the scheduled tasks. Currently my code just exports the last task for each machine.
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
New-Object -TypeName PSCustomObject -Property #{
'Name' = $_.name
'Path' = $_.path
'Server' = $computername
} | Export-Csv $("C:\Users\MyName\Desktop\" + $computername + ".csv")
}
}
}
}
I've tried putting the Export-Csv at the end of each of the curly braces and none output what I want. Any ideas what I'm doing wrong?
EDIT
I'm not sure this fully addresses the issue, as some parts of the code seem weired to me such as $tasks = $folder.GetTasks. However OP title clearly says that the CSV is showing the last element meaning to me that his code mostly works except for the Export-CSV part.
Export-Csv expects an array as input, so the code below uses the elements in $task to generate the array of objects. Select-Object is there to decide in which order the properties are stored in the CSV.
Try this:
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
$result = #()
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
$result += New-Object -TypeName PSCustomObject -Property #{
Name = $_.name;
Path = $_.path;
Server = $computername
}
}
}
}
$result | select Name, Path, Server | Export-Csv $($exportPath + $computername + ".csv")
}
This new version saves results on a per computer basis.
You are not changing either $exportPath or $computername in your loop, so each time through your outer loop, the csv file is being overwritten. Also, this
if ($Tasks = $Folder.GetTasks(0)) {
may be wrong. If you're trying to determine if $Tasks is equal to $Folder.GetTasks(0), you'd need this:
if ($Tasks -eq $Folder.GetTasks(0)) {

PowerShell Compare content of two strings

I am trying to compare the values of two variables but the contents of those two strings are in different orders
Example:
$Var1 = "item1"
$Var1 += "item2"
$Var2 = "item2"
$Var2 = "item1"
How can I compare those two variables to see if they both are equal?
===== UPDATED WITH EXAMPLE =====
EXAMPLE: Get objects and sort them.
$Computers = (Get-Content "$PWD\Computers.txt").GetEnumerator() | Sort-Object {"$_"}
EXAMPLE: Add the results and sort them.
$Successful += $Computer
$Successful = $Successful.GetEnumerator() | Sort-Object {"$_"}
EXAMPLE SCRIPT: Used the examples above to create the following script. The example allowed me to check the results, instead of count, but by content allowing me to get more accurate comparison. Before I was using "Successful.count -eq Computers.count" which wouldn't check if a computer was inputted twice.
$Computers = (Get-Content "$PWD\Computers.txt").GetEnumerator() | Sort-Object {"$_"}
$HotFixes = Get-Content "$PWD\HotFixes.csv"
CLS
While (!$Successful -OR $Successful -ne $Computers) {
foreach ($Computer in $Computers) {
$MissingCount = 0
IF (!$Successful -NotLike "*$Computer*") {
Write-Host "$Computer`: Connecting"
If (Test-Connection -ComputerName $Computer -Count 1 -quiet) {
Write-Host "$Computer`: Connected"
[string]$Comparison = get-hotfix -ComputerName $Computer | Select -expand HotFixID
ForEach ($HotFix in $HotFixes) {
IF ($Comparison -NotLike "*$HotFix*") {
$Results += "$Computer,$HotFix"
$MissingCount++
}
}
Write-Host "$Computer`: $MissingCount Patches Needed"
$Successful += $Computer
$Successful = $Successful.GetEnumerator() | Sort-Object {"$_"}
} ELSE {
Write-Host "$Computer`: Unable to connect"
}
} ELSE {
Write-Host "$Computer already completed"
}
Write-Host "$Computer`: Complete"
Write-Host
}
}
$Results
If you want to find if the content is equal, regardless of characters position, you could break the string to its characters, sort the result and then use the Compare-Object cmdlet. No result means the variables are equal:
$v1 = $Var1.GetEnumerator() | Sort-Object {"$_"}
$v2 = $Var2.GetEnumerator() | Sort-Object {"$_"}
compare $v1 $v2

Find next available computer name

I'm trying to find the next available computer name in out domain. Our computers use a naming format
departmentName001
departmentName003
departmentName004
...
departmentName999
I can find the existing computer accounts and add 1 but I can't work out for to get it to start looking at 001, I'm aware of the use of "{0:d3}" -f but I'm not using it correctly. Can anyone help?
function GetComputerList($ComputerName)
{
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = “LDAP://dc=domain,dc=local”
$objSearcher.Filter = ("(&(objectCategory=computer)(name=$ComputerName))")
$colProplist = "name"
$objSearcher.PageSize = 1000
foreach ($i in $colPropList){[void]$objSearcher.PropertiesToLoad.Add($i)}
$colResults = $objSearcher.FindAll()
foreach ($objResult in $colResults)
{$objComputer = $objResult.Properties; $objComputer.name}
}
$HostName = Finance
$unit="{0:d3}" -f $_
$num = GetComputerList("$HostName*") | Foreach {[int]($_.Name)} | Sort-Object | Select-Object -Last 1
$name = $HostName+($unit+($num+1))
Try this, it gets all computer with name starting with 'departmentName', strips all a-z characters, leaving just the numbers, converts the numbers to integers and sorting them to find the largest one:
$searcher = [ADSISearcher]'(&(objectCategory=computer)(name=departmentName*))'
$searcher.PageSize = 1000
$last = $searcher.FindAll() | Foreach-Object { [int]($_.Properties.name -replace '\D').Trim() } | Sort-Object | Select-Object -Last 1
$digitLength = "$last".Length
$NewComputerName = "{0}{1:D$digitLength}" -f 'departmentName',($last+1)
$NewComputerName
EDIT:
# get next available number in a range of numbers. returns 5 for 1,2,3,4,6,7,9
$number = $searcher.FindAll() | Foreach-Object { [int]($_.Properties.name -replace '\D').Trim() } | Sort-Object
for($i=0; $i -lt $number.length; $i++) {if( $number[$i+1]-$number[$i] -gt 1) {$number[$i]+1; break} }
try this:
$searcher = [ADSISearcher]'(&(objectCategory=computer)(name=Finance*))'
$searcher.PageSize = 1000
$last = $searcher.FindAll() | Foreach-Object {
[string]($_.Properties.name -replace '\D') } | Sort-Object
$i = 0
$last | % { if ($i -ne [int]$_ ) { $new = $i.tostring().padleft(3,'0'); break }
else
{ $i++ }}
$newComputerName = "finance" + $new
based on the information in this post I have made a few changes and tweaks to the code for my environment to check more than just AD.. and also fixed it not filling in blanks at the start of a range.. I have blogged it here: AutoGeneratingServer Names
copy of the code here too, and I know it can be refactored lots!
[CmdletBinding()]
param()
# ********************************************************
$startOfName = "xxxYYYZZWEB"
# ********************************************************
# VMWare Details
$ADVIServers = #("vsphere1.blah.local","vsphere2.blah.local","vsphere3.blah.local","vsphere4.blah.local")
$StandAloneHosts = #()
# DNS Details
$DNSServer = "xxxxxx.blah.local"
# SCCM 2012 Details
$SCCM2012SiteServer = "sccm2012.blah.local"
$SCCM2012SiteCode = 'SiteCode'
# SCCM 2007 Details
$SCCM2007SiteServer = "sccm2007.blah.local"
$SCCM2007SiteCode = 'SiteCode2'
# SCOM 2007 Details
$SCOMServer = "scom.blah.local"
# Create Empty Arrays
$VMNumbers = #()
$ADnumbers = #()
$DNSNumbers = #()
$SCCM2012Numbers = #()
$SCCM2007Numbers = #()
$SCOM2007Numbers = #()
# VMWare
Write-Verbose "Processing VMware"
Add-PSSnapin vmware.vimautomation.core -ErrorAction SilentlyContinue
# Set options for certificates and connecting to multiple enviroments
$null = Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$False
$null = Set-PowerCLIConfiguration -DefaultVIServerMode Multiple -Scope User -Confirm:$False
# Connect to each AD Authenticated viServer
foreach ($VIServer in $ADVIServers){$null = Connect-VIServer $VIServer -verbose:$false}
# Connect to standalone host
foreach ($Host in $StandAloneHosts){$null = Connect-VIServer $Host -User 'usernamehere' -Password 'passwordhere' -verbose:$false}
# get next available number in a range of numbers.
$VMNames = Get-VM -Name "$($startOfName)*" -verbose:$false |select Name
$VMNames |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
$VMNumbers = $VMNames |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
Write-Verbose "$($VMNumbers.Count) Matching entries found"
# Active Directory
Write-Verbose "Processing Active Directory"
# Issue Query
$searcher = [ADSISearcher]"(&(objectCategory=computer)(name=$($StartOfName)*))"
$searcher.PageSize = 1000
# get next available number in a range of numbers. returns 5 for 1,2,3,4,6,7,9 From AD
$ADNames = $searcher.FindAll() | Foreach-Object {[string]$_.Properties.name} | Sort-Object
$ADNames | Foreach-Object {Write-Verbose $_} | Sort-Object
$ADnumbers = $ADNames | Foreach-Object {[int]($_ -replace '\D').Trim() } | Sort-Object
Write-Verbose "$($ADnumbers.Count) Matching entries found"
# Search DNS
Write-Verbose "Processing DNS"
# Import DNS module
Import-Module dnsShell -Verbose:$false
$DNSNames = get-dnsRecord -server $DNSServer -RecordType A -Zone blah.local | select Name |where {$_.Name -like "$($startOfName)*"}
$DNSNames | Foreach-Object {Write-Verbose $_.Name} | Sort-Object -Unique
$DNSNumbers = $DNSNames | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object -Unique
Write-Verbose "$($DNSNumbers.Count) Matching entries found"
# Search SCCM
Write-Verbose "Processing SCCM 2012"
# Query SCCM2012 Env
$SCCM2012Members = Get-WmiObject -ComputerName $SCCM2012SiteServer -Namespace "ROOT\SMS\site_$SCCM2012SiteCode" -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='SMS00001' AND Name LIKE '$($startOfName)%' order by name" | select Name -Unique
$SCCM2012Members |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
$SCCM2012Numbers = $SCCM2012Members |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
Write-Verbose "$($SCCM2012Numbers.Count) Matching entries found"
Write-Verbose "Processing SCCM 2007"
# Query SCCM2007 Env
$SCCM2007Names = Get-WMIObject -ComputerName $SCCM2007SiteServer -Namespace "root\sms\site_$SCCM2007SiteCode" -class "SMS_R_System" -filter "Name LIKE `"$startOfName%`"" |select Name | Sort-Object -Property Name -Unique
$SCCM2007Names |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
$SCCM2007Numbers = $SCCM2007Names |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
Write-Verbose "$($SCCM2007Numbers.Count) Matching entries found"
# Search Production SCOM 2007
Write-Verbose "Processing SCOM 2007"
#Initialize SCOM SnapIn
Add-PSSnapin Microsoft.EnterpriseManagement.OperationsManager.Client -ErrorAction SilentlyContinue -verbose:$false
#Connect to Production SCOM 2007 Env.
$null = New-ManagementGroupConnection -ConnectionString $SCOMServer
#Connect to SCOM Provider
Push-Location 'OperationsManagerMonitoring::'
# Get Agents Matching Name
$SCOM2007Names = Get-ManagementServer |Get-Agent |Where {$_.Name -like "$($startOfName)*"}
$SCOM2007Names | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
$SCOM2007Numbers = $SCOM2007Names | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
Write-Verbose "$($SCOM2007Numbers.Count) Matching entries found"
# Return to previous location
Pop-Location
# Merge arrays adding a zero so we allways start issuing numbers from the beginning (ie 001)
$list = #(0) + $VMNumbers + $ADnumbers + $DNSNumbers + $SCCM2012Numbers + $SCCM2007Numbers + $SCOM2007Numbers
# Remove Duplicates numbers from the array and sort into numerical order
$list = $list | Sort-Object -Unique
Write-Verbose "Used numbers after sorting: $($list)"
# Determine if next server name is a gap in the sequence in the array
for($i=0; $i -lt $list.length; $i++) {
if( $list[$i+1]-$list[$i] -gt 1) {
# The gap between the current server number and the next element in the array is greater than 1
# So we have an available number we can use.
# TODO: - Add support for consecutive numbers IE build 6 servers with consecutive numbers.
$num = "{0:000}" -f ($list[$i]+1)
break
}
}
# If no gap found in the sequence then use the next number from the sequence in the array
if ($num -eq $null) {
$num = "{0:000}" -f (($list[-1]+1))
}
# Construct new name
$NewComputerName = "{0}{1}" -f $startOfName,$num
# Create DNS Record to 'reserve / mark the name as in use'
Write-Verbose "Creating DNS Reservation"
New-DnsRecord -Name $NewComputerName -IPAddress "127.0.0.1" -Zone blah.local -Type A -Server $DNSServer
write-output $NewComputerName