zvmRemoteScripts_utils.ps1

$ZAPPLIANCE_USER = "zadmin"

function Invoke-ZVMScriptWithTimeout {
    <#
        .SYNOPSIS
        Executes the shell script on the ZVM VM with a timeout
 
        .OUTPUTS
        [VMScriptResultImpl]
        Result .ExitCode contains script success/failure
        Result .ScriptOutput must be used with StartsWith() or Contains() because output ends with extra \n newline character
        Result .TrimmedOutput contains clean script output
    #>

    param (
        [ValidateNotNullOrEmpty()]
        [string]$ScriptText,

        [ValidateNotNullOrEmpty()]
        [string]$ActionName,

        [ValidateRange(1, 60)]
        [int]$TimeoutMinutes = 30
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $ZVM = Get-VM -Name $ZVM_VM_NAME

    Write-Host "Invoking '$ActionName' with $TimeoutMinutes minutes timeout."
    # Start the script asynchronously
    $task = Invoke-VMScript -VM $ZVM -ScriptText $ScriptText -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -RunAsync

    # Calculate the timeout time
    $timeoutTime = (Get-Date).AddMinutes($TimeoutMinutes)

    while ((Get-Date) -lt $timeoutTime) {
        # Check the task state periodically
        switch ($task.State) {
            'Success' {
                # The 'Success' state indicates that the remote script was executed, but does not reflect the script success or failure.
                Write-Host "$ActionName execution done."

                # task.Result is VMScriptResultImpl type, and we add a new dynamic property to it.
                $task.Result | Add-Member -MemberType NoteProperty -Name "TrimmedOutput" -Value $task.Result.ScriptOutput.TrimEnd("`n")

                return $task.Result
            }
            'Error' {
                throw "$ActionName execution error: $($task.TerminatingError.Message)"
                # In case of wrong VM credentials, the error message would be "Failed to authenticate with the guest operating system using the supplied credentials."
            }
            default {
                # If the task is 'Running' or in any other state, wait briefly before rechecking.
                Start-Sleep -Seconds 10
            }
        }
    }

    # If the loop exits, it means the timeout was reached
    # Note that the task is not aborted, so the script could eventually succeed
    throw "Timeout, '$ActionName' did not complete within the allotted time of $TimeoutMinutes minutes."
}

function Assert-ZertoInitialized {
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    $startTime = Get-Date
    $action = {
        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            Write-Error "$ZVM_VM_NAME doesn't exists" -ErrorAction Stop
        }

        $res = Invoke-VMScript -VM $ZVM -ScriptText "whoami" -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction SilentlyContinue
        if ($null -eq $res -or $res.ScriptOutput.Trim() -ne $ZAPPLIANCE_USER) {
            throw "ZVMA failed to initialize, authentication failed."
        }

        #TODO: This single check is enough to determine if ZVM is initialized, split between null, when authentication fails and when ZVM is not initialized
        $zvmInitStatusFile = "/opt/zerto/zvr/initialization-files/zvm_initialized"
        $res = Invoke-VMScript -VM $ZVM -ScriptText "[ -e $zvmInitStatusFile ] && echo true || echo false" -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction SilentlyContinue
        if ($null -eq $res -or $res.ScriptOutput.Trim() -ne 'true') {
            throw "ZVMA failed to initialize, initialization file not found."
        }
    }
    Invoke-Retry -Action $action -ActionName "TestZertoInitialized" -RetryCount 15 -RetryIntervalSeconds 60
    Write-Host "Zerto initialization took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."
}

function Set-ZertoVmPassword {
    param(
        [SecureString]$NewPassword
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    $newPasswordText = ConvertFrom-SecureString -SecureString $NewPassword -AsPlainText
    $action = {
        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            Write-Error "$ZVM_VM_NAME doesn't exists" -ErrorAction Stop
        }
        # We need to write result to variable to avoid module logging issues
        # We need SilentlyContinue because when in-guest script changes the password for that very same account during execution, the authenticated session may no longer be valid by the time the cmdlet tries to finalize or return results. This leads to the "Failed to authenticate..." error even though the password is successfully changed.
        $passChange = Invoke-VMScript -VM $ZVM -ScriptText "echo '$($ZAPPLIANCE_USER):$newPasswordText' | sudo chpasswd" -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction SilentlyContinue
        $res = Invoke-VMScript -VM $ZVM -ScriptText "whoami" -GuestUser $ZAPPLIANCE_USER -GuestPassword $newPasswordText -ErrorAction SilentlyContinue
        if ($null -eq $res -or $res.ScriptOutput.Trim() -ne $ZAPPLIANCE_USER) {
            throw "Failed to change ZVML VM password"
        }
        $PersistentSecrets.ZappliancePassword = $newPasswordText
    }
    Invoke-Retry -Action $action -ActionName "ChangeZvmVmPassword" -RetryCount 10 -RetryIntervalSeconds 60
}

