install_utils.ps1

$ZERTO_USER_NAME = "ZertoDR"
$ZERTO_ROLE = "ZertoRole"
$DOMAIN = "vsphere.local"
$CLOUD_ADMINS_GROUP = "CloudAdmins"
$ZAPPLIANCE_PASSWORD_OVF_PROPERTY_NAME = "ZertoAdminPassword"
$ZERTO_SECURE_FOLDER = "avs-zerto"
$TEMP_ZERTO_DOWNLOAD_PATH = "$(Get-Location)/zerto"

function Initialize-ZertoTempFolder {

    if ($env:ENVIRONMENT -eq "TEST") {
        Write-Host "Skipping $TEMP_ZERTO_DOWNLOAD_PATH initialization in test environment."
        return
    }

    if (-not (Test-Path $TEMP_ZERTO_DOWNLOAD_PATH)) {
        New-Item -Path  $TEMP_ZERTO_DOWNLOAD_PATH -ItemType Directory -Force | Out-Null -ErrorAction Stop
        Write-Host "Zerto temp download folder created."
    }
    else {
        try {
            Get-ChildItem -Path $TEMP_ZERTO_DOWNLOAD_PATH -Recurse | Remove-Item -Force -Recurse
        }
        catch {
            Write-Host "Cannot clear Zerto temp download folder. Continuing anyway. Problem: $_"
        }
        Write-Host "Zerto temp download folder cleared."
    }
}

function Get-ZertoOVAFile {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "MyZerto Token")]
        [string]$MyZertoToken
    )

    process {
        Initialize-ZertoTempFolder

        $LocalFolderPath = $TEMP_ZERTO_DOWNLOAD_PATH

        try {
            Write-Host "Getting Zerto download links"
            $ZertoUrlsJson = Get-ZertoDownloadLinksFromMyZerto -Token $MyZertoToken

            Write-Host "Parsing download links"
            $OvaPresignedUrl = $ZertoUrlsJson.data.files.ova_file
            $OvaSignaturePresignedUrl = $ZertoUrlsJson.data.files.ova_signature

            $OvaFileName = Get-FileNameFromDownloadLink -Url $OvaPresignedUrl
            $OvaSignatureFileName = Get-FileNameFromDownloadLink -Url $OvaSignaturePresignedUrl

            $OvaFilePath = Join-Path -Path "$LocalFolderPath" -ChildPath "$OvaFileName"
            $OvaSignatureFilePath = Join-Path -Path "$LocalFolderPath" -ChildPath "$OvaSignatureFileName"

            Write-Host "Downloading OVA signature file $OvaSignatureFileName"
            Download-File -FileUrl $OvaSignaturePresignedUrl -LocalFilePath $OvaSignatureFilePath | Out-Null

            Write-Host "Downloading OVA file $OvaFileName"
            Download-File -FileUrl $OvaPresignedUrl -LocalFilePath $OvaFilePath | Out-Null
        }
        catch {
            throw "Failed to download Zerto OVA files. Problem: $_"
        }

        if ((Validate-FileBySignature -FilePath $OvaFilePath -SignatureFilePath $OvaSignatureFilePath) -ne $true) {
            throw "OVA signature validation failed."
        }

        return $OvaFilePath
    }
}

function Get-ZertoDownloadLinksFromMyZerto {
    param (
        [ValidateNotNullOrEmpty()]
        [string]$Token
    )

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

        try {
            # Valid Token is a hexadecimal case-insensitive string
            $Token = $Token.ToLower()
            if (-not ($Token -match '^[a-f0-9]+$')) {
                throw "Wrong token format."
            }
            $url = "https://www.zerto.com/myzerto/wp-json/services/zerto/s3-ova?key=$Token"
            $response = Invoke-WebRequest -Uri $url -TimeoutSec 30 -ErrorAction Stop
            Write-Host "Zerto download links received successfully."
            return ($response | ConvertFrom-Json)
        }
        catch {
            $errorObject = $_

            if (($errorObject.Exception.PSObject.Properties.Name -contains 'Response') -and
                ($errorObject.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::BadRequest)) {

                $responseBody = $errorObject.ToString() | ConvertFrom-Json # {"success":false,"data":[],"error":"invalid_key","error_description":"Key not found"}
                $responseBody = $errorObject.ErrorDetails.Message | ConvertFrom-Json

                switch ($responseBody.error) {
                    'key_used' { $errorMessage = 'The provided token has already been used, please generate a new one.' }
                    'expired_key' { $errorMessage = 'The provided token has expired, please generate a new one.' }
                    'invalid_key' { $errorMessage = 'The provided token is invalid.' }
                    default { $errorMessage = $responseBody.error_description }
                }
            }
            else {
                $errorMessage = $errorObject.Exception.Message
            }

            throw "Failed to receive download links for the ZVM Appliance. $errorMessage"
        }
    }
}

