image_pdfimage_print

WSUS – Konfiguration für MDT Umgebung

Ich habe mich in letzter Zeit mit MDT (Microsoft Deployment Toolkit) beschäftigt und mir da auch Gedanken zum WSUS gemacht. In meinem Umfeld wird MDT vor allem für die Installation/Wartung von Citrix Workern (Terminalserver und VDI) genutzt. Diese sind sehr oft auch aus dem Internet zugänglich und sollten daher auch auf einem zeitnahen Patch-Stand sein. Nur wie löst man dies am einfachsten?

Weiterlesen

WSUS – Automatisiertes Update Management

So cool WSUS (Windows Server Update Service) auch ist, so hat er sich leider seit dem Erscheinen mit Windows Server 2003 nicht wirklich weiter entwickelt. Im Bereich der Update-Pflege (Freigabe, Ablehnung, etc.) ist mir der WSUS noch immer nicht flexibel genug. Glücklicherweise kann man hier aber mit PowerShell Abhilfe schaffen und dafür habe ich in meinem Lab mehrere Scripte integriert.

Weiterlesen

WSUS – Clients zum Update Report zwingen

Wahrscheinlich kennt jeder Administrator, welcher auch WSUS im Einsatz hat die Thematik, dass die Clients mit den Reports auf sich warten lassen.

Nach ein wenig Recherche bin ich bei div. Blogs auf Scripts gestossen, welche den WU Client dazu bringen, dem WSUS sofort einen entsprechenden Report zu senden. Genanntes Script habe ich für mich so modifiziert, dass es nicht vom WSUS aus mittels „Invoke“ sondern direkt auf dem Client (Desktop OS wie auch Server OS) ausgeführt wird.

Voraussetzung ist natürlich, dass die entsprechenden GPO Einstellungen für die WSUS Kommunikation eingerichtet und funktionell sind. ;-)

Das unten verfügbare Script kann dann mittels geplantem Task als „SYSTEM“ regelmässig ausgeführt werden, damit der WSUS auch stets den aktuellen Patch-Stand seiner Zielcomputer weiss.

Parameter für den geplanten Task:
– Programm: PowerShell.exe
– Parameter: -ExecutionPolicy Bypass <Pfad zum Script>

Viel Spass beim Nachbauen. :-)

Installierte .NET Framework Version prüfen

Disclaimer: Bei diesem Artikel handelt es sich definitiv nicht um eine eigene „Entwicklung“.

Nicht selten setzen Programme eine bestimmte .NET Version voraus. Windows bietet leider keine einfache Möglichkeit um diese herauszufinden. Einzig der definierte Registry Wert gibt uns einen Aufschluss auf die interne Build-Nummer, aber nicht auf die konkrete Version.

Ich bin bei Microsoft selbst auf ein Skript gestossen, welches ich seither auf mit der entsprechenden Build Tabelle nachgeführt habe:

    $dotNet4Builds = @{
        '30319'  = @{ Version = [System.Version]'4.0'                                                     }
        '378389' = @{ Version = [System.Version]'4.5'                                                     }
        '378675' = @{ Version = [System.Version]'4.5.1'   ; Comment = '(8.1/2012R2)'                      }
        '378758' = @{ Version = [System.Version]'4.5.1'   ; Comment = '(8/7 SP1/Vista SP2)'               }
        '379893' = @{ Version = [System.Version]'4.5.2'   ; Comment = '(all Windows OS)'                  }
        '380042' = @{ Version = [System.Version]'4.5'     ; Comment = 'and later with KB3168275 rollup'   }
        '393295' = @{ Version = [System.Version]'4.6'     ; Comment = '(Windows 10)'                      }
        '393297' = @{ Version = [System.Version]'4.6'     ; Comment = '(NON Windows 10)'                  }
        '394254' = @{ Version = [System.Version]'4.6.1'   ; Comment = '(Windows 10)'                      }
        '394271' = @{ Version = [System.Version]'4.6.1'   ; Comment = '(NON Windows 10)'                  }
        '394802' = @{ Version = [System.Version]'4.6.2'   ; Comment = '(Windows 10 Anniversary Update)'   }
        '394806' = @{ Version = [System.Version]'4.6.2'   ; Comment = '(NON Windows 10)'                  }
        '460798' = @{ Version = [System.Version]'4.7'     ; Comment = '(Windows 10 Creators Update)'      }
        '460805' = @{ Version = [System.Version]'4.7'     ; Comment = '(NON Windows 10)'                  }
        '461308' = @{ Version = [System.Version]'4.7.1'   ; Comment = '(Windows 10 Fall Creators Update)' }
        '461310' = @{ Version = [System.Version]'4.7.1'   ; Comment = '(NON Windows 10)'                  }
        '461808' = @{ Version = [System.Version]'4.7.2'   ; Comment = '(Windows 10 / 1803)'               }
        '461814' = @{ Version = [System.Version]'4.7.2'   ; Comment = '(other OS than Windows 10 1803)'   }
        '528040' = @{ Version = [System.Version]'4.8'     ; Comment = '(Windows 10 / 1905 & 1911)'        }
        '528372' = @{ Version = [System.Version]'4.8'     ; Comment = '(Windows 10 / 2005 & 2010 & 2105)' }
        '528449' = @{ Version = [System.Version]'4.8'     ; Comment = '(Windows 11 / Server 2022)'        }
        '528049' = @{ Version = [System.Version]'4.8'     ; Comment = '(other OS or Windows 10 builds)'   }
        '533320' = @{ Version = [System.Version]'4.8.1'   ; Comment = '(Windows 11 / 2022)'               }
        '533325' = @{ Version = [System.Version]'4.8.1'   ; Comment = '(other OS or Windows 10 builds)'   }
    }

