PowerCLI

 View Only

 Assistance with PowerCLI Start-Job

Jump to  Best Answer
mk_ultra's profile image
mk_ultra posted Sep 19, 2024 11:27 AM
Hello,
I referenced this LucD article in attempt to get my PowerCLI VM migration script to spawn jobs instead of running through clusters sequentially.
Running a background job - LucD notes
LucD notes remove preview
Running a background job - LucD notes
Tweet PowerShell has a nifty feature that allows you to run code in [...]
View this on LucD notes >
I'm having some issues, maybe to do with passing variables? The jobs create successfully but finish immediately without doing anything.
I apologize for the massive code block. I assume the issue is within the $code ScriptBlock or the $sJob parameter block.

$ScriptName = "VM_Balance_and_Migration"
$Version = "1.1"
$LastModified = (Get-Item .).LastWriteTime
$Author = "Johnny Appleseed"
$DateFormat = "%m%d%Y_%H%M%S"
$DateTime = Get-Date -UFormat $DateFormat
$ScriptLogName = "$ScriptName"+"_Log_"+"$DateTime"
$ScriptLogPath = "C:\scripts\logs\$ScriptLogName.log"
$VerbosePreference = "Continue" # Default = "SilentlyContinue"
$ErrorActionPreference = "Continue" # Default = "Continue"
$WarningPreference = "Continue" # Default = "Continue"
$ErrorView = "NormalView" # Default = "NormalView"
$ConfirmPreference = "None" # Default = "High"
$PSDefaultParameterValues["Write-Host:ForegroundColor"] = "Green"
$PSDefaultParameterValues["Write-Host:BackgroundColor"] = "Black"
$Host.UI.RawUI.WindowTitle = "$ScriptName"
$White = @{ForegroundColor = "White"; BackgroundColor = "Black"}
$Cyan = @{ForegroundColor = "Cyan"; BackgroundColor = "Black"}
$Green = @{ForegroundColor = "Green"; BackgroundColor = "Black"}
$Yellow = @{ForegroundColor = "Yellow"; BackgroundColor = "Black"}
$Red = @{ForegroundColor = "Red"; BackgroundColor = "Black"}
$Blue = @{ForegroundColor = "Blue"; BackgroundColor = "Black"}
$Magenta = @{ForegroundColor = "Magenta"; BackgroundColor = "Black"}

Function Write-Log {
<#
.SYNOPSIS
  Write-Log writes a message to a specified log file and/or the host with the current time stamp.
.DESCRIPTION
  Author: Johnny Appleseed
  Write-Log writes a message to a specified log file and/or the host with the current time stamp.
  Uses "C:\scripts\logs\Default_Log.log" as the default log file if no path/file name is specified.
  Define $ScriptLogPath variable in the script to change the default log location.
.PARAMETER Message
  Message is the content that you wish to add to the log file.
.PARAMETER Level
  Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER LogPath
  Specify a path and file name for the log file.
.PARAMETER NoHost
  Generates log entries without outputting to the host.
.EXAMPLE
  Write-Log -Message "Warning log message" -Level Warning -NoHost
  Writes a warning message to the log file without outputting to the host.
.EXAMPLE
  Write-Log "Folder does not exist." -LogPath "C:\Scripts\Logs\test1.log" -Level Error
  Writes a message to the host and the log file located at "C:\Scripts\Logs\test1.log", and writes the message to the error pipeline.
.EXAMPLE
  Write-Log "Informational message"
  Writes the message to the host and logs it as informational in the default or predefined log file location.
.LINK
  Inspired by:
  https://www.techtarget.com/searchwindowsserver/tutorial/Build-a-PowerShell-logging-function-for-troubleshooting
#>
    [CmdletBinding()]
    Param (
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true,
            Position=0)]
        [ValidateNotNullorEmpty()]
        [String]$Message,
        [Parameter(Position=1)]
        [ValidateSet("Information","Warning","Error","Debug","Verbose")]
        [String]$Level = 'Information',
        [String]$LogPath = $ScriptLogPath,
        [Switch]$NoHost
    )
    Begin {
        if (!(Test-Path $LogPath)) {
            $null = New-Item -Path $LogPath -Force
        }
    }
    Process {
        $DateFormat = "%m/%d/%Y %H:%M:%S"
        If (-Not $NoHost) {
            Switch ($Level) {
                "Information" {
                    Write-Host ("[{0}] {1}" -F (Get-Date -UFormat $DateFormat), $Message)
                }
                "Warning" {
                    Write-Warning ("[{0}] {1}" -F (Get-Date -UFormat $DateFormat), $Message)
                }
                "Error" {
                    Write-Error ("[{0}] {1}" -F (Get-Date -UFormat $DateFormat), $Message)
                }
                "Debug" {
                    Write-Debug ("[{0}] {1}" -F (Get-Date -UFormat $DateFormat), $Message) -Debug:$true
                }
                "Verbose" {
                    Write-Verbose ("[{0}] {1}" -F (Get-Date -UFormat $DateFormat), $Message) -Verbose:$true
                }
            }
        }
        Add-Content -Path $LogPath -Value ("[{0}] ({1}) {2}" -F (Get-Date -UFormat $DateFormat), $Level, $Message)
    }
}