function Download-File {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "file url to download from")]
        [string]$FileUrl,
        [Parameter(Mandatory = $true, HelpMessage = "local file path")]
        [string]$LocalFilePath
    )

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

        Write-Host "Testing if the file exists at $LocalFilePath"
        if (Test-Path "$LocalFilePath") {
            Write-Host "The file $LocalFilePath already exists. Skipping download."
            return $false
        }

        try {
            $start_time = Get-Date
            #TODO:GK Print file size and download progress

            Write-Host "The download might take a while, please wait..."

            # Avoid using `-Resume` due to https://github.com/PowerShell/PowerShell/issues/23948
            Invoke-WebRequest -Uri $FileUrl -OutFile $LocalFilePath -TimeoutSec 10800 -ErrorAction Stop
            Write-Host "Download completed successfully, duration: $((Get-Date).Subtract($start_time).TotalSeconds.ToString("F0")) seconds."
            return $true
        }
        catch {
            throw "Failed to download $FileUrl. Problem: $_"
        }
    }
}

function Get-FileNameFromDownloadLink {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "file url to download from")]
        [string]
        $Url
    )

    process {
        try {
            $uri = New-Object System.Uri($url)
            $FileName = [System.IO.Path]::GetFileName($uri.LocalPath)
            return $FileName
        }
        catch {
            throw "Failed getting file name from $Url. Problem: $_"
        }
    }
}

function Deploy-Vm {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Path for the OVA file")]
        [ValidateNotNullOrEmpty()][string]
        $OvaPath,

        [Parameter(Mandatory = $true, HelpMessage = "Host Name")]
        [ValidateNotNullOrEmpty()][string]
        $VMHostName,

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

        [Parameter(Mandatory = $true, HelpMessage = "Zvm IP address")]
        [ValidateNotNullOrEmpty()][string]
        $ZVMLIp,

        [Parameter(Mandatory = $true, HelpMessage = "Network name for ZVML")]
        [ValidateNotNullOrEmpty()][string]
        $NetworkName,

        [Parameter(Mandatory = $true, HelpMessage = "SubnetMask address")]
        [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 = "Azure Tenant Id, Globally unique identifier, found in Azure portal")]
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [Parameter(Mandatory = $true, HelpMessage = "Azure Client ID - Application ID, found in Azure portal")]
        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [Parameter(Mandatory = $true, HelpMessage = "The ID of the target subscription")]
        [ValidateNotNullOrEmpty()][string]
        $AvsSubscriptionId,

        [Parameter(Mandatory = $true, HelpMessage = "AVS resources that are all in the same AVS Region")]
        [ValidateNotNullOrEmpty()][string]
        $AvsResourceGroup,

        [Parameter(Mandatory = $true, HelpMessage = "Private cloud name")]
        [ValidateNotNullOrEmpty()][string]
        $AvsCloudName
    )

    process {

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

            $ovfConfig = Set-OvfProperties -OvaPath $OvaPath -ZVMLIp $ZVMLIp -NetworkName $NetworkName -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS

            $datastore = Get-Datastore -Name $DatastoreName

            $vmHost = Get-VMHost -Name $VMHostName

            Write-Host "The deployment process might take a while, please wait..."
            $start_time = Get-Date
            $secure_folder = [AVSSecureFolder]::GetOrCreate($ZERTO_SECURE_FOLDER) # Works reliably only in single-datacenter vCenter environments, which is the case for AVS.
            Import-VApp -Source $OvaPath -OvfConfiguration $ovfConfig -Name $ZVM_VM_NAME -VMHost $vmHost -InventoryLocation $secure_folder -Datastore $datastore -ErrorAction stop | Out-Null
            [AVSSecureFolder]::Secure($secure_folder)
            Write-Host "$ZVM_VM_NAME was deployed successfully, duration: $((Get-Date).Subtract($start_time).TotalSeconds.ToString("F0")) seconds."

            Set-Platform-OvfProperties -AzureTenantId $AzureTenantId -AzureClientID $AzureClientID -AvsSubscriptionId $AvsSubscriptionId -AvsResourceGroup $AvsResourceGroup -AvsCloudName $AvsCloudName
        }
        catch {
            # Import-VApp error may contain a useful inner exception. Exception.ToString() prints inner exceptions too.
            throw "Failed to deploy $ZVM_VM_NAME ZVML. Exception: $($_.Exception.ToString())"
        }

    }
}

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

        $secure_folder = [AVSSecureFolder]::GetOrCreate($ZERTO_SECURE_FOLDER)
        $zvm = Get-Vm -Name $ZVM_VM_NAME
        Move-VM -VM $zvm -Destination $secure_folder | Out-Null #TODO: Use InventoryLocation instead of Destination, but only used in Migration Flow
        [AVSSecureFolder]::Secure($secure_folder)
    }
    catch {
        throw "Failed to move $ZVM_VM_NAME to the secure folder. Problem: $_"
    }
}