function Set-ZertoConfiguration ([string]$DNS, [bool]$IsVaio) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    Set-DnsConfiguration -DNS $DNS

    Stop-ZVM
    Start-ZVM

    Write-Host "Configuring Zerto, this might take a while..."
    $startTime = Get-Date
    #TODO: we need to rename key that holds VC user password. It sound confusing
    $scriptLocation = "/opt/zerto/zlinux/avs/configure_zerto.py"
    $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$($PersistentSecrets.ZertoPassword)' --avsClientSecret '$($PersistentSecrets.AvsClientSecret)'$($IsVaio ? ' --isVaio' : '')"
    $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Configure ZVM"
    Write-Host "Zerto configuration took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."

    if ($result.ScriptOutput.Contains("Success")) {
        Write-Host "Zerto configured successfully."
    }
    elseif ($result.ScriptOutput.Contains("Warning:")) {
        $message = $result.ScriptOutput
        Write-Host $message
        Write-Warning $message
    }
    elseif ($result.ScriptOutput.Contains("Error:")) {
        $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
        throw $cleanErrMsg
    }
    else {
        throw "An unexpected error occurred while configuring Zerto. Please reinstall Zerto."
    }
}

function Update-ZertoConfiguration {
    param(
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [ValidateNotNullOrEmpty()][string]
        $AvsSubscriptionId,

        [ValidateNotNullOrEmpty()][string]
        $AvsResourceGroup,

        [ValidateNotNullOrEmpty()][string]
        $AvsCloudName
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    Write-Host "Reconfiguring Zerto, this might take a while..."

    $scriptLocation = "/opt/zerto/zlinux/avs/reconfigure_zvm.py"
    $ZertoUserWithDomain = "$ZERTO_USER_NAME@$DOMAIN"

    $commandToExecute = "sudo python3 $scriptLocation " +
    "--avsClientSecret '$($PersistentSecrets.AvsClientSecret)' " +
    "--azureTenantId '$AzureTenantId' " +
    "--azureClientID '$AzureClientID' " +
    "--avsSubscriptionId '$AvsSubscriptionId' " +
    "--avsResourceGroup '$AvsResourceGroup' " +
    "--avsCloudName '$AvsCloudName' " +
    "--vcIp '$VC_ADDRESS' " +
    "--vcUsername '$ZertoUserWithDomain' " +
    "--vcPassword '$($PersistentSecrets.ZertoPassword)' " +
    "--zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)'"

    $startTime = Get-Date
    $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Reconfigure ZVM"
    Write-Host "Zerto reconfiguration took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."

    if ($result.ScriptOutput.Contains("Success")) {
        Write-Host "Zerto reconfigured successfully."
    }
    elseif ($result.ScriptOutput.Contains("Warning:")) {
        $message = $result.ScriptOutput
        Write-Host $message
        Write-Warning $message
    }
    elseif ($result.ScriptOutput.Contains("Error:")) {
        $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
        throw $cleanErrMsg
    }
    else {
        throw "An unexpected error occurred while reconfiguring Zerto. Please reinstall Zerto."
    }
}


function Test-ZertoPassword {
    <#
    .SYNOPSIS
    Checks validity of 'admin' and 'zadmin' passwords stored in PersistentSecrets
 
    .DESCRIPTION
    The Zerto 'admin' password is checked explicitely – try_zerto_login.py will return "Success" if the password is valid.
    The Console 'zadmin' password is checked implicitely – Invoke-ZVMScriptWithTimeout will fail with authentication error if the password is invalid.
#>

    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."
        $scriptLocation = "/opt/zerto/zlinux/avs/try_zerto_login.py"
        $commandToExecute = "sudo python3 $scriptLocation --zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)'"
        try {
            $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Validate Zerto password" -TimeoutMinutes 5
            if ($result.ScriptOutput.Contains("Success")) {
                Write-Host "Zerto password is valid."
            }
            else {
                throw "Zerto password validation failed."
            }
        }
        catch {
            #TODO: The user message and Write-Error happen inside this function. Refactor to only throw inside the function.
            $errorMessage = "Please provide valid Zerto password. Problem: $_"
            Write-Host $errorMessage
            Write-Error $errorMessage -ErrorAction Stop
        }
    }
}

