For loop used on 3 groups

Hello to all,

I’m trying to find a solution for my case where I have 3 groups that contains 2 members each, and want to pass each member group to a for loop.

Members for the groups are actually VMs (like DC, proxy) and I want to keep them on different vcenter hosts.

So, instead of $vm1 and $vm2, I tried to define groups and run the if into a loop, but can’t figure it out how to pass gr1[0] and gr[1] to first for loop and so on.

Any advice or idea will be great.

Thanks,

Adrian

$gr1 = @($vm1,$vm2)
$gr2 = @($vm3,$vm4)
$gr3 = @($vm5,$vm6)

$gr = @($gr1,$gr2,$gr3)

for ($i in $gr) { 
if ...
$on8 = Get-VM | ? {$_.Name -match "^$vm1" -or $_.Name -match "^$vm2" -and $_.VMHost -like "$host8"}
$on16 = Get-VM | ? {$_.Name -match "^$vm1" -or $_.Name -match "^$vm2" -and $_.VMHost -like "$host16"}

##### testing
$vm1 = Get-VM | ? {$_.Name -match "^NAME_TEST1"}
$vm2 = Get-VM | ? {$_.Name -match "^NAME_TEST2"}
$vm3 = Get-VM | ? {$_.Name -match "^NAME_TEST3"}
$vm4 = Get-VM | ? {$_.Name -match "^NAME_TEST4"}
$vm5 = Get-VM | ? {$_.Name -match "^NAME_TEST5"}
$vm6 = Get-VM | ? {$_.Name -match "^NAME_TEST6"}
##### testing

# Define host status check
$host_check = Get-VMHost | ? {$_.Powerstate -like "poweredon" -and $_.ConnectionState -like "connected"}

<#
# Windows cluster VMs migration
if ($host_check -Match "$host8" -and $host_check -Match "$host16") {
if ($on8 -Match "$vm2" -and $on8 -Match "$vm1") {
Move-VM "$vm2" -Destination "$host16" | out-null
$vm_check = Get-VM | ? {$_.Name -match "^vm2" -and $_.VMHost -like "$host16"}
if ($vm_check -Match "^$vm2") {
$out="migrated successfuly" 
}
else {
$out="NOT MIGRATED"
}
Write-Output "$(Get-Date): $vm1 still runs on $host8 and VM $vm2 $out to $host16" >> /root/test.txt
}

elseif ($on16 -Match "^$vm1" -and $on16 -Match "^$vm2") {
Move-VM "$vm1" -Destination "$host8" | out-null
$vm_check = Get-VM | ? {$_.Name -match "^$vm1" -and $_.VMHost -like "$host8"}
if ($vm_check -Match "^$vm1") {
$out="migrated successfuly"
}
else {
$out="NOT MIGRATED"
} 
Write-Output "$(Get-Date): $vm1 $out to $host8 and $vm2 still runs on $host16" >> /root/test.txt
}
#else {
#Write-Output "$(Get-Date): Everything looks good, VMs are running on different hosts." >> /root/test.txt
#}
}
else {
exit
}

Simply use a foreach loop:

$gr1 = @(’$vm1’, ‘$vm2’)
$gr2 = @(’$vm3’, ‘$vm4’)
$gr3 = @(’$vm5’, ‘$vm6’)

$gr = @($gr1, $gr2, $gr3)

foreach ($group in $gr) {
$group
}


… output …
$vm1
$vm2
$vm3
$vm4
$vm5
$vm6

Tried that, but I can’t pass both members from gr1 to the first run.

Line19: if ($on8 -Match “$gr1[1]” -and $on8 -Match “$gr1[0]”)

LE: just noticed that also $on8 and $on16 should be placed inside the foreach loop

[quote quote=221028]
Line19: if ($on8 -Match “$gr1[1]” -and on8 -Match “gr1[0]“)[/quote]

Did you check what’s in $on8? Can it match both - $vm1 AND $vm2?

Well, if it can’t match, it will exit the “if” without any action. that’s not a issue (commented lines 42-44)

I actually still don’t know what your issue is. Did you try to output $On8 and on16. If you use -like in a condition you should add an asterisk (<strong>*</strong>). So instead of <strong>.VMHost -like "host8"</strong> it should be <strong>.VMHost -like “$host8*”. Why do you use -match to filter for the name of your VMs. Don’t you know their names? What is it what you ACTUALLY try to achieve?

Output for $on8 and $on16 is OK, I get the VMs that are running on them and match the name, no need for host8*.

 

I want to do a foreach loop and parse group members to IF statement, like this:

if ($on8 -Match “$gr1_mebmer1” -and $on8 -Match “$gr1_member2”) {
Move-VM “$gr1_member2” -Destination “$host16” | out-null

and repeat this for gr2 and 3. But I can’t figure it out how to do it.

 

This script will be used to keep vms from those groups (1, 2 and 3) to run on different hosts, if both hosts are available. I don’t care if vm1 from gr1 runs on same host as vm1 from gr2, the idea is to keep separate the vms from the same group.

I’m sorry if I’m not explaining very clear, I’m trying …

[quote quote=221052]I want to do a foreach loop and parse group members to IF statement, like this:

if ($on8 -Match “$gr1_mebmer1” -and $on8 -Match “$gr1_member2”) {

Move-VM “$gr1_member2” -Destination “$host16” | out-null[/quote]

OK, but $on8 is not a plain list of VM names, right? It should be an array of objects with properties if I got everything right. You could use if($on8.name -contains $vm1) to determine if an element is in an array of elements.

Edit: Or actually it should be if($on8.name -contains $vm1.name)

This is the output of $on8:

Name PowerState Num CPUs MemoryGB
---- ---------- -------- --------
vm_name PoweredOn 4 8.000

So, if the output match both vm1 and vm2, the script will continue will migrate the one that I specify.

But, again, my issue is not there. Is on the FOR part.

### this part should dissapear
$vm1 = Get-VM | ? {$_.Name -match "^NAME_TEST1"}
$vm2 = Get-VM | ? {$_.Name -match "^NAME_TEST2"}
$vm3 = Get-VM | ? {$_.Name -match "^NAME_TEST3"}
$vm4 = Get-VM | ? {$_.Name -match "^NAME_TEST4"}
$vm5 = Get-VM | ? {$_.Name -match "^NAME_TEST5"}
$vm6 = Get-VM | ? {$_.Name -match "^NAME_TEST6"}
###

### this should replace the above variables
$gr1 = @($vm1,$vm2)
$gr2 = @($vm3,$vm4)
$gr3 = @($vm5,$vm6)

$gr = @($gr1,$gr2,$gr3)

for ($i in $gr) { 
$on8 = Get-VM | ? {$_.Name -match "^$vm1" -or $_.Name -match "^$vm2" -and $_.VMHost -like "$host8"} ### $vm1 should be replaced first with $gr1[0], then with $gr2[0], then with $gr3[0] and $vm2 should be replaced with $gr1[1] etc..
$on16 = Get-VM | ? {$_.Name -match "^$vm1" -or $_.Name -match "^$vm2" -and $_.VMHost -like "$host16"} ## same here
if ($on8 -Match "$vm2" -and $on8 -Match "$vm1") { ### same on the rest of the script
Move-VM "$vm2" -Destination "$host16" | out-null
$vm_check = Get-VM | ? {$_.Name -match "^vm2" -and $_.VMHost -like "$host16"}
if ($vm_check -Match "^$vm2") {
$out="migrated successfuly" 
}
else {
$out="NOT MIGRATED"
}
Write-Output "$(Get-Date): $vm1 still runs on $host8 and VM $vm2 $out to $host16" >> /root/test.txt
}

I don’t know how to parse grX[Y] values to $i

I want this to avoid to write the same IF statement for every group of VMs that I want to keep on different hosts. It’s easier to define a new group than to copy entire IF statement, change vm1 and vm2 with new names etc…

Please clarify the blessed use case. For example;

  • I wish to ensure certain VMs are not hosted on the same ESXi host

Also, I’m assuming there’s a reason you wouldn’t just use the native VMWare Affinity rules and anti-affinity rules

$gr1 = @('vm1','vm2')
$gr2 = @('vm3','vm4')
$gr3 = @('vm5','vm6')

$gr = @($gr1,$gr2,$gr3)

# 1. Iterate through the elements of parent array
0..($gr.Count-1) | foreach {
    "This is parent array #$($_+1), whose elements are $($gr[$_] -join ', '| Out-String)"
}

# 2. Use Nested loops to deifferentiate between elements of the parent vs child arrays
foreach ($ParentArrayElement in $gr) {
    "This is parent array '$(($ParentArrayElement -join ', '| Out-String).Trim())', whose elements are:"
    foreach ($ChildArrayElement in $ParentArrayElement) {
        "    $ChildArrayElement"
    }
}


# 3. Use a 2 dimensional array as explained in https://superwidgets.wordpress.com/2018/01/01/practical-guide-to-powershell-arrays/
$gr = [String[,]]::new(3,2)
# Populating the 2D array with test data:
$gr[0,0] = 'vm1'
$gr[0,1] = 'vm2'
$gr[1,0] = 'vm3'
$gr[1,1] = 'vm4'
$gr[2,0] = 'vm5'
$gr[2,1] = 'vm6'

foreach ($VMList in 0..$gr.GetUpperBound(0)) {
    foreach ($HostList in 0..$gr.GetUpperBound(1)) {
        "gr[$VMList,$HostList]    $($gr[$VMList,$HostList])"
    }
}

[quote quote=221064]This is the output of $on8:

So, if the output match both vm1 and vm2, the script will continue will migrate the one that I specify.[/quote]

But your condition is wrong and cannot work reliably. You should only compare the according properties with each other as I wrote above “if($on8.name -contains $vm1.name)”.

That’s the point. As I mentioned above - you should use a foreach loop - not a for loop. And you create a variable $i in your loop statement but you never use in your script block. If you don’t use it why using such a loop at all?

Shouldn’t this be set up in your virtualisation solution setup? I know you can set those thinkgs in a Hyper-V cluster for example.

Hi Sam,

Can’t use affinity rules because it’s a standard license and DRS is not included.

For example, gr1 contains 2 Domain Controllers so, if a vcenter node will enter in error state, we don’t want downtime untill the HA is making his job, that’s why I want to implement this script. Same situations for gr 2 and 3: adfs and adfs-proxy servers.

 

Thanks for the details. I’ll try and get back.

 

LE: @Olaf, $gr1[0], $gr1[1] etc should be autofilled from that $i statement.

20 some years ago when I was taking the 3-credit Data-Structures college course, I thought it was the dumbest thing I ever heard, and it will be almost never useful and it was for pure academic interest. Boy was I wrong.
IMHO the core of the solution depends on using the most suitable data structure for the task. Here being a hash table not multidimensional array. In the solution below I use PS object which is based on hash tables.

# https://powershell.org/forums/topic/for-loop-used-on-3-groups
<# 
PS script to perform same functionality as VMWare Anti-Affinity groups
Definition: Anti-Affinity group is a group of VMs that should not reside on teh same hypervisor
Sam Boutros - 21 April 2020
#>

[CmdletBinding()]

#region Input

$AntiAffinityGroupList = @(
    [PSCustomObject][Ordered]@{
        Name    = 'Domain Controllers'
        Members = @('DC1','DC2')
    }
    [PSCustomObject][Ordered]@{
        Name    = 'ADFS Servers'
        Members = @('ADFS1','ADFS2')
    }
)

#endregion


#region Process

# Get Current VM Information
$CompleteVMList = Get-VM | select Name,@{n='Host';e={$_.guest.hostname}}

foreach ($AntiAffinityGroup in $AntiAffinityGroupList) {

    Write-Verbose "Processing AntiAffinity Group '$($AntiAffinityGroup.Name)'"
    $myVMList = $AntiAffinityGroup.Members | foreach { $CompleteVMList | where Name -EQ $_ }
    Write-Verbose ($myVMList | FT -a | Out-String).trim()

    if (($myVMList.Host | select -Unique).Count -eq 1) { # They're all on the same host
        # Code to V-Motion the VM to another host
    } 

}

#endregion

Then your loop definition was wrong and I did not see such a piece of code in your post. :wink: … it should have been something like this:

$gr1 = @($vm1, $vm2)
$gr2 = @($vm3, $vm4)
$gr3 = @($vm5, $vm6)

$gr = @($gr1, $gr2, $gr3)

for($i = 0 ; $i -lt $gr.Count; $i++) {
$gr[$i]
}

(of course you should remove the quotes around the variables when you use the code in production where you have filled in your variables :wink: )

Thanks a lot guys!

 

I managed to understand and solve it using the info and solutions you provided. This is the final version that’s working (host 8 and 16 are also defined)

# define Windows VMs
$adfs_g = @('adfs2','adfs1')
$proxy_g = @('adfs-proxy1','adfs-proxy2')
$dc_g = @('dc1','dc2')
#$test = @('test1','test2')

### WIN CLUSTER migration
# define general group
$vmg = @($adfs_g,$proxy_g,$dc_g)

# Define host status check
$win_host_check = Get-VMHost | ? {$_.Powerstate -like "poweredon" -and $_.ConnectionState -like "connected"}

# First part from Sam: 1. Iterate through the elements of parent array
0..($vmg.Count-1) | foreach {
#    "This is parent array #$($_+1), whose elements are $($vmg[$_] -join ', '| Out-String)"
}

# Second part from Sam: 2. Use Nested loops to differentiate between elements of the parent vs child arrays
foreach ($ParentArrayElement in $vmg) {
	### Get info of the host wher VMs are running 
	$on8 = Get-VM | ? {$_.Name -match "^$($ParentArrayElement[0])" -or $_.Name -match "^$($ParentArrayElement[1])" -and $_.VMHost -like "$host8"}
	$on16 = Get-VM | ? {$_.Name -match "^$($ParentArrayElement[0])" -or $_.Name -match "^$($ParentArrayElement[1])" -and $_.VMHost -like "$host16"}
#echo $ParentArrayElement[0] $ParentArrayElement[1]
    if ($win_host_check -Match "$host8" -and $win_host_check -Match "$host16") {
        if ($on8 -Match "^$($ParentArrayElement[1])" -and $on8 -Match "^$($ParentArrayElement[0])") {
	        Move-VM $ParentArrayElement[1] -Destination "$host16" | out-null
	        $vm_check = Get-VM | ? {$_.Name -match "^$($ParentArrayElement[1])" -and $_.VMHost -like "$host16"}
		        if ($vm_check -Match "^$($ParentArrayElement[1])") {
			        $out="migrated successfuly"	
		        }
		        else {
			        $out="NOT MIGRATED"
		        }
		        Write-Output "$(Get-Date): $($ParentArrayElement[0]) still runs on $host8 and VM $($ParentArrayElement[1]) $out to $host16" >> /root/test.txt
	        }

	        elseif ($on16 -Match "^$($ParentArrayElement[0])" -and $on16 -Match "^$($ParentArrayElement[1])") {
		        Move-VM $ParentArrayElement[0] -Destination "$host8" | out-null
		        $vm_check = Get-VM | ? {$_.Name -match "^$($ParentArrayElement[0])" -and $_.VMHost -like "$host8"}
		        if ($vm_check -Match "^$($ParentArrayElement[0])") {
			        $out="migrated successfuly"
			        }
		        else {
			        $out="NOT MIGRATED"
		        } 
		        Write-Output "$(Get-Date): $($ParentArrayElement[0]) $out to $host8 and $($ParentArrayElement[1]) still runs on $host16" >> /root/test.txt
	        }
	        #else {
	        #Write-Output "$(Get-Date): Everything looks good, VMs are running on different hosts." >> /root/test.txt
	        #}
        }
        else {
        exit
        }
}

I’ll do almost the same for the second cluster (linux), where are 3 nodes and VM groups will contain 3 VMs.

 

Sam, I want to try the latest script you posted, but I’m having trouble understanding this part:

if (($myVMList.Host | select -Unique).Count -eq 1) { # They're all on the same host
# Code to V-Motion the VM to another host
}

I refer to “# they’re all on the same host”. For the next line, I understand that it’s the Move-VM command.

[quote quote=221142]Sam, I want to try the latest script you posted, but I’m having trouble understanding this part:
if (($myVMList.Host | select -Unique).Count -eq 1) { # They’re all on the same host
[/quote]

$myVMList will contain information on the VMs that belong to a given $AntiAffinityGroup
such as DC1 and DC2
specifically the VM’s Name and Host properties such as DC1 and Host8

$myVMList.Host will have the hosts of DC1 and DC2 in this example
such as Host8 and Host8

($myVMList.Host | select -Unique) will return Host8 in this example
If the VMs were on 2 different hosts like Host1 and Host8, ($myVMList.Host | select -Unique) will return an array with 2 elements: Host1, Host8

($myVMList.Host | select -Unique).Count will be the count of elements returned from this expression
If this count is 1, then DC1 and DC2 reside on the same host. If it’s more than 1 then they reside on more than 1 host

Thanks again for detailed explanations and for your time!

 

 

Looks like I still need some help.

I tested the second script, and I have the following question:

This part should return Host where VM is running, right?

# Get Current VM Information
$CompleteVMList = Get-VM | select Name,@{n='Host';e={$_.guest.hostname}}

If yes, then VMHost parameter should be used. If not, what’s the point of getting the guest hostname?

Also, if what I said aboe is right, on $myVMList.Host, should be VMHost.

 

I tested with the mentioned changes, but on the following line I get no value for $myVMList:

$myVMList = $AntiAffinityGroup.Members | foreach { $CompleteVMList | where Name -EQ $_ }

 

I get output for first echo, but nothing (except 1- and 2-) from next echos

echo START $AntiAffinityGroup.Members $CompleteVMList

$myVMList = $AntiAffinityGroup.Members | foreach { $CompleteVMList | where Name -EQ $_ }
echo 1-$myVMList
Write-Verbose ($myVMList | FT -a | Out-String).trim()
echo 2-$myVMList