zvmRemoteScripts_utils.ps1

$ZAPPLIANCE_USER = "zadmin" #TODO Make global if possible, used as private variable elsewhere

function Invoke-ZVMLScriptWithTimeout {
    <#
        .SYNOPSIS
        Executes the shell script on the ZVML VM with a timeout asynchronously
        Used to execute long-running Python scripts
 
        .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
    if ($null -eq $ZVM) {
        throw "$ZVM_VM_NAME VM does not exist."
    }

    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
    #TODO Maybe ask user to restart the ZVML, to avoid leaving the script running in unclear state
    throw "Timeout, '$ActionName' did not complete within the allotted time of $TimeoutMinutes minutes."
}

function Invoke-ZVMLScript {
    <#
        .SYNOPSIS
        Executes the shell script on the ZVML VM synchronously
 
        .OUTPUTS
        [VMScriptResultImpl]
        Result .ExitCode contains script success/failure
        Result .ScriptOutput contains the clean script output
    #>

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

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

    $res = Invoke-VMScript -VM $ZVM -ScriptText $ScriptText -GuestUser $ZAPPLIANCE_USER -GuestPassword $PersistentSecrets.ZappliancePassword -ErrorAction Stop
    if ($null -eq $res) {
        throw "Failed to invoke VM script."
    }

    return [PSCustomObject]@{
        ExitCode     = $res.ExitCode
        ScriptOutput = $res.ScriptOutput.TrimEnd("`n")
    }
}

function Assert-ZertoInitialized {
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    $startTime = Get-Date
    $action = {
        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            throw "$ZVM_VM_NAME VM does not exist."
        }

        $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 not initialized, 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 not initialized, initialization file not found."
        }
    }
    Invoke-Retry -Action $action -ActionName "TestZvmaInitialized" -RetryIntervalSeconds 120
    Write-Host "Zerto initialization took: $((Get-Date).Subtract($startTime).TotalSeconds.ToString("F0")) seconds."
}

function Set-ZertoVmPassword {
    <#
    .SYNOPSIS
        Sets the ZVM VM console password and updates the PersistentSecrets
    #>

    param(
        [ValidateNotNullOrEmpty()]
        [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) {
            throw "$ZVM_VM_NAME VM does not exist."
        }

        # Clear the password history and change password
        $cmdClearHistoryChangePassword = "sudo truncate -s 0 /etc/security/opasswd; echo '$($ZAPPLIANCE_USER):$newPasswordText' | sudo chpasswd"
        # We need to write result to variable to avoid module logging issues
        # We need SilentlyContinue because when an in-guest script changes its own account password, the authenticated session may no longer be valid by the time the cmdlet attempts to finalize and return a result.
        # This causes a "Failed to authenticate..." error, even though the password was successfully changed.
        # For the same reason, ScriptOutput will also be unavailable.
        $empty = Invoke-VMScript -VM $ZVM -ScriptText $cmdClearHistoryChangePassword -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 "ChangeZvmlVmPassword" -RetryIntervalSeconds 20 -RetryCount 3
}

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

    # This was commented for 10u7+ because static DNS should now be set correctly by appliance from ovf, fixed in ZER-150945.
    # If no issues are found in production, this code can be removed in the next release.
    # Set-DnsConfiguration -DNS $DNS
    # Stop-ZVM
    # Start-ZVM

    Write-Host "Configuring Zerto, this might take a while..."
    $startTime = Get-Date
    $scriptLocation = "/opt/zerto/zlinux/avs/configure_zerto.py"
    $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$($PersistentSecrets.ZertoPassword)' --avsClientSecret '$($PersistentSecrets.AvsClientSecret)'$($IsVaio ? ' --isVaio' : '')"
    $result = Invoke-ZVMLScriptWithTimeout -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-ZVMLScriptWithTimeout -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 explicitly – try_zerto_login.py will return "Success" if the password is valid.
    The Console 'zadmin' password is checked implicitly – Invoke-ZVMLScriptWithTimeout will fail with authentication error if the password is invalid.
#>

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

    $scriptLocation = "/opt/zerto/zlinux/avs/try_zerto_login.py"
    $commandToExecute = "sudo python3 $scriptLocation --zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)'"
    try {
        $result = Invoke-ZVMLScriptWithTimeout -ScriptText $commandToExecute -ActionName "Validate Zerto password" -TimeoutMinutes 5
        if ($result.ScriptOutput.Contains("Success")) {
            Write-Host "Zerto password is valid."
        }
        else {
            throw "Provided Zerto password is not valid."
        }
    }
    catch {
        throw "Zerto password validation failed. Problem: $_"
    }
}