function Set-Platform-OvfProperties {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Azure Tenant Id, Globally unique identifier, found in Azure portal")]
        [ValidateNotNullOrEmpty()][string]
        $AzureTenantId,

        [Parameter(Mandatory = $true, HelpMessage = "Azure Client ID - Application ID, found in Azure portal")]
        [ValidateNotNullOrEmpty()][string]
        $AzureClientID,

        [Parameter(Mandatory = $true, HelpMessage = "The ID of the target subscription")]
        [ValidateNotNullOrEmpty()][string]
        $AvsSubscriptionId,

        [Parameter(Mandatory = $true, HelpMessage = "AVS resources that are all in the same AVS Region")]
        [ValidateNotNullOrEmpty()][string]
        $AvsResourceGroup,

        [Parameter(Mandatory = $true, HelpMessage = "Private cloud name")]
        [ValidateNotNullOrEmpty()][string]
        $AvsCloudName
    )
    process {

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

        try {
            $ZVM = Get-VM -Name $ZVM_VM_NAME
            if ($null -eq $ZVM) {
                throw "$ZVM_VM_NAME VM does not exist."
            }
            else {
                $vappProperties = $ZVM.ExtensionData.Config.VAppConfig.Property
                # Create a new Update spec based on the # of OVF properties to update
                $spec = New-Object VMware.Vim.VirtualMachineConfigSpec
                $spec.vAppConfig = New-Object VMware.Vim.VmConfigSpec
                $propertySpec = New-Object VMware.Vim.VAppPropertySpec[](10)

                #Starting from, the last property Key of VM. Otherwise, we will override existing properties
                $array = $vappProperties | Sort-Object -Property Key -Descending
                $propertyKey = $array[0].Key + 1

                # AVS properties
                $AzureTenantIdProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $AzureTenantId -PropertyId "AzureTenantId" -PropertyType "string" -Operation "add"
                $propertySpec += ($AzureTenantIdProperty)
                $AzureClientIDProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $AzureClientID -PropertyId "AzureClientID" -PropertyType "string" -Operation "add"
                $propertySpec += ($AzureClientIDProperty)
                $AvsSubscriptionIdProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $AvsSubscriptionId -PropertyId "AvsSubscriptionId" -PropertyType "string" -Operation "add"
                $propertySpec += ($AvsSubscriptionIdProperty)
                $AvsAvsResourceGroupProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $AvsResourceGroup -PropertyId "AvsResourceGroup" -PropertyType "string" -Operation "add"
                $propertySpec += ($AvsAvsResourceGroupProperty)
                $AvsCloudNameProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $AvsCloudName -PropertyId "AvsCloudName" -PropertyType "string" -Operation "add"
                $propertySpec += ($AvsCloudNameProperty)
                Write-Host "Added AVS properties to ZVM"

                #VC Properties
                $ZertoUserWithDomain = "$ZERTO_USER_NAME@$DOMAIN"
                $VcUserProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $ZertoUserWithDomain -PropertyId "VcUsername" -PropertyType "string" -Operation "add"
                $propertySpec += ($VcUserProperty)
                $VcIpProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $VC_ADDRESS -PropertyId "VcIp" -PropertyType "string" -Operation "add"
                $propertySpec += ($VcIpProperty)
                Write-Host "Added VC properties to ZVM"

                #ZVM Properties
                $ZertoAdminProperty = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $PersistentSecrets.ZertoAdminPassword -PropertyId $ZAPPLIANCE_PASSWORD_OVF_PROPERTY_NAME -PropertyType "password" -Operation "add"
                $propertySpec += ($ZertoAdminProperty)
                $ZappliancePassword = Create-OvfProperty ([ref]$propertyKey) -PropertyValue $PersistentSecrets.ZappliancePassword -PropertyId "ZappliancePassword" -PropertyType "password" -Operation "add"
                $propertySpec += ($ZappliancePassword)

                $spec.VAppConfig.Property = $propertySpec

                # Reconfiguring VM with a new properties
                $task = $ZVM.ExtensionData.ReconfigVM_Task($spec)
                $task1 = Get-Task -Id ("Task-$($task.value)")

                #!!! Print of a Wait-Task breaks logs in AVS, so we need to direct it to null
                $task1 | Wait-Task > $null
            }
        }
        catch {
            throw "Failed to add dynamic properties for Zero VM. Problem: $_"
        }
    }
}

