ZertoAVSModule.psm1

$SDDC_RESOURCE_ID_PATTERN = "/subscriptions/(?<AvsSubscriptionId>[^/]+)/resourceGroups/(?<AvsResourceGroup>[^/]+)/providers/Microsoft\.AVS/privateClouds/(?<AvsCloudName>[^/]+)"

# Public Cmdlets

function Install-Zerto {
    <#
        .DESCRIPTION
 
        Install Zerto appliance
 
            .PARAMETER MyZertoToken
            My Zerto token for downloading the OVA and signature file
 
            .PARAMETER HostName
            vSphere host name on which to deploy the ZVM Appliance
 
            .PARAMETER DatastoreName
            Datastore name on which to deploy the ZVM Appliance
 
            .PARAMETER AzureTenantId
            Azure tenant ID unique global identifier, which can be found in the Azure portal
 
            .PARAMETER AzureClientID
            Azure Client ID
 
            .PARAMETER AvsClientSecret
            AVS Client Secret
 
            .PARAMETER NetworkName
            Network name for the ZVM Appliance
 
            .PARAMETER ApplianceIp
            ZVM Appliance IP address
 
            .PARAMETER SubnetMask
            Subnet mask
 
            .PARAMETER DefaultGateway
            Default gateway
 
            .PARAMETER DNS
            DNS name server for the appliance
 
            .PARAMETER ZertoAdminPassword
            Password to be used to authenticate to the ZVM Appliance
 
        .EXAMPLE
        Install-Zerto -MyZertoToken <MyZertoToken> -HostName xxx.xxx.xxx.xxx -ApplianceIp xxx.xxx.xxx.xxx -DatastoreName <DatastoreName> -AzureTenantId <AzureTenantId>
         -AzureClientID <AzureClientID> -AvsClientSecret ********* -NetworkName <NetworkName> -SubnetMask xxx.xxx.xxx.xxx -DefaultGateway xxx.xxx.xxx.xxx
         -DNS xxx.xxx.xxx.xxx -ZertoAdminPassword password1!
    #>

    [CmdletBinding()]
    [AVSAttribute(60, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Token from MyZerto")]
        [ValidateNotNullOrEmpty()][string]
        $MyZertoToken,

        [Parameter(Mandatory = $false, HelpMessage = "The name of the vSphere host on which to deploy the ZVM Appliance")]
        [string]
        $HostName,

        [Parameter(Mandatory = $true, HelpMessage = "The name of the datastore on which to deploy the ZVM Appliance")]
        [ValidateNotNullOrEmpty()][string]
        $DatastoreName,

        [Parameter(Mandatory = $true, HelpMessage = "Your Microsoft Entra tenant ID")]
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [Parameter(Mandatory = $true, HelpMessage = "Your Application (client) ID, found in Azure ""App registrations""")]
        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [Parameter(Mandatory = $true, HelpMessage = "Your client secret value, found under your application's ""Certificates & secrets""")]
        [ValidateNotNullOrEmpty()][SecureString]
        $AvsClientSecret,

        [Parameter(Mandatory = $true, HelpMessage = "The name of the network used for the ZVM Appliance")]
        [ValidateNotNullOrEmpty()][string]
        $NetworkName,

        [Parameter(Mandatory = $true, HelpMessage = "ZVM Appliance IP address")]
        [ValidateNotNullOrEmpty()][string]
        $ApplianceIp,

        [Parameter(Mandatory = $true, HelpMessage = "Subnet mask")]
        [ValidateNotNullOrEmpty()][string]
        $SubnetMask,

        [Parameter(Mandatory = $true, HelpMessage = "Default gateway")]
        [ValidateNotNullOrEmpty()][string]
        $DefaultGateway,

        [Parameter(Mandatory = $true, HelpMessage = "DNS server address")]
        [ValidateNotNullOrEmpty()][string]
        $DNS,

        [Parameter(Mandatory = $true, HelpMessage = "Password to authenticate to the ZVM GUI. Password should contain at least one uppercase letter, one non-alphanumeric character, one digit, and be at least 8 characters long.")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword
    )

    process {

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

        $MyZertoToken = $MyZertoToken.Trim()
        $HostName = $HostName.Trim()
        $DatastoreName = $DatastoreName.Trim()
        $AzureTenantId = $AzureTenantId.Trim()
        $AzureClientID = $AzureClientID.Trim()
        $NetworkName = $NetworkName.Trim()
        $ApplianceIp = $ApplianceIp.Trim()
        $SubnetMask = $SubnetMask.Trim()
        $DefaultGateway = $DefaultGateway.Trim()
        $DNS = $DNS.Trim()
        #TODO:GK Check extra spaces in AvsClientSecret and ZertoAdminPassword

        try {
            $DeployVmStarted = $false

            if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $true) {
                throw "$ZVM_VM_NAME already exists. Please remove it with 'Uninstall-Zerto' first."
            }

            if ($SddcResourceId -match $SDDC_RESOURCE_ID_PATTERN) {
                $AvsSubscriptionId = $matches['AvsSubscriptionId']
                $AvsResourceGroup = $matches['AvsResourceGroup']
                $AvsCloudName = $matches['AvsCloudName']
            }
            else {
                throw "Could not find the AvsSubscriptionId, AvsResourceGroup, and AvsCloudName combination in SddcResourceId ($SddcResourceId). Please contact Microsoft support."
            }

            Validate-AvsParams -TenantId $AzureTenantId -ClientId $AzureClientID -ClientSecret $AvsClientSecret -SubscriptionId $AvsSubscriptionId -ResourceGroupName $AvsResourceGroup -AvsCloudName $AvsCloudName
            Validate-VcEnvParams -DatastoreName $DatastoreName -NetworkName $NetworkName -ZVMLIp $ApplianceIp -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS
            Validate-ZertoPassword -Password $ZertoAdminPassword

            $validatedHostName = Get-ValidatedHostName -HostName $HostName -NetworkName $NetworkName -DatastoreName $DatastoreName

            Initialize-ZertoTempFolder
            New-ZertoUser

            $OvaFilePath = Get-ZertoOVAFile -MyZertoToken $MyZertoToken

            $PersistentSecrets.ZappliancePassword = New-RandomPassword
            $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
            $PersistentSecrets.AvsClientSecret = ConvertFrom-SecureString -SecureString $AvsClientSecret -AsPlainText

            $DeployVmStarted = $true

            Deploy-Vm -OvaPath $OvaFilePath -VMHostName $validatedHostName -DatastoreName $DatastoreName -ZVMLIp $ApplianceIp -NetworkName $NetworkName -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS -AzureTenantId $AzureTenantId -AzureClientID $AzureClientID -AvsSubscriptionId $AvsSubscriptionId -AvsResourceGroup $AvsResourceGroup -AvsCloudName $AvsCloudName

            Write-Host "The ZVM Appliance (ZVMA) was successfully deployed."
        }
        catch {
            $DeploymentFailureMessage = "Failed to deploy the ZVM Appliance (ZVMA). Resolve the reported issues and run the installation again.`n Reasons for failure include: $_"

            try {
                if ($DeployVmStarted -eq $true) {
                    Remove-ZVMAppliance
                }
            }
            catch {
                Write-Host $_
                Write-Error $_
            }

            Write-Host $DeploymentFailureMessage
            Write-Error $DeploymentFailureMessage -ErrorAction Stop
        }
        finally {
            Clear-ZertoTempFolderLeniently
        }

        try {
            Start-ZVM
            Set-ZertoConfiguration -DNS $DNS
        }
        catch {
            $ConfigurationFailureMessage = "Failed to configure the ZVM Appliance (ZVMA). Resolve the reported issues and run the installation again.`n Reasons for failure include: $_"
            Write-Host $ConfigurationFailureMessage
            Write-Error $ConfigurationFailureMessage -ErrorAction Stop
        }

        # The following success message will not be written if script execution was aborted with ErrorAction Stop
        Write-Host "To navigate to Zerto UI, in a browser go to https://$ApplianceIp"
    }
}

function Uninstall-Zerto {
    <#
        .DESCRIPTION
 
        ⚠️ Remove VRAs using the Zerto UI, before running this Uninstall command. This command removes the ZVMA and associated Zerto users and roles.
 
        .PARAMETER Confirmation
        Confirmation for uninstalling Zerto appliance that check if the input is "uninstall"
 
        .PARAMETER IgnoreExistingVRAs
        If enabled, the uninstallation will proceed even if VRAs are detected
 
        .EXAMPLE
        Uninstall-Zerto -Confirmation "uninstall"
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Confirmation for unistall, type 'uninstall' to confirm.")]
        [ValidateNotNullOrEmpty()][string]
        $Confirmation,

        [Parameter(Mandatory = $true, HelpMessage = "If enabled, the uninstallation will proceed even if VRAs are detected.")]
        [ValidateNotNullOrEmpty()][switch]
        $IgnoreExistingVRAs
    )

    process {

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

        $Confirmation = $Confirmation.Trim()

        if ($Confirmation -ne "uninstall") {
            Write-Error "Type 'uninstall' to confirm Zerto uninstall." -ErrorAction Stop
        }

        if (-not $IgnoreExistingVRAs -and (Test-VmExists -VmName $VRA_VM_PATTERN)) {
            Write-Error "A VM named ""$VRA_VM_PATTERN"" has been detected. We recommend uninstalling the existing VRAs from the ZVML GUI first. To bypass this warning and proceed, use 'IgnoreExistingVRAs'." -ErrorAction Stop
        }

        try {
            Remove-ZertoUserAndRole
            Remove-ZVMAppliance

            Write-Host "ZVM Appliance uninstall was completed successfully"
        }
        catch {
            $Message = "Failed to uninstall ZVM Appliance. $_"
            Write-Host $Message
            Write-Error $Message -ErrorAction Stop
        }
    }
}

