validation_utils.ps1

$PUBLIC_KEY = ('{0}/ZertoPublicKey.pem' -f $psScriptRoot)
$MIN_PASSWORD_LEN = 14
$PASSWORD_REGEX = [regex]"^(?=.*[A-Z])(?=.*[^A-Za-z])(?=.*\d)(?=.*[\W_]).{$MIN_PASSWORD_LEN,}$"
$UNSUPPORTED_LETTERS_IN_PASSWORD_REGEX = [regex]'[\$"\s]'
$UNSUPPORTED_FIRST_LETTER_IN_PASSWORD = "[]{}>|*&!%#`@,"

function Validate-FileBySignature {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "File to verify")]
        [string]$FilePath,

        [Parameter(Mandatory = $true, HelpMessage = "Signature file to verify")]
        [string]$SignatureFilePath
    )

    process {
        Write-Host "Verifying signature for $FilePath"
        Write-Host "The verification process might take a while, please wait..."

        $isVerified = (openssl dgst -sha256 -verify $PUBLIC_KEY -signature $SignatureFilePath $FilePath 2>&1) -join ";"

        if ($isVerified -eq "Verified OK") {
            Write-Host "File signature was verified successfully for $FilePath by $SignatureFilePath"
            return $true
        }
        else {
            Write-Host "Could not verify $FilePath signature by $SignatureFilePath"
            return $false
        }
    }
}

function Validate-VcEnvParams {
    param(
        [ValidateNotNullOrEmpty()]
        [string]$DatastoreName,

        [ValidateNotNullOrEmpty()]
        [string]$NetworkName,

        [ValidateNotNullOrEmpty()]
        [string]$ApplianceIp,

        [ValidateNotNullOrEmpty()]
        [string]$SubnetMask,

        [ValidateNotNullOrEmpty()]
        [string]$DefaultGateway,

        [ValidateNotNullOrEmpty()]
        [string]$DNS
    )

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

        if (-not (Validate-DatastoreName -DatastoreName $DatastoreName)) {
            throw "Datastore '$DatastoreName' validation failed."
        }

        if (-not (Validate-NetworkName -NetworkName $NetworkName)) {
            throw "Network '$NetworkName' validation failed."
        }

        Validate-NetworkSettings -ApplianceIp $ApplianceIp -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS
    }
}