function Update_OvfProperty {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Property name to update")]
        [ValidateNotNullOrEmpty()][string]
        $PropertyName,

        [Parameter(Mandatory = $true, HelpMessage = "Property value")]
        [ValidateNotNullOrEmpty()][string]
        $PropertyValue
    )
    process {
        $ZVM = Get-VM -Name $ZVM_VM_NAME
        if ($null -eq $ZVM) {
            throw "$ZVM_VM_NAME VM does not exist."
        }
        else {
            $vappProperties = $ZVM.ExtensionData.Config.VAppConfig.Property
            # Create a new Update spec based on the # of OVF properties to update
            $spec = New-Object VMware.Vim.VirtualMachineConfigSpec
            $spec.vAppConfig = New-Object VMware.Vim.VmConfigSpec

            $propToUpdate = ($vappProperties | Where-Object { $_.Id -eq $PropertyName })[0]

            $VcPasswordProperty = Create-OvfProperty ([ref]$propToUpdate.Key)`
                -PropertyValue "$PropertyValue"`
                -PropertyId $propToUpdate.Id`
                -PropertyType $propToUpdate.Type`
                -Operation "edit"
            $propertySpec = @($VcPasswordProperty)

            $spec.VAppConfig.Property = $propertySpec

            # Reconfiguring VM with a new properties
            $task = $ZVM.ExtensionData.ReconfigVM_Task($spec)
            $task1 = Get-Task -Id ("Task-$($task.value)")

            $task1 | Wait-Task > $null
            Write-Host "OVF property $PropertyName was successfully updated"
        }
    }

}

function Create-OvfProperty {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Ovf property Key, should be unique")]
        [ref]
        [int]
        $PropertyKey,

        [Parameter(Mandatory = $true, HelpMessage = "Ovf property Value")]
        [string]
        $PropertyValue,

        [Parameter(Mandatory = $true, HelpMessage = "Ovf property Id")]
        [string]
        $PropertyId,

        [Parameter(Mandatory = $true, HelpMessage = "Ovf property Type")]
        [string]
        $PropertyType,

        [Parameter(Mandatory = $true, HelpMessage = "Ovf property operation: edit, add, remove.")]
        [string]
        $Operation
    )

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

        $property = New-Object VMware.Vim.VAppPropertySpec
        $property.Operation = $Operation
        $property.Info = New-Object VMware.Vim.VAppPropertyInfo
        $property.Info.Key = $PropertyKey.Value
        $property.Info.value = $PropertyValue
        $property.Info.Id = $PropertyId
        $property.Info.type = $PropertyType
        $PropertyKey.Value++

        return $property
    }
}

function Set-OvfProperties {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Path for the OVA file")]
        [string]
        $OvaPath,

        [Parameter(Mandatory = $true, HelpMessage = "Zvm IP address")]
        [ValidateNotNullOrEmpty()][string]
        $ZVMLIp,

        [Parameter(Mandatory = $true, HelpMessage = "Network device for ZVML")]
        [ValidateNotNullOrEmpty()][string]
        $NetworkName,

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

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

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

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

        try {
            $ovfConfig = Get-OvfConfiguration -Ovf $OvaPath -ErrorAction stop

            Write-Host "The OVF configuration was fetched successfully for $ovaPath"

            $networkOvfPropertyName = ($ovfConfig.NetworkMapping.PSObject.Properties | Select-Object -Index 0).Name
            $ovfConfig.NetworkMapping.$networkOvfPropertyName.Value = $NetworkName

            $ovfConfig.net.ipAddress.Value = $ZVMLIp
            $ovfConfig.net.gateway.Value = $DefaultGateway
            $ovfConfig.net.netmask.Value = $SubnetMask
            $ovfConfig.net.dns.Value = $DNS

            return $ovfConfig
        }
        catch {
            throw "Failed to set OVF properties for $ovfPath. Problem: $_"
        }
    }
}