function Restart-ZertoAppliance {
    <#
        .DESCRIPTION
 
        Restart Zerto Appliance
 
        .PARAMETER Confirmation
        Confirmation for restarting Zerto appliance that check if the input is "yes"
 
 
        .EXAMPLE
        Restart-Zerto -Confirmation "restart"
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Confirmation for restart, type 'restart' to confirm.")]
        [ValidateNotNullOrEmpty()][string]
        $Confirmation
    )

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

        $Confirmation = $Confirmation.Trim()

        if ($Confirmation -ne "restart") {
            Write-Error "Type 'restart' to confirm appliance restart." -ErrorAction Stop
        }

        try {
            if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $true) {
                Stop-ZVM
                Start-ZVM

                Write-Host "Zerto restart was completed successfully .."
            }
            else {
                Write-Error "$ZVM_VM_NAME does not exist, failed to restart Zerto."
            }
        }
        catch {
            $Message = "Failed to restart Zerto. $_"
            Write-Host $Message
            Write-Error $Message -ErrorAction Stop
        }
    }
}

function Invoke-ZertoVcPasswordRotation {
    <#
        .DESCRIPTION
        Rotate password for system Zerto VC user and ZVML user
 
            .PARAMETER ZertoAdminPassword
            Enter the current Zerto Appliance admin Password. This must be a valid password for an existing Zerto admin user account (not a new desired password).
 
            .PARAMETER IgnoreZvmLoginErrors
            Ignore Zerto login errors. This may lead to breaking the Zerto application. Use only when the Zerto password is unknown.
 
        .EXAMPLE
 
        Invoke-ZertoVcPasswordRotation -ZertoAdminPassword password1!
    #>

    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Enter the current Zerto Appliance admin Password. This must be a valid password for an existing Zerto admin user account (not a new desired password).")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword,

        [Parameter(Mandatory = $false, HelpMessage = "Ignore Zerto login errors. This may lead to breaking the Zerto application. Use only in emergency case when the Zerto password is unknown.")]
        [bool]
        $IgnoreZertoLoginErrors = $false
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $false) {
            Write-Error "Zerto is not installed, nothing to rotate. You may run 'Uninstall-Zerto' to forcibly clean up the Zerto user and role." -ErrorAction Stop
        }

        $zertoUser = Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN
        if (!$zertoUser) {
            Write-Error "Cannot reset VC password for $ZERTO_USER_NAME, user does not exist." -ErrorAction Stop
        }

        # We have to set PersistentSecrets.ZertoAdminPassword and immediately test it to be consistent in case the user has previousely changed admin password in Keycloak
        $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
        if (!$IgnoreZertoLoginErrors) {
            Test-ZertoPassword
        }

        try {
            $newVcPassword = New-RandomPassword
            Set-SsoPersonUser -User $zertoUser -NewPassword $newVcPassword -ErrorAction Stop | Out-Null
            $PersistentSecrets.ZertoPassword = $newVcPassword
        }
        catch {
            Write-Error "Failed to rotate VC password for $ZERTO_USER_NAME. Please try again. Problem: $_" -ErrorAction Stop
        }

        try {
            Update-VcPasswordInZvm `
                -NewVcPassword $PersistentSecrets.ZertoPassword `
                -ZertoAdminPassword $PersistentSecrets.ZertoAdminPassword `
                -ClientSecret $PersistentSecrets.AvsClientSecret
            Write-Host "Rotated VC password set successfully in ZVM."
        }
        catch {
            Write-Error "Failed to update VC password in ZVM. Please try again. Problem: $_" -ErrorAction Stop
        }

        try {
            $newZappliancePassword = New-RandomPassword
            Set-ZertoVmPassword -NewPassword $(ConvertTo-SecureString $newZappliancePassword -AsPlainText -Force) #TODO: Rarameter is secured and then immediately unsecured, rafactor
            $PersistentSecrets.ZappliancePassword = $newZappliancePassword
        }
        catch {
            Write-Error "Failed to rotate Zerto VM password. Please try again. Problem: $_" -ErrorAction Stop
        }
    }
}

function Update-ClientCredentials {
    <#
        .DESCRIPTION
        Update expired Azure client credentials for Zerto
 
            .PARAMETER AzureTenantId
            Azure tenant ID unique global identifier, which can be found in the Azure portal
 
            .PARAMETER AzureClientID
            Azure application client ID
 
            .PARAMETER AvsClientSecret
            Azure client secret
 
            .PARAMETER ZertoAdminPassword
            Current Zerto Appliance admin Password
 
    #>

    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Your existing Microsoft Entra tenant ID")]
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [Parameter(Mandatory = $true, HelpMessage = "Your new or existing Application (client) ID, found in Azure ""App registrations""")]
        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [Parameter(Mandatory = $true, HelpMessage = "Your new client secret value, found under your application's ""Certificates & secrets""")]
        [ValidateNotNullOrEmpty()][SecureString]
        $AvsClientSecret,

        [Parameter(Mandatory = $true, HelpMessage = "Enter the current Zerto Appliance admin Password. This must be a valid password for an existing Zerto admin user account (not a new desired password).")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $AzureClientID = $AzureClientID.Trim()
        #TODO:GK Check extra spaces in AvsClientSecret and ZertoAdminPassword

        try {

            if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $false) {
                throw "Zerto is not installed, nothing to update."
            }

            if ($SddcResourceId -match $SDDC_RESOURCE_ID_PATTERN) {
                $AvsSubscriptionId = $matches['AvsSubscriptionId']
                $AvsResourceGroup = $matches['AvsResourceGroup']
                $AvsCloudName = $matches['AvsCloudName']
            }
            else {
                throw "Could not find the AvsSubscriptionId, AvsResourceGroup, and AvsCloudName combination in SddcResourceId ($SddcResourceId). Please contact Microsoft support."
            }

            Validate-AvsParams -TenantId $AzureTenantId -ClientId $AzureClientID -ClientSecret $AvsClientSecret -SubscriptionId $AvsSubscriptionId -ResourceGroupName $AvsResourceGroup -AvsCloudName $AvsCloudName

            $newAzureClientID = $AzureClientID
            $newAvsClientSecret = ConvertFrom-SecureString -SecureString $AvsClientSecret -AsPlainText

            # We have to set PersistentSecrets.ZertoAdminPassword and immediately test it to be consistent in case the user has previousely changed admin password in Keycloak
            $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
            Test-ZertoPassword

            $canUpdateClientID = Update-ClientCredentialsInZvm -NewClientId $newAzureClientID -NewClientSecret $newAvsClientSecret
            if (-not $canUpdateClientID) {
                # Update-ClientCredentialsInZvm is meant to update both, Client ID and Client Secret, however this feature is not available below z10u5p2.
                # To allow customer to at least update the expired Client Secret we are using this workaround:
                # Update-VcPasswordInZvm py script was not meant to update the Client Secret, but it can do that, so we temporarily use it.
                # When all the customers are upgraded to z10u5p2+ this code should be removed and refactored accordingly.

                Write-Warning "Your ZVMA version does not support updating Client ID."
                Write-Host "Client ID update not supported. Trying to update Client Secret only."

                $zvml = Get-VM -Name $ZVM_VM_NAME
                $ovfProperties = $zvml.ExtensionData.Config.VAppConfig.Property
                $currentClientId = $ovfProperties | Where-Object { $_.Id -eq "AzureClientID" } | Select-Object -ExpandProperty Value

                if ($AzureClientID -ne $currentClientId)
                {
                    throw "Your ZVMA version does not support updating Client ID $currentClientId. Please upgrade your ZVMA."
                }

                Update-VcPasswordInZvm `
                    -NewVcPassword $PersistentSecrets.ZertoPassword `
                    -ZertoAdminPassword $PersistentSecrets.ZertoAdminPassword `
                    -ClientSecret $newAvsClientSecret # ClientSecret is updated as a side effect
                # Hence, don't be confused with "New VC password set successfully in ZVM." message
            }

            $PersistentSecrets.AvsClientSecret = $newAvsClientSecret

            Write-Host "New Azure client credentials updated successfully in ZVM."

        }
        catch {
            Write-Error "Failed to update Azure client credentials in ZVM. Please try again. Problem: $_" -ErrorAction Stop
        }
    }
}