Die Ausführung sieht dann wie folgt aus:

Ich denke mit diesen Informationen kann man mehr anfangen als den reinen Build Nummern. ;-)

Viel Spass beim Nachbauen :-)

Installierte Windows Sprachpakete prüfen

In unseren Terminalservern und VDIs wollten wir mittels Scripten die Benutzer die OS Sprache ändern lassen. Die Aufbereitung des Images mittels Citrix AppLayering hatte so seine Tücken und für eine einfache Prüfung, ob und welche Sprachen zur Auswahl stehen, habe ich folgendes kleines Skript erstellt.

Es liest über WMI die OS Parameter aus und listet diese im Anschluss in der PowerShell Ausgabe:

$OSInfo = Get-WmiObject -Class Win32_OperatingSystem
$LanguagePacks = $OSInfo.MUILanguages
$LanguagePacks

Die Ausgabe sieht dann wie folgt aus:

Viel Spass beim Nachbauen :-)

Citrix PVS Store – Replikationsskript V2

Im kürzlich erschienene Artikel habe ich die erste Fassung des Replikationsskriptes vorgestellt. In der aktuell überarbeiteten Version habe ich einerseits die Ausgaben ein wenig „verschönert“ und vor allem werden die Stores nun direkt aus dem PVS ausgelesen.

Voraussetzungen dafür sind:

  • Citrix PVS PowerShell Module an den Standardpfaden (kann in einer Variable angepasst werden)
  • Der ausführende Benutzer muss entsprechende PVS Rechte besitzen
  • Die Stores sollten verständliche Namen (Auswahl) und Beschreibungen (Hilfetexte bei Auswahl) vorweisen.

Zuerst wird nun also der Store Array dynamisch aus dem PVS ausgelesen. Voraussetzung dazu ist ein Laden des entsprechenden PVS PowerShell Moduls:

# Script variables
$PVSModulePath = "C:\Program Files\Citrix\Provisioning Services Console\Citrix.PVS.SnapIn.dll"

# Load PVS PowerShell module
Import-Module $PVSModulePath
# Get PVS vDisk stores from PVS configuration
# Create an array for later use
$Stores = Get-PvsStore

$StoreArray = $null
$StoreArray = @()
$StoreID = 0
ForEach ($Store in $Stores){
    $StoreID = $StoreID +1
    $SplitChar = $Store.Path.IndexOf(":")
    $StoreDisk = $Store.Path.Substring(0,$SplitChar)
    $StorePath = $Store.Path.Substring($SplitChar+1)
    $StoreName = $Store.StoreName
    $StoreArray += [pscustomobject]@{StoreID=$StoreID;Store=$StoreName;StoreDisk=$StoreDisk;StorePath=$StorePath;StoreDescription=$Store.Description}
}

Nun wird aus dem Array eine entsprechende PowerShell Auswahl generiert, wobei hier aktuell auch eine Limite von maximal 5 Stores eingebaut ist:

# Prompt for Store choice
$StoreTitle = "PVS vDisk stores"
$StoreMessage = "Chose the store to replicate:"
$CancelAll = New-Object System.Management.Automation.Host.ChoiceDescription "&Cancel", "Skip this operation and all subsequent operations."

# Generate choice output of each vDisk store to replicate
$StoreOptionCount = 0
ForEach ($Store2 in $StoreArray) {
    $StoreOptionCount = $StoreOptionCount + 1
    $ChoiceID = $Store2.StoreID
    $ChoiceStore = $Store2.Store
    $ChoiceHelp = $Store2.StoreDescription
    $ChoiceCmd = New-Object System.Management.Automation.Host.ChoiceDescription "&$ChoiceID $ChoiceStore", $ChoiceHelp
    New-Variable "StoreOption$ChoiceID" $ChoiceCmd
}

# Generate $StoreOptions variable for final choice output depending on the amount of options
Switch ($StoreOptionCount) {
    0 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll)}
    1 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll, $StoreOption1)}
    2 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll, $StoreOption1, $StoreOption2)}
    3 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll, $StoreOption1, $StoreOption2, $StoreOption3)}
    4 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll, $StoreOption1, $StoreOption2, $StoreOption3, $StoreOption4)}
    5 {$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($CancelAll, $StoreOption1, $StoreOption2, $StoreOption3, $StoreOption4, $StoreOption5)}
}

Zu guter Letzt werden anhand der Auswahl die Variablen definiert:

# Prepare variables after the user entry
If ($Store2Replicate -eq 0) {
    # Cancel all
    return; break
}
Else {
    $IDStore = $Store2Replicate-1
    $StoreDisk = $StoreArray[$IDStore].StoreDisk
    $StorePath = $StoreArray[$IDStore].StorePath
    $LocalStore = $StoreDisk + ":" + $StorePath + "\"
    $RemoteStore = "\\" + $PVSRemote + "\" + $StoreDisk + "$" + $StorePath + "\"
    $LocalStorePath = $LocalStore + "*"
    $RemoteStorePath = $RemoteStore + "*"
}

Der restliche Teil ist soweit gleich geblieben und im ersten Artikel bereits beschrieben.

Viel Spass beim Nachbauen :-)

Citrix PVS Store – Replikationsskript

Citrix Provisioning ist seit Jahren eine super Methode um Citrix Worker zu provisionieren. In der Konsole gibt es zwar seit ich denken kann die Möglichkeit den Replikationsstatus zu prüfen, jedoch bietet Citrix da keine eigenen Mechanismen innerhalb des PVS.

Wie viele andere auch haben wir seit Jahren auf einfache Skripte gesetzt, welche die lokalen Verzeichnisse zwischen den Servern kopiert. Mit dem Nachteil, dass bei mehreren Admins es zu grossen Kopierjobs kommen kann, wenn verschiedene vDisks gepflegt werden. Citrix hat dafür schon seit längerem das vDisk Replicator Utility veröffentlicht, welches wir jedoch nicht installieren wollten. Es ist für unsere Zwecke auch ein wenig überdimensioniert.

Ich habe mich dafür einmal hingesetzt und auf Basis unseres einfachen Skriptes ein neues mit Powershell kreiert, welches folgende Anforderungen erfüllen sollte:

  • das Skript sollte evaluieren, welches der lokale und welches der Remote PVS ist
  • das Skript sollte für mehrere Stores nutzbar sein – mit Auswahl
  • das Skript sollte innerhalb des gewählten Stores die möglichen zu kopierenden vDisks als Auswahl bereitstellen

Die erste Anforderung war leicht und schnell implementiert:

# Script variables
$PVS01 = "pvs01.domain.pit"
$PVS02 = "pvs02.domain.pit"

# Enumerate the local and the remote PVS
$PVSLocal = $env:COMPUTERNAME + ".domain.pit"
If ($PVSLocal -eq $PVS01) {
    $PVSRemote = $PVS02
}
Else {
    $PVSRemote = $PVS01
}

Für die Store-Auswahl habe ich mich in dieser ersten Skript-Version mit einem statischen Array begnügt:

# Define PVS vDisk stores
# Create an array for each store and let the user chose which one should be checked
# If a store is added to the array, the option has to be added for the choice as well
$StoreArray = @()
$StoreArray += [pscustomobject]@{Store="VDI";StoreDisk="V";StorePath="\vDisks\VDI"}
$StoreArray += [pscustomobject]@{Store="RDSH";StoreDisk="R";StorePath="\vDisks\RDSH"}
$StoreArray += [pscustomobject]@{Store="Test";StoreDisk="T";StorePath="\vDisks\Test"}

# Prompt for Store choice
$StoreTitle = "PVS Store"
$StoreMessage = "Which PVS store will you replicate"
$Store1 = New-Object System.Management.Automation.Host.ChoiceDescription "&VDI Store", "VDI"
$Store2 = New-Object System.Management.Automation.Host.ChoiceDescription "&RDSH Store", "RDSH"
$Store3 = New-Object System.Management.Automation.Host.ChoiceDescription "&Test Store", "TEST"
$StoreOptions = [System.Management.Automation.Host.ChoiceDescription[]]($Store1, $Store2, $Store3)
$Store2Replicate=$host.ui.PromptForChoice($StoreTitle, $StoreMessage, $StoreOptions, 2)

Anhand der Auswahl werden die nächsten Variablen definiert und das lokale und remote Verzeichnis verglichen, ohne den WriteCache Ordner und allfällige .lok oder .i.vhdx Dateien:

# Prepare variables after the user entry
$StoreID = $Store2Replicate
$StoreDisk = $StoreArray[$StoreID].StoreDisk
$StorePath = $StoreArray[$StoreID].StorePath
$LocalStore = $StoreDisk + ":" + $StorePath + "\"
$RemoteStore = "\\" + $PVSRemote + "\" + $StoreDisk + "$" + $StorePath + "\"
$LocalStorePath = $LocalStore + "*"
$RemoteStorePath = $RemoteStore + "*"

# Compare servers
$LocalStoreContent = Get-ChildItem -Path $LocalStorePath -Exclude WriteCache,*.lok,*.i.vhdx
$RemoteStoreContent = Get-ChildItem -Path $RemoteStorePath -Exclude WriteCache,*.lok,*.i.vhdx
$StoreDiff = Compare-Object -ReferenceObject $LocalStoreContent -DifferenceObject $RemoteStoreContent -Property Name, LastWriteTime

Die gefundenen Differenzen werden verglichen. Uns interessiert bei Ausführung des Skriptes jedoch nur, was auf unserem lokalen Server zum Kopieren ist (SideIndicator „<=“), sowie die eigentlichen vDisks (*.vhd*) ohne die Konfigurationsdateien. Aus diesen Informationen wird das nächste Array erstellt:

# Enumerate a list of vDisks
# Create an array with all disk names
$DiskArray = $null
$DiskArray = @()
$DiskID = 1
ForEach ($File in $StoreDiff) {
    If ($File.SideIndicator -eq "<="){
        $Filename = $File.Name
        If ($Filename -like "*.vhd*") {
            $DiskID = $DiskID+1
            $SplitChar = $Filename.IndexOf(".")
            $DiskName = $Filename.Substring(0, $SplitChar)
            $PVPName = $DiskName + ".pvp"
            $DiskArray += [pscustomobject]@{DiskID=$DiskID;vDisk=$Filename;vDiskName=$DiskName;PVPName=$PVPName}        
        }
    }
}

Aus diesem Array heraus wird dann die nächste Auswahl generiert. Die Auswahl enthält als erstes und auch Standardauswahl die Option alles zu kopieren und die Variante abzubrechen. Aktuell ist vorgesehen, dass maximal fünf vDisks zur Auswahl gestellt werden können für einen einzelnen Kopier-Job:

# Prompt for replication choice
$ReplTitle = "vDisk replication"
$ReplMessage = "Choose what to replicate"
$vDiskAll = New-Object System.Management.Automation.Host.ChoiceDescription "&All vDisks","Continue with all vDisks."
$CancelAll = New-Object System.Management.Automation.Host.ChoiceDescription "&Cancel", "Skip this operation and all subsequent operations."

# Generate choice output of each vDisk to replicate
$ReplOptionCount = 0
ForEach ($vDisk in $DiskArray) {
    $ReplOptionCount = $ReplOptionCount +1
    $ChoiceID = $vDisk.DiskID
    $ChoiceDisk = $vDisk.vDiskName
    $ChoiceCmd = New-Object System.Management.Automation.Host.ChoiceDescription "&$ChoiceID $ChoiceDisk", "Replicate selected vDisk."
    New-Variable "ReplOption$ChoiceID" $ChoiceCmd
}

