VM-Speicherverwaltung auf den CSVs im Hyper-V-Cluster

Nicht immer und überall gibt es den Luxus des System Center Virtual Machine Managers, sodass ich hier einmal ein kleines PowerShell Script geschrieben habe, welches sich der VM-Speicherverwaltung im Hyper-V-Cluster widmet.

$VMs = @()

function GetCluster{
    if($([string]$(Get-Service | ? Name -eq "ClusSvc").Status).ToLower() -eq "running"){
        $Cluster = Get-Cluster
        $Cluster = $($Cluster.Name + "." + $Cluster.Domain)
    } else{
        do{
            $Cluster = Read-Host "Bitte den Namen des Clusters eingeben"
            $Cluster = Get-Cluster -Name $Cluster -ErrorAction SilentlyContinue
            if($Cluster){
                $Cluster = $($Cluster.Name + "." + $Cluster.Domain)
            }
        }until($cluster)
    }
    return $Cluster
}

function GetSANPath{
    # SAN Luns auflisten:
    do{
        $SAN = @()
        $CSVS = Get-ClusterSharedVolume -Cluster $Cluster | ? State -eq "Online"
        foreach ($CSV in $CSVS ) {  
            $CSVInfos = $CSV | select -Property Name -ExpandProperty SharedVolumeInfo  
            foreach ($CSVInfo in $CSVInfos ) {  
                $Temp_CSV = New-Object PSObject -Property @{  
                    Name = $CSV.Name
                    Size = $([math]::round((($CSVInfo.Partition.Size) / 1GB), 2))
                    FreeSpace = $([math]::round((($CSVInfo.Partition.FreeSpace) / 1GB), 2))
                    UsedSpace = $([math]::round((($CSVInfo.Partition.UsedSpace) / 1GB), 2))
                    PercentFree = $CSVInfo.Partition.PercentFree
                    Path = $CSVInfo.FriendlyVolumeName
                }  
 
                $SAN += $temp_csv
            }  
        }
        $CSV = $SAN | Sort-Object FreeSpace | Out-GridView -Title "Bitte das Cluster Shared Volume wählen!" -PassThru
        $CSV = $CSV.Path

        if(!($CSV)){
            do{
                $Cancel = Read-Host "Vorgang wirklich abbrechen? (j/n)"
            }until($Cancel.ToLower() -eq "j" -or $Cancel.ToLower() -eq "n")
        }
        if($Cancel.ToLower() -eq "j"){
            $CSV = "Abbruch"
        }

    }until($CSV)
    return $CSV
}

$Cluster = GetCluster