function Debug-Zerto {
    <#
        .DESCRIPTION
 
        Diagnosing and troubleshooting Zerto components
 
        .EXAMPLE
        Debug-Zerto
        Debug-Zerto -HostName <HostName> -DatastoreName <DatastoreName>
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $false,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,

        [Parameter(Mandatory = $false,
            HelpMessage = "Datastore Name")]
        [string]$DatastoreName

    )

    process {

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

        try {
            Test-Connection
            Test-ZertoRoleExists
            Test-ZertoUserExists

            if ($HostName) {
                Write-Host "Host Provided by user: $HostName"
                Get-HostSecureBootStatus -HostName $HostName
                Test-ZertoDriverLoaded -HostName $HostName
                Get-ZertoFilesListFromHost -HostName $HostName

                if ($DatastoreName) {
                    Write-Host "Datastore Provided by user: $DatastoreName"
                    Initialize-ZertoTempFolder
                    Get-DriverLogsFromHost -HostName $HostName -DatastoreName $DatastoreName
                    Clear-ZertoTempFolderLeniently
                }
            }
        }
        catch {
            $Message = "Failed to run Debug-Zerto, $_"
            Write-Host $Message
            Write-Error $Message -ErrorAction Stop
        }
    }
}

# Internal Cmdlets

function Set-SSHTimeout {
    <#
        .DESCRIPTION
        Determines how long SSH session remains open
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
            .PARAMETER SSHTimeout
            SSH timeout value
 
        .EXAMPLE
 
        SetSSHTimeout -HostName <HostName> -SSHTimeout <SSHTimeout>
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "SSH timeout value")]
        [string]$SSHTimeout
    )

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

        $vmHost = Get-VMHost -Name $HostName

        Get-AdvancedSetting -Entity $vmHost -Name "UserVars.ESXiShellInteractiveTimeOut" -ErrorAction SilentlyContinue | Set-AdvancedSetting -Value $SSHTimeout -Confirm:$false -ErrorAction SilentlyContinue
        Write-Host "Set configuration setting ""UserVars.ESXiShellInteractiveTimeOut"" on $HostName to $SSHTimeout"
    }
}