enum PasswordsValidationResult {
    PasswordsValid = 0;
    ZertoPasswordInvalid = 1;
    ConsolePasswordInvalidOrExpired = 2;
}

function Test-ZertoPasswordResult {
    <#
    .SYNOPSIS
        Checks validity of 'admin' and 'zadmin' passwords stored in PersistentSecrets and returns a result code.
 
    .OUTPUTS
        [PasswordsValidationResult]
        PasswordsValid - Both passwords are valid
        ZertoPasswordInvalid - The 'admin' password is not valid
        ConsolePasswordInvalidOrExpired - The 'zadmin' password is not valid
        Throws an error if any other issue occurs during validation.
    #>

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

    try {
        Test-ZertoPassword
        return [PasswordsValidationResult]::PasswordsValid
    }
    catch {
        if ($_ -match "Provided Zerto password is not valid") {
            Write-Host 'The Zerto "admin" password is not valid.'
            return [PasswordsValidationResult]::ZertoPasswordInvalid
        }
        if ($_ -match "Failed to authenticate with the guest operating system using the supplied credentials") {
            Write-Host 'The Console "zadmin" password is not valid.'
            return [PasswordsValidationResult]::ConsolePasswordInvalidOrExpired
        }
        throw $_
    }

}

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-ZVMLScriptWithTimeout -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 "The new VC password in ZVM set successfully."
        }
        else {
            if ($result.ScriptOutput.Contains("Error:")) {
                $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
                throw $cleanErrMsg #TODO: Standardize error messages for Error and Unknown cases, here and elsewhere, review unit tests
            }
            throw "Unexpected error occurred while updating 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-ZVMLScriptWithTimeout -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) {
    #TODO: Once the u7 is published, this method can be removed, because static DNS should be set correctly by appliance from ovf, fixed in ZER-150945
    Write-Host "Starting $($MyInvocation.MyCommand)..."
 
    try {
        $action = {
            $ZVM = Get-VM -Name $ZVM_VM_NAME
            if ($null -eq $ZVM) {
                throw "$ZVM_VM_NAME VM does not exist."
            }
            $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 Get-Nameservers {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        # Unlock resolv.conf file which could have been locked during the installation of previous versions, no need to lock again, fixed in ZER-150945
        $commandUnlockFile = 'sudo chattr -i /etc/resolv.conf'
        $res = Invoke-ZVMLScript -ScriptText $commandUnlockFile
        if ($res.ExitCode -ne 0) {
            throw "Failed to unlock resolv.conf. $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }

        # awk searches for lines starting with 'nameserver' and prints the second field (the IP address)
        $commandGetDnsEntries = "awk '/^[[:space:]]*nameserver[[:space:]]/ {print `$2}' /etc/resolv.conf || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandGetDnsEntries
        if ($res.ExitCode -ne 0) {
            throw "Failed to read resolv.conf, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
        $output = $res.ScriptOutput
        $ips = $output -split "`n"

        return $ips | ForEach-Object {
            [System.Net.IPAddress]$_
        }
    }
    catch {
        throw "Failed to get ZVML DNS nameserver entries. Problem: $_"
    }
}

function Add-Nameserver ([System.Net.IPAddress]$DnsIp) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        # tee adds the line to the end of file
        $commandAddDnsEntry = "echo 'nameserver $DnsIp # AVS $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')' | sudo tee -a /etc/resolv.conf || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandAddDnsEntry
        if ($res.ExitCode -ne 0) {
            throw "Failed to edit resolv.conf, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
    }
    catch {
        throw "Failed to add ZVML DNS nameserver entry. Problem: $_"
    }

}

function Remove-Nameserver ([System.Net.IPAddress]$DnsIp) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        # sed /{pattern}/{command} with /d removes the matched line starting with 'nameserver'
        $commandRemoveDnsEntry = "sudo sed -i '/^[[:space:]]*nameserver[[:space:]]\+$DnsIp/d' /etc/resolv.conf || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandRemoveDnsEntry
        if ($res.ExitCode -ne 0) {
            throw "Failed to edit resolv.conf, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
    }
    catch {
        throw "Failed to remove ZVML DNS nameserver entry. Problem: $_"
    }
}

function Check-Nameserver ([System.Net.IPAddress]$DnsIp) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $destinationHost = "management.azure.com"

        # dig queries DNS using the specified DNS server for the specified host
        $commandCheckDnsEntry = "dig @$DnsIp $destinationHost A +short +time=10 +retry=3 || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandCheckDnsEntry
        if ($res.ExitCode -ne 0) {
            throw "Failed to query DNS, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
    }
    catch {
        throw "Failed to check ZVML DNS nameserver entry. Problem: $_"
    }
}

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

    try {
        # test -f can only return 0 if file exists or 1
        $commandFileExists = "test -f $FileLocation || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandFileExists
        if ($res.ExitCode -eq 0) {
            return $true
        }
        else {
            return $false
        }
    }
    catch {
        throw "Failed to check file in ZVM. Problem: $_"
    }
}

