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