function Test-HostConnectivity {
    <#
        .DESCRIPTION
 
        Check if the host is up and running (For Internal Use)
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
        .EXAMPLE
 
        Test-HostConnectivity -HostName xxx.xxx.xxx.xxx
    #>

    [CmdletBinding()]
    [AVSAttribute(5, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName
    )

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

        $Command = "echo testing123"
        return Invoke-SSHCommands -HostName $HostName -Commands $Command
    }
}

function Get-HostEsxiVersion {
    <#
        .DESCRIPTION
 
        Retrieve the ESXi version (For Internal Use)
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
        .EXAMPLE
 
        Get-HostEsxiVersion -HostName xxx.xxx.xxx.xxx
    #>

    [CmdletBinding()]
    [AVSAttribute(5, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName
    )

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

        $Command = "vmware -l"
        return Invoke-SSHCommands -HostName $HostName -Commands $Command
    }
}

function Get-HostTempFolderInfo {
    <#
        .DESCRIPTION
 
        Display information about the available disk space (For Internal Use)
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
        .EXAMPLE
 
        Get-HostTempFolderInfo -HostName xxx.xxx.xxx.xxx
    #>

    [CmdletBinding()]
    [AVSAttribute(5, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName
    )

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

        $Command = "vdf"
        return Invoke-SSHCommands -HostName $HostName -Commands $Command
    }
}

