common_utils.ps1

Set-StrictMode -Version 3.0

$global:ZVM_VM_NAME = "ZVML"
$global:VRA_VM_PATTERN = "Z-VRA*"
$global:ZERTO_FOLDER_ON_HOST = "/var/zerto"
$global:SDDC_RESOURCE_ID_PATTERN = "/subscriptions/(?<AvsSubscriptionId>[^/]+)/resourceGroups/(?<AvsResourceGroup>[^/]+)/providers/Microsoft\.AVS/privateClouds/(?<AvsCloudName>[^/]+)"

function Get-StrictMode {

    try { $a = @(1); ($null -eq $a[2]) | Out-Null }
    catch { return 3 }

    try { ($null -eq "Not-a-Date".Year) | Out-Null }
    catch { return 2 }

    try { ($undefined -gt 1) | Out-Null }
    catch { return 1 }

    return 0
}

function Start-ZVM {

    try {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            throw "$ZVM_VM_NAME VM does not exist."
        }
        else {
            if ($ZVM.PowerState -eq 'PoweredOff') {
                Write-Host "$ZVM_VM_NAME is powered off, going to power it on"
                Start-VM $ZVM -ErrorAction Stop | Out-Null
            }
            else {
                Write-Host "$ZVM_VM_NAME is up and running"
            }
        }
    }
    catch {
        throw "Failed to start $ZVM_VM_NAME. Problem: $_"
    }
}

function Stop-ZVM {

    try {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            throw "$ZVM_VM_NAME VM does not exist."
        }
        else {
            if ($ZVM.PowerState -eq 'PoweredOn') {
                Write-Host "$ZVM_VM_NAME is powered on, going to power it off"
                Stop-VM -VM $ZVM -Confirm:$False -ErrorAction Stop | Out-Null
            }
            else {
                Write-Host "$ZVM_VM_NAME is off"
            }
        }
    }
    catch {
        throw "Failed to stop $ZVM_VM_NAME. Problem: $_"
    }
}

function New-RandomPassword {

    # Generate a password with at least 4 uppercase, 4 lowercase, 3 digits and 3 special characters from !@#%^*()
    # The first character must be a letter
    # The password length will vary between 14 and 18 symbols

    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $extlen = { 0, 1 | Get-SecureRandom }

    $firstRand = 'a'..'z' | Get-SecureRandom
    $lowerRand = 'a'..'z' | Get-SecureRandom -Count (3 + (& $extlen)) # Eventually, the password will have at least 4 lowercase letters when firstRand is added
    $upperRand = 'A'..'Z' | Get-SecureRandom -Count (4 + (& $extlen))
    $numericRand = '0'..'9' | Get-SecureRandom -Count (3 + (& $extlen))
    $specialRand = [char[]]'!@#%^*()' | Get-SecureRandom -Count (3 + (& $extlen))

    $allRand = $upperRand + $lowerRand + $numericRand + $specialRand

    $shuffledRand = $allRand | Get-SecureRandom -Count $allRand.Length

    $password = $firstRand + ($shuffledRand -join '')

    return $password
    #TODO: return secure string
}

function New-ZertoFolderOnHost {
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName
    )

    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $Command = "mkdir -p $ZERTO_FOLDER_ON_HOST"
        $Res = Invoke-SSHCommands -HostName $HostName -Commands $Command
        $ExitStatus = $Res["0_exitStatus"];

        if ( $ExitStatus -ne '0' ) {
            throw "failed to create $ZERTO_FOLDER_ON_HOST on host $HostName. Exit status for ""$Command"" is $ExitStatus"
        }
        else {
            Write-Host "Zerto folder ($ZERTO_FOLDER_ON_HOST) was created on $HostName."
        }
    }
}