function Test-FeatureFlagEnabled {
    param(
        [ValidateNotNullOrEmpty()]
        [string]$Flag
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    # -i: Makes the search case-insensitive.
    # -x: Matches only entire lines (not partial matches).
    # -F: Interprets the pattern as a fixed string, not a regular expression.
    # -q: Runs silently, suppressing output and returning only the exit code.
    $commandCheckFlag = "test -f '/opt/zerto/zlinux/avs/feature-flags.conf' && grep -ixFq '$Flag' '/opt/zerto/zlinux/avs/feature-flags.conf' || exit 1"

    $res = Invoke-ZVMLScript -ScriptText $commandCheckFlag
    if ($res.ExitCode -ne 0) {
        throw "Your ZVMA version does not support '$Flag' feature in AVS. Please use a version which supports '$Flag' feature."
    }

    Write-Host "ZVMA version supports '$Flag' feature."
}

function Set-ZertoVmPasswordExpiration {
    <#
    .SYNOPSIS
        Resets the ZVM VM console password expiration
    #>

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

    try {
        # grep -Pq, -P to use Perl-compatible regular expressions, -q to be quiet without outputting the matching lines
        # returns 0 exit code if 'maxdays -1' is set and then 'Maximum -1' pattern is found
        $chageCommand = "sudo chage --maxdays -1 $ZAPPLIANCE_USER && chage -l $ZAPPLIANCE_USER | grep -Pq '^Maximum number of days.*-1$' || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $chageCommand
        if ($res.ExitCode -ne 0) {
            throw "Failed to chage, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
    }
    catch {
        throw "Failed to set password expiry. Problem: $_"
    }
}

function Set-AzureResourceGroup {
    <#
    .SYNOPSIS
        Sets the Azure Resource Group name in the ZVMA
    #>

    param (
        [string]$resourceGroupName
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $scriptLocation = "/opt/zerto/zlinux/avs/change_resource_group.py"
    $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$($PersistentSecrets.ZertoPassword)' --avsClientSecret '$($PersistentSecrets.AvsClientSecret)' --zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)' --resourceGroupName '$resourceGroupName'"
    $result = Invoke-ZVMLScriptWithTimeout -ScriptText $commandToExecute -ActionName "Change resource group"

    if ($result.ScriptOutput.Contains("Success")) {
        Write-Host "Resource group set successfully."
    }
    elseif ($result.ScriptOutput.Contains("Error:")) {
        $cleanErrMsg = $result.ScriptOutput -replace "Error: ", ""
        throw $cleanErrMsg
    }
    else {
        throw "An unexpected error occurred while changing the resource group."
    }
}

function Enable-VAIOConfiguration {
    <#
    .SYNOPSIS
        Enable Zerto protection using VAIO (vSphere API for IO filtering)
    #>

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

    $scriptLocation = '/opt/zerto/zlinux/avs/enable_vaio.py'
    $commandToExecute = "sudo python3 $scriptLocation --vcPassword '$($PersistentSecrets.ZertoPassword)' --avsClientSecret '$($PersistentSecrets.AvsClientSecret)' --zertoAdminPassword '$($PersistentSecrets.ZertoAdminPassword)'"
    $result = Invoke-ZVMLScriptWithTimeout -ScriptText $commandToExecute -ActionName 'Enable VAIO'

    if ($result.ScriptOutput.Contains('Success')) {
        Write-Host 'VAIO enabled successfully.'
    }
    elseif ($result.ScriptOutput.Contains('Error:')) {
        $cleanErrMsg = $result.ScriptOutput -replace 'Error: ', ''
        throw $cleanErrMsg
    }
    else {
        throw 'An unexpected error occurred while enabling VAIO.'
    }
}

function New-ZertoLogs {
    param (
        [Parameter(Mandatory = $false)]
        [bool]$WithPrometheus
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $scriptParams = $WithPrometheus ? ' --with-prometheus' : ''
        $commandToExecute = "sudo python3 /opt/zerto/zlinux/avs/collect_logs.py$scriptParams"

        $res = Invoke-ZVMLScriptWithTimeout -ScriptText $commandToExecute -ActionName 'Create ZVML logs archive' -TimeoutMinutes 30

        $LOG_FILE = 'LogPath'

        $scriptOutput = $res.TrimmedOutput

        Write-Host "ZVML logs collection script output:`n$scriptOutput"

        if (-not ($scriptOutput -match "(?m)^Success\.[^']+'(?<$LOG_FILE>[^']+?)'\s+created\.$")) {
            throw 'No log file created.'
        }

        $logFile = $matches[$LOG_FILE]

        return $logFile
    }
    catch {
        throw "Failed to create ZVML log archive. Problem: $_"
    }
}

function Cleanup-LogBundles {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    function Test-LogBundlesExist {
        # find all files in both directories, print0 to handle spaces in filenames, xargs -0 to handle null-terminated input, -r to avoid running stat if no files found
        $commandGetLogBundles = "find /var/log/zerto/zvr/bundles /var/log/zerto/zvr/temp-bundles -type f -print0 | xargs -0 -r stat -c '%n | Size %s bytes' || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandGetLogBundles
        if ($res.ExitCode -ne 0) {
            throw "Failed to get log bundles files, $($res.ScriptOutput)" # ScriptOutput should contain bash error
        }
        $foundFilesList = $res.ScriptOutput

        $filesExist = (-not [string]::IsNullOrWhiteSpace($foundFilesList))

        return @($filesExist, $foundFilesList)
    }

    try {
        $filesExist, $foundFilesList = Test-LogBundlesExist

        if (-not $filesExist) {
            Write-Host "No log bundle files found. Nothing to cleanup."
            return
        }

        Write-Host "Found log bundle files:"
        Write-Host $foundFilesList

        $totalBytes = 0
        $lines = $foundFilesList -split "`n"
        foreach ($line in $lines) {
            if ($line -match "Size (\d+) bytes") {
                $sizeBytes = [long]$matches[1]
                $totalBytes += $sizeBytes
            }
        }

        $totalMB = [math]::Round($totalBytes / 1MB, 2)
        Write-Host "Total bundles size: $totalMB MB"

        Write-Host "Deleting log bundles..."

        # find all files in both directories, -mindepth 1 to avoid deleting the directories themselves, -delete to remove the files
        $commandDeleteLogBundles = "find /var/log/zerto/zvr/bundles /var/log/zerto/zvr/temp-bundles -mindepth 1 -delete || exit 1"

        $res = Invoke-ZVMLScript -ScriptText $commandDeleteLogBundles
        if ($res.ExitCode -ne 0) {
            throw "Failed to delete log bundles files, $($res.ScriptOutput)"
        }

        $filesExist, $foundFilesList = Test-LogBundlesExist
        if ($filesExist) {
            throw "Failed to delete some log bundles files: `n$foundFilesList"
        }

        Write-Host "Log bundles cleanup complete. Freed $totalMB MB of disk space."
    }
    catch {
        throw "Failed to cleanup log bundles. Problem: $_"
    }
}

function Invoke-ZvmaNetDiagnostics {
    <#
    .SYNOPSIS
        Performs network connectivity diagnostics for ZVMA
    #>

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

    Write-Host "### Executing connectivity diagnostics for '$TargetUri'"
    try {
        $uri = [System.Uri]$TargetUri
        $hostname = $uri.Host

        Write-Host "## DNS lookup result:`n$(Invoke-ZvmaNsLookup -TargetHost $hostname)"
        Write-Host "## DNS dig lookup result:`n$(Invoke-ZvmaDigLookup -TargetHost $hostname)"

        Write-Host "## Network traceroute result:`n$(Invoke-ZvmaTraceroute -TargetHost $hostname)"
        Write-Host "## TCP netcat connectivity test result:`n$(Invoke-ZvmaNetcat -TargetHost $hostname)"

        Write-Host "## TLS connectivity test result:`n$(Invoke-ZvmaOpenSslCheck -TargetHost $hostname)"
        Write-Host "## HTTP connectivity test result:`n$(Invoke-ZvmaCurl -TargetHost $TargetUri)"
    }
    catch {
        throw "Failed to run connectivity diagnostics. Problem: $_"
    }
}

function Invoke-ZvmaNsLookup {
    <#
    .SYNOPSIS
        Executes nslookup command on the ZVMA to test DNS resolution
    #>

    param (
        [ValidateNotNullOrEmpty()]
        [string]$TargetHost
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    return Invoke-ZvmaNetDiagnosticCommand -Command "nslookup" -TargetHost $TargetHost -CommandDescription "DNS lookup"
}

function Invoke-ZvmaDigLookup {
    <#
    .SYNOPSIS
        Executes dig command on the ZVMA for DNS resolution and detailed diagnostics
    #>

    param (
        [ValidateNotNullOrEmpty()]
        [string]$TargetHost
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    return Invoke-ZvmaNetDiagnosticCommand -Command "dig" -TargetHost $TargetHost -CommandDescription "DNS dig lookup"
}

function Invoke-ZvmaTraceroute {
    <#
    .SYNOPSIS
        Executes traceroute command on the ZVMA to trace network path
    #>

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

        [bool]$UseTcp = $true,

        [int]$Port = 443
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $commandArgs = "-p $Port"
    if ($UseTcp) {
        $commandArgs += " -T "
    }

    return Invoke-ZvmaNetDiagnosticCommand -Command "sudo traceroute" -TargetHost $TargetHost -CommandArgs $commandArgs -CommandDescription "Network traceroute"
}

function Invoke-ZvmaNetcat {
    <#
    .SYNOPSIS
        Executes netcat command on the ZVMA to test TCP port connectivity
 
    .PARAMETER ZeroIo
        Enables netcat's zero-I/O mode (-z).
        When $true (default), netcat checks if a port is open without sending data.
        Set to $false to make a full connection and allow data transfer.
        #>

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

        [int]$Port = 443,

        [bool]$IsVerbose = $true,

        [bool]$ZeroIo = $true
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $commandArgs = "$Port"
    if ($ZeroIo) {
        $commandArgs += " -z"
    }
    if ($IsVerbose) {
        $commandArgs += " -v"
    }

    return Invoke-ZvmaNetDiagnosticCommand -Command "nc" -TargetHost $TargetHost -CommandArgs $commandArgs -CommandDescription "TCP netcat connectivity test"
}

function Invoke-ZvmaOpenSslCheck {
    <#
    .SYNOPSIS
        Executes openssl s_client command on the ZVMA to test TLS connectivity
    #>

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

        [int]$Port = 443,

        [int]$TimeoutSeconds = 10,

        [ValidateSet("tls1", "tls1_1", "tls1_2", "tls1_3")]
        [string]$TlsVersion = "tls1_3"
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."
    return Invoke-ZvmaNetDiagnosticCommand -Command "timeout $TimeoutSeconds openssl s_client -connect" -TargetHost "$($TargetHost):$Port" -CommandArgs "-$TlsVersion" -CommandDescription "TLS connectivity test"
}

function Invoke-ZvmaCurl {
    <#
    .SYNOPSIS
        Executes curl command on the ZVMA to test HTTP connectivity and verbose output
    #>

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

        [bool]$IsVerbose = $true
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $commandText = "curl"
    if ($IsVerbose) {
        $commandText += " -v"
    }

    return Invoke-ZvmaNetDiagnosticCommand -Command $commandText -TargetHost "$TargetHost" -CommandDescription "HTTP connectivity test"
}

function Invoke-ZvmaNetDiagnosticCommand {
    <#
    .SYNOPSIS
        Executes a network diagnostics command on the ZVMA
    #>

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

        [ValidateNotNullOrEmpty()]
        [string]$TargetHost,

        [string]$CommandArgs = "",

        [ValidateNotNullOrEmpty()]
        [string]$CommandDescription
    )
    Write-Host "Starting Invoke-ZvmaNetDiagnosticCommand for $CommandDescription..."

    try {

        $fullCommand = "$Command $TargetHost $CommandArgs"
        Write-Host "Executing command: $fullCommand"
        $res = Invoke-ZVMLScript -ScriptText $fullCommand

        Write-Host "$CommandDescription for $TargetHost completed."
        return $res.ScriptOutput
    }
    catch {
        $errorMessage = "Failed to perform $CommandDescription on ZVM. Problem: $_"
        Write-Error $errorMessage
        return "Error: $errorMessage"
    }
}