function Install-Driver {
    <#
        .DESCRIPTION
 
        Install the driver
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
            .PARAMETER DatastoreName
            Datastore Name
 
            .PARAMETER BiosUuid
            Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid
 
            .PARAMETER EsxiVersion
            Esxi version
 
        .EXAMPLE
        Install-Driver -HostName xxx.xxx.xxx.xxx -DatastoreName <DatastoreName> -BiosUuid <UUID> -EsxiVersion xx
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Datastore Name")]
        [string]$DatastoreName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")]
        [string]$BiosUuid,
        [Parameter(Mandatory = $true,
            HelpMessage = "Esxi version")]
        [string]$EsxiVersion,
        [Parameter(Mandatory = $true,
            HelpMessage = "Driver memory in MB for Zerto driver")]
        [string]$DriverMemoryInMB,
        [Parameter(Mandatory = $true,
            HelpMessage = "Use explicit argument for zloadmod script (True / False)")]
        [string]$UseExplicitDriverArgs #TODO:GK This parameter appears to be always True and can be removed
    )

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

        if (((Validate-DatastoreName -DatastoreName $DatastoreName) -ne $true) -or
            ((Validate-BiosUUID -DatastoreName $DatastoreName -BiosUuid $BiosUuid) -ne $true) -or
            ((Validate-DigitsOnly -InputString $EsxiVersion) -ne $true) -or
            ((Validate-DigitsOnly -InputString $DriverMemoryInMB) -ne $true)) {
            throw "DatastoreName/BiosUUID/DigitsOnly validation failed."
        }

        if (Test-ZertoDriverLoaded $HostName) {
            Write-Host "Warning! Zerto driver is already loaded on $HostName"
        }

        $zloadmod = ('{0}/zloadmod.sh' -f $ZERTO_FOLDER_ON_HOST)

        Copy-FilesFromDatastoreToHost -HostName $HostName -DatastoreName $DatastoreName -BiosUuid $BiosUuid

        $datastoreUuid = Get-DatastoreUUID($DatastoreName)
        if ($UseExplicitDriverArgs -eq $true) {
            $driverArgs = "init -ds `"$datastoreUuid`" -uid $BiosUuid -ver $EsxiVersion -mem $DriverMemoryInMB -avs -manipulate_exec_installed_only";
        }
        else {
            $driverArgs = "init `"$datastoreUuid`" $BiosUuid 0 $EsxiVersion 1";
        }

        $Commands = ('chmod a+x {0}' -f $zloadmod),
                    ('{0} {1} > /etc/vmware/zloadmod.txt' -f $zloadmod, $driverArgs)

        return Invoke-SSHCommands -HostName $HostName -Commands $Commands
    }
}