function Test-ZertoUserExists {
    <#
    .SYNOPSIS
        Checks whether the Zerto user exists in vCenter
    #>

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

    $zertoUser = Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN -ErrorAction SilentlyContinue
    if ($zertoUser) {
        Write-Host "$ZERTO_USER_NAME user exists in $DOMAIN on $VC_ADDRESS"
        if ($zertoUser.Locked) {
            $msgWarning = "$ZERTO_USER_NAME user is locked in vCenter"
            Write-Host $msgWarning
            Write-Warning $msgWarning
        }
        return $true;
    }
    else {
        Write-Host "$ZERTO_USER_NAME user does not exist in $DOMAIN on $VC_ADDRESS"
        return $false;
    }
}

function Test-ZertoRoleExists {
    <#
    .SYNOPSIS
        Checks whether the Zerto role exists in vCenter
    #>

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

    If (Get-VIRole -Name $ZERTO_ROLE -ErrorAction SilentlyContinue) {
        Write-Host "$ZERTO_ROLE role exists on $VC_ADDRESS"
        return $true
    }
    else {
        Write-Host "$ZERTO_ROLE role does not exist on $VC_ADDRESS"
        return $false;
    }
}

function New-ZertoUser {
    <#
    .SYNOPSIS
        Creates a Zerto user with a role and a required permission
    #>

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

    try {
        if ((Test-ZertoUserExists) -or (Test-ZertoRoleExists)) {
            throw "Zerto user/role already exists in vCenter. Run [Uninstall-Zerto] to clean it up."
        }

        # Check if the predefined CloudAdmins group exists
        $avsSsoGroup = Get-SsoGroup -Name $CLOUD_ADMINS_GROUP -Domain $DOMAIN
        if (-not $avsSsoGroup) {
            throw "$CLOUD_ADMINS_GROUP group does not exist in $DOMAIN on $VC_ADDRESS"
        }

        # Create Zerto user
        $PersistentSecrets.ZertoPassword = New-RandomPassword
        New-SsoPersonUser -UserName $ZERTO_USER_NAME -Password $PersistentSecrets.ZertoPassword -Description "Zerto DR user" -EmailAddress "ZertoDR@zerto.com" -FirstName "Zerto" -LastName "DR" -ErrorAction Stop | Out-Null

        # Add user to the predefined CloudAdmins group
        Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN -ErrorAction Stop | Add-UserToSsoGroup -TargetGroup $avsSsoGroup -ErrorAction Stop | Out-Null

        Write-Host "User $ZERTO_USER_NAME created successfully."

        # Create a Zerto role and assign it to the Zerto user
        Assign-NewZertoRole
    }
    catch {
        throw "Failed to create Zerto user. Problem: $_"
    }
}