function Invoke-Retry {
    param (
        [Parameter(Mandatory = $true)]
        [ScriptBlock]$Action,

        [Parameter(Mandatory = $true)]
        [string]$ActionName,

        [Parameter(Mandatory = $true)]
        [int]$RetryCount,

        [Parameter(Mandatory = $true)]
        [int]$RetryIntervalSeconds
    )

    #TODO: This method must be reworked to get rid of Write-Output, use Write-Host but ensure it is no too verbose
    for ($i = 1; $i -le $RetryCount; $i++) {
        try {
            Write-Output "Attempt $i of $RetryCount executing $ActionName."
            & $Action
            Write-Output "$ActionName succeeded."
            return
        }
        catch {
            Write-Output "Attempt $i of $RetryCount on $ActionName wasn't successful."

            if ($i -ne $RetryCount) {
                Write-Output "Waiting for $RetryIntervalSeconds seconds before retrying $ActionName..."
                Start-Sleep -Seconds $RetryIntervalSeconds
            }
            else {
                Write-Output "Error: All attempts to execute $ActionName failed. Exiting."
                throw
            }
        }
    }
}

function Get-DatastoreUUID ($DatastoreName) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    $datastore = Get-Datastore -Name $DatastoreName
    $uuid = ($datastore.ExtensionData.Info.Url -replace '.*volumes/', '').TrimEnd('/') # Url format is 'ds:///vmfs/volumes/vsan:529ecf79732d3104-202d349462b20b76/'
    Write-Host "Datastore '$DatastoreName' UUID: $uuid"
    return $uuid
}

function Invoke-SSHCommands {
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Commands to execute")]
        [String[]]$Commands,
        [Parameter(Mandatory = $false,
            HelpMessage = "Action on exitStatus 1")]
        [string]$ExitStatusAction = "Stop"
    )

    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $NamedOutputs = @{}
        Set-Variable -Name NamedOutputs -Value $NamedOutputs -Scope Global

        $i = 0
        foreach ($Command in $Commands) {
            $SSH = Invoke-SSHCommand -SSHSession $SSH_Sessions[$HostName].Value -Command $Command

            if (-not $SSH) {
                throw "Failed to Invoke-SSHCommand '$Command' on host $HostName"
            }

            $ExitStatus = $SSH.ExitStatus
            $SshError = $SSH.Error
            $Output = ($SSH.Output -join ";")

            if ($ExitStatus -ne 0 -or $SshError) {
                if (($ExitStatus -eq 1) -and (!$SshError) -and ($ExitStatusAction -eq "Skip")) {
                    Write-Host "ExitStatus of '$Command' is 1, while ExitStatusAction = Skip. Skipping..."
                }
                else {
                    throw "Failed to run '$Command' on host $HostName, ExitStatus: $ExitStatus, Output: $Output, Error: $SshError, ExitStatusAction: $ExitStatusAction"
                }
            }

            Write-Host "Command was completed successfully '$Command' on host $HostName, ExitStatus: $ExitStatus, Output: $Output, Error: $SshError"

            $NamedOutputs["$($i)_cmd"] = $Command
            $NamedOutputs["$($i)_exitStatus"] = $ExitStatus
            $NamedOutputs["$($i)_output"] = $Output
            $NamedOutputs["$($i)_error"] = $SshError

            $i++;
        }

        return $NamedOutputs
    }
}