do{
    Write-Host "Bitte das Quell-ClusterSharedVolume wählen"

    do{
        $CSVSource = GetSANPath
    }until($CSVSource)

    if($CSVSource -ne "Abbruch"){
        Write-Host "Bitte das Quell-ClusterSharedVolume wählen -> Ok!"

        Write-Host "Alle S- und E-VMs im Cluster ($Cluster) werden ausgelesen"
        $AllVMs = Get-VM -ComputerName $Cluster | where { $_.Name.ToLower().StartsWith("s") -or $_.Name.ToLower().StartsWith("e") } | Sort-Object VMName
        Write-Host "Alle S- und E-VMs im Cluster ($Cluster) werden ausgelesen -> Ok!"

        Write-Host "Es wird geprüft welche VMs auf dem Quell CSV ($CSVSource) liegen"
        foreach($VM in $AllVMs){
            $TotalFileSize = 0
            foreach($VHD in $(Get-VHD -ComputerName $VM.ComputerName -VMId $VM.VMId | where { $_.Path.ToLower().StartsWith($CSVSource.ToLower()) })){
                    $TotalFileSize += $VHD.FileSize
                    $VHDPath = $VHD.Path
            }
            if($TotalFileSize -gt 0){
                if(!($VMs.VMName -contains $VM.VMName)){
                    $temp_VM = New-Object PSObject -Property @{
                        VMName = $VM.VMName.ToUpper()
                        VHDPath = $($CSVSource + "\VM\" + $VM.VMName.ToUpper())
                        TotalSize = $([math]::round($TotalFileSize/1GB, 2))
                        Host = $VM.ComputerName
                    }
                $VMs += $temp_VM
                }
            }
        }
        Write-Host "Es wird geprüft welche VMs auf dem Quell CSV ($CSVSource) liegen -> Ok!"

        Write-Host "Auswahl der zu verschiebenden VM"
        do{
            $VMtoMove = $VMs | Select-Object VMName, TotalSize, Host, VHDPath | Sort-Object TotalSize -Descending | Out-GridView -Title "Welche VM soll verschoben werden?" -PassThru
            if(!($VMtoMove)){
                do{
                    $Cancel = Read-Host "Vorgang wirklich abbrechen? (j/n)"
                }until($Cancel.ToLower() -eq "j" -or $Cancel.ToLower() -eq "n")
            $VMtoMove = "Abbruch"
            }
        }until($VMtoMove)
        if($VMtoMove -ne "Abbruch"){
            Write-Host "Auswahl der zu verschiebenden VM -> Ok!"

            Write-Host "Bitte das Ziel-ClusterSharedVolume wählen"
            do{
                $CSVDestination = GetSANPath
            }until($CSVDestination)
            if($CSVDestination -ne "Abbruch"){
                Write-Host "Bitte das Ziel-ClusterSharedVolume wählen -> Ok!"

                Write-Host "Verschiebe VM ($($VMtoMove.VMName)) von $CSVSource auf $CSVDestination"
                Move-VMStorage -ComputerName $VMtoMove.Host -Name $VMtoMove.VMName -DestinationStoragePath $($CSVDestination + "\VM\" + $VMtoMove.VMName)
                Write-Host "Verschiebe VM ($($VMtoMove.VMName)) von $CSVSource auf $CSVDestination -> Ok!"

                $CSVSourceUNC = $("\\" + $VMtoMove.Host + "\" + $CSVSource.Replace(":","$"))

                Write-Host "Bereinige $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName)"
                foreach($Folder in $(Get-ChildItem -Path $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName) -Recurse)){
                    if(!(Test-Path -Path $($Folder.FullName + "\*"))){
                        Remove-Item -Path $Folder.FullName
                    } else{
                        Write-Host "Bitte $($Folder.FullName) prüfen!" -ForegroundColor Red
                    }
                }
                if(!(Test-Path -Path $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName + "\*"))){
                    Remove-Item -Path $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName)
                    Write-Host "Bereinige $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName) -> Ok!"
                } else{
                    Write-Host "Bitte $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName) manuell prüfen!" -ForegroundColor Red
                    Start-Process -FilePath $($env:windir + "\explorer.exe") -ArgumentList $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName)
                    Write-Host "Bereinige $($CSVSourceUNC + "\VM\" + $VMtoMove.VMName) -> Fehler!" -ForegroundColor Red
                }
            } else{
                Write-Host "Bitte das Ziel-ClusterSharedVolume wählen -> Abbruch!" -ForegroundColor Red
                Write-Host "Vorgang wurde abgebrochen!"
            }
        } else{
            Write-Host "Auswahl der zu verschiebenden VM -> Abbruch!" -ForegroundColor Red
            Write-Host "Vorgang wurde abgebrochen!"
        }
    } else{
        Write-Host "Bitte das Quell-ClusterSharedVolume wählen -> Abbruch!" -ForegroundColor Red
        Write-Host "Vorgang wurde abgebrochen!"
    }
    do{
        $Cancel = Read-Host "Weiteres CluserSharedVolume verwalten? (j/n)"
    }until($Cancel.ToLower() -eq "j" -or $Cancel.ToLower() -eq "n")
}until($Cancel.ToLower() -eq "n")