# Print script information with Write-Host animation
$String = "Script: $ScriptName"
$StringLength = $String.Length
$Char = 0
do {
    $DisplayChar = $String[$Char]
    Write-Host -NoNewLine "$DisplayChar" @MTron
    Start-Sleep -Milliseconds 35
    $Char++
} until ($Char -eq $StringLength)
Write-Host ""
Start-Sleep 1
$String = "Version: $Version"
$StringLength = $String.Length
$Char = 0
do {
    $DisplayChar = $String[$Char]
    Write-Host -NoNewLine "$DisplayChar" @MTron
    Start-Sleep -Milliseconds 35
    $Char++
} until ($Char -eq $StringLength)
Write-Host ""
Start-Sleep 1
$String = "Last Modified: $LastModified"
$StringLength = $String.Length
$Char = 0
do {
    $DisplayChar = $String[$Char]
    Write-Host -NoNewLine "$DisplayChar" @MTron
    Start-Sleep -Milliseconds 35
    $Char++
} until ($Char -eq $StringLength)
Write-Host ""
Start-Sleep 1
$String = "Author: $Author"
$StringLength = $String.Length
$Char = 0
do {
    $DisplayChar = $String[$Char]
    Write-Host -NoNewLine "$DisplayChar" @MTron
    Start-Sleep -Milliseconds 35
    $Char++
} until ($Char -eq $StringLength)
Write-Host ""
Start-Sleep 1

# Print script start time and add log entry
Write-Log -Message "Script $ScriptName started."
$StartTime = Get-Date

# Connect to vCenter
$vCenter = "examplevcenter.net"
Write-Log "Connecting to vCenter $vCenter"
Connect-VIServer $vCenter

# Define input file path
$InputFilePath = "C:\scripts\karwosm\ClusterList.txt"
# Show contents of the input list, allow user to make changes until 'y' is entered
do {
    Write-Host "`nPlease confirm that the text file located at '$InputFilePath' contains a full list of the store clusters on which to perform migrations, one per line. They must all be located in the connected vCenter: $vCenter`nCurrent list contents are displayed below:" @MattyIce
    Get-Content $InputFilePath
    $Answer1 = Read-Host "`nIf the above list is correct, enter 'y' to continue. Otherwise, update the file and enter 'n' to show file contents again."
} until ($Answer1 -eq "y")
# Set input file variable
$InputFile = Get-Content $InputFilePath

