ShieldedVMRecovery.psm1
################################################ ##### SHIELD VM RECOVERY FUNCTIONS ##### ################################################ ################################################ ##### #1 Export Shielded VM to VMRE ##### ################################################ function Export-ShieldedVMToVMRE { <# .SYNOPSIS This function runs by the fabric admin, to export the troubled shielded VM to a data VHDx file, and attach it to VMRE Repair garage. .DESCRIPTION This function creates a small data VHDx file (5GB), and export the troubled VM to the data VHDx. This is to enable the tenant admin to recover the shielded VM in VMRE. To optimize the export time, all the disks mapped to the troubled VM will be removed before export. The mapping is stored in the data VHDx file. The assumption is that the recoveryVM has only 1 SCSI controller, and the script will add a new one, all the disks from the troubled VM will be added to the new SCSI controller. The VMRE will be turned at the beginning of the function. .PARAMETER TroubledVMName The name of the problematic VM. .PARAMETER VMREVMName The name of the recovery VM, a.k.a VMRE .PARAMETER RepairGarageDataVHD The VHDx file which was created to export the troubled VM to. .EXAMPLE Start-ShieldedVMRecoveryOnFabric -TroubledVMName ContosoVM1 -RepairGarageVMName ContosoVMRE -RepairGarageDataVHD d:\tenant\temp\contosodata.vhdx #> [CmdletBinding()] Param( [Parameter (ValueFromPipeline=$true, Position=0,Mandatory=$true)][string] $TroubledVMName, [Parameter (ValueFromPipeline=$true, Position=1,Mandatory=$true)][string] $VMREVMName, [Parameter (ValueFromPipeline=$true, Position=2,Mandatory=$true)][string] $RepairGarageDataVHD ) $VerbosePreference = "Continue" Write-Verbose "Validating input parameters" $VM1 = Get-VM -VMName $VMREVMName -ErrorAction Stop $VM2 = Get-VM -VMName $TroubledVMName -ErrorAction Stop if (Test-Path $RepairGarageDataVHD) { Write-Error "VHD file already exist, for security, specify a non-existing file name." return } # Power off VMRE to enable nested virtualization Write-Verbose "Enabling VMRE nested virtualization" if ( (Get-VM -VMName $VMREVMName).State -ne 'Off') { Stop-VM -VMName $VMREVMName } Set-VMProcessor -VMName $VMREVMName -ExposeVirtualizationExtensions $true Write-Verbose "Creating recovery data VHDX file" $datavhd = New-VHD -Path $RepairGarageDataVHD -Dynamic -SizeBytes 5GB Write-Verbose "Attaching the data VHDX" $disk = Mount-VHD -Path $RepairGarageDataVHD -Passthru Write-Verbose "Initialize the data VHDX" Initialize-Disk -Number $disk.Number Write-Verbose "Create partition" $partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize Write-Verbose "Formatting volume" $volume = Format-Volume -Partition $partition -FileSystem NTFS -NewFileSystemLabel "VMREData" -Force -Confirm:$false Write-Verbose "Assigning drive letter" $partition | Add-PartitionAccessPath -AssignDriveLetter $driveletter = (Get-Volume |? UniqueId -eq $volume.UniqueId).DriveLetter Write-Verbose "shutdown $TroubledVMName" if ( (Get-VM -VMName $TroubledVMName).State -ne 'Off') { Stop-VM -VMName $TroubledVMName } #need to test with different data disks with bitlocker encrypted $DataDrive = $driveletter + ":" Write-Verbose "Save the disk mappings of the $TroubledVMName to $DataDrive, in order to save time for import/export" $timer = 0 $access = $false while (($timer -le 60) -and !($access)) { Start-Sleep -Seconds 1 if (Test-Path $DataDrive) { $access = $true break } $vol = Get-Volume -DriveLetter $driveletter if ($vol) { Write-Verbose "Data volume has mounted as $vol" $dataDrive = $vol.DriveLetter + ":" } if (Get-Item $DataDrive) { $access = $true break } Write-Verbose "Trying to access $DataDrive in $timer times" $timer ++ } if (!($access)) { Write-Host "Unable to access $DataDrive" return } $disks = Get-VMHardDiskDrive -VMName $TroubledVMName If (!$disks) { Write-Host "Unable to get the disk mapping, so not removing the disk mappings" } else { $mapfile = "$DataDrive\diskmapping.xml" Write-Verbose "Export the disk mapping to $mapfile, remove it from $TroubledVMName" $disks | Export-Clixml -Path $mapfile -ErrorAction Stop Get-VMHardDiskDrive -VMName $TroubledVMName | Remove-VMHardDiskDrive } Write-Verbose "Export $TroubledVMName to data VHDX" Export-VM -Name $TroubledVMName -Path $DataDrive Write-Verbose "Dismount data VHDX from host" Dismount-VHD -path $RepairGarageDataVHD #need to test with differen controller Write-Verbose "Add data VHDX to recovery VM" Add-VMHardDiskDrive -VMName $VMREVMName -Path $RepairGarageDataVHD Write-Verbose "all the disks from the $TroubledVMName moved to the $VMREVMName" $controller = Add-VMScsiController -vmname $VMREVMName -Passthru if ($disks) { $i = 0 foreach ($d in $disks) { Write-Host "Adding $d to $VMREVMName to ControllerLocation $i" Add-VMHardDiskDrive -VMName $VMREVMName -ControllerNumber $controller.ControllerNumber -ControllerLocation $i -Path $d.Path $i++ } } Write-Verbose "Start $VMREVMName" Start-VM -VMName $VMREVMName # Now the tenant can RDP into recovery VM to run enable-troubleShooting Write-Host "$(Get-Date):Export data has completed, and ready for tenant admin to proceed" -ForegroundColor Yellow } ################################################ ##### #2 Export Shielded VM to VMRE ##### ################################################ function Import-ShieldedVMInVMRE { <# .SYNOPSIS This script runs by the tenant admin, to import the troubled VM for troubleshooting. .DESCRIPTION This script will import the troubled VM in the recovery VM, and after tenant admin to get the new Key protector, the troubled VM can be connected through console for troubleshooting. .PARAMETER TroubledVMName The name of the problematic VM. .PARAMETER CertsPath The location where the keyprotector and guardian files will be stored. #> [CmdletBinding()] Param( [Parameter (ValueFromPipeline=$true, Position=0,Mandatory=$true)][string] $TroubledVMName, [Parameter (ValueFromPipeline=$true, Position=1,Mandatory=$true)][string] $CertPath ) $VerbosePreference = "Continue" #create the folder Write-Verbose "Checking the inputs" If ((Test-Path $CertPath) -eq $false) { Write-Verbose "Creating the cert folder $certpath" New-Item $CertPath -Type Directory } #Check if Hyper-V is installed in VMRE $HyperVFeature = Get-WindowsFeature Hyper-V If (!($HyperVFeature.Installed)) { Write-Verbose "Installing Hyper-V" Install-WindowsFeature $HyperVFeature -IncludeAllSubFeature -IncludeManagementTools -Restart return } #bring the data disk online #the assumption of the data disk is 5GB. The only way to identify the data disk is by size. $datadisk = Get-disk | where {$_.Size -eq 5GB} if ($datadisk) { Write-Verbose "Bring the data VHDX $datadisk online" if ($datadisk.OperationalStatus -eq 'Offline') { Set-disk -Number $datadisk.Number -IsOffline $false Set-disk -Number $datadisk.Number -IsReadOnly $false } } #Find the drive letter of the data volume $dataDrive = get-volume | where {$_.FileSystemLabel -eq "VMREData"} Write-Verbose "Found VMRE data volumne: $($dataDrive.DriveLetter)" #Import the troubledVM Write-Verbose "Import $TroubledVMName" $VMpath = "$($dataDrive.DriveLetter):\$TroubledVMName\Virtual Machines" if (!(Test-Path $VMpath)) { Write-Verbose "Path $VMPath doesn't exist." return } #Get the VM configuration file $configFile = Get-childitem -path $VMpath -File '*.vmcx' Write-Verbose "$TroubledVMName config file is $configFile, under $VMPath" #check if the VM has been imported $VM = Get-VM -Name $TroubledVMName -ErrorAction SilentlyContinue If (!$VM) { #if not imported yet try { $report = Compare-VM -Path $configFile.FullName #check for incompabilities if ($report.Incompatibilities[0].MessageId -eq 33012) { #disconnect the network adapter $report.Incompatibilities[0].Source | Disconnect-VMNetworkAdapter } $VM= Import-VM -CompatibilityReport $report } catch { Write-Host "Unable to import the $TroubledVMName, please try to use the Hyper-V manager to import, and fix the incompatibility issues, and rerun the script." return } } #Adding the disks to the troubled VM, the assumption is to add all the offline disks to the troubled VM Write-Verbose "Adding the offline disks as passthru to the $TroubledVMName" $disks = Get-Disk | where {$_.OperationalStatus -eq 'Offline'} foreach ($d in $disks) { Write-Verbose "Adding disk $($d.disknumber) as passthru to the $TroubledVMName" Add-VMHardDiskDrive -VMName $TroubledVMName -DiskNumber $d.DiskNumber } #check if the VM is shielded Write-Verbose "Check the security policy of the $TroubledVMName" $policy = Get-VMSecurity -VMName $TroubledVMName if (!$policy.TpmEnabled -or !$policy.Shielded) { Write-host "VM is not shielded, you can connect to console from the recovery VM directly" return } # Retrieve current KP from DVM $kpfile = $CertPath + '\current.kp' Write-Verbose "Retrieve the existing KP from the $TroubledVMName, and write it to $kpfile" $kp = Get-VMKeyProtector -VMName $TroubledVMName $kp | Out-File -Filepath $kpfile #create a temp guardian $recoveryGuardian = $null; try { Write-Verbose "Get a temporary guardian for the Recovery garage" $recoveryGuardian = Get-HgsGuardian -Name 'TempRecoveryGuardian' -ErrorAction SilentlyContinue if ($recoveryGuardian -eq $null) { Write-Verbose "Create a temporary guardian for the Recovery garage" $recoveryGuardian = New-HgsGuardian -Name 'TempRecoveryGuardian' -GenerateCertificates } } catch { Write-Verbose "Create a temporary guardian for the Recovery garage" $recoveryGuardian = New-HgsGuardian -Name 'TempRecoveryGuardian' -GenerateCertificates } if ($recoveryGuardian -eq $null) { Write-Host "Unable to get the Recovery Guardian" return } #write recovery guardian to a file $outfile = $certpath + '\RecoveryGuardian.xml' Export-HgsGuardian -InputObject $recoveryGuardian -Path $outfile Write-Host "$(Get-Date): Copy the files under $certpath to the tenant host, and run Grant-VMREAccess" $newkpfile = $certpath + '\new.kp' write-Host "$(Get-Date): Once the new KP is generated, copy it back to the recoveryVM under this path: $newkpfile" #add recovery guardian to KP (must run on the tenant host) Read-Host "Press ENTER after the tenant admin has grant recovery guardian KP" #get the new KP $newkp = Get-Content $newkpfile #set new KP on the troubleVM Set-VMKeyProtector -VMName $TroubledVMName -KeyProtector $newkp #turn off shielding Set-VMSecurityPolicy -VMName $TroubledVMName -Shielded $false #start trouble VM and connect to the console Write-Host "$(Get-Date): Imported the shielded VM in VMRE and changed the security policy to allow console connect." Write-Host "$(Get-Date): Ready for console connection to troubleshoot after restart" } ################################################ ##### #3 Export Shielded VM to VMRE ##### ################################################ function Grant-VMREAccess { <# .SYNOPSIS This script can add the guardian from the Recovery VM (VMRE) to the current KeyProtector of the troubled VM. .DESCRIPTION Before running this script, you have already extracted the KP from the troubled VM, and also created the guardian from the recovery VM. This script will enable the RecoveryVM to run the troubled shielded VM by adding the guardian from the recovery VM to a new key protector. .PARAMETER CertsPath The location where the keyprotector and guardian files will be stored. #> [CmdletBinding()] Param( [Parameter (ValueFromPipeline=$true, Position=0,Mandatory=$true)][string] $CertPath ) $VerbosePreference = "Continue" #specify the share to copy the kp and guardian to the local machine $kpfile = $CertPath + '\current.kp' Write-Verbose "Get KP from $kpfile" $currentKp = Get-Content $kpfile $currentHgsKp = ConvertTo-HgsKeyProtector -byte $currentKp #import the guardian for recovery VM $rgfile = $CertPath +'\RecoveryGuardian.xml' Write-Verbose "Get RecoveryGuardian from $rgfile" $RecoveryGuardian = Import-HgsGuardian -Path $rgfile -Name 'TempRecoveryGuardian' -AllowUntrustedRoot #grant access Write-Verbose "Grant RecoveryGuardian access" $newhgsKP = Grant-HgsKeyProtectorAccess -KeyProtector $currentHgsKp -Guardian $RecoveryGuardian -AllowUntrustedRoot #export newkp $newkpfile = $CertPath + '\new.kp' Write-Verbose "Create the new KP file $newkpfile" $newhgsKP.RawData | Out-File $newkpfile #delete the temp guardian Remove-HgsGuardian -Name 'TempRecoveryGuardian' Write-host "$(Get-Date): Granted VMRE with access to the shielded VM." Write-host "$(Get-Date): Next step: copy the new KP to the recovery VM." } ################################################ ##### #4 Clean up after recovery in VMRE ##### ################################################ function Export-RecoveredVMtoFabric { <# .SYNOPSIS Run this script in the recovery VM to export the Troubled VM and clean up. .DESCRIPTION The script will add the shielding back to the VM you have troubleshoot, and export it. The VM which can then be added back to the Fabric. .PARAMETER TroubledVMName The name of the VM which was problematic and recovered. .PARAMETER CertPath The script will delete the cert files created during the troubleshooting process. #> [CmdletBinding()] Param( [Parameter (ValueFromPipeline=$true, Position=0,Mandatory=$true)][string] $TroubledVMName, [Parameter (ValueFromPipeline=$true, Position=1,Mandatory=$true)][string] $CertPath ) $VerbosePreference = "Continue" #turn off the VM If ( (Get-VM -VMName $TroubledVMName).State -ne 'Off') { Stop-VM -VMName $TroubledVMName } if (Get-HgsGuardian -name TempRecoveryGuardian) { #turn on shielding Write-Verbose "Changing security policy to Sheidling on $TroubledVMName" Set-VMSecurityPolicy -VMName $TroubledVMName -Shielded $true #delete temp guardian Write-Verbose "Clean up temp guardians" Remove-HgsGuardian -name 'TempRecoveryGuardian' } else { Write-host "VM is not shielded, don't change it's policy" } #remove the disks Get-VMHardDiskDrive -VMName $TroubledVMName | Remove-VMHardDiskDrive Write-Verbose "Remove the $TroubledVMName" Remove-VM -Name $TroubledVMName -Force #delete the files $files = $CertPath + '\*' Write-Verbose "Delete the files $files" Remove-Item -Path $files Write-Host "$(Get-Date): Recovery is completed, Shielded VM has exported to $exportpath." Write-Host "$(Get-Date): Next step: Fabric admin can import the shielded VM on the Fabric." } ################################################ ##### #5 Clean up after recovery on hosts ##### ################################################ function Import-RecoveredVMtoFabric { <# .SYNOPSIS This script runs by the fabric admin, to copy out the exported VM, and remove the data VHD . .DESCRIPTION Before running this script, the tenant admin has fixed the troubled VM, and exported to the data VHD in the recovery VM. This script will import the shielded VM back on the host, and clean up. .PARAMETER TroubledVMName The name of the problematic VM. .PARAMETER VMREVMName The name of the recovery VM, a.k.a VMRE .PARAMETER RepairGarageDataVHD The VHDx file which was created to export the troubled VM. #> [CmdletBinding()] Param( [Parameter (ValueFromPipeline=$true, Position=0,Mandatory=$true)][string] $TroubledVMName, [Parameter (ValueFromPipeline=$true, Position=1,Mandatory=$true)][string] $VMREVMName, [Parameter (ValueFromPipeline=$true, Position=2,Mandatory=$true)][string] $RepairGarageDataVHD ) # For demoing simply uncomment the following line - this way all verbose output will be written to the screen: $VerbosePreference = "Continue" #check for input $VMRE = Get-VM -VMName $VMREVMName -ErrorAction Stop If (!(Test-Path $RepairGarageDataVHD )) { Write-Host "$(Get-Date): $RepairGarageDataVHD doesn't exist." return } if ( $VMRE.State -ne 'Off') { Write-Verbose "Shutdown $VMREVMName " Stop-VM -VMName $VMREVMName } Write-Verbose "Detach the data VHDX from $VMREVMName" Get-VMHardDiskDrive -VMName $VMREVMName | Remove-VMHardDiskDrive -Passthru -ErrorAction SilentlyContinue Write-Verbose "Mounting recovery data vhdx" $dataDrive = Mount-VHD -Path $RepairGarageDataVHD -Passthru | Get-Disk | Get-Partition | Get-Volume if (!($dataDrive)) { Write-Host "$(Get-Date): Unable to mount the $RepairGarageDataVHD, please check if it has been mounted, and eject it before trying again." return } Write-Verbose "Get the disk mapping to the $TroubledVMName" $diskmapping = "$($dataDrive.DriveLetter):\diskmapping.xml" Write-Verbose "Check for diskmap file access" If (!(Test-Path -Path $diskmapping )) { Write-Host "$(Get-Date): $diskmapping doesn't exist." return } $disks = Import-Clixml -Path $diskmapping -ErrorAction Stop Write-Verbose "Add the disks to the $TroubledVMName" foreach ($d in $disks) { Write-Host "Setting VHD $d" Add-VMHardDiskDrive -VMName $TroubledVMName -ControllerType $d.ControllerType -ControllerNumber $d.ControllerNumber -ControllerLocation $d.ControllerLocation -Path $d.Path } Write-Verbose "dismount recovery data vhdx from host" Dismount-VHD -path $RepairGarageDataVHD Write-Verbose "Delete the $RepairGarageDataVHD" Remove-Item $RepairGarageDataVHD Write-Host "$(Get-Date): Troubleshooting completed!" } |