Wie so häufig, die Frage, was macht das Script denn eigentlich?

  1. Die Funktion „GetCluster“ prüft, ob es den Cluster-Dienst (CluSvc) auf dem aktuellen Server / PC gibt. Ist das der Fall, wird der Cluster ausgelesen. Sind wir auf einer Management-Workstation, so muss der Clusternamen eingegeben werden.
  2. Die Erläuterungen zur Funktion „GetSANPath“ findet sich hier.
  3. GetCluster“ wird aufgerufen, um den Cluster zu ermitteln
  4. Die diversen Schleifen dienen als Abbruch- bzw. Neu-Ausführbedingungen. Nach dem Script ist schließlich vor dem Script! 😉
  5. Es wird mittels „GetSANPath“ das Quell-CSV ausgelesen.
  6. In meinem Fall interessieren mich nur File- und Exchange-Server. Diese Server fangen bei uns mit „S“ oder „E“ an weshalb ich alle VMs filtere, die eben nicht mit „S“ oder „E“ anfangen.
  7. Sobald die VMs ausgelesen sind, schaue ich mir mit „Get-VHD“ die virtuellen Harddisks der VMs auf dem Quell-CSV an und erstelle ein Objekt mit allen VMs auf dem CSV, dem Pfad zum CSV, der Größe aller VHDs sowie dem Host, wo die virtuelle Maschine derzeit läuft.
  8. Aus dem in „7“ erstellten Objekt wird als nächstes in einem „GridView“ die zu verschiebende VM gewählt.
  9. Jetzt wird das Ziel-CSV (CSVDestination) mittels der „GetSANPath“ Funktion ermittelt.
  10. *Trommelwirbel* Jetzt kommt der eigentliche Vorgang, der die gesamte VM auf das Ziel Cluster Shared Volume mittels „Move-VMStorage“ verschiebt. 🙂
  11. Leider wird nach einem erfolgreichen Verschiebevorgang nicht aufgeräumt. Dies erfolgt mit den diversen „Test-Path“ und „Remove-Item“ Commandlets. (Der Aufräumvorgang erfolgt per UNC-Pfad, da das Script bei mir auf einer Management-Workstation läuft, die keinen Zugriff auf die Cluster Shared Volumes unter „C:\ClusterSharedVolumes“ hat.)
  12. Sollte es beim Aufräumen und somit auch beim Verschieben einen Fehler gegeben haben, öffnet das Script die Quelle im Windows Explorer und es sollte manuell geprüft werden, was dort schiefgelaufen ist.
  13. Die „VM-Speicherverwaltung“ ist fertig – Yay!

Den einzigen Hyper-V Host in die Domäne aufnehmen?

Mythbusters Hyper-V Edition: Einen einzelnen Hyper-V Host in die Domäne aufnehmen oder lieber nicht?

Wieso und warum? In der Regel betreibt man eine Windows Domäne ja nicht aus Spaß an der Freude.* Wenn sie also da ist, sollte man sie auch nutzen. 😉 Ebenso vereinfacht sich dadurch, insbesondere im Fall einer Server Core Installation, das (Remote) Management des Hyper-V Hosts.
* Das ist in meinen Augen allerdings auch ein Punkt, warum ich grundsätzlich, auch in kleinen Umgebungen, mindestens zwei (2) Domänen Controller empfehle. Hier tut es schon ein kleiner Mini- / Microserver der bekannten Hersteller oder gar ein Intel NUC.

Kommen wir zum Mythos bzw. den Mythen:

  • Henne-Ei-Problem„: Der Hyper-V Host kann den DC / VMs nicht starten ohne die Domäne zu erreichen
  • Eine Anmeldung am Hyper-V ist ohne Verfügbarkeit der Windows Domäne nicht möglich
  • Bei Active Directory Problemen komme ich nicht an meine Umgebung

Hier verweise ich einfach auf die Anmerkung zu mindestens zwei (2) Domain Controller in einer Active Directory Domäne im Abschnitt „Wieso und warum?“. Thema erledigt! – Nein, so einfach mache ich mir das jetzt nicht, da auch in unserer Kundschaft „sparsame“ Kunden unterwegs sind.

Das wichtigste in der Umgebung mit nur einem Host, der zugleich den einzigen DC hosted, ist die automatische Start-Aktion eben dieser einzelnen virtuellen Domain Controller VM. Entgegen der ein oder anderen Erwartung kann der domain-joined Hyper-V Host VMs, und somit auch den einzigen DC, starten ohne das die Domäne erreichbar ist.
Anmerkung: Auch wenn es mehrere Hosts oder ein Hyper-V Cluster gibt, ist es dennoch immer eine gute Idee, die wichtigen virtuellen Maschinen für „Infrastrukturdienste“ immer automatisch starten zu lassen :).

Hyper-V virtuellen Domain Controller immer automatisch starten
Hyper-V virtuellen Domain Controller immer automatisch starten