Write-Host "Migration options:" @Green
Write-Host "1 - Migrate all VM's to the 555 host and place the 666 host in maintenance mode" @Cyan
Write-Host "2 - Migrate all VM's to the 666 host and place the 555 host in maintenance mode" @Cyan
Write-Host "3 - Remove both hosts from maintenance mode and balance VM's across hosts (111/222 on 555, 333/444 on 666)" @Cyan
do {
    $Answer2 = Read-Host "What migration actions would you like to perform on all clusters in the above input file? (Enter 1, 2, or 3)"
} until (($Answer2 -eq "1") -or ($Answer2 -eq "2") -or ($Answer2 -eq "3"))
if ($Answer2 -like "1") {
    $Confirm = Read-Host "Migrate all VM's to the 555 host and place the 666 host into maintenance mode for all clusters in the above input list? Enter 'y' to confirm"
} elseif ($Answer2 -like "2") {
    $Confirm = Read-Host "Migrate all VM's to the 666 host and place the 555 host into maintenance mode for all clusters in the above input list? Enter 'y' to confirm"
} elseif ($Answer2 -like "3") {
    $Confirm = Read-Host "Remove both hosts from maintenance mode and balance VM's across hosts (111/222 on 555, 333/444 on 666) for all clusters in the above input list? Enter 'y' to confirm"
}
if ($Confirm -ne "y") {
    Write-Log "Actions were not confirmed. Exiting script." -Level Error
    throw
}

# Set progress bar variables
$TotalItems = $InputFile.count
$CurrentItem = 1
foreach ($I in $InputFile) {
    $Cluster = Get-Cluster $I
    $ClusterName = $Cluster.Name
    # Check that the cluster is a standard store cluster (So that the script can be run in test vCenter)
    $Check555 = $Cluster | Get-VMHost | where {$_.Name -like "*555*"}
    $Check666 = $Cluster | Get-VMHost | where {$_.Name -like "*666*"}
    $Check111 = $Cluster | Get-VM | where {$_.Name -like "*111*"}
    $Check222 = $Cluster | Get-VM | where {$_.Name -like "*222*" -and $_.Name -notlike "*replica*"}
    $Check333 = $Cluster | Get-VM | where {$_.Name -like "*333*"}
    $Check444 = $Cluster | Get-VM | where {$_.Name -like "*444*"}
    $Count111 = $Check111.count
    $Count222 = $Check222.count
    $Count333 = $Check333.count
    $Count444 = $Check444.count
    if ($Check555-and $Check666 -and $Check111 -and $Check222 -and $Check333 -and $Check444 -and $Count111 -eq "1" -and $Count222 -eq "1" -and $Count333 -eq"1" -and $Count444 -eq "1") {
        # Display progress bar
        Write-Progress -Activity "Starting migration job on cluster $ClusterName" -Status "Cluster $CurrentItem of $TotalItems" -PercentComplete (($CurrentItem/$TotalItems)*100)

        $code = {
            param(
            [string]$Server,
            [string]$SessionId,
            [string]$Cluster,
            [string]$Answer2
            )
            Set-PowerCLIConfiguration -DisplayDeprecationWarnings $false -Confirm:$false | Out-Null
            Connect-VIServer -Server $Server -Session $SessionId

            # Define cluster variables
            $Host555 = $Cluster | Get-VMHost | where {$_.Name -like "*555*"}
            $Host555Name = $Host555.name
            $Host666 = $Cluster | Get-VMHost | where {$_.Name -like "*666*"}
            $Host666Name = $Host666.name
            $AllVMs = $Cluster | Get-VM | where {$_.Name -notlike "*replica*" -and $_.Name -notlike "*vCLS*"}
            $555VMs = $Cluster | Get-VM | where {($_.Name -notlike "*replica*" -and $_.Name -notlike "*vCLS*") -and ($_.Name -like "*111*" -or $_.Name -like "*222*")}
            $666VMs = $Cluster | Get-VM | where {($_.Name -notlike "*replica*" -and $_.Name -notlike "*vCLS*") -and ($_.Name -like "*333*" -or $_.Name -like "*444*")}
            # Check that both hosts are powered on
            if (($Host555.PowerState -eq "PoweredOn") -and ($Host666.PowerState -eq "PoweredOn")) {
                # Start migration job
                if ($Answer2 -like "1") {
                    # Check if host 555 is in maintenance mode, if so remove it from maintenance mode
                    $VMHostStatus = $Host555.State
                    if ($VMHostStatus -eq "Maintenance") {
                        $Host555 | Set-VMHost -State Connected
                    } else {}
                    # Migrate all VM's to host 555
                    $Relo555VMs = $AllVMs | where {$_.VMHost.Name -ne $Host555Name}
                    $Relo555VMs | Move-VM -Destination $Host555 -VMotionPriority High -RunAsync
                    # Place host 666 in maintenance mode
                    $VMHostStatus = $Host666.State
                    if ($VMHostStatus -eq "Maintenance") {
                    } else {
                        $spec = new-object VMware.Vim.HostMaintenanceSpec
                        $spec.VsanMode = new-object VMware.Vim.VsanHostDecommissionMode
                        $spec.VsanMode.ObjectAction = "ensureObjectAccessibility"
                        $Host666.ExtensionData.EnterMaintenanceMode(0, $false, $spec)
                    }
                } elseif ($Answer2 -like "2") {
                    # Check if host 666 is in maintenance mode, if so remove it from maintenance mode
                    $VMHostStatus = $Host666.State
                    if ($VMHostStatus -eq "Maintenance") {
                        $Host666 | Set-VMHost -State Connected
                    } else {}
                    # Migrate all VM's to host 666
                    $Relo666VMs = $AllVMs | where {$_.VMHost.Name -ne $Host666Name}
                    $Relo666VMs | Move-VM -Destination $Host666 -VMotionPriority High -RunAsync
                    # Place host 555 in maintenance mode
                    $VMHostStatus = $Host555.State
                    if ($VMHostStatus -eq "Maintenance") {
                    } else {
                        $spec = new-object VMware.Vim.HostMaintenanceSpec
                        $spec.VsanMode = new-object VMware.Vim.VsanHostDecommissionMode
                        $spec.VsanMode.ObjectAction = "ensureObjectAccessibility"
                        $Host555.ExtensionData.EnterMaintenanceMode(0, $false, $spec)
                    }
                } elseif ($Answer2 -like "3") {
                    # Remove both hosts from maintenance mode
                    $Host555 | Set-VMHost -State Connected
                    $Host666 | Set-VMHost -State Connected
                    # Balance VM's across both hosts
                    $555VMs | Move-VM -Destination $Host555 -VMotionPriority High -RunAsync
                    $666VMs | Move-VM -Destination $Host666 -VMotionPriority High -RunAsync
                }
            } else {}
        }
        
        $sJob = @{
            ScriptBlock = $code
            ArgumentList = $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId, $Cluster, $Answer2
        }
        
        Start-Job @sJob

        $CurrentItem++
    } else {
        Write-Log "Cluster $ClusterName is not a store cluster. Skipping." -Level Error
    }
}