# Generate $ReplOptions variable for final choice output depending on the amount of options
Switch ($ReplOptionCount) {
    0 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll)}
    1 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll, $ReplOption2)}
    2 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll, $ReplOption2, $ReplOption3)}
    3 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll, $ReplOption2, $ReplOption3, $ReplOption4)}
    4 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll, $ReplOption2, $ReplOption3, $ReplOption4, $ReplOption5)}
    5 {$ReplOptions = [System.Management.Automation.Host.ChoiceDescription[]]($vDiskAll, $CancelAll, $ReplOption2, $ReplOption3, $ReplOption4, $ReplOption5, $ReplOption6)}
}

$vDisk2Replicate=$host.ui.PromptForChoice($ReplTitle, $ReplMessage, $ReplOptions, 0)

Nach der Auswahl werden entweder alle vDisks oder die ausgewählte kopiert, oder das Skript abgebrochen:

If ($vDisk2Replicate -eq 0) {
    # Copy all found vDisks and PVP files
    ForEach ($Disk in $DiskArray) {
        $SourcevDisk = $null
        $SourcePVP = $null
        $DestvDisk = $null
        $DestPVP = $null
        $vDiskFile = $Disk.vDisk
        $PVPFile = $Disk.PVPName

        $SourcevDisk = $LocalStore + $vDiskFile
        $SourcePVP = $LocalStore + $PVPFile
        $DestvDisk = $RemoteStore + $vDiskFile
        $DestPVP = $RemoteStore + $PVPFile

        Copy-Item $SourcevDisk -Destination $DestvDisk
        Copy-Item $SourcePVP -Destination $DestPVP
    }
}
ElseIf ($vDisk2Replicate -eq 1) {
    # Cancel all
    return; break
}
Else {
    # Copy the selected vDisk
    # ID has to be reduced by 2 to match the array IDs
    $SourceID = $vDisk2Replicate - 2
    $SourcevDisk = $null
    $SourcePVP = $null
    $DestvDisk = $null
    $DestPVP = $null
    $vDiskFile = $DiskArray[$SourceID].vDisk
    $PVPFile = $DiskArray[$SourceID].PVPName

    $SourcevDisk = $LocalStore + $vDiskFile
    $SourcePVP = $LocalStore + $PVPFile
    $DestvDisk = $RemoteStore + $vDiskFile
    $DestPVP = $RemoteStore + $PVPFile

    Copy-Item $SourcevDisk -Destination $DestvDisk
    Copy-Item $SourcePVP -Destination $DestPVP
}

Für einen ersten Wurf find ich das Skript nicht schlecht. Sollte ich die Zeit finden, könnte ich mir vorstellen, dass ich die Store-Auswahl auch noch dynamischer gestalte, in dem ich mittels Powershell die PVS Konfiguration abfrage. Ebenfalls fehlt aktuell noch so etwas wie ein Statusbalken. Das Ende des Kopierens erkennt man nur am geschlossenen Skriptfenster.

Viel Spass beim Nachbauen :-)

PRTG – Verfügbare VDIs überwachen

Im letzten Artikel habe ich beschrieben, wir mittels PRTG der Load Index der Terminalserver überwacht werden kann.

Wenn wir uns die VDIs (single user OS), dann interessiert uns da normalerweise die Auslastung weniger. Dafür wollen wir wissen, wie viele VDIs überhaupt noch zur Verfügung stehen im Kontext von „random non-static“.

Im Vergleich zu den Terminalservern ist hier die Erstellung eines Custom Sensors sogar einfacher, da uns der Citrix Controller mit allen notwendigen Informationen versorgt:

###
# Prepare an array and get the information from a Citrix controller by an Invoke-Command
# Citrix PS commands are only available on those servers
$Data = $null
$Data=@()

$Data += Invoke-Command -Computername $server -credential $credentials -ScriptBlock {

    # Load Citrix PS Snapin
    add-pssnapin citrix*

    # Get delivery groups with the needed information
    $DeliveryGroups=Get-BrokerDesktopGroup | Select-Object -Property Name, TotalDesktops, DesktopsAvailable, InMaintenanceMode

    # Set the array for later work
    $DG=@()
    ForEach ($group in $DeliveryGroups){
       $DG += [PSCustomObject]@{Name=$group.Name;Total=$group.TotalDesktops;Available=$group.DesktopsAvailable;Maintenance=$group.InMaintenanceMode} 
    }

    # Return data to PRTG probe
    return $DG
}