function Update-StartupFile {
    <#
        .DESCRIPTION
 
        Responsible for loading the driver when the host is booting.
        /etc/rc.local.d/local.sh file is executed after all the normal system services are started
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
            .PARAMETER DatastoreName
            Datastore Name
 
            .PARAMETER BiosUuid
            "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid"
 
        .EXAMPLE
 
        Update-StartupFile -HostName xxx.xxx.xxx.xxx -DatastoreName xxx -BiosUuid xxx
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Datastore Name")]
        [string]$DatastoreName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")]
        [string]$BiosUuid,
        [Parameter(Mandatory = $true,
            HelpMessage = "Zerto driver memory size in MB")]
        [string]$DriverMemoryInMB,
        [Parameter(Mandatory = $true,
            HelpMessage = "Use explicit argument for zloadmod script (True / False)")]
        [string]$UseExplicitDriverArgs #TODO:GK This parameter appears to be always True and can be removed
    )

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

        if (((Validate-DatastoreName -DatastoreName $DatastoreName) -ne $true) -or
            ((Validate-BiosUUID -DatastoreName $DatastoreName -BiosUuid $BiosUuid) -ne $true) -or
            ((Validate-DigitsOnly -InputString $DriverMemoryInMB) -ne $true)) {
            throw "DatastoreName/BiosUUID/DigitsOnly validation failed."
        }

        $zloadmod = ('{0}/zloadmod.sh' -f $ZERTO_FOLDER_ON_HOST)

        Copy-FilesFromDatastoreToHost -HostName $HostName -DatastoreName $DatastoreName -BiosUuid $BiosUuid

        $startupFile = ('{0}/startup_file.sh' -f $ZERTO_FOLDER_ON_HOST)

        $datastoreUuid = Get-DatastoreUUID($DatastoreName)
        if ($UseExplicitDriverArgs -eq $true) {
            $driverArgs = "load -ds `"$datastoreUuid`" -uid $BiosUuid -mem $DriverMemoryInMB -avs -manipulate_exec_installed_only"
        }
        else {
            $driverArgs = "load `"$datastoreUuid`" $BiosUuid 0 `"`" 1"
        }

        $Commands = ('grep -v "ZeRTO\|exit 0" /etc/rc.local.d/local.sh > {0}' -f $startupFile),
        ('echo \#ZeRTO\ >> {0}' -f $startupFile),
        ('echo sh {0} {1} \> /etc/vmware/zloadmod.txt \2\>\&\1 \#ZeRTO\ >> {2}' -f $zloadmod, $driverArgs, $startupFile),
        ('echo \#ZeRTO\ >> {0}' -f $startupFile),
        ('echo "exit 0" >> {0}' -f $startupFile),
        ('cp -f {0} /etc/rc.local.d/local.sh' -f $startupFile),
        ('chmod a+x {0}' -f $zloadmod)

        return Invoke-SSHCommands -HostName $HostName -Commands $Commands
    }
}