function Validate-NetworkSettings {
    param (
        [ValidateNotNullOrEmpty()]
        [string]$ApplianceIp,

        [ValidateNotNullOrEmpty()]
        [string]$SubnetMask,

        [ValidateNotNullOrEmpty()]
        [string]$DefaultGateway,

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

    # Validate the correct format of the IP addresses xxx.xxx.xxx.xxx

    try {
        $ipBytes = [System.Net.IPAddress]::Parse($ApplianceIp).GetAddressBytes()
    }
    catch {
        throw "The provided ZVMA IP '$ApplianceIp' format is invalid."
    }

    try {
        $maskBytes = [System.Net.IPAddress]::Parse($SubnetMask).GetAddressBytes()
    }
    catch {
        throw "The provided Subnet Mask '$SubnetMask' format is invalid."
    }

    try {
        $gwBytes = [System.Net.IPAddress]::Parse($DefaultGateway).GetAddressBytes()
    }
    catch {
        throw "The provided Default Gateway '$DefaultGateway' format is invalid."
    }

    try {
        [System.Net.IPAddress]::Parse($DNS).GetAddressBytes() | Out-Null
    }
    catch {
        throw "The provided DNS '$DNS' format is invalid."
    }

    # Validate the subnet mask, by ensuring contiguous 1s followed by 0s

    $binaryMask = -join ($maskBytes | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') })
    $isContiguous = $binaryMask -match '^1+0+$'

    if (-not $isContiguous) {
        throw "The provided Subnet Mask '$SubnetMask' is invalid."
    }

    # Validate the IP and Gateway are in the same subnet, by ensuring the network parts of the IP and Gateway are the same

    for ($i = 0; $i -lt $maskBytes.Length; $i++) {
        if (($ipBytes[$i] -band $maskBytes[$i]) -ne ($gwBytes[$i] -band $maskBytes[$i])) {
            throw "The provided ZVMA IP '$ApplianceIp' and Default Gateway '$DefaultGateway' are not in the same subnet."
        }
    }

}

function Get-ValidatedHostName ($HostName, $NetworkName, $DatastoreName) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $allValidMatchingHostsNames = Select-ValidMatchingHostsNames -NetworkName $NetworkName -DatastoreName $DatastoreName

    if ($HostName) {
        if ($allValidMatchingHostsNames -contains $HostName) {
            Write-Host "Host provided by the user is valid: $HostName"
            return $HostName
        }
        else {
            throw "Host provided by the user is not valid or does not match the specified network and datastore: $HostName"
        }
    }
    else {
        $validHostName = $allValidMatchingHostsNames | Select-Object -First 1
        Write-Host "No host provided by the user. A host selected automatically: $validHostName"
        return $validHostName
    }

}

function Select-ValidMatchingHostsNames ($NetworkName, $DatastoreName) {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $datastore = Get-Datastore -Name $DatastoreName -ErrorAction SilentlyContinue # When $DatastoreName is wrong, Get-Datastore fails with 'Datastore was not found using the specified filter(s).' error.
    $network = Get-View -ViewType Network -Property Name -Filter @{"Name" = $NetworkName }

    $hosts = Get-VMHost | Where-Object {
        # Select hot state
        ($_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn") -and
        # Check if the host has access to the datastore
        ($null -ne $datastore) -and ($_.ExtensionData.Datastore -contains $datastore.ExtensionData.MoRef) -and
        # Check if the host has access to the network
        ($null -ne $network) -and ($_.ExtensionData.Network -contains $network.MoRef)
    }
    if (@($hosts).Count -eq 0) {
        throw "No powered-on hosts with access to both datastore '$DatastoreName' and network '$NetworkName' were found."
    }
    Write-Host "Total number of hosts with access to both datastore '$DatastoreName' and network '$NetworkName': $($hosts.Count)"

    $hostsNames = $hosts | Sort-Object -Property Name | Select-Object -ExpandProperty Name

    return $hostsNames
}

function Validate-ZertoParams {
    param (
        [Parameter(Mandatory = $true)]
        [SecureString]$ZertoAdminPassword,
        [Parameter(Mandatory = $true)]
        [bool]$IsVaio
    )
    process {
        Validate-ZertoPassword -Password $ZertoAdminPassword
        if (-not $IsVaio) {
            Validate-HostsForNonVaio
        }
    }
}

function Validate-ZertoPassword {
    param (
        [Parameter(Mandatory = $true)]
        [SecureString]$Password
    )
    process {
        $pass = ConvertFrom-SecureString -SecureString $Password -AsPlainText
        if ($pass -notmatch $PASSWORD_REGEX) {
            throw "Zerto password requirements are not met. Password should contain at least one uppercase letter, one digit, one non-alphanumeric character, and be at least $MIN_PASSWORD_LEN characters long."
        }
        if ($pass -match $UNSUPPORTED_LETTERS_IN_PASSWORD_REGEX) {
            throw 'Zerto password requirements are not met. Password should not contain $, ", or a space characters.'
        }
        if ($UNSUPPORTED_FIRST_LETTER_IN_PASSWORD.Contains($pass[0])) {
            throw "Zerto password requirements are not met. Password should not begin with the following characters $UNSUPPORTED_FIRST_LETTER_IN_PASSWORD ."
        }
    }
}

function Validate-HostsForNonVaio {
    param(
        [string]$HostName
    )
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    $vmHosts = if ($HostName) { Get-VMHost -Name $HostName } else { Get-VMHost }

    $AV64_HOST_MANUFACTURER = "Microsoft"
    $av64Hosts = $vmHosts | Where-Object { $_.Manufacturer -eq $AV64_HOST_MANUFACTURER }

    if ($av64Hosts) {
        $hostNames = $av64Hosts.Name -join ", "
        throw "Non-VAIO ZVMA cannot operate with AV64 hosts. Found AV64 host(s): $hostNames."
    }
}

function Test-VmExists {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "VM name pattern")]
        [string]$VmName
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $vm = Get-VM -Name $VmName -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($null -eq $vm) {
            Write-Host "'$VmName' VM does not exist"
            return $false
        }
        else {
            Write-Host "'$($vm.Name)' VM exists"
            return $true
        }
    }
}

function Validate-BiosUUID {
    param(
        [Parameter(Mandatory = $true, HelpMessage = "Valid Datastore name")]
        [string]$DatastoreName,
        [Parameter(Mandatory = $true, HelpMessage = "Host BIOS UUID || mob-> Property Path: host.hardware.systemInfo.uuid")]
        [string]$BiosUuid # The parameter expects <BIOS UUID without hyphens>_<Host name> format
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $Datastore = Get-Datastore -Name $DatastoreName | Select-Object -first 1

        $TEMP_DRIVE = "TEMP_DRIVE"
        New-PSDrive -Name $TEMP_DRIVE -Location $Datastore -PSProvider VimDatastore -Root '/' | Out-Null
        $exists = Test-Path "$($TEMP_DRIVE):/zagentid/$BiosUuid"
        Remove-PSDrive -Name $TEMP_DRIVE | Out-Null

        if ($exists) {
            Write-Host "BiosUuid '$BiosUuid' exists. Validation successful."
            return $true
        }
        else {
            Write-Host "BiosUuid '$BiosUuid' does not exist. Validation failed."
            return $false
        }

    }
}

function Validate-DigitsOnly {
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "Input string to validate all the characters are numeric")]
        [string]$InputString
    )

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

        if ($InputString -match "^\d+$") {
            Write-Host "InputString=$InputString contains digits only"
            return $true
        }

        Write-Error "Validation failed. InputString=$InputString contains non-numeric characters"
        return $false
    }
}