function Update-VcPasswordInZvm {
    param (
        [ValidateNotNullOrEmpty()][string] $NewVcPassword,
        [ValidateNotNullOrEmpty()][string] $ZertoAdminPassword,
        [ValidateNotNullOrEmpty()][string] $ClientSecret
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $scriptLocation = "/opt/zerto/zlinux/avs/change_vc_password.py"
        $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$NewVcPassword' --zertoAdminPassword '$ZertoAdminPassword' --avsClientSecret '$ClientSecret'"

        $startTime = Get-Date
        $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Change VC password in ZVM" -TimeoutMinutes 20
        Write-Host "Zerto reconfiguration took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."

        if ($result.ScriptOutput.Contains("Success")) {
            Write-Host "New VC password set successfully in ZVM."
        }
        else {
            if ($result.ScriptOutput.Contains("Error:")) {
                $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
                throw $cleanErrMsg
            }
            throw "Unexpected error occurred while changing VC password in ZVM."
        }
    }
}

function Update-ClientCredentialsInZvm {
    param (
        [ValidateNotNullOrEmpty()][string] $NewClientId,
        [ValidateNotNullOrEmpty()][string] $NewClientSecret
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $scriptLocation = "/opt/zerto/zlinux/avs/change_azure_client_credentials.py" # change_azure_client_credentials is available starting ZVM 10u5p2

        $scriptExists = Test-FileExistsInZVM -FileLocation $scriptLocation #TODO: Consider extracting check to caller method, method should not return bool value
        if ($scriptExists -eq $false) {
            return $false # ZVMA version does not support updating Client Credentials
        }

        $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$($PersistentSecrets.ZertoPassword)' --zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)' --azureClientId '$NewClientId' --avsClientSecret '$NewClientSecret'"

        $startTime = Get-Date
        $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Change Azure client credentials in ZVM" -TimeoutMinutes 20
        Write-Host "Zerto reconfiguration took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."

        if ($result.ScriptOutput.Contains("Success")) {
            Write-Host "New Azure client credentials set successfully in ZVM."
        }
        else {
            if ($result.ScriptOutput.Contains("Error:")) {
                $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
                throw $cleanErrMsg
            }
            throw "Unexpected error occurred while changing Azure client credentials in ZVM."
        }

        return $true
    }
}

function Set-DnsConfiguration($DNS) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $action = {
            $ZVM = Get-VM -Name $ZVM_VM_NAME
            if ($null -eq $ZVM) {
                Write-Error "$ZVM_VM_NAME doesn't exists" -ErrorAction Stop
            }
            $setDnsCommand = "grep -qxF 'nameserver $DNS' /etc/resolv.conf || echo 'nameserver $DNS' | sudo tee -a /etc/resolv.conf"
            $res = Invoke-VMScript -VM $ZVM -ScriptText $setDnsCommand -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction SilentlyContinue

            $checkDnsCommand = "grep -qF 'nameserver $DNS' /etc/resolv.conf && echo 'true' || echo 'false'"
            $res = Invoke-VMScript -VM $ZVM -ScriptText $checkDnsCommand -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction SilentlyContinue
            if ($null -eq $res -or $res.ScriptOutput.Trim() -ne "true") {
                throw "Failed to force set DNS"
            }

            $lockFileCommand = 'sudo chattr +i /etc/resolv.conf'
            $res = Invoke-VMScript -VM $ZVM -ScriptText $lockFileCommand -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword
            Write-Host "DNS successfully set"
        }
        Invoke-Retry -Action $action -ActionName "SetDNS" -RetryCount 4 -RetryIntervalSeconds 30
    }
    catch {
        $message = "Failed to set DNS. Configuration may fail. Problem: $_"
        Write-Host $message
        Write-Warning $message
    }
}

function Test-FileExistsInZVM($FileLocation) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            throw "$ZVM_VM_NAME VM does not exist."
        }

        $existsFileCommand = "test -f $FileLocation && echo 'true' || echo 'false'"

        $res = Invoke-VMScript -VM $ZVM -ScriptText $existsFileCommand -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction Stop

        switch (${res}?.ScriptOutput?.Trim()) {
            { $_ -eq $null } { throw "Unknown error." }
            { $_ -eq "true" } { return $true }
            { $_ -eq "false" } { return $false }
            default { throw "Unexpected output '$_'." }
        }
    }
    catch {
        throw "Failed to check file in ZVM. Problem: $_"
    }
}