function Uninstall-Driver {
    <#
        .DESCRIPTION
 
        Uninstall the driver
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
            .PARAMETER DatastoreName
            Datastore Name
 
            .PARAMETER BiosUuid
            Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid
 
 
        .EXAMPLE
        Uninstall-Driver -HostName xxx.xxx.xxx.xxx -DatastoreName <DatastoreName> -BiosUuid <UUID>
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Datastore Name")]
        [string]$DatastoreName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")]
        [string]$BiosUuid
    )

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

        if (Test-ZertoDriverLoaded $HostName) {
            $zunloadmod = ('{0}/zunloadmod.sh' -f $ZERTO_FOLDER_ON_HOST)

            Copy-FilesFromDatastoreToHost -HostName $HostName -DatastoreName $DatastoreName -BiosUuid $BiosUuid

            $Commands = ('chmod a+x {0}' -f $zunloadmod),
                        ('{0} cleanup > /etc/vmware/zunloadmod.txt' -f $zunloadmod)

            return Invoke-SSHCommands -HostName $HostName -Commands $Commands
        }

        else {
            throw "Error! Failed to run Uninstall-Driver, Zerto driver is not loaded on $HostName."
        }
    }
}

function Test-Connection {

    [AVSAttribute(5, UpdatesSDDC = $false, AutomationOnly = $true)]
    param()

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

        return "Test-Connection"
    }

}

function Enable-EsxiHostSecurity {
    <#
        .DESCRIPTION
 
        Enables execInstalledOnly on a host
 
            .PARAMETER HostName
            Host Name to connect with SSH
 
            .PARAMETER DatastoreName
            Datastore Name
 
            .PARAMETER BiosUuid
            Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid
 
 
        .EXAMPLE
        Enable-EsxiHostSecurity -HostName xxx.xxx.xxx.xxx -DatastoreName <DatastoreName> -BiosUuid <UUID>
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false, AutomationOnly = $true)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Name to connect with SSH")]
        [string]$HostName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Datastore Name")]
        [string]$DatastoreName,
        [Parameter(Mandatory = $true,
            HelpMessage = "Host Bios Uuid || mob-> Property Path: host.hardware.systemInfo.uuid")]
        [string]$BiosUuid
    )

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

        try {
            Enable-HostSecurity -HostName $HostName -DatastoreName $DatastoreName -BiosUuid $BiosUuid
            Write-Host "Succesfully enabled execInstalledOnly on a host $HostName"
        }
        catch {
            Write-Error "Failed to turn on security on $HostName. Problem: $_" -ErrorAction Stop
        }
    }
}