# Print script end time and add log entry
$ElapsedTime = (Get-Date) - $StartTime
$TotalTime = "{0:HH:mm:ss}" -f ([datetime]$ElapsedTime.Ticks)
Write-Log "Script $ScriptName completed. Total run time: $TotalTime"
Write-Host "Log file located at $ScriptLogPath" @Green

# Disconnect from vCenter
Disconnect-VIServer

LucD's profile image
LucD  Best Answer

With the -Wait switch on the Receive-Job cmdlet, the script will effectively wait till the job is done.

Where did you place that Receivve-Job cmdlet?
Inside the foreach loop?

A better way would be to launch all job, and then after the foreach loop do a Get-Job for all the background jobs.
Only do a Receive-Job when the status for all the jobs is not "running" anymore.

Also the cluster parameter you pass is still the cluster object, not a String as you defined in the code block.
Unless you already changed that.

mk_ultra's profile image
mk_ultra

OK, I realized that I casted my $Cluster variable wrong. I tried casting it to [VMware.VimAutomation.ViCore.Impl.V1.Inventory.ComputeResourceImpl] but the job it creates still fails.

LucD's profile image
LucD

Is there any output from the background jobs when you do a Receive-Job?
If not, try adding some Write-Host lines in the $code block.
For example after the Connect-VIServer.

mk_ultra's profile image
mk_ultra

Hey LucD,

When I start the job and assign it a variable and receive the job, the script works as intended:

$j = Start-Job @JobParams

Receive-Job -Job $j -Wait
However, when I add multiple clusters to my input list, the script then has to wait for each migration job to finish before starting the next one, so it's not achieving parallelization.
mk_ultra's profile image
mk_ultra

Moving the Receive-Job command outside of the foreach loop was the fix. Thanks LucD!!!