ZertoAVSModule.psm1

<#
    Public Cmdlets - are run by a User
#>


function Install-Zerto {
    <#
    .DESCRIPTION
        Installs the Zerto Appliance.
    #>

    [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 14 characters long.")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword,

        [Parameter(Mandatory = $true, HelpMessage = "If True, ZVML will be configured to use VAIO Filter for replication.")]
        [ValidateNotNullOrEmpty()][switch]
        $VAIOFilter
    )
    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: Check extra spaces in AvsClientSecret and ZertoAdminPassword

    try {
        $isDeployVmStarted = $false
        $isZertoUserCreated = $false

        if (Test-VmExists -VmName $ZVM_VM_NAME) {
            #TODO Extract to a ZVMLexists function, and standardize error messages
            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 -ApplianceIp $ApplianceIp -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS
        Validate-ZertoParams -ZertoAdminPassword $ZertoAdminPassword -IsVaio $VAIOFilter.IsPresent

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

        New-ZertoUser
        $isZertoUserCreated = $true

        $OvaFilePath = Get-ZertoOVAFile -MyZertoToken $MyZertoToken

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

        $isDeployVmStarted = $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 was successfully deployed."
    }
    catch {
        $msgDeploymentFailure = "Failed to deploy the ZVM Appliance. Resolve the reported issues and run the installation again.`n Reasons for failure include: $_"

        try {
            if ($isDeployVmStarted -eq $true) {
                Remove-ZVMAppliance
                # We want to remove the ZVMA only if it wasn't yet successfully deployed, to avoid leaving a non-secured deployment potentially exposing VC credentials.
                # If the ZVMA was already deployed, but failed later during the configuration, we want to keep it to collect configuration logs.
            }
            if ($isZertoUserCreated -eq $true) {
                Remove-ZertoUserAndRole
                # We want to remove the Zerto user and role only if configuration hasn't started yet, otherwise to keep it for analysis.
            }
        }
        catch {
            $msgRollbackFailure = "Failed to rollback after unsuccessful ZVM Appliance deployment. Problem: $_"
            # Write the error but don't rethrow it, as we want to keep the original Deployment Failure message
            Write-Host $msgRollbackFailure
            Write-Error $msgRollbackFailure
        }

        Write-Host $msgDeploymentFailure
        Write-Error $msgDeploymentFailure -ErrorAction Stop
    }

    try {
        Start-ZVM

        Write-Host "Waiting for ZVM Appliance to start, this might take a while..."
        Assert-ZertoInitialized

        if ($VAIOFilter) {
            Test-FeatureFlagEnabled -Flag 'VAIO'
        }

        Set-ZertoVmPasswordExpiration

        # Configuration should be executed at the very end
        Set-ZertoConfiguration -DNS $DNS -IsVaio $VAIOFilter.IsPresent # Explicitly passing the switch value as boolean
    }
    catch {
        $msgConfigurationFailure = "Failed to configure the ZVM Appliance. Resolve the reported issues and run the installation again.`n Reasons for failure include: $_"
        Write-Host $msgConfigurationFailure
        Write-Error $msgConfigurationFailure -ErrorAction Stop
    }

    Write-Host "To navigate to Zerto UI, in a browser go to https://$ApplianceIp"
}

function Uninstall-Zerto {
    <#
    .DESCRIPTION
        Uninstalls the Zerto Appliance and associated vCenter users and roles.
        ⚠️ It is highly advised to uninstall VRAs using the Zerto UI, before running the Uninstall-Zerto command.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Confirmation for uninstall, 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
    )
    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-ZVMAppliance
        Remove-ZertoUserAndRole

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

function Restart-ZertoAppliance {
    <#
    .DESCRIPTION
        Restarts the Zerto Appliance.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Confirmation for restart, type 'restart' to confirm.")]
        [ValidateNotNullOrEmpty()][string]
        $Confirmation
    )
    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) {
            Stop-ZVM
            Start-ZVM

            Write-Host "ZVM Appliance restart has been successfully initiated."
        }
        else {
            throw "Cannot restart ZVM Appliance. $ZVM_VM_NAME does not exist."
        }
    }
    catch {
        $msgFailure = "Failed to restart ZVM Appliance. Problem: $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Invoke-ZertoVcPasswordRotation {
    <#
    .DESCRIPTION
        Rotates passwords for Zerto vCenter user and VM console user.
        ⚠️ Rotation of passwords is prohibited while a snapshot of the ZVML exists, as it will invalidate the ZVML snapshot passwords.
    #>

    [CmdletBinding()]
    [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 = "Also rotates the ZVML VM console password. Do not enable, unless you are sure the console password was compromised.")]
        [bool]
        $RotateConsolePassword = $false,

        [Parameter(Mandatory = $false, HelpMessage = "‼️ This will disrupt the functionality of the Zerto application. Use only in emergency case when the Zerto password is unknown.")]
        [bool]
        $ForceIgnoreLoginErrors = $false
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    if (-not (Test-VmExists -VmName $ZVM_VM_NAME)) {
        Write-Error "Zerto is not installed, nothing to rotate. You can run [Uninstall-Zerto] to forcibly clean up the Zerto user and role in vCenter." -ErrorAction Stop
    }

    if (Test-ZVMSnapshotExists) {
        Write-Error "Rotation of passwords is prohibited while a snapshot of the ZVML exists, as it will invalidate the ZVML snapshot passwords. Remove the snapshots using 'Invoke-ZertoSnapshotOperation' with the 'Delete' operation parameter, before executing 'Invoke-ZertoVcPasswordRotation'." -ErrorAction Stop
    }

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

    # We must set PersistentSecrets.ZertoAdminPassword and immediately test it for consistency - in case the user has previously changed the admin password in Keycloak
    $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
    if (-not $ForceIgnoreLoginErrors) {

        # Attempt to identify if the Console password is expired and change it if required - this can be removed in future when no expired passwords would exist
        # Change-ExpiredConsolePasswordIfRequired | Out-Null #TODO: Consider removing the method

        # We must test Zerto Appliance admin password and VM Console password before rotating the VC password to ensure successful execution of Update-VcPasswordInZvm, otherwise VC connectivity will be broken.
        Test-ZertoPassword
    }
    else {
        Write-Warning "Ignoring Zerto login errors. This will disrupt the functionality of the Zerto application."
    }

    $oldVcPassword = $PersistentSecrets.ZertoPassword
    $newVcPassword = New-RandomPassword

    $transactionSuccess = $false

    # VC password rotation - transaction step 1 - Change password in VC
    try {
        Set-SsoPersonUser -User $zertoUser -NewPassword $newVcPassword -ErrorAction Stop | Out-Null # Unlocks the locked user as a side effect
        Write-Host "VC password changed for $ZERTO_USER_NAME."
    }
    catch {
        Write-Error "Failed to change VC password for $ZERTO_USER_NAME. Problem: $_" -ErrorAction Stop # This will stop the transaction
    }

    # VC password rotation - transaction step 2 - Set changed VC password into ZVM
    # will fail if VM Console zadmin password is unknown
    # will fail if Zerto Appliance admin password is wrong
    # will fail if Client Secret is expired
    # will fail if VC password is not correct
    try {
        Update-VcPasswordInZvm `
            -NewVcPassword $newVcPassword `
            -ZertoAdminPassword $PersistentSecrets.ZertoAdminPassword `
            -ClientSecret $PersistentSecrets.AvsClientSecret

        $transactionSuccess = $true
    }
    catch {
        if (-not $ForceIgnoreLoginErrors) {
            # We wish to proceed to transaction rollback, so we don't stop
            Write-Error "Failed to update VC password in ZVM. Problem: $_" -ErrorAction Continue
        }
        else {
            # We wish to keep password in VC changed even if it wasn't set into ZVML, so we stop
            Write-Error "Failed to update VC password in ZVM. As expected, ZVM will be disconnected from VC. Problem: $_" -ErrorAction Stop
        }
    }

    if ($transactionSuccess) {
        $PersistentSecrets.ZertoPassword = $newVcPassword
        Write-Host "VC password rotation complete."
    }
    else {
        Write-Host "Rolling back VC password change for $ZERTO_USER_NAME."

        # VC password rotation - compensating transaction step - Rollback changed password in VC
        try {
            Set-SsoPersonUser -User $zertoUser -NewPassword $oldVcPassword -ErrorAction Stop | Out-Null # Unlocks the locked user as a side effect
            Write-Host "Rolled back VC password for $ZERTO_USER_NAME."
        }
        catch {
            Write-Error "Failed to rollback VC password for $ZERTO_USER_NAME. ZVM will be disconnected from VC. Problem: $_" -ErrorAction Stop
        }

        Write-Error "VC password rotation did not happen." -ErrorAction Stop # We do not want to proceed to VM Console password rotation
    }

    if ($RotateConsolePassword) {
        Write-Host "Also rotating the VM console password."

        # ZVMA VM Console password rotation
        # must be performed last to ensure state consistency - no exceptions must happen after this step
        # if an exception occurs after this step, the updated password will not be persisted to $PersistentSecrets.ZappliancePassword, leading to an unrecoverable loss of VM Console access.
        try {
            Set-ZertoVmPassword -NewPassword (New-RandomPassword | ConvertTo-SecureString -AsPlainText -Force)

            Set-ZertoVmPasswordExpiration #TODO: Consider combining with Set-ZertoVmPassword, but keeping unit tests for both methods independent
        }
        catch {
            Write-Error "Failed to rotate VM console password. Problem: $_" -ErrorAction Stop
        }

        Write-Host "VM console password rotation complete."
    }
}

function Update-ClientCredentials {
    <#
    .SYNOPSIS
        Updates expired Azure client credentials for the Zerto Appliance.
    #>

    [CmdletBinding()]
    [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
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

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

    if (-not (Test-VmExists -VmName $ZVM_VM_NAME)) {
        Write-Error "Zerto is not installed, nothing to update." -ErrorAction Stop
    }

    try {
        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 must set PersistentSecrets.ZertoAdminPassword and immediately test it for consistency - in case the user has previously changed the 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.

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

            $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 an expected 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 ZVMA."

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

function Update-AzureResourceGroup {
    <#
    .SYNOPSIS
        Updates the Azure resource group for the deployed Zerto Appliance.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Enter the current Zerto Appliance admin password.")]
        [ValidateNotNullOrEmpty()][SecureString]
        $ZertoAdminPassword
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    if (-not (Test-VmExists -VmName $ZVM_VM_NAME)) {
        Write-Error "Zerto is not installed, nothing to update." -ErrorAction Stop
    }

    try {
        Test-FeatureFlagEnabled -Flag 'CHANGE_RESOURCE_GROUP'

        # We must set PersistentSecrets.ZertoAdminPassword and immediately test it for consistency - in case the user has previously changed the admin password in Keycloak
        $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
        Test-ZertoPassword

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

        Set-AzureResourceGroup -ResourceGroupName $resourceGroupName
    }
    catch {
        $msgFailure = "Failed to update the Azure resource group for ZVMA. To restore ZVMA functionality, please move the AVS private cloud back to its original resource group. Problem: $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Invoke-ZertoSnapshotOperation {
    <#
    .SYNOPSIS
        Executes VMware Snapshot operations on the ZVML:
        • 'Take' to create a VMware snapshot of the ZVML (max of one snapshot);
        • 'Info' to view snapshot details;
        • 'Revert' to revert the ZVML to the VMware Snapshot;
        • 'Delete' to remove the ZVML VMware snapshot.
        Snapshots are taken without virtual machine memory.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Input the required operation: 'Take' / 'Info' / 'Revert' / 'Delete'")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Take', 'Info', 'Revert', 'Delete', ErrorMessage = "'{0}' is not a supported operation. Valid operations are 'Take', 'Info', 'Revert' or 'Delete'")]
        [string]$Operation
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        Invoke-ZVMSnapshotOperation -Operation $Operation
    }
    catch {
        $msgFailure = "Failed to invoke ZVMA snapshot operation. Problem: $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Invoke-ZertoDNSConfiguration {
    <#
    .SYNOPSIS
        Manages DNS configuration for the Zerto Appliance:
        • 'Info' to view DNS nameserver entries;
        • 'Add' to add a new DNS nameserver entry;
        • 'Remove' to remove an existing DNS nameserver entry.
        ⚠️ You must manually restart the Zerto Appliance for the DNS configuration changes to take effect.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $false, HelpMessage = "Valid operations are 'Info', 'Add', 'Remove'. Default is 'Info'.")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Info', 'Add', 'Remove', ErrorMessage = "'{0}' is not a supported operation. Valid operations are 'Info', 'Add' or 'Remove'.")]
        [string]$Operation = 'Info',

        [Parameter(Mandatory = $false, HelpMessage = "DNS IP address. Required for 'Add' and 'Remove' operations.")]
        [string]$DNS
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        if ($Operation -in 'Add', 'Remove') {
            $DNS = $DNS.Trim()
            if ([string]::IsNullOrEmpty($DNS)) {
                throw "DNS IP must be provided for 'Add/Remove' operations."
            }
            if (-not [System.Net.IPAddress]::TryParse($DNS, [ref]$null)) {
                throw "The provided DNS value '$DNS' is not a valid IP."
            }
        }

        switch ($Operation) {
            'Info' {
                $dnsIPs = Get-ZVMLDnsServers
                Write-Host "Current DNS nameserver entries:"
                foreach ($ip in $dnsIPs) {
                    Write-Host "* $ip"
                }
            }
            'Add' {
                Add-ZVMLDnsServer -DnsIp $DNS
            }
            'Remove' {
                Remove-ZVMLDnsServer -DnsIp $DNS
            }
            default {}
        }

        if ($Operation -in 'Add', 'Remove') {
            Write-Host "<!> Important: Restart the ZVMA with [Restart-ZertoAppliance] command to apply the DNS changes."
        }
    }
    catch {
        $msgFailure = "Failed to invoke ZVMA DNS configuration operation. Problem: $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Enable-ZertoVAIOSupport {
    <#
    .SYNOPSIS
        Enables Zerto protection using VAIO (vSphere API for IO filtering).
        ‼️ Once enabled and configured, VAIO Support can not be disabled.
        ⚠️ VAIO requires a minimum of 4 hosts in a cluster.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Confirmation for enabling Zerto protection using VAIO, type 'enable' to confirm.")]
        [ValidateNotNullOrEmpty()]
        [string]$Confirmation,

        [Parameter(Mandatory = $true, HelpMessage = "Enter the current Zerto Appliance admin password.")]
        [ValidateNotNullOrEmpty()]
        [SecureString]$ZertoAdminPassword
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    if ($Confirmation.Trim() -ne "enable") {
        Write-Error "Type 'enable' to confirm enabling Zerto protection using VAIO (vSphere API for IO filtering)." -ErrorAction Stop
    }

    if (-not (Test-VmExists -VmName $ZVM_VM_NAME)) {
        Write-Error "Zerto is not installed, nothing to update." -ErrorAction Stop
    }

    try {
        Test-FeatureFlagEnabled -Flag 'ENABLE_VAIO'

        # We must set PersistentSecrets.ZertoAdminPassword and immediately test it for consistency - in case the user has previously changed the admin password in Keycloak
        $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
        Test-ZertoPassword

        # Reset role privileges to match VAIO requirements
        Remove-ZertoRole
        Assign-NewZertoRole

        Enable-VAIOConfiguration
    }
    catch {
        $msgFailure = "Failed to enable VAIO for ZVMA. Problem: $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Set-ZertoReplicationComponent {
    <#
    .SYNOPSIS
        Assigns Zerto Replication component to the custom storage policy.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Custom storage policy name.")]
        [ValidateNotNullOrEmpty()]
        [string]$StoragePolicyName,

        [Parameter(Mandatory = $false, HelpMessage = "Replication component name. If left blank, the default 'Zerto Replication' will be used.")]
        [ValidateNotNullOrEmpty()]
        [string]$ReplicationComponentName = "Zerto Replication"
    )

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

        $customStoragePolicy = Get-SpbmStoragePolicy -Name $StoragePolicyName

        if (-not $customStoragePolicy) {
            throw "'$($StoragePolicyName)' storage policy does not exist."
        }

        $storagePolicyComponentTypes = ($customStoragePolicy.CommonStoragePolicyComponent).LineOfService
        if ($storagePolicyComponentTypes -contains [VMware.VimAutomation.Storage.Types.V1.Spbm.SpbmLineOfServiceType]::Replication) {
            throw "Provided storage policy already contains the Replication component."
        }

        # We can't fetch the available SP Components from vCenter directly, so we fetch the Components from all existing Storage Policies instead
        $allUsedStoragePolicyComponents = (Get-SpbmStoragePolicy | Select-Object CommonStoragePolicyComponent -Unique).CommonStoragePolicyComponent;
        $zertoReplicationComponent = $allUsedStoragePolicyComponents | Where-Object Name -eq $ReplicationComponentName | Select-Object -First 1

        if (-not $zertoReplicationComponent) {
            throw "'$($ReplicationComponentName)' component does not exist."
        }

        Write-Host "'$($zertoReplicationComponent.Name)' component exists."

        # Assign Zerto Replication component to the private field of the custom storage policy _commonStoragePolicyComponent using reflection
        $type = $customStoragePolicy.GetType()
        $field = $type.GetField("_commonStoragePolicyComponent", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic)
        $oldStoragePolicyComponents = ([VMware.VimAutomation.Storage.Types.V1.Spbm.SpbmStoragePolicyComponent[]]$field.GetValue($customStoragePolicy))
        if ($oldStoragePolicyComponents) {
            # Append the Zerto Replication component to the existing components
            $field.SetValue($customStoragePolicy, [VMware.VimAutomation.Storage.Types.V1.Spbm.SpbmStoragePolicyComponent[]]@($zertoReplicationComponent; $oldStoragePolicyComponents))
        }
        else {
            # Set the Zerto Replication component as the only component
            $field.SetValue($customStoragePolicy, [VMware.VimAutomation.Storage.Types.V1.Spbm.SpbmStoragePolicyComponent[]]$zertoReplicationComponent)
        }
        Set-SpbmStoragePolicy -StoragePolicy $customStoragePolicy

        Write-Host "'$ReplicationComponentName' component assigned to '$StoragePolicyName' successfully."
    }
    catch {
        Write-Error "Failed to modify storage policy. Problem: $_" -ErrorAction Stop
    }
}

function Debug-Zerto {
    <#
    .SYNOPSIS
        Performs diagnostics on Zerto components for troubleshooting.
    #>

    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Check Zerto user.")]
        [ValidateNotNullOrEmpty()][switch]
        $CheckUser,

        [Parameter(Mandatory = $true, HelpMessage = "Check Zerto connectivity.")]
        [ValidateNotNullOrEmpty()][switch]
        $CheckConnectivity,

        [Parameter(Mandatory = $true, HelpMessage = "Clean up Zerto log bundles. ‼️ Do not run while ZVMA Log Collection is in progress.")]
        [ValidateNotNullOrEmpty()][switch]
        $CleanupLogBundles,

        [Parameter(Mandatory = $true, HelpMessage = "Check Zerto driver.")]
        [ValidateNotNullOrEmpty()][switch]
        $CheckDriver,

        [Parameter(Mandatory = $false, HelpMessage = "Host name for driver check.")]
        [string]$HostName,

        [Parameter(Mandatory = $false, HelpMessage = "Datastore name for driver check.")]
        [string]$DatastoreName
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        if ($CheckUser) {
            Write-Host "### Checking Zerto user and role in vCenter ###"
            Test-ZertoUserExists
            Test-ZertoRoleExists
        }

        if ($CheckConnectivity) {
            if (Test-VmExists -VmName $ZVM_VM_NAME) {
                Write-Host "### Checking Zerto connectivity ###"
                Invoke-ZvmaNetDiagnostics -TargetUri 'https://management.azure.com'
                Invoke-ZvmaNetDiagnostics -TargetUri 'https://login.microsoftonline.com'
                Invoke-ZvmaNetDiagnostics -TargetUri 'https://management.core.windows.net'
            }
            else {
                Write-Host "Zerto is not installed, skipping connectivity checks."
            }
        }

        if ($CleanupLogBundles) {
            Write-Host "### Cleaning Zerto log bundles ###"
            Cleanup-LogBundles
        }

        if ($CheckDriver) {
            Write-Host "### Checking Zerto driver on hosts ###"
            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"
                    Get-DriverLogsFromHost -HostName $HostName -DatastoreName $DatastoreName
                }
            }
        }
    }
    catch {
        $msgFailure = "Failed to Debug-Zerto, $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

function Invoke-ZertoLogsCollection {
    <#
    .SYNOPSIS
        Packs ZVML logs to zip and uploads archive to the specified datastore in the 'zertoSupport' directory.
    #>

    [CmdletBinding()]
    [AVSAttribute(60, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Datastore name to upload ZVML logs archive to.")]
        [ValidateNotNullOrEmpty()]
        [string]$DatastoreName,

        [Parameter(Mandatory = $false, HelpMessage = "Add the prometheus metrics to the ZVML logs archive.")]
        [switch]$WithPrometheus
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        if (-not (Validate-DatastoreName -DatastoreName $DatastoreName)) {
            throw "No datastore '$DatastoreName' found."
        }

        Collect-ZertoLogs -DatastoreName $DatastoreName -WithPrometheus $WithPrometheus.IsPresent
    }
    catch {
        $msgFailure = "Failed to Invoke-ZertoLogsCollection, $_"
        Write-Host $msgFailure
        Write-Error $msgFailure -ErrorAction Stop
    }
}

<#
    Internal Cmdlets - are invoked by the ZVMA
#>


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)..."

        Validate-HostsForNonVaio -HostName $HostName

        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."
        }
    }
}

#TODO remove per ZER-175036
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 "Successfully 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>
    #>

    [CmdletBinding()]
    [AVSAttribute(60, UpdatesSDDC = $false, AutomationOnly = $true)] # Explicitly marked as AutomationOnly to hide from public cmdlets
    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 = "The current ZVML VM Console user password")]
        [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.AvsClientSecret = ConvertFrom-SecureString -SecureString $AvsClientSecret -AsPlainText

        $PersistentSecrets.ZappliancePassword = ConvertFrom-SecureString -SecureString $ZertoVmPassword -AsPlainText
        $PersistentSecrets.ZertoAdminPassword = ConvertFrom-SecureString -SecureString $ZertoAdminPassword -AsPlainText
        Test-ZertoPassword # Will test the ZappliancePassword and ZertoAdminPassword

        $isNewUserCreated = $false
        try {
            if (-not (Test-VmExists -VmName $ZVM_VM_NAME)) {
                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)

            Set-ZertoVmPasswordExpiration
        }
        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
        }
    }
}