Las opiniones reflejadas en este Blog son personales. La información se proporciona "como está" sin garantías de ninguna clase, y no otorga ningún derecho.
Hola
En este post vamos a tratar un procedimiento, que deberíamos intentar evitar tener que llevar a cabo en la medida de lo posible, pero que en ocasiones puede llegar a ser el último recurso que nos quede para recuperar el estado de una máquina virtual sin tener que volverla a montar de nuevo a partir de los discos virtuales. En abril de 2008 ya tratamos este tema para la primera versión de Hyper-V, y aunque la esencia sigue siendo la misma, aquel procedimiento ya no resulta válido para ni para Hyper-V 2008 R2 ni para Hyper-V 2008 R2 SP1. Este es el enlace al post original, y ya en los comentarios alguno de vosotros proponíais soluciones para las versiones actuales.
Anatomía de una máquina virtual
A grandes rasgos, una máquina virtual en Hyper-V se compone de:
Problema: la configuración por defecto de Hyper-V tiende a desligar los ficheros de configuración de los discos virtuales almacenándolos en diferentes carpetas, y además lo hace por intrincadas ramas del árbol de directorios de C:\. (C:\ProgramData\Microsoft\Windows\Hyper-V y C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks). A mi personalmente me suele gustar cambiar esto en el Hyper-V Manager para que toda máquina virtual que se cree por defecto en base al asistente lo haga en un punto que esta bajo control, si es posible en otra unidad que no sea la de sistema. Es decir cambiar de:
a:
Si utilizas System Center Virtual Machine Manager, o el proceso de Export/Import que veremos en el próximo post, las máquinas virtuales quedan perfectamente ordenaditas en una cierta carpeta. Algo a lo que deberíamos tender cuando las creamos con el asistente del Hyper-V Managera. Para ello basta con marcar esta casilla, que genera una subcarpeta con el nombre de la máquina virtual dentro del path especificado, y donde luego tendemos también la precaución de colocar los VHDs:
Para cerrar este capítulo, y aunque no venga mucho al caso, este asistente gasta otra pequeña broma, que a la larga seguramente pueda llegar a causar problemas. Los VHDs que se crean por defecto son dinámicos y de 127 Gb. Cuando nos damos cuenta de que los discos dinámicos penalizan el rendimiento y lo queremos pasar a fijo, se nos solicitan 127 GB de espacio libre en el datastore donde resida la máquina.
Enlaces simbólicos y permisos
Cuando se genera una máquina virtual, o cuando se llevan a cabo snapshots, sucede lo siguiente:
Todo esto es fácilmente comprobable en cualquier máquina virtual que tengáis creada sobre Hyper-V. El Hyper-V Manager, o mejor dicho el servicio de gestión de máquinas virtuales que corre en la partición padre, solo considera máquinas las máquinas virtuales que tengan dichos enlaces simbólicos creados y los ficheros sobre los cuales tenga permisos. Si algo de esto se rompe, la VM desaparece de la consola, o no puede arrancar al intentar acceder a algún rincón de la configuración.
NOTA: Para probar esto y lo que viene a continuación, obviamente sobre una máquina virtual de prueba, no hay que borrarla del Hyper-V Manager, ya que esto eliminaría los ficheros de configuración. Bastará con borrar los hardlinks, y si se quiere probar del todo eliminar de los permisos el SID que quedará huérfano.
Recuperación de una máquina virtual que nunca fue exportada
Nos encontraremos en esta situación cuando hayamos perdido el host por alguna clase de fatalidad, o cuando nos encontremos ante la necesidad de usar una máquina virtual cuyos ficheros tenemos en su totalidad disponibles en el almacenamiento (p.e, un amigo te pasa un USB lleno de máquinas virtuales que tiene en uso en su entrono y que por tanto no se ha preocupado en exportar). En un escenario de Disaster Recovery habremos perdido todos los hosts pero contaremos con una réplica del almacenamiento. Esto debería de estar planificado de otra manera (existen los clusteres, las copias de seguridad…) , pero como último recurso, y antes de ponernos a crear todas las máquinas virtuales desde cero y reconectarlas a sus discos, podemos probar algo así. El script que proponemos a continuación asume que los paths donde se almacenan las máquinas virtuales no cambian. De ser así, tendríamos que, manual o automatizadamente, cambiarlos el contenido de los ficheros XML previamente al uso del script, o modificarlo para que nos vaya preguntando el nuevo path:
# RegisterVM.ps1 - Registers a Hyper-V’s VM that was not previously exported # Usage: ./RegisterVM.ps1 <path to VM's xml file> # Example: ./RegisterVM.ps1 "D:\VirtualMachines\test\Virtual Machines\D73AF4BF-3F52-4DE0-85BA-ADB457E31C37.xml" param([parameter(Mandatory=$TRUE,ValueFromPipeline=$TRUE)] $VMXMLPath) # Adding MKLINK function as CreateSymbolicLink $signature = @" using System; using System.Runtime.InteropServices; namespace System { public class MkLink { [DllImport("kernel32.dll")] public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); } } "@ Add-Type -TypeDefinition $Signature # Hard-coded default Virtual Machines and Snapshots paths in Hyper-V Host $VMsLNKS = "C:\ProgramData\Microsoft\Windows\Hyper-V\Virtual Machines" $SnapshotsLNKS = "C:\ProgramData\Microsoft\Windows\Hyper-V\Snapshots" # Read VM's configuration file Trap [System.Management.Automation.ErrorRecord] {write-host("File not found, or it is invalid") -Foregroundcolor Yellow; Break} [xml]$VMCONFIG = get-content $VMXMLPath -EA Stop # Get VM's XML file name and Service ID from VM's configuration file name. If you are using a localized version of Windows you must translate this string $VMFile = [io.path]::GetFileName($VMXMLPath) $VMPath = [io.path]::GetDirectoryName($VMXMLPath) $ServiceID = "NT VIRTUAL MACHINE\"+$VMFile.Replace(".xml", "") # Get VM's Name. If the attribute does not exist, we assume this is not a VM's XML Configuration file if ($vmconfig.configuration.properties.name -eq "properties"){ write-host("The file does not look like a VM's XML file") -Foregroundcolor Yellow Throw ("Now exiting") } $VMname = $vmconfig.Configuration.properties.name.Get_InnerText() # Read available VM's storage $Disks = $vmconfig.configuration.GetElementsByTagName("pathname") # Create VM's Hard Link and set ACLs write-host ("Rebuilding Hardlink") -Foregroundcolor Yellow $hardlink=[system.MkLink]::CreateSymbolicLink($VMsLNKS+"\"+$VMFile,$VMXMLPath,0) IF (!$hardlink){ write-host("Failed to create the hard link") -Foregroundcolor Yellow } # Wait 10 seconds until VMMS Service creates de NT VIRTUALMACHINE\VM ID user. write-host ("Waiting for Service ID") -Foregroundcolor Yellow Start-Sleep 10 write-host ("Securing Hard Link and VM's State files") -Foregroundcolor Yellow icacls $VMsLNKS"\"$VMFile /grant $ServiceID":(F)" /L icacls $VMPath /grant $ServiceID":(F)" /T # Check whether Snapshots exists or not, and read them. Create their Hard Links and set ACLs Trap [System.Management.Automation.RuntimeException] {write-host("Snapshots directory is not set in VM's XML configuration file") -Foregroundcolor Yellow ; Continue} If ($vmconfig.configuration.global_settings.snapshots.list.size.Get_InnerText() -ne "0"){ write-host ("Rebuilding Snapshots") -Foregroundcolor Yellow $Snapshotspath = $vmconfig.configuration.global_settings.snapshots.data_root.Get_InnerText() $Snapshots = $vmconfig.configuration.GetElementsByTagName("guid") foreach ($_ in $snapshots) { if ($_.Get_InnerText()){ $snap = $_.Get_InnerText()+".xml" [system.MkLink]::CreateSymbolicLink($SnapshotsLNKS+"\"+$snap,$Snapshotspath+"\Snapshots\"+$_.Get_InnerText()+".xml",0) icacls $SnapshotsLNKS"\"$snap /grant $ServiceID":(F)" /L icalcs $Snapshotspath /grant $ServiceID":(F)" /T } } } # Set ACLs for every VHD (not pass-through Disks) write-host ("Rebuilding VHD Permissions") -Foregroundcolor Yellow foreach ($_ in $Disks) { if ($_.Get_InnerText().ToLower().Contains("vhd")){ $Disk = $_.Get_InnerText() icacls $disk /grant $ServiceID":(F)" } } write-host ("Done: Check Hyper-V Manager for the virtual machine") -Foregroundcolor Yellow
# RegisterVM.ps1 - Registers a Hyper-V’s VM that was not previously exported # Usage: ./RegisterVM.ps1 <path to VM's xml file> # Example: ./RegisterVM.ps1 "D:\VirtualMachines\test\Virtual Machines\D73AF4BF-3F52-4DE0-85BA-ADB457E31C37.xml"
param([parameter(Mandatory=$TRUE,ValueFromPipeline=$TRUE)] $VMXMLPath)
# Adding MKLINK function as CreateSymbolicLink
$signature = @" using System; using System.Runtime.InteropServices;
namespace System { public class MkLink { [DllImport("kernel32.dll")] public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags); } } "@
Add-Type -TypeDefinition $Signature
# Hard-coded default Virtual Machines and Snapshots paths in Hyper-V Host
$VMsLNKS = "C:\ProgramData\Microsoft\Windows\Hyper-V\Virtual Machines" $SnapshotsLNKS = "C:\ProgramData\Microsoft\Windows\Hyper-V\Snapshots"
# Read VM's configuration file
Trap [System.Management.Automation.ErrorRecord] {write-host("File not found, or it is invalid") -Foregroundcolor Yellow; Break} [xml]$VMCONFIG = get-content $VMXMLPath -EA Stop
# Get VM's XML file name and Service ID from VM's configuration file name. If you are using a localized version of Windows you must translate this string
$VMFile = [io.path]::GetFileName($VMXMLPath) $VMPath = [io.path]::GetDirectoryName($VMXMLPath) $ServiceID = "NT VIRTUAL MACHINE\"+$VMFile.Replace(".xml", "")
# Get VM's Name. If the attribute does not exist, we assume this is not a VM's XML Configuration file
if ($vmconfig.configuration.properties.name -eq "properties"){ write-host("The file does not look like a VM's XML file") -Foregroundcolor Yellow Throw ("Now exiting") }
$VMname = $vmconfig.Configuration.properties.name.Get_InnerText()
# Read available VM's storage
$Disks = $vmconfig.configuration.GetElementsByTagName("pathname")
# Create VM's Hard Link and set ACLs
write-host ("Rebuilding Hardlink") -Foregroundcolor Yellow
$hardlink=[system.MkLink]::CreateSymbolicLink($VMsLNKS+"\"+$VMFile,$VMXMLPath,0) IF (!$hardlink){ write-host("Failed to create the hard link") -Foregroundcolor Yellow }
# Wait 10 seconds until VMMS Service creates de NT VIRTUALMACHINE\VM ID user.
write-host ("Waiting for Service ID") -Foregroundcolor Yellow Start-Sleep 10
write-host ("Securing Hard Link and VM's State files") -Foregroundcolor Yellow icacls $VMsLNKS"\"$VMFile /grant $ServiceID":(F)" /L icacls $VMPath /grant $ServiceID":(F)" /T
# Check whether Snapshots exists or not, and read them. Create their Hard Links and set ACLs
Trap [System.Management.Automation.RuntimeException] {write-host("Snapshots directory is not set in VM's XML configuration file") -Foregroundcolor Yellow ; Continue} If ($vmconfig.configuration.global_settings.snapshots.list.size.Get_InnerText() -ne "0"){ write-host ("Rebuilding Snapshots") -Foregroundcolor Yellow $Snapshotspath = $vmconfig.configuration.global_settings.snapshots.data_root.Get_InnerText() $Snapshots = $vmconfig.configuration.GetElementsByTagName("guid") foreach ($_ in $snapshots) { if ($_.Get_InnerText()){ $snap = $_.Get_InnerText()+".xml" [system.MkLink]::CreateSymbolicLink($SnapshotsLNKS+"\"+$snap,$Snapshotspath+"\Snapshots\"+$_.Get_InnerText()+".xml",0) icacls $SnapshotsLNKS"\"$snap /grant $ServiceID":(F)" /L icalcs $Snapshotspath /grant $ServiceID":(F)" /T } } }
# Set ACLs for every VHD (not pass-through Disks)
write-host ("Rebuilding VHD Permissions") -Foregroundcolor Yellow
foreach ($_ in $Disks) { if ($_.Get_InnerText().ToLower().Contains("vhd")){ $Disk = $_.Get_InnerText() icacls $disk /grant $ServiceID":(F)" } }
write-host ("Done: Check Hyper-V Manager for the virtual machine") -Foregroundcolor Yellow
El script simplemente necesita como parámetro el path completo al fichero de configuración de la máquina virtual. A partir de aquí:
DISCLAIMER: El presente script solamente ha sido probado por mi, en un entorno de pruebas, y por tanto su correcto funcionamiento en cualquier situación no está garantizado. Aunque he intentado capturar gran parte de las posibles excepciones, estoy seguro de que puede haber situaciones que no estén contempladas. Si llegáis aquí desesperados por haber perdido alguna máquina importante, espero que os ayude a recuperarla o que la información del port os ayude a realizar los pasos manualmente. Si por el contrario os planteáis incluir este script en vuestro maletín de herramientas, por favor, probadlo bien, y enviad cualquier problema o mejora que consideréis pertinente.
Saludos
David Cervigón
Gran articulo David!