An dieser Stelle lasse ich den automatischen Start des Domain Controllers absichtlich aus und nehme den Hyper-V Host in meine Domäne auf. Folgerichtig kann ich mich nicht an der Domäne anmelden. Also ist das ganze hier doch kein Mythos? – Nein! Ich melde mich dann einfach mit einem lokalen Konto an welches mindestens in der Gruppe der „Hyper-V-Administratoren“ ist. Sogesehen vermutlich mit dem Konto mit dem ich den Hyper-V bis eben grade noch in der Workgroup administriert habe.

Lokaler User in der Hyper-V-Administratoren Gruppe
Lokaler User in der Hyper-V-Administratoren Gruppe

Nach dem Reboot stelle ich mit Erschrecken fest, ich habe doch glatt vergessen den einzigen Active Directory Domänen Controller automatisch starten zu lassen!

Zum Glück kenne ich die lokalen Anmeldedaten der Konten „hvadmin“ und „localAdmin“ mit denen ich mich anmelde, die VM-Konfiguration anpasse sowie den Start des DCs ausführe. Sobald der DC gebooted ist kann ich mich auch mit einem passenden User aus der Domäne anmelden. Da „GUI“ unendlich langweilig ist, hier die PowerShell Befehle:

Set-VM <VMName des DCs> -AutomaticStartAction Start -AutomaticStartDelay 5
Start-VM jans-dc01

Behandeln wir jetzt („nochmals“) den Fall der Fälle, dass es Probleme mit der VM des DCs oder der AD-Domäne gibt. Hier gibt es zwei nicht so spannende Möglichkeiten:

  1. Ich melde mich, wie oben bereits beschrieben, mit einem lokalen Konto an und troubleshoote die VM / das Active Directory
  2. Ich melde mich mit meinem Domänen User an, obwohl die Domäne offline ist. Dafuq? Ja – Dank „Cached Credentials“. Kurz: Per Default speichert ein Windows Server / Windows Client zehn Anmeldeinformationen zwischen mit denen auch eine Anmeldung ermöglich wird, wenn eben kein DC erreichbar ist. Weitere Informationen zu „Cached Credentials“ sowie Best Practice gibts bei Microsoft -> https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/interactive-logon-number-of-previous-logons-to-cache-in-case-domain-controller-is-not-available

Hyper-V GetSANPath Funktion

Eine kleine Funktion „GetSANPath“, um die Cluster Shared Volumes inkl. ein paar Infos in einem Grid View auszugeben und auszuwählen.

Die PowerShell Funktion GetSANPath liest alle gemounteten Cluster Shared Volumes aus und holt sich Informationen zu Größe und freiem sowie genutztem Speicherplatz. Die Informationen werden dann ein wenig sortiert und in einem Grid View ausgegeben. Das ausgewählte CSV wird anschließend in eine Variable geschrieben, auf die gewünschten Infos „Name“ und „Pfad“ gekürzt und zurückgegeben.

Den größten bzw. wichtigsten Teil des Scriptes habe ich im Script Center des Technets gefunden: https://gallery.technet.microsoft.com/scriptcenter/Monitor-Cluster-Shared-21de7554

function GetSANPath {
    $Output = @()
    $csvs = Get-ClusterSharedVolume
    foreach ( $csv in $csvs ) {
        $csvinfos = $csv | select -Property Name -ExpandProperty SharedVolumeInfo
        foreach ( $csvinfo in $csvinfos ) {
            $temp_csv = New-Object PSObject -Property @{
                Name = $csv.Name
                Size = $([math]::round((($csvinfo.Partition.Size) / 1GB), 2))
                FreeSpace = $([math]::round((($csvinfo.Partition.FreeSpace) / 1GB), 2))
                UsedSpace = $([math]::round((($csvinfo.Partition.UsedSpace) / 1GB), 2))
                PercentFree = $csvinfo.Partition.PercentFree
                Path = $csvinfo.FriendlyVolumeName
            }  
 
            $Output += $temp_csv
        }
    }
do {
    $SAN = $Output | Select-Object Name, @{l="Größe in GB"; e={$_.Size}}, @{l="Freier Speicher in GB"; e={$_.FreeSpace}}, @{l="Benutzter Speicher in GB"; e={$_.UsedSpace}}, @{l="Freier Speicher in %"; e={$_.PercentFree}}, @{l="Pfad"; e={$_.Path}} | Out-GridView -PassThru
} until ($SAN)
 
$SAN = @{
            Name = $SAN.Name
            Path = $SAN.Pfad
        }
 
return $SAN
}