function Validate-DatastoreName {
    param(
        [ValidateNotNullOrEmpty()]
        [string]$DatastoreName
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $datastore = Get-Datastore -Name $DatastoreName -ErrorAction SilentlyContinue | Select-Object -first 1
        if ($null -eq $datastore) {
            Write-Host "Datastore '$DatastoreName' does not exist. Validation failed."
            return $false
        }

        Write-Host "Datastore '$DatastoreName' exists. Validation successful."
        return $true
    }
}

function Validate-NetworkName {
    param(
        [ValidateNotNullOrEmpty()]
        [string]$NetworkName
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)..."

        $network = Get-VirtualNetwork -Name $NetworkName -ErrorAction SilentlyContinue | Select-Object -first 1
        if ($null -eq $network) {
            Write-Host "Network '$NetworkName' does not exist. Validation failed."
            return $false
        }

        Write-Host "Network '$NetworkName' exists. Validation successful."
        return $true
    }
}

function Validate-ZvmSupportsVaio {
    Write-Host "Starting $($MyInvocation.MyCommand)..."

    # -i: Makes the search case-insensitive.
    # -x: Matches only entire lines (not partial matches).
    # -F: Interprets the pattern as a fixed string, not a regular expression.
    # -q: Runs silently, suppressing output and returning only the exit code.
    $commandToExecute = 'test -f "/opt/zerto/zlinux/avs/feature-flags.conf" && grep -ixFq "VAIO" "/opt/zerto/zlinux/avs/feature-flags.conf" && echo Success || echo Error'
    $result = Invoke-ZVMScriptWithTimeout -ScriptText $commandToExecute -ActionName "Check if VAIO is supported" -TimeoutMinutes 10 #TODO: Change to Invoke-VMScript, add tests

    if ($result.ScriptOutput.Contains("Success")) {
        Write-Host "ZVMA version supports VAIO in AVS."
    }
    elseif ($result.ScriptOutput.Contains("Error")) {
        throw "Your ZVMA version does not support VAIO in AVS. Please use a version which supports VAIO."
    }
    else {
        throw "An unexpected error occurred while checking if ZVMA version supports VAIO in AVS."
    }
}

function Validate-AvsParams {
    param (
        [Parameter(Mandatory = $true, HelpMessage = "Tenant ID")][string] $TenantId,
        [Parameter(Mandatory = $true, HelpMessage = "Cleint ID")][string] $ClientId,
        [Parameter(Mandatory = $true, HelpMessage = "Client Secret")][SecureString] $ClientSecret,
        [Parameter(Mandatory = $true, HelpMessage = "Subscription ID")][string] $SubscriptionId,
        [Parameter(Mandatory = $true, HelpMessage = "Resource Group Name")][string] $ResourceGroupName,
        [Parameter(Mandatory = $true, HelpMessage = "Avs Cloud Name")][string] $AvsCloudName
    )
    process {
        Write-Host "Starting $($MyInvocation.MyCommand)"

        $body = @{
            'client_id'     = $ClientId
            'client_secret' = (ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText)
            'grant_type'    = "client_credentials"
            'resource'      = "https://management.core.windows.net/"
        }
        try {
            $authResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/token" `
                -Body $body -ContentType "application/x-www-form-urlencoded"
        }
        catch {
            Write-Error "Authorization failed for Azure. Please check the values of the TenantID-ClientID-ClientSecret combintaion." `
                -ErrorAction Stop
        }

        $authToken = $authResponse.access_token

        $subscriptionUri = "https://management.azure.com/subscriptions/$SubscriptionId/?api-version=2020-01-01"
        try {
            [void] (Invoke-RestMethod -Method Get -Headers @{Authorization = ("Bearer " + $authToken) } -Uri $subscriptionUri)
        }
        catch {
            Write-Error "The subscription '$SubscriptionId' could not be found for the tenant '$TenantId'." -ErrorAction Stop
        }

        $avsCloudNameUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.AVS/privateClouds/$AvsCloudName/?api-version=2020-03-20"
        try {
            [void] (Invoke-RestMethod -Method Get -Headers @{Authorization = ("Bearer " + $authToken) } -Uri $avsCloudNameUri)
        }
        catch {
            Write-Error "The private cloud '$AvsCloudName' could not be found for the tenant '$TenantId'." -ErrorAction Stop
        }

        Write-Host "AVS parameters are valid"
    }
}

function Assert-ReconfigurationToken ($Token) {
    Write-Host "Starting $($MyInvocation.MyCommand)"
    try {
        $Url = "https://www.zerto.com/myzerto/wp-json/services/zerto/s3-ova-employee?key=" + $Token
        $response = Invoke-WebRequest -Uri $Url -ErrorAction Stop -TimeoutSec 1800
        $content = $response.Content
        if ($content -ne '{"success":true}') {
            throw "Reconfiguration token is invalid."
        }
        Write-Host "Reconfiguration token is valid"
    }
    catch {
        throw "Reconfiguration token is invalid."
    }
}