function Assign-NewZertoRole {
    <#
    .SYNOPSIS
        Creates a new Zerto role with required privileges
        Assigns the Zerto role to the Zerto user on the vCenter root folder permission
    #>

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

    try {
        if (-not (Test-ZertoUserExists $ZERTO_USER_NAME)) {
            throw "$ZERTO_USER_NAME does not exist on $VC_ADDRESS"
        }

        #Create a new role

        $zertoPrivileges = @(
            'Alarm.Create',
            'Alarm.Delete',
            'Authorization.ModifyPermissions',
            'Cryptographer.Access',
            'Datastore.AllocateSpace',
            'Datastore.Browse',
            'Datastore.Config',
            'Datastore.DeleteFile',
            'Datastore.FileManagement',
            'Datastore.UpdateVirtualMachineFiles',
            'StoragePod.Config',
            'Extension.Register',
            'Extension.Unregister',
            'Folder.Create',
            'Global.CancelTask',
            'Global.Diagnostics',
            'Global.DisableMethods',
            'Global.EnableMethods',
            'Global.LogEvent',
            'Host.Config.AdvancedConfig',
            'Host.Config.AutoStart',
            'Host.Config.Settings',
            'Host.Config.NetService',
            'Host.Config.Patch',
            'Host.Inventory.EditCluster',
            'Network.Assign',
            'Resource.AssignVAppToPool',
            'Resource.AssignVMToPool',
            'Resource.ColdMigrate',
            'Resource.HotMigrate',
            'Sessions.ValidateSession',
            'Task.Create',
            'Task.Update',
            'VApp.ApplicationConfig',
            'VApp.AssignResourcePool',
            'VApp.AssignVM',
            'VApp.Create',
            'VApp.Delete',
            'VApp.Import',
            'VApp.PowerOff',
            'VApp.PowerOn',
            'VirtualMachine.Config.AddExistingDisk',
            'VirtualMachine.Config.AddNewDisk',
            'VirtualMachine.Config.AddRemoveDevice',
            'VirtualMachine.Config.AdvancedConfig',
            'VirtualMachine.Config.CPUCount',
            'VirtualMachine.Config.DiskExtend',
            'VirtualMachine.Config.EditDevice',
            'VirtualMachine.Config.ManagedBy',
            'VirtualMachine.Config.Memory',
            'VirtualMachine.Config.RawDevice',
            'VirtualMachine.Config.RemoveDisk',
            'VirtualMachine.Config.Resource',
            'VirtualMachine.Config.Settings',
            'VirtualMachine.Config.SwapPlacement',
            'VirtualMachine.Config.UpgradeVirtualHardware',
            'VirtualMachine.Interact.PowerOff',
            'VirtualMachine.Interact.PowerOn',
            'VirtualMachine.Inventory.CreateFromExisting',
            'VirtualMachine.Inventory.Create',
            'VirtualMachine.Inventory.Register',
            'VirtualMachine.Inventory.Delete',
            'VirtualMachine.Inventory.Unregister',
            'VirtualMachine.State.RemoveSnapshot',
            # The following privileges are required for VAIO
            'Extension.Update',
            'Host.Config.Maintenance',
            'StorageProfile.Update',
            'StorageProfile.View'
        )

        New-VIRole -name $ZERTO_ROLE -Privilege (Get-VIPrivilege -id $zertoPrivileges) -ErrorAction Stop | Out-Null
        Write-Host "Role $ZERTO_ROLE created on $VC_ADDRESS"

        # Create permission on vCenter object by assigning role to user

        $rootFolder = Get-Folder -NoRecursion # Essentially, this is the Datacenters folder, same as `Get-Folder -Type Datacenter`
        $zertoPrincipal = $DOMAIN + "\" + $ZERTO_USER_NAME
        New-VIPermission -Entity $rootFolder -Principal $zertoPrincipal -Role $ZERTO_ROLE -Propagate:$true -ErrorAction Stop | Out-Null
        Write-Host "Role $ZERTO_ROLE assigned to $zertoPrincipal successfully."
    }
    catch {
        throw "Failed to create Zerto role. Problem: $_"
    }
}

function Remove-ZertoUser {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $user = Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN

        if ($null -ne $user) {
            Remove-SsoPersonUser -User $user
            Write-Host "User '$ZERTO_USER_NAME' has been successfully removed."
        }
        else {
            Write-Host "User '$ZERTO_USER_NAME' not found."
        }
    }
    catch {
        throw "An error occurred while removing the user. Problem: $_"
    }
}

function Remove-ZertoRole {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    try {
        $role = Get-VIRole -Name $ZERTO_ROLE -ErrorAction SilentlyContinue # When no role is found, Get-VIRole returns an error by default

        if ($null -ne $role) {
            Remove-VIRole -Role $role -Force:$true -Confirm:$false
            Write-Host "Role '$ZERTO_ROLE' has been successfully removed."
        }
        else {
            Write-Host "Role '$ZERTO_ROLE' not found."
        }
    }
    catch {
        throw "An error occurred while removing the role. Problem: $_"
    }
}

function Remove-ZertoUserAndRole {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    Remove-ZertoRole
    Remove-ZertoUser
}

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

        try {
            if (Test-VmExists -VmName $ZVM_VM_NAME) {
                $zvm = Get-VM -Name $ZVM_VM_NAME

                Stop-ZVM #TODO Try force delete VM if it is not stopped

                Write-Host "Deleting $ZVM_VM_NAME VM from disk..."
                Remove-VM -VM $zvm -DeletePermanently -confirm:$false

                Write-Host "ZVM Appliance removed successfully."
            }
            else {
                Write-Host "$ZVM_VM_NAME does not exist."
            }
        }
        catch {
            throw "An error occurred while removing the ZVM Appliance. Problem: $_"
        }
    }
}