Diese Informationen können wir in einer weiteren Schlaufe dann auch gleich für die PRTG Ausgabe benutzen. Das komplette Skript steht zum Download bereit, daher beschreibe ich hier nur nochmals ein paar Eckpunkte.

Aus den Daten können die noch verfügbaren VDIs in Prozent umgerechnet und erneut für PRTG auf die nächste Ganzzahl gerundet werden:

    # Prepare variables based on the array data
    $DGName = $Dataset.Name
    $DGTotal = $Dataset.Total
    $DGAvailable = $Dataset.Available
    # Calculate the percentage of the available VDIs
    $DGPercent = "{0:N0}" -f (100/$DGTotal*$DGAvailable)
    $DGMaintenance = $Dataset.Maintenance

Kommt im Namen der Bereitstellungsgruppe „test“ vor, so wollen wir im PRTG nie einen Alarm sondern höchstens eine Warnung auslösen:

    # Determine if a delivery group is a test group
    # Test DGs will never go into alarm state
    # Test DGs in maintenance will have a percentage value of 100 to avoid an alarm
    If ($DGName -like "*test*"){
        $AlertString = $null
        $AlertLimit = $null
        $WarningString = "Warning - Test VDI capacity reachs the end"
        If ([Int64]$DGPercent -ge 20) {
            $RetState = $returnStateOK
            $returnState += [PSCustomObject]@{Name=$DGName;State=[Int64]$RetState} 
        }
        ElseIf ($DGMaintenance){
            $RetState=$returnStateWarning
            $DGPercent=100
        }
        Else {
            $RetState = $returnStateWarning
            $returnState += [PSCustomObject]@{Name=$DGName;State=[Int64]$RetState} 
        }
    }

Ist eine Bereitstellungsgruppe im Wartungsmodus, so geben wir eine Verfügbarkeit von 100% an PRTG aus, um einen Alarm oder eine Warnung komplett zu vermeiden:

    # Determine if a delivery group is in maintenance
    # DGs in maintenance goes into warning state, but doesn't affect the whole sensor
    # DGs in maintenance will not be stated based on her usage
    # DGs in maintenance will have a percentage value of 100 to avoid an alarm
    ElseIf ($DGMaintenance){
        $RetState=$returnStateWarning
        $DGPercent=100
    }

Die restlichen Bereitstellungsgruppen werden anhand der Prozentwerte eingestuft und entsprechende PRTG Ausgaben aufbereitet.

Die Ausgabe sieht dann folgendermassen aus, wobei die ausgegrauten Channels ehemalige bereits gelöschte Bereitstellungsgruppen sind:

Viel Spass beim Nachbauen :-)

PRTG – Terminalserver Auslastung (Load Index) überwachen

Die Last eines Terminalservers wird in der Welt von Citrix Virtual Apps and Desktops mittels des Load Indexes angegeben. Dieser Wert geht von 0 (0% – keine Last) bis 10’000 (100% – Volllast). Volllast bedeutet ja noch nicht zwingend, dass ein Server keine Luft mehr hat, dennoch kann sich kein neuer Benutzer mehr anmelden.

Da wir die Berechnungskriterien für den Load Index selbst definieren können (z.B. CPU, RAM, Anzahl Benutzer), können wir uns im Monitoring nicht auf einen uns bekannten Wert wie z.B. die Prozessorlast verlassen.

Ich hatte daher die Idee einen Sensor zu erstellen, welcher für jede Bereitstellungsgruppe die durchschnittliche Last anhand des Load Indexes berechnet uns ausgibt.

Der Sensor selbst wird im PRTG innerhalb des Citrix Controllers erstellt und nicht pro Worker.

Im ersten Schritt werden alle Bereitstellungsgruppen vom Controller ausgelesen und in einem Array gespeichert. Es wird hier bereits geprüft, ob eine Bereitstellungsgruppe ggf. im Wartungsmodus ist.

###
# Prepare an array of all delivery groups from a Citrix controller by an Invoke-Command
# Citrix PS commands are only available on those servers
$Data = $null
$Data=@()

