install_utils.ps1
$ZERTO_USER_NAME = "ZertoDR" $ZERTO_ROLE = "ZertoRole" $DOMAIN = "vsphere.local" $ZAPPLIANCE_PASSWORD_OVF_PROPERTY_NAME = "ZertoAdminPassword" $ZERTO_SECURE_FOLDER = "avs-zerto" $TEMP_ZERTO_DOWNLOAD_PATH = "$(Get-Location)/zerto" function Get-ZertoOVAFile { param ( [Parameter(Mandatory = $true, HelpMessage = "MyZerto Token")] [string]$MyZertoToken ) process { $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 Initialize-ZertoTempFolder { #TODO: Probably we can always use Clear-ZertoTempFolderLeniently instead if (Test-Path $TEMP_ZERTO_DOWNLOAD_PATH) { Get-ChildItem -Path $TEMP_ZERTO_DOWNLOAD_PATH -Recurse | Remove-Item -Force -Recurse Write-Host "Zerto temp download folder cleared." } else { New-Item -Path $TEMP_ZERTO_DOWNLOAD_PATH -ItemType Directory -Force | Out-Null Write-Host "Zerto temp download folder created." } } function Clear-ZertoTempFolderLeniently { try { Initialize-ZertoTempFolder } catch { Write-Warning "Failed to clear Zerto temp download folder on the scripting VM. Error: $_" } } function Get-ZertoDownloadLinksFromMyZerto { param ( [Parameter(Mandatory = $true, HelpMessage = "My Zerto Zoken")] [string]$Token ) process { Write-Host "Starting $($MyInvocation.MyCommand)" try { $Url = "https://www.zerto.com/myzerto/wp-json/services/zerto/s3-ova?key=$Token" $Response = Invoke-WebRequest -Uri $Url -ErrorAction Stop -TimeoutSec 30 | ConvertFrom-Json Write-Host "Zerto download links JSON received successfully." return $Response } catch { $ErrorMessage = $_ if ($ErrorMessage -match "invalid_key") { throw "Failed to receive download links for the ZVM Appliance. Invalid token." } else { throw "Failed to receive download links for the ZVM Appliance." } } } } 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) 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 { Write-Error "Failed to deploy $ZVM_VM_NAME ZVML. Problem: $_" -ErrorAction Stop } } } 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 [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) { Write-Error "$ZVM_VM_NAME does not exist" -ErrorAction Stop } 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) # TODO: Pavlo, is this working? $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) { Write-Error "$ZVM_VM_NAME does not exist" -ErrorAction Stop } 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 { <# .DESCRIPTION Get a zertoUsername and a domain, and return whether or not the user exists in the domain. .EXAMPLE Test-ZertoUserExists #> process { Write-Host "Starting $($MyInvocation.MyCommand)..." if (Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN -ErrorAction SilentlyContinue) { Write-Host "$ZERTO_USER_NAME already exists in $VC_ADDRESS, domain: $DOMAIN." return $true; } Write-Host "$ZERTO_USER_NAME does not exist in $VC_ADDRESS, domain: $DOMAIN." return $false; } } function Test-ZertoRoleExists { <# .DESCRIPTION Return true if ZertoRole exists, otherwise return false. .EXAMPLE Test-ZertoRoleExists #> process { Write-Host "Starting $($MyInvocation.MyCommand)..." If (Get-VIRole -Name $ZERTO_ROLE -ErrorAction SilentlyContinue) { Write-Host "$ZERTO_ROLE already exists in $VC_ADDRESS" return $true } Write-Host "$ZERTO_ROLE does not exist in $VC_ADDRESS" return $false; } } function New-ZertoUser { <# .DESCRIPTION Create a ZertoDR user and role which includes required privileges. #> process { Write-Host "Starting $($MyInvocation.MyCommand)..." try { if (Test-ZertoUserExists) { throw "$ZERTO_USER_NAME user already exists in $VC_ADDRESS. Run Uninstall-Zerto to delete it." } elseif (Test-ZertoRoleExists) { throw "ZERTO_ROLE role already exists in $VC_ADDRESS. Run Uninstall-Zerto to delete it." } else { # 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 CloudAdmins group $group = "CloudAdmins" $SsoGroup = Get-SsoGroup -Name $group -Domain $DOMAIN Get-SsoPersonUser -Name $ZERTO_USER_NAME -Domain $DOMAIN -ErrorAction Stop | Add-UserToSsoGroup -TargetGroup $SsoGroup -ErrorAction Stop | Out-Null Write-Host "ZertoUser ($ZERTO_USER_NAME) creation was completed successfully" } New-ZertoRole } catch { Write-Error "Failed to create Zerto User. Problem: $_" -ErrorAction Stop } } } function New-ZertoRole { <# .DESCRIPTION Update a ZertoDR user with the new role which includes required privileges. #> process { Write-Host "Starting $($MyInvocation.MyCommand)..." if ((Test-ZertoUserExists $ZERTO_USER_NAME) -eq $false) { Write-Host "$ZERTO_USER_NAME does not exist in $VC_ADDRESS..." throw "$ZERTO_USER_NAME does not exist in $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 permissions are required to use VAIO "StorageProfile.Update", "StorageProfile.View", "Extension.Update", "Host.Cim.CimInteraction", "Host.Config.Maintenance" ) New-VIRole -name $ZERTO_ROLE -Privilege (Get-VIPrivilege -Server $VC_ADDRESS -id $zertoPrivileges) -Server $VC_ADDRESS -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 $zertoPrincipal = $DOMAIN + "\" + $ZERTO_USER_NAME New-VIPermission -Entity $rootFolder[0] -Principal $zertoPrincipal -Role $ZERTO_ROLE -Propagate:$true -ErrorAction Stop | Out-Null Write-Host "ZertoRole ($ZERTO_ROLE) update for ZertoUser ($ZERTO_USER_NAME) was completed successfully" } } function Remove-ZertoUser { process { 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 { Write-Host "An error occurred while removing the user: $_" } } } function Remove-ZertoRole { process { try { $role = Get-VIRole -Name $ZERTO_ROLE 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 { Write-Host "An error occurred while removing the role: $_" } } } function Remove-ZertoUserAndRole { Remove-ZertoUser Remove-ZertoRole } function Remove-ZVMAppliance { process { Write-Host "Starting $($MyInvocation.MyCommand)..." try { if ((Test-VmExists -VmName $ZVM_VM_NAME) -eq $true) { $VM = Get-VM $ZVM_VM_NAME Stop-ZVM #TODO:GK can VM be force deleted if it is not stopped? Write-Host "Deleting $ZVM_VM_NAME VM from disk" Remove-VM -VM $VM -DeletePermanently -confirm:$false Write-Host "ZVM Appliance removed successfully" } else { Write-Host "$ZVM_VM_NAME does not exist" } } catch { Write-Error "Failed to remove ZVM Appliance. $_" -ErrorAction Stop } } } |