function Set-KeystrokesToZVMA {
    <#
    .SYNOPSIS
        Sends a series of character keystrokse to ZVMA VM
    #>

    param (
        [Parameter(Mandatory)]
        [string]$KeystrokesInput
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $hidCharMap = @{
        "a"  = "0x04";
        "b"  = "0x05";
        "c"  = "0x06";
        "d"  = "0x07";
        "e"  = "0x08";
        "f"  = "0x09";
        "g"  = "0x0a";
        "h"  = "0x0b";
        "i"  = "0x0c";
        "j"  = "0x0d";
        "k"  = "0x0e";
        "l"  = "0x0f";
        "m"  = "0x10";
        "n"  = "0x11";
        "o"  = "0x12";
        "p"  = "0x13";
        "q"  = "0x14";
        "r"  = "0x15";
        "s"  = "0x16";
        "t"  = "0x17";
        "u"  = "0x18";
        "v"  = "0x19";
        "w"  = "0x1a";
        "x"  = "0x1b";
        "y"  = "0x1c";
        "z"  = "0x1d";
        "1"  = "0x1e";
        "2"  = "0x1f";
        "3"  = "0x20";
        "4"  = "0x21";
        "5"  = "0x22";
        "6"  = "0x23";
        "7"  = "0x24";
        "8"  = "0x25";
        "9"  = "0x26";
        "0"  = "0x27";
        "!"  = "0x1e";
        "@"  = "0x1f";
        "#"  = "0x20";
        "$"  = "0x21";
        "%"  = "0x22";
        "^"  = "0x23";
        "&"  = "0x24";
        "*"  = "0x25";
        "("  = "0x26";
        ")"  = "0x27";
        "_"  = "0x2d";
        "+"  = "0x2e";
        "{"  = "0x2f";
        "}"  = "0x30";
        "|"  = "0x31";
        ":"  = "0x33";
        "~"  = "0x35";
        "<"  = "0x36";
        ">"  = "0x37";
        "?"  = "0x38";
        "-"  = "0x2d";
        "="  = "0x2e";
        "["  = "0x2f";
        "]"  = "0x30";
        "\"  = "0x31";
        ";"  = "0x33";
        ","  = "0x36";
        "."  = "0x37";
        "/"  = "0x38";
        " "  = "0x2c";
        "`'" = "0x34";
        "`"" = "0x34";
        "`n" = "0x28";
    }

    $ZVM = Get-View -ViewType VirtualMachine -Filter @{"Name" = "^$($ZVM_VM_NAME)$" }
    if ($null -eq $ZVM) {
        throw "$ZVM_VM_NAME VM does not exist."
    }

    $hidKeyEvents = @()
    foreach ($char in $KeystrokesInput.ToCharArray()) {

        if (-not $hidCharMap.ContainsKey([string]$char)) {
            throw "Character $char is not supported."
        }

        $hidCodeHexStr = $hidCharMap[[string]$char]
        $hidCodeHexToInt = [Convert]::ToInt64($hidCodeHexStr, "16")
        $hidCodeValue = ($hidCodeHexToInt -shl 16) -bor 0007 # Expected value for UsbHidCode of 'a' is '0x04 0007'

        $hidEvent = New-Object VMware.Vim.UsbScanCodeSpecKeyEvent

        # Add LeftShift for capital letters and ~!@#$%^&*()_+{}:"<>? characters
        if ( ($char -cmatch "[A-Z]") -or ($char -match '[~!@#$%^&*()_+{}:"<>?]') ) {
            $modifier = New-Object Vmware.Vim.UsbScanCodeSpecModifierType
            $modifier.LeftShift = $true
            $hidEvent.Modifiers = $modifier
        }

        $hidEvent.UsbHidCode = $hidCodeValue
        $hidKeyEvents += $hidEvent
    }

    $spec = New-Object Vmware.Vim.UsbScanCodeSpec
    $spec.KeyEvents = $hidKeyEvents

    $len = $ZVM.PutUsbScanCodes($spec)

    return $len
}

function Change-ExpiredConsolePasswordIfRequired {
    <#
    .SYNOPSIS
        Changes the ZVM VM console password if it is locked out due to expiration
        username ⟶ current pass ⟶ current pass ⟶ new pass ⟶ new pass
    #>

    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $ZAPPLIANCE_USER = "zadmin"

    $testBefore = Test-ZertoPasswordResult
    if ($testBefore -eq [PasswordsValidationResult]::ConsolePasswordInvalidOrExpired) {
        Write-Host "Attempting to change a possibly expired password..."

        $oldPassword = $PersistentSecrets.ZappliancePassword
        $newPassword = New-RandomPassword

        $len = Set-KeystrokesToZVMA -KeystrokesInput "$ZAPPLIANCE_USER`n"
        $len = Set-KeystrokesToZVMA -KeystrokesInput "$oldPassword`n"
        $len = Set-KeystrokesToZVMA -KeystrokesInput "$oldPassword`n"
        $len = Set-KeystrokesToZVMA -KeystrokesInput "$newPassword`n"
        $len = Set-KeystrokesToZVMA -KeystrokesInput "$newPassword`n"

        $PersistentSecrets.ZappliancePassword = $newPassword

        $testAfter = Test-ZertoPasswordResult
        if ($testAfter -eq [PasswordsValidationResult]::PasswordsValid) {
            Write-Host "Expired console password was changed successfully."
            return $true
        }
        else {
            throw "Failed to change the possibly expired password."
        }
    }

    return $false
}