$Data += Invoke-Command -Computername $server -credential $credentials -ScriptBlock {

    # Load Citrix PS Snapin
    add-pssnapin citrix*

    # Get delivery groups with the needed information
    $DeliveryGroups=Get-BrokerDesktopGroup | Select-Object Name, InMaintenanceMode

    # Set the array for later work
    $DG=@()
    ForEach ($group in $DeliveryGroups){
       $DG += [PSCustomObject]@{Name=$group.Name;InMaintenance=$group.InMaintenanceMode} 
    }

    # Return data to PRTG probe
    return $DG
}

Im weiteren Schritt wird jede einzelne Bereitstellungsgruppe (DG) abgearbeitet. Ist eine DG im Wartungsmodus, dann wird ein pseudo Load Index von 99/9% zurück gegeben, damit im Monitoring kein Alarm ausgelöst wird.

Jede weitere DG wird nun erneut beim Controller abgefragt. Nun werden jedoch sämtliche Worker mit ihrem aktuellen Load Index Wert in einen Array gespeichert. Danach wird die Summe aller Lastwerte errechnet und durch die Anzahl Worker geteilt (einfache Mathematik ^^) und zu guter Letzt der Prozentwert auf eine Zahl ohne Komma gerundet, da PRTG keine Kommawerte in einem Prozentfeld ausgeben kann.

Diese Daten werden in einen weiteren Array geschrieben, welcher im Anschluss benutzt wird um die PRTG Ausgabe zu erstellen:

###
# Prepare an array of the load of all delivery groups from a Citrix controller by an Invoke-Command
# Citrix PS commands are only available on those servers
$Load = $null
$Load=@()

ForEach ($RDSHGrp in $Data) {

    $RDSHArg = $null
    $RDSHArg = $RDSHGrp.Name

    # Determine if a delivery group is in maintenance
    # DGs in maintenance goes into warning state, but doesn't affect the whole sensor
    # DGs in maintenance will not be stated based on her usage
    # DGs in maintenance will have fake values of 9% and 99 to avoid an alarm
    If ($RDSHGrp.InMaintenance) {
        $RDSHWorkers=99
        $RDSHTotalLoad=99
        $RDSHAvgLoad=99
        $RDSHAvgPercent=9
    }
    Else {
        $RDSHLoad = $null
        $RDSHLoad = @()


        $RDSHScriptBlock =
            {
                 # Load Citrix PS Snapin
                add-pssnapin citrix*

                # Get delivery groups with the needed information
                $RDSHWorker=Get-BrokerMachine | Where-Object DesktopGroupName -eq $args[0] | Select-Object DNSName, LoadIndex

                # Set the array for later work
                $RDSH=@()
                ForEach ($Worker in $RDSHWorker){
                   $RDSH += [PSCustomObject]@{Name=$Worker.DNSName;Load=$Worker.LoadIndex} 
                }

                # Return data to PRTG probe
                return $RDSH
             }

        # Get the load index of all workers in the delivery group
        $RDSHLoad += Invoke-Command -Computername $server -credential $credentials -ScriptBlock $RDSHScriptBlock -ArgumentList $RDSHArg

        # Count the workers
        $RDSHWorkers = $RDSHLoad.count
        $RDSHTotalLoad = 0
        # Calculate the sum of all load indexes
        $RDSHLoad.Load | ForEach{$RDSHTotalLoad += $_}
        # Calculate the average load index for the delivery group
        $RDSHAvgLoad = "{0:F0}" -f ($RDSHTotalLoad/$RDSHWorkers)
        # Calculate the average percent value of the load index for the delivery group
        $RDSHAvgPercent = "{0:F0}" -f ($RDSHAvgLoad/100)

    }
    $Load += [PSCustomObject]@{Name=$RDSHArg;Worker=$RDSHWorkers;TotalLoad=$RDSHTotalLoad;AvgLoad=$RDSHAvgLoad;AvgPercent=$RDSHAvgPercent}
}

Die PRTG Ausgabe selbst ist durch die vorherigen Berechnungen keine Raketenwissenschaft mehr:

###
# Prepare the data and the PRTG XML output

# Reset some variables and define standard values
$returnState=$null
$returnState=@()
$returnStateOK = 0
$returnStateWarning = 1
$returnStateCritical = 2
$AlertLevel=90
$AlertString = "ALERT - high load index"
$WarningLevel=80
$WarningString = "Warning - load index reaches a high level"

# Start preparing XML output
$retXml = "<prtg>`n"