function Set-AvsConfiguration {
    <#
        .DESCRIPTION
 
        Reconfigure ZVML to work with AVS
 
            .PARAMETER AzureTenantId
            Azure tenant ID: unique global identifier, which can be found in the Azure portal
 
            .PARAMETER AzureClientID
            Azure Client ID
 
            .PARAMETER AvsClientSecret
            AVS Client Secret
 
            .PARAMETER ZertoAdminPassword
            Password to be used to authenticate to the ZVM Appliance
 
            .PARAMETER ZertoVmPassword
            Password to be used to authenticate to the ZVM VM
 
            .PARAMETER ZertoMigrationToken
            A one-time token that should be validated against MyZerto
 
            .PARAMETER ForceRecreateVcUser
            If enabled, the ZertoDR user and Zerto role will be recreated during reconfiguration
 
        .EXAMPLE
        Set-AvsConfiguration -AzureTenantId <AzureTenantId> -AzureClientID <AzureClientID> -AvsClientSecret *********
        -ZertoAdminPassword password1! -ZertoVmPassword password2! -ZertoMigrationToken <ZertoMigrationToken>
    #>

    # TODO:IU Make automation only when the first testing interaction is complete
    [CmdletBinding()]
    [AVSAttribute(60, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Your Microsoft Entra tenant ID")]
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [Parameter(Mandatory = $true, HelpMessage = "Your Application (client) ID, found in Azure ""App registrations""")]
        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [Parameter(Mandatory = $true, HelpMessage = "Your client secret value, found under your application's ""Certificates & secrets""")]
        [ValidateNotNullOrEmpty()][SecureString]
        $AvsClientSecret,

        [Parameter(Mandatory = $true, HelpMessage = "The current Zerto Appliance admin Password. This must be a valid password for an existing Zerto admin user account (not a new desired password).")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword,

        [Parameter(Mandatory = $true, HelpMessage = "Password to ZVM VM 'zadmin' user")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoVmPassword,

        [Parameter(Mandatory = $true, HelpMessage = "A one-time token that should be validated against MyZerto")]
        [ValidateNotNullOrEmpty()][string]
        $ZertoMigrationToken,

        [Parameter(Mandatory = $true, HelpMessage = "If enabled, the ZertoDR user and Zerto role will be recreated during reconfiguration")]
        [ValidateNotNullOrEmpty()][switch]
        $ForceRecreateVcUser
    )
    process {

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

        if ($SddcResourceId -match $SDDC_RESOURCE_ID_PATTERN) {
            $AvsSubscriptionId = $matches['AvsSubscriptionId']
            $AvsResourceGroup = $matches['AvsResourceGroup']
            $AvsCloudName = $matches['AvsCloudName']
        }
        else {
            throw "Could not find the AvsSubscriptionId, AvsResourceGroup, and AvsCloudName combination in SddcResourceId ($SddcResourceId). Please contact Microsoft support."
        }

        Validate-AvsParams -TenantId $AzureTenantId -ClientId $AzureClientID -ClientSecret $AvsClientSecret -SubscriptionId $AvsSubscriptionId -ResourceGroupName $AvsResourceGroup -AvsCloudName $AvsCloudName
        Assert-ReconfigurationToken -Token $ZertoMigrationToken

        $PersistentSecrets.ZappliancePassword = ConvertFrom-SecureString -SecureString $ZertoVmPassword -AsPlainText
        $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
        $PersistentSecrets.AvsClientSecret = ConvertFrom-SecureString -SecureString $AvsClientSecret -AsPlainText

        Test-ZertoPassword # Will implicitely test the ZappliancePassword and explicitely test ZertoAdminPassword

        $isNewUserCreated = $false
        try {
            if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $false) {
                throw "The $ZVM_VM_NAME VM was not found in the inventory"
            }
            if ($ForceRecreateVcUser) {
                Remove-ZertoUserAndRole
            }

            New-ZertoUser
            $isNewUserCreated = $true
            Update-ZertoConfiguration -AzureTenantId $AzureTenantId -AzureClientId $AzureClientID -AvsSubscriptionId $AvsSubscriptionId -AvsResourceGroup $AvsResourceGroup -AvsCloudName $AvsCloudName
            Move-ZvmToTheSecureFolder
            Set-ZertoVmPassword -NewPassword (New-RandomPassword | ConvertTo-SecureString -AsPlainText -Force)
        }
        catch {
            try {
                if ($isNewUserCreated) {
                    Remove-ZertoUserAndRole
                }
            }
            catch {
                Write-Error "Failed to cleanup the user $ZERTO_USER_NAME. Please remove it manually."
            }
            Write-Error "Failed to reconfigure Zerto. The VPG protection has been paused. Please check the parameters and try again. Error: $_" -ErrorAction Stop
        }
    }
}