# Get data for each delivery group from the array created on the Citrix Controller
ForEach ($Dataset in $Load){
    # Prepare variables based on the array data
    $DGName = $Dataset.Name
    $DGTotal = $Dataset.Worker
    $DGLoad = $Dataset.AvgLoad
    # Calculate the percentage of the RDSH delivery groups
    $DGLoadPercent = $Dataset.AvgPercent
    $DGMaintenance = $Dataset.Maintenance

    # Determine return state if delivery group is not in maintenance
    # The values depend on the size of the delivery group
    If ([Int64]$DGLoadPercent -gt $AlertLevel){
        $RetState = $returnStateCritical
        $returnState += [PSCustomObject]@{Name=$DGName;State=[Int64]$RetState} 
    }
    ElseIf ([Int64]$DGLoadPercent -le $WarningLevel) {
        $RetState = $returnStateOK
        $returnState += [PSCustomObject]@{Name=$DGName;State=[Int64]$RetState} 
    }
    Else {
        $RetState = $returnStateWarning
        $returnState += [PSCustomObject]@{Name=$DGName;State=[Int64]$RetState} 
    }


    #$retXml += "  <result>`n"
    #$retXml += "    <channel>$DGName State</channel>`n"
    #$retXml += "    <value>$RetState</value>`n"
    #$retXml += "  </result>`n"
    $retXml += "  <result>`n"
    $retXml += "    <channel>$DGName Load %</channel>`n"
    $retXml += "    <value>$DGLoadPercent</value>`n"
    $retXml += "    <unit>Percent</unit>`n"
    $retXml += "    <limitMode>1</limitMode>`n"
    $retXml += "    <limitMaxError>$AlertLevel</limitMaxError>`n"
    $retXml += "    <limitErrorMsg>$AlertString</limitErrorMsg>`n"
    $retXml += "    <limitMaxWarning>$WarningLevel</limitMaxWarning>`n"
    $retXml += "    <limitWarningMsg>$WarningString</limitWarningMsg>`n"
    $retXml += "  </result>`n"
    $retXml += "  <result>`n"
    $retXml += "    <channel>$DGName Load Index</channel>`n"
    $retXml += "    <value>$DGLoad</value>`n"
    $retXml += "    <unit>Count</unit>`n"
    $retXml += "  </result>`n"
}

# Determine return string depends on the several states
If ($returnState.State -contains 2) {
    $RetString = $AlertString    
}
ElseIf ($returnState.State -contains 1) {
    $RetString = $WarningString    
}
Else {
    $RetString = "OK"    
}

$retXml += "  <text>$RetString</text>`n"
$retXml += "</prtg>`n"

###
# Return info to PRTG
write-host $retXml

Das Ergebnis sieht dann wie folgt aus:

Viel Spass beim Nachbauen. :-)

Neustart nicht registrierter VDAs

Beim Start eines Workers werden bei uns mittels Startup Script noch div. Tasks abgearbeitet. Um eine Anmeldung zu verhindern, stoppt dieses Script als erstes den Citrix Desktop Dienst. Leider haben wir ab und an das Phänomen bei den Terminalservern, dass genau dieser Dienst zum Schluss entweder nicht gestartet wird oder die Registration nicht klappt.

Bisher gingen wir dann den einfach weg eines Neustarts (war sowieso kein Benutzer angemeldet). Dies habe ich nun mit einem einfachen Skript automatisiert:

# Load Citrix Cmdlets
asnp Citrix*

# Check for stopped VDAs not in maintenance mode
$UnregisteredVDAs = get-brokermachine -Filter {(InMaintenanceMode -eq $False) -and (RegistrationState -eq "Unregistered")}
$UnregisteredVDAs | FT DNSName, InMaintenanceMode, RegistrationState -AutoSize

ForEach($VM in $UnregisteredVDAs) {
    New-BrokerHostingPowerAction -Action Restart -MachineName $VM.HostedMachineName
}

Das Skript prüft nach Workern, welche nicht im Wartungsmodus und trotzdem unregistriert sind. Der geplante Task wird dann mit einem Serviceuser ausgeführt, welcher auch die entsprechenden Rechte innerhalb der VAD Umgebung besitzt.

Das Skript löst zwar noch nicht die Ursache der fehlerhaften Registrierung, aber es automatisiert den händischen Aufwand.

Viel Spass beim Nachbauen. :-)