Citrix XenDesktop 5.6 Umgebung mit Hilfe von Powershell automatisiert bereitstellen und verwalten

Seit einiger Zeit ermöglicht Citrix immer mehr Automatismen im Bereich XenDesktop und XenApp. Schon für den Presentation Server 4.5 stand Citrix.XenApp.Commands als ein Powershell Snap-In zum Nachinstallieren zur Verfügung. Ab XenApp 6.0 bzw. XenDesktop 5.6 wurden die jeweiligen Snap-Ins in die Installationsquellen integriert und stehen somit immer zur Verfügung. Sie sind nicht nur bei der Installation hilfreich, sondern ermöglichen ein vollständiges Desaster Recovery einer XenApp Farm, bzw. XenDesktop Site, sowie, in bestimmten Fällen, auch eine automatisierte Verwaltung und Konfiguration. In den letzten Monaten durfte ich (tatkräftig unterstützt im Bereich OS Virtualisierung von meinem Kollegen Maziar) bei einem großen Kunden, der allerdings anonym bleiben möchte, eine XenDesktop Umgebung implementieren, die besonderen Anforderungen genügen musste. Schon alleine wegen der Größe der Umgebung (> 30000 Endgeräte) war es notwendig, sowohl die Installation der Controller und VDA´s als auch die Konfiguration der Umgebung und spätere Verwaltung vollständig zu automatisieren. Da das Projekt nicht nur XenDesktop betraf, werde ich andere beteiligte Komponenten anreißen, allerdings nicht tiefer darauf eingehen.

Die Umgebung und die Anforderungen

Die Umgebung ist auf mehr als 100 Standorte verteilt. An jedem Standort sind Serverressourcen verfügbar (Fileserver, ESD, etc.). Auf der Clientseite kommt Windows 7 zum Einsatz. Die XenDesktop Umgebung sollte Remotearbeitsplätze für unterschiedliche Zwecke bereitstellen (Homeworker, 3rd Party Support, Remotewartung, Entwicklerarbeitsplätze, etc.). Initial wurden knapp 3000 VDI Arbeitsplätze geplant, wobei die Umgebung beliebig skalieren kann. Es gilt der Grundsatz, dass die Standorte nur auf eigene, bzw. zentral bereitgestellte Ressourcen zugreifen können, nicht aber auf die Ressourcen der anderen Standorte. Daraus ergab sich der Umstand, dass die VDA´s nicht zentral, sondern an jedem Standort platziert werden mussten. Als Hypervizor für die VDA´s wurde der kostenlose Hyper-V Server 2012 gewählt. Daraus ergibt sich allerdings, dass der Hypervizor nicht zentral und somit auch nicht mit Hilfe der XenDesktop Werkzeugen verwaltet werden kann. Die Standorte wurden je nach Größe mit einem und bis zu sechs Hyper-V Hosts ausgestattet. Der Zugriff auf die VDA´s erfolgt über den Citrix Receiver Enterprise 3.4. Für die Konfiguration der Receiver wurden zwei Instanzen des Citrix WebInterface 5.4 aufgesetzt, welche hinter einem F5 Loadbalancer zentral stehen. Auf jedem Endgerät ist ein ESD Werkzeug verfügbar. Damit erfolgt sowohl die Installation des Betriebssystems, wie auch der Software. Die VDA´s sollten sowohl als permanent wie auch als random zur Verfügung stehen. Bei den permanent VDA´s erfolgt die Benutzerzuordnung mit einem eigenen Verwaltungstool des Kunden, welches zu diesem Zweck erweitert wurde. Das Verwaltungstool wird hauptsächlich vom dezentralen IT-Personal verwendet. Die Random VDA´s fügen sich während der ESD Phase selbst der XD Umgebung hinzu. Alle VDA´s erzeugen bei Bedarf einen Catalog, bzw. eine entsprechende Desktop Group und fügen sich selbst dort hinzu. Bei der Verwendung von Citrix Policies, welche auf Desktop Groups gefiltert werden, müssen die Filter dynamisch mit jeder neuen Desktop Group angepasst werden.

Die Aufgaben

Die Teilaufgaben des Projektes umfassten folgende Bereiche:

  • Installation und Konfiguration der Hyper-V Hosts
  • Automatische Erzeugung der virtuellen Maschinen auf Hyper-V
  • Automatische Installation und Konfiguration von WebInterface Server
  • Automatische Installation und Konfiguration der XenDesktop Controller
  • Automatische Installation und Konfiguration der VDA´s
  • Automatische Erzeugung von Catalogs, Desktop Groups und Benutzerzuordnungen
  • Rebootmanagement der VDA´s
  • Dynamische Konfiguration von Citrix Policies

Installation und Konfiguration der Hyper-V Hosts.

Die Installation der Hyper-V Hosts wurde in die ESD Umgebung des Kunden eingebunden. Es wurde ein angepasstes WIM Image erstellt, in dem Hardwarespezifische Treiber abgelegt wurden. Da das ESD Werkzeug offiziell keine Softwareverteilung auf Hyper-V Server 2012 unterstützt, wurden lediglich einige Powershell Skripte für die Einrichtung, sowie für die spätere Verwaltung der VM´s abgelegt. Ein Scheduled Task (OnStart) fungiert nach der Betriebssysteminstallation als Trigger für die Konfiguration. Hier wird bei jedem Neustart des Hosts das Skript PreStart.ps1 aufgerufen. Es dient der Überwachung des Installationsprozesses, sowie dem Aufruf weiterer Skripte, welche einzelne Aufgaben übernehmen. Die Informationen über die Skriptausführung werden in der Registry abgelegt, so dass nach einzelnen Schritten auch Reboots möglich sind. Ist die initiale Installation und Konfiguration abgeschlossen, beendet sich das Skript PreStart.ps1 ohne weitere Aktionen. Während der Ersteinrichtung werden folgende Aufgaben durchgeführt:

  • Installation und Konfiguration von SNMP
  • Installation der Hardwarespezifischen Treiber
  • Hyper-V Konfiguration
  • Erstellung eines Teams auf zwei von vier Netzwerkkarten mit OS Mitteln
  • Aufbau eines virtuellen Switches auf Basis des erzeugten Teams
  • Konfiguration von RDP, WinRM, Firewall
  • Erzeugung der virtuellen Maschinen

Im letzten Schritt wird immer die gleiche Anzahl von VM´s pro Host erzeugt. Die Daten der virtueller Maschinen (Name, MAC Adresse) sind zentral inventarisiert. Zur Laufzeit werden sie abgerufen und für den Aufbau der VM´s verwendet. Alle virtuellen Maschinen haben den gleichen Aufbau im Bezug auf Hauptspeicher, CPU, Netzwerk und Festplatten. Nach diesem Schritt ist die Installation und die Konfiguration der Hyper-V Hosts abgeschlossen. Die virtuellen Maschinen können anschließend mit dem gleichen Verfahren installiert werden, wie auch die physikalischen Clients. Beides wird von dem ESD Tool übernommen.

Installation und Konfiguration der WebInterface Server.

Die WebInterface Server übernehmen die Aufgabe, den Citrix Receiver mit der entsprechenden Konfiguration zu versehen. Hierfür wurden zwei Services Sites angelegt, welche unterschiedliche Receiver Konfigurationen wiederspiegeln. Darüber hinaus wurde noch eine Web Site angelegt. In allen WebInterface Sites ist die XenDesktop Site, sowie einige zentral aufgestellte Citrix Presentation Server Farmen definiert, so dass die WebInterface Infrastruktur einen einheitlichen Zugang zu allen Citrix Ressourcen bietet.

Die Installation der WebInterface Server erfolgt mit Hilfe von sepagoLogiX, daher möchte ich an dieser Stelle nicht näher darauf eingehen. Hier wurde sepagoLogiX in das ESD Tool des Kunden integriert. Die Generisch entwickelte Installation wurde lediglich mit den kundenbezogenen Sitebeschreibungen in Form von CSV Dateien versorgt. Initial wurden zwei WebInterface Server installiert und hinter einem F5 Loadbalancer platziert. Während des Rollouts der XenDesktop Umgebung werden die beiden Server überwacht. Für den Fall, dass Ressourcen Engpässe entstehen, kann durch die automatische Installation jeder Zeit weiter skaliert werden.

Installation und Konfiguration der XenDesktop Controller.

Auch die XenDesktop Controller wurden mit Hilfe von sepagoLogiX installiert. Auch hier wurde LogiX in das ESD Tool integriert. Die Installation ist so aufgebaut, dass die Sitedatenbank automatisch erzeugt wird, es ist also als Voraussetzung lediglich ein SQL Server und die entsprechenden Anmeldedaten notwendig. Der erste sich installierende Server prüft, ob eine Datenbank bereits vorhanden ist, und legt sie bei Bedarf an. Jeder Server ermittelt während der Installation, ob eine XenDesktop Site bereits in der Datenbank vorhanden ist. Ist das nicht der Fall, wird die Site erzeugt, im anderen Fall fügt sich der Server der existierenden Site hinzu. Vorausgesetzt, die Sitedatenbank wird regelmäßig gesichert, ist jeder Zeit eine Neuinstallation der XenDesktop Controller möglich und damit ein Desaster Recovery realisiert.

Die Datenbank bezogenen Operationen in sepagoLogiX wurden von meinem Kollegen Timm entwickelt. Und sie haben viel Zeit gekostet… Wir agieren hier im Kontext eines Domänen Benutzers, welcher natürlich über entsprechende Berechtigungen auf dem SQL Server verfügen muss. Da die Installation des XenDesktop Servers aus technischen Gründen unter einem anderen Account läuft (Dienstkonto des ESD Tools), müssen die Zugriffe auf den SQL Server impersoniert werden, daher muss hier CredSSP als Authentifizierungsmethode verwendet werden, weil die Anmeldedaten mehr als ein mal weiter gegeben werden (lokal und remote). Dazu wird zunächst mal ein PSCredential Objekt aufgebaut:

 $CtxProperty_Database_Password = $(Secure-Password -Decrypt -EncryptedPWD $CtxProperty_Database_Password) | ConvertTo-SecureString -asPlainText -Force
 $CtxInstall_DBCredObject = New-Object System.Management.Automation.PSCredential($CtxProperty_Database_User,$CtxProperty_Database_Password)

Der Benutzername in Form von Domain\UserName ist zuvor in der Registry des XenDesktop Controllers abgelegt worden. Ebenso das verschleierte Passwort. sepagoLogiX sorgt zur Laufzeit dafür, dass diese Werte in globale Powershell Variablen überführt werden (hier z.B. $CtxProperty_Database_Password) und somit jedem Installationsschritt zur Verfügung stehen. Für die eigentliche Kommunikation mit dem SQL Server sind folgende Komponenten auf dem XenDesktop Server notwendig: Microsoft SQL Server System CLR Types, Microsoft SQL Server Management Objects, Microsoft SQL Server PowerShell Tools. Sie werden in der System Precustomizing Phase auf das System gebracht. Die SQL Operationen werden mit Hilfe von Invoke-Command durchgeführt. Zuvor wird noch ein Skriptblock definiert (hier ein Beispiel für die Ermittlung der Siteexistenz und der Aktion für den aktuellen Server: Join oder Create):

 $ScriptBlock_QueryDatabase = {
 param(
     $CtxProperty_Database_Server,
     $CtxProperty_DatabaseName
 )
 import-module sqlps -DisableNameChecking
 $ScriptBlock_QueryResult = Invoke-Sqlcmd -ServerInstance $CtxProperty_Database_Server -Query "SELECT [name] FROM sys.databases WHERE [name] = '$CtxProperty_DatabaseName'"
  
 if ($($ScriptBlock_QueryResult).name -eq $CtxProperty_DatabaseName)
 {
     return "Join"
 }
 else
 {
     return "Create"
 }
 }

welcher anschließend mit Invoke-Command verwendet wird:

 $CtxProperty_InstallAction = Invoke-Command -ComputerName $env:COMPUTERNAME -SessionOption (New-PSSessionOption -NoMachineProfile) -Credential $CtxInstall_DBCredObject -Authentication Credssp -ScriptBlock $ScriptBlock_QueryDatabase -ArgumentList $CtxProperty_Database_Server,$CtxProperty_DatabaseName

Die Besonderheit ist, dass Invoke-Command nicht remote, sondern lokal aufgerufen wird. Genau deswegen ist es notwendig, CredSSP zu verwenden, was vorher konfiguriert werden muss. Hier agiert der XenDesktop Controller sowohl als Client wie auch als Server:

 Enable-WSManCredSSP -Role Client -DelegateComputer *.$DomainDNS -Force
 Enable-WSManCredSSP -Role Client -DelegateComputer $Env:ComputerName -Force
 Enable-WSManCredSSP -Role Server –Force

Die eigentliche Erzeugung der Site und das Hinzufügen von XenDesktop Controllern ist hervorragend hier http://www.archy.net/citrix-xendesktop-5-automation/ beschrieben, daher verzichte ich auf weitere Details aus diesem Bereich.

Automatische Installation und Konfiguration der VDA´s .

Die virtuellen Clients werden komplett mit Hilfe des ESD Tools installiert. Für die Vorbereitung des VDA wird hier ebenfalls sepagoLogiX verwendet. Es handelt sich hier aber nicht nur um die Installation des Agents, sondern um weitere Konfiguration der Maschine. Es werden folgende Schritte ausgeführt:

  • Entfernen von unnötigen Windows Features (z.B. Tablet PC Komponenten)
  • Optimierung des Bootprozesses (Kein Bootlog, Quickboot, etc.)
  • WMI Konfiguration
  • Konfiguration der Energieoptionen
  • Installation des Agents
  • Installation des Citrix Profile Managements
  • Installation Online und Offline Plug-In
  • Konfiguration von WinRM

Im Letzten Schritt startet der Client den Versuch, sich der XenDesktop Umgebung hinzuzufügen (siehe auch weiter unten). Dies geschieht durch den Remoteaufruf des Powershell Skriptes Register-XDClient.ps1, welches sich an einer definierten Stelle auf jedem XenDesktop Controler befindet. Dieses Skript wird im Kontext eines dedizierten Citrix Administrators ausgeführt. Bei diesem Aufruf wird lediglich der Name des Clients als Parameter übergeben. Dadurch ist die Registrierung nur für die VDA´s vom Typ Random erfolgreich. Bei VDA´s vom Typ Permanent erwartet das Registrierungsskript zusätzlich noch den Benutzernamen, welcher dem Client zugeordnet werden soll. Da der Benutzername zum Zeitpunkt der VDA Installation nicht bekannt ist, erfolgt die Registrierung für die Clients vom Typ Permanent später durch das dezentrale IT-Personal.

Automatische Erzeugung von Catalogs, Desktop Groups und Benutzerzuordnungen.

Die Umgebung soll unterschiedliche Desktop Groups bereitstellen. Es handelt sich sowohl um Desktop Groups vom Typ Permanent wie auch Random. Die Desktop Group “Homeworker” ist vom Typ Permanent. Die Mitglieder dieser Desktop Group haben immer eine 1:1 Zuordnung zu einem Benutzer. Alle anderen Desktop Groups sind vom Typ Random und stehen einer Benutzergruppe zur Verfügung. Die gesamte XenDesktop Umgebung wurde in einer XML Datei “XDConfig.xml” beschrieben, welche folgende Struktur aufweist:

 <?xml version="1.0" encoding="utf-8"?>
 <ZR xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
     <XDControllers>
         <XDController>
             <Name>XDCtrl01</Name>
         </XDController>
         <XDController>
             <Name>XDCtrl02</Name>
         </XDController>
     </XDControllers>
     <Locations>
         <Location>
             <Name>000</Name>
             <StartUpTime>06:30</StartUpTime>
             <ShutDownTime>22:00</ShutDownTime>
             <VirtualClients>
                 <VirtualClient>
                     <Name>VDA000001</Name>
                     <HostName>HV000101</HostName>
                     <Role>ZR</Role>
                     <Locked>0</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>ZR</XDPoolName>
                     <XDPoolType>Random</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000002</Name>
                     <HostName>HV000101</HostName>
                     <Role>ZR</Role>
                     <Locked>0</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>ZR</XDPoolName>
                     <XDPoolType>Random</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000003</Name>
                     <HostName>HV000101</HostName>
                     <Role>ExternalSupport</Role>
                     <Locked>0</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>ExternalSupport</XDPoolName>
                     <XDPoolType>Random</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000004</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000005</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000006</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000007</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000008</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000009</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000010</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000011</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000012</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000013</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000014</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000015</Name>
                     <HostName>HV000101</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000016</Name>
                     <HostName>HV000102</HostName>
                     <Role>ZR</Role>
                     <Locked>0</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>ZR</XDPoolName>
                     <XDPoolType>Random</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000017</Name>
                     <HostName>HV000102</HostName>
                     <Role>ExternalSupport</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>ExternalSupport</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000018</Name>
                     <HostName>HV000102</HostName>
                     <Role>Developer</Role>
                     <Locked>0</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Developer</XDPoolName>
                     <XDPoolType>Random</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000019</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000020</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000021</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000022</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000023</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000024</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000025</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000026</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000027</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000028</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000029</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
                 <VirtualClient>
                     <Name>VDA000030</Name>
                     <HostName>HV000102</HostName>
                     <Role>Homeworker</Role>
                     <Locked>1</Locked>
                     <StartUpTime />
                     <ShutDownTime />
                     <XDPoolName>Homeworker</XDPoolName>
                     <XDPoolType>Permanent</XDPoolType>
                     </VirtualClient>
             </VirtualClients>
         </Location>
         <Location>
         ...
         </Location>
         ...
     </Locations>
 </ZR>
  
  

Darin ist die Struktur des Kunden zum Teil abgebildet. An oberster Stelle steht der Knoten “ZR” (Zentrales Rechenzentrum). Da die XenDesktop Controller zentral aufgestellt sind, bilden sie unterhalb von ZR einen eigenen Knoten. Weiterhin wird jeder Standort mit seinen Clients definiert. Auf der Ebene der Standorte sind zwei Uhrzeiten definiert, welche für den gesamten Standort die Zeit zum Herunterfahren und Starten der Maschinen definieren. Das explizite Handling der Reboots ist deswegen notwendig, da die Hypervizor nicht gemanaged sind. Die Rebootzeiten können aber auf der Ebene eines Clients überschrieben werden, falls sie vom Standard des Standorts abweichen sollen. Jeder Client weist folgende Eigenschaften auf:

  • Name – ist der Hostname des virtuellen Clients
  • Hostname – der Name des Hyper-V Hosts, auf dem der Client läuft. Das wird hauptsächlich für die Verwaltung der Clients auf der Hypervizor Ebene benötigt (Rebootsteuerung, etc.)
  • Rolle – bezeichnet den Typ der Maschine und deckt sich in den meisten Fällen mit dem Namen der Desktop Group
  • Locked – ein Flag, welcher steuert, ob der Client in dem Verwaltungstool sichtbar wird
  • StartUpTime – Die Uhrzeit für das Starten des Clients. Wenn angegeben, überschreibt sie den Standard des Standortes
  • ShutDownTime – Analog zu StartUpTime
  • XDPoolName – Name der Desktop Group
  • XDPoolType – kann Random oder Permanent sein

Die XML Datei ist zentral abgelegt und wird bei Änderungen automatisiert auf alle Standorte verteilt.

Der Registrierungsprozess wird, wie schon erwähnt, durch unterschiedliche Mechanismen initiiert. Grundsätzlich gilt, dass Desktops vom Typ Permanent, welche eine Benutzerzuordnung benötigen, über das Verwaltungstool vom dezentralen IT-Personal konfiguriert werden. Das Verwaltungstool ruft im Hintergrund das Skript Register-XDClient.ps1 mit den entsprechenden Parametern auf (Name des VDA und Benutzername des zuzuordnenden Benutzers). Dies geschieht ebenfalls im Kontext des Citrix Administrators (Der Kontextwechsel wird vom Verwaltungstool vorgenommen).

Nun kann ich zu dem wohl interessantesten Teil übergehen, nämlich zu dem Registrierungsskript Smiley

Beginnen wir mit dem Param-Statement:

 Param (
     [Parameter(Mandatory=$True)]
     [String] $ClientName,
     [String] $UserName,
     [Switch] $RemoveUser
 )

Neben den bereits beschriebenen Parametern existiert noch ein Switch, welcher es ermöglicht, eine bestehende Zuordnung eines Benutzers zum VDA aufzuheben. Als nächstes werden eine Reihe von Funktionen definiert, welche im eigentlichen Skript verwendet werden. Hier werde ich lediglich auf die “interessanten” eingehen.

Für die Interaktion mit dem Verwaltungstool habe ich eine Funktion “Create-Message” entwickelt, welche prinzipiell eine beliebige Menge an Informationen zurückgeben kann:

 Function Create-Message(
  
     [Parameter(Mandatory=$True)]
     [Int32] $MessageID,
     [Switch] $Stop,
     [String] $CustomField1
 )
 {
     $Message = New-Object PSObject
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Id" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Name" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Description" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Description_EN" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Category" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Severity" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "CustomField1" -Value $Null
     $SMessage = $Messages.Messages.Message | Where-Object {$_.Id -eq $MessageId}
     $Message.ID = $SMessage.Id
     $Message.Name = $SMessage.Name
     $Message.Description = $SMessage.Description
     $Message.Description_EN = $SMessage.Description_EN
     $Message.Category = $SMessage.Category
     $Message.Severity = $SMessage.Severity
     $Message.CustomField1 = $CustomField1
     $Message
     If ($Stop)
     {
         Exit $Message.ID
     }
 }

Hier wird ein PSObject erstellt, welchem verschiedene Eigenschaften hinzugefügt werden. Diese können bei Bedarf beliebig erweitert werden. Die eigentlichen Informationen befinden sich in einer XML Datei “Messages.xml”, welche im Skriptverzeichnis erwartet wird:

 <?xml version="1.0" encoding="utf-8"?>
 <Messages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
     <Message>
         <Id>1048577</Id>
         <Name>CLIENT_NOT_FOUND_IN_XML</Name>
         <Description_EN>
 The given client could not be found in VirtualHorst.xml file.
         </Description_EN>
         <Description>
 Der angegebene Client kann in der Datei VirtualHorst.xml nicht gefunden werden.
         </Description>
         <Category>InputData</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048578</Id>
         <Name>ERROR_LOADING_CITRIX_PSSNAPINS</Name>
         <Description_EN>
 Coult not load all required Citrix PS Snapins.
         </Description_EN>
         <Description>
 Erforderliche Citrix Snapins koennen nicht geladen werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>    
     <Message>
         <Id>1048579</Id>
         <Name>MISSING_USER_NAME_FOR_PRIVATE_DESKTOP</Name>
         <Description_EN>
 Missing user name for a private desktop.
         </Description_EN>
         <Description>
 Fuer einen privaten Desktop fehlt der Benutzername.
         </Description>
         <Category>InputData</Category>
         <Severity>Error</Severity>
     </Message>    
     <Message>
         <Id>1048580</Id>
         <Name>USER_NAME_NOT_FOUND_IN_ACTIVE_DIRECTORY</Name>
         <Description_EN>
 Could not find given user in Active Directory.
         </Description_EN>
         <Description>
 Der angegebene Benutzer kann im Active Directory nicht gefunden werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048581</Id>
         <Name>ERROR_ADDING_CLIENT_TO_CATALOG</Name>
         <Description_EN>
 Could not add client to catalog.
         </Description_EN>
         <Description>
 Der angegebene Client kann dem Catalog nicht hinzugefuegt werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048582</Id>
         <Name>ERROR_CREATING_DESKTOP_GROUP</Name>
         <Description_EN>
 Could not create DesktopGroup
         </Description_EN>
         <Description>
 Die Desktopgruppe kann nicht erstellt werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048583</Id>
         <Name>ERROR_ASSIGNING_AD_GROUP_TO_RANDOM_DESKTOP_GROUP</Name>
         <Description_EN>
 Could not assign Active Directory Group to random Desktop Group
         </Description_EN>
         <Description>
 Die Aktive Directory Gruppe kann der Desktopgruppe nicht zugeordnet werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>    
     <Message>
         <Id>1048584</Id>
         <Name>ERROR_ASSIGNING_CLIENT_TO_DESKTOP_GROUP</Name>
         <Description_EN>
 Could not assign client to Desktop Group
         </Description_EN>
         <Description>
 Der Client kann nicht zu der Desktopgruppe hinzugefuegt werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>    
     <Message>
         <Id>1048585</Id>
         <Name>ERROR_ASSIGNING_USER_TO_CLIENT</Name>
         <Description_EN>
 Could not assign user to client
         </Description_EN>
         <Description>
 Der Benutzer kann dem Client nicht zugewiesen werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>0</Id>
         <Name>OPERATION_COMPLETED_SUCCESSFULLY</Name>
         <Description_EN>
 Operation completed successfully!
         </Description_EN>
         <Description>
 Aktion erfolgreich abgeschlossen!
         </Description>
         <Category>Runtime</Category>
         <Severity>Info</Severity>
     </Message>
     <Message>
         <Id>1048587</Id>
         <Name>USER_ALREADY_ASSIGNED_TO_CLIENT</Name>
         <Description_EN>
 A user is already assigned to client
         </Description_EN>
         <Description>
 Ein Benutzer ist dem Client bereits zugewiesen.
         </Description>
         <Category>Runtime</Category>
         <Severity>Warning</Severity>
     </Message>
     <Message>
         <Id>1048588</Id>
         <Name>XML_CONFIG_FILE_NOT_FOUND</Name>
         <Description_EN>
 Can not find config XML file XDConfig.xml.
         </Description_EN>
         <Description>
 Die Konfigurationsdatei XDConfig.xml kann nicht gefunden werden.
         </Description>
         <Category>InputData</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048589)</Id>
         <Name>ERROR_REMOVING_USER_FROM_CLIENT</Name>
         <Description_EN>
 Can not remove user from Client.
         </Description_EN>
         <Description>
 Fehler beim entfernen des Benutzers vom Client.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>    
     <Message>
         <Id>1048590</Id>
         <Name>ERROR_DETERMINING_CLIENT_DESKTOPGROUP</Name>
         <Description_EN>
 Can not determine, if client is in desktop group.
         </Description_EN>
         <Description>
 Es kann nicht ueberprueft werden, ob der client einer Desktop Gruppe gehoert.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048591</Id>
         <Name>BROKER_SERVICE_NOT_AVAILIBLE</Name>
         <Description_EN>
 The service BrokerService is not available.
         </Description_EN>
         <Description>
 Der Dienst BrokerService antwortet nicht.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048592</Id>
         <Name>CREATE_PSDRIVE_FAILED</Name>
         <Description_EN>
 Could not create PS Drive for LocalFarmGPO.
         </Description_EN>
         <Description>
 Das PS Drive LocalFarmGPO kann nicht erstellt werden.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
     <Message>
         <Id>1048593</Id>
         <Name>UPDATE_CITRIX_POLICY_FILTER_FAILED</Name>
         <Description_EN>
 Could not update filter for Citrix Policy.
         </Description_EN>
         <Description>
 Die Aktualisierung des Citrix Policy Filters ist fehlgeschlagen.
         </Description>
         <Category>Runtime</Category>
         <Severity>Error</Severity>
     </Message>
 </Messages>

Die Funktion ist so konstruiert, dass sie die gesamte Skriptausführung unterbrechen kann, sofern der Switch “-Stop” angegeben wurde. Zusätzlich besteht die Möglichkeit, ein “CustomField” mit Laufzeitinformationen zu füllen.

Zwei weitere Funktionen erleichtern das Suchen nach der Uid eines Catalogs bzw. einer Desktop Group:

 Function Get-CatalogUid(
     [Parameter(Mandatory=$True)]
     [String] $CatalogName
 )
 {
     (Get-BrokerCatalog | Where-Object {$_.Name -eq $CatalogName}).UId
 }
  
 Function Get-DesktopGroupUid(
     [Parameter(Mandatory=$True)]
     [String] $DesktopGroupName
 )
 {
     (Get-BrokerDesktopGroup | Where-Object {$_.Name -eq $DesktopGroupName}).UId
 }

Auf ähnliche Weise können die beiden nächsten Funktionen die Suche nach einem Catalog oder einem VDA erleichtern:

 Function Check-Catalog(
     [Parameter(Mandatory=$True)]
     [String] $CatalogName
 )
 {
     If(Get-BrokerCatalog | Where-Object {$_.Name -eq $CatalogName})
     {
         $True
     }
     Else
     {
         $False
     }
 }
  
 Function Check-Client(
     [Parameter(Mandatory=$True)]
     [String] $ClientName
 )
 {
     If(Get-BrokerMachine | Where-Object {$_.MachineName -eq $ClientName})
     {
         $True
     }
     Else
     {
         $False
     }
 }

Im eigentlichen Skript wird zunächst überprüft, ob der Broker Service ansprechbar ist:

 #Check, if the BrokerService is OK
 $BrokerServiceStatus = Get-BrokerServiceStatus
 If ($BrokerServiceStatus -eq "OK")
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - BrokerService status is: $($BrokerServiceStatus)"
 }
 Else
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not reach BrokerService, the status is: $($BrokerServiceStatus). Can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+15) -Stop -CustomField1 $BrokerServiceStatus
 }

Als nächstes wird die Konfigurationsdatei XDConfig.xml geladen und der entsprechende Eintrag für den aktuellen Client ermittelt und als XML Struktur in der Variablen $vClient abgelegt. Danach wird der Typ des Clients ermittelt:

 #Get the type of desktop
 If ($vClient.XDPoolType -eq "Random")
 {
     $DesktopKind = "Shared"
 }
 Else
 {
     $DesktopKind = "Private"
 }

Es folgt die Überprüfung, ob eine Benutzerzuordnung aufgehoben werden soll:

 #Check, if User should be removed from private Desktop
 If ($RemoveUser)
 {
     Get-BrokerUser -MachineUid (Get-BrokerMachine | Where-Object {$_.MachineName -eq $(Join-Path $CurrentDomainNBName $ClientName)}).Uid | Foreach-object {Remove-BrokerUser -InputObject $_ -Machine $(Join-Path $CurrentDomainNBName $ClientName) -ErrorAction SilentlyContinue}
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Error removing users from Client `"$ClientName`"."
         Create-Message -MessageID $([math]::pow(2,20)+13) -Stop
     }
     Else
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - Successfuly removed users from Client `"$ClientName`"."
         Create-Message -MessageID 0 -Stop
     }
 }

Bei Clients vom Typ Permanent muss ein Benutzername als Parameter übergeben werden. Ist das nicht der Fall, wird das gesamte Skript beendet:

 # If desktop is private, a username must be provided
 If ($DesktopKind -eq "Private")
 {
     If (-not $UserName)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - The desktop kind is `"$DesktopKind`" and no username was provided. Can not continue an must exit."
         Create-Message -MessageID $([math]::pow(2,20)+3) -Stop
     }
 }

Wenn für den Client noch kein Catalog existiert, wird er angelegt:

 #Check, if Catalog exists -> Create Catalog
 If (-not (Check-Catalog -CatalogName $vClient.XDPoolName))
 {
     New-BrokerCatalog -Name $vClient.XDPoolName -AllocationType $vClient.XDPoolType -CatalogKind Unmanaged | Out-Null
 }

Anschließend wird der Client dem Catalog hinzugefügt:

 #Check, if Client is in Catalog -> Add to Catalog
 If (-not (Check-Client -ClientName $(Join-Path $CurrentDomainNBName $ClientName)))
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding Client `"$ClientName`" to catalog `"$($vClient.XDPoolName)`"..."
     $NewBrokerMachine = New-BrokerMachine -MachineName $ClientName -CatalogUid $(Get-CatalogUid -CatalogName $vClient.XDPoolName)
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not add client `"$ClientName`" to Catalog `"$($vClient.XDPoolName)`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+5) -Stop
     }
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Client `"$($ClientName)`" is already in Catalog `"$($vClient.XDPoolName)`"."
 }

Im nächsten Schritt wird überprüft, ob eine entsprechende Desktop Group existiert. Sie wird bei Bedarf angelegt:

 #Check, if DesktopGroup exists -> Create DesktopGroup
 $DesktopGroupName = "$($Location)_$($vClient.XDPoolName)"
 If (-not (Get-BrokerDesktopGroup | Where-Object {$_.Name -eq $DesktopGroupName}))
 {
     $CurrentDesktopGroup = New-BrokerDesktopGroup -Name $DesktopGroupName -DesktopKind $DesktopKind -PublishedName $DesktopGroupName
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not create DesktopGroup `"$DesktopGroupName`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+6) -Stop
     }
     New-BrokerAccessPolicyRule -AllowedConnections "NotViaAG" -AllowedProtocols @("RDP","HDX") -AllowedUsers "AnyAuthenticated" -AllowRestart $True -Enabled $True -IncludedDesktopGroupFilterEnabled $True -IncludedDesktopGroups @("$DesktopGroupName") -IncludedSmartAccessFilterEnabled $True -IncludedUserFilterEnabled $True -Name "$($DesktopGroupName)_Direct_1"
     New-BrokerAccessPolicyRule -AllowedConnections "ViaAG" -AllowedProtocols @("RDP","HDX") -AllowedUsers "AnyAuthenticated" -AllowRestart $True -Enabled $True -IncludedDesktopGroupFilterEnabled $True -IncludedDesktopGroups @("$DesktopGroupName") -IncludedSmartAccessFilterEnabled $True -IncludedSmartAccessTags @() -IncludedUserFilterEnabled $True -Name "$($DesktopGroupName)_AG"
     New-BrokerPowerTimeScheme -DaysOfWeek "Weekdays" -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -DisplayName "Weekdays" -Name "$($DesktopGroupName)_Weekdays_1" -PeakHours @($False,$False,$False,$False,$False,$False,$False,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$False,$False,$False,$False,$False) -PoolSize @(0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0)
     New-BrokerPowerTimeScheme -DaysOfWeek "Weekend" -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -DisplayName "Weekend" -Name "$($DesktopGroupName)_Weekend_1" -PeakHours @($False,$False,$False,$False,$False,$False,$False,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$False,$False,$False,$False,$False) -PoolSize @(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
     If ($DesktopKind -eq "Shared")
     {
         #Select a AD Group for Entitlement Policy
         Switch ($vClient.XDPoolName)
         {
             "ZR" {$EntitlementGroup = "$($Location)_G_ZR"}
             "ExternalSupport" {$EntitlementGroup = "$($Location)_G_ExternalSupport"}
             "Developer" {$EntitlementGroup = $(Join-Path $CurrentDomainNBName "$($Location)_G_Developer")}
         }
         Write-Message Info "$($MyInvocation.MyCommand.Name) - DesktopGroup is shared, so assigning an apropriate AD Group `"`" to it..."
         New-BrokerEntitlementPolicyRule -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -IncludedUsers $EntitlementGroup -Name "$($DesktopGroupName)_EntitlementPolicyRule_1"
         If (-not $?)
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not assign AD Group `"`" to the DekstopGroup `"$DesktopGroupName`", could not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+7) -Stop
         }
     }
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - DesktopGroup `"$($DesktopGroupName)`" already exists."
 }

Die Namen der Desktop Groups sind so gewählt, dass sie den Namen des Standortes beinhalten. Das ist notwendig, um der Forderung zu genügen, dass die Ressourcen eines Standortes lediglich von den Benutzern des Standortes verwendet werden dürfen. Nach der Erstellung der Desktop Group müssen noch Access Policies definiert werden. Handelt es sich um eine Desktop Group vom Typ Random, wird diese noch auf eine Sicherheitsgruppe berechtigt (ab Zeile 18 im obigen Listing). Diese Gruppen sind im AD zwar vom Typ Global, werden aber logisch für den jeweiligen Standort verwendet. Dies ist deswegen notwendig, damit die Möglichkeit gegeben ist, eine bestimmte Benutzergruppe doch noch standortübergreifend auf die Desktops zugreifen zu lassen. Das ist hier de Fall für die Gruppe ZR. Sie ist für zentrale Mitarbeiter vorgesehen, welche an einem bestimmten Standort tätig sein müssen.

Anschließend wird der Client der Desktop Group hinzugefügt:

 #Check, if Client is in DesktopGroup -> Add Client to DesktopGroup. If DG is Permanennt, assign user
 $CurrentDesktop = Get-BrokerDesktop -DesktopGroupName $DesktopGroupName -MachineName $(Join-Path $CurrentDomainNBName $ClientName) -AdminAddress $Env:ComputerName -ErrorAction SilentlyContinue
 If ((-not $?) -and ($Error[0].ToString() -ne "Object does not exist"))
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not determine, if the client `"$($ClientName)`" is in desktop group `"$($DesktopGroupName)`", can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+14) -Stop
 }
 If (-not $CurrentDesktop)
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding client `"$ClientName`" to DesktopGroup `"$DesktopGroupName`"..."
     $AddResult = Add-BrokerMachinesToDesktopGroup -Catalog $vClient.XDPoolName -DesktopGroup $DesktopGroupName -Count 1
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not add client `"$ClientName`" to the DekstopGroup `"$DesktopGroupName`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+8) -Stop
     }        
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Client `"$ClientName`" is already in DesktopGroup `"$DesktopGroupName`"."
 }

Zuletzt wird noch überprüft, ob ein Benutzer dem Desktop zugeordnet werden soll. Dies gilt natürlich lediglich für die VDA´s vom Typ Permanent:

 #Check, if a user should be assigned
 If ($vClient.XDPoolType -eq "Permanent")
 {
 #Check, if a user is already assigned
     $CurrentlyAssignedUser = $(Get-BrokerMachine -MachineName $(Join-Path $CurrentDomainNBName $ClientName)).AssociatedUserNames
     If (-not $CurrentlyAssignedUser)
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - Assigning user `"$UserName`" to client `"$ClientName`"..."
         $AddedUser = Add-BrokerUser -Name $(Join-Path $CurrentDomainNBName $UserName) -Machine $(Join-Path $CurrentDomainNBName $ClientName)
         If (-not $?)
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not assign user `"$UserName`" to the client `"$ClientName`", could not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+9) -Stop -CustomField1 $UserName
         }        
     }
     Else
     {
         Write-Message Warning "$($MyInvocation.MyCommand.Name) - The user `"$CurrentlyAssignedUser`" is already associated with the client `"$ClientName`"."
         Create-Message -MessageID $([math]::pow(2,20)+11) -CustomField1 "$($CurrentlyAssignedUser)" -Stop
     }
 }

An dieser Stelle ist zu sehen, wie in der Funktion Create-Message das Argument CustomField1 verwendet wird. Es beinhaltet den Benutzernamen, der zugeordnet werden sollte.

Und nun das vollständige Skript:

 Param (
     [Parameter(Mandatory=$True)]
     [String] $ClientName,
     [String] $UserName,
     [Switch] $RemoveUser
 )
  
 ###
 ### Function definition
 ###
  
 Function Write-Message (
     [parameter(Mandatory=$True)]
     [ValidateSet("INFO", "WARNING", "ERROR")] [String] $Severity,
     [parameter(Mandatory=$True)] [String] $Message    
 )
 {
     switch ($Severity)
     {
         "INFO"        {$Color="Green"}
         "WARNING"    {$Color="Yellow"}
         "ERROR"        {$Color="Red"}
     }
     If (-not (Test-Path -Path $LogFileName)) {New-Item -Path $LogFileName -Type file -Force}
     $TimeDate = ([System.DateTime]::Now).ToString()
     Write-Host ([System.DateTime]::Now).ToString() $Severity $Message -ForegroundColor $Color
     Out-File "$LogFileName"  -InputObject "$TimeDate  $Severity  $Message" -Append -Force
 }
  
 Function Create-Message(
  
     [Parameter(Mandatory=$True)]
     [Int32] $MessageID,
     [Switch] $Stop,
     [String] $CustomField1
 )
 {
     $Message = New-Object PSObject
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Id" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Name" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Description" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Description_EN" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Category" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "Severity" -Value $Null
     Add-Member -InputObject $Message -MemberType NoteProperty -Name "CustomField1" -Value $Null
     $SMessage = $Messages.Messages.Message | Where-Object {$_.Id -eq $MessageId}
     $Message.ID = $SMessage.Id
     $Message.Name = $SMessage.Name
     $Message.Description = $SMessage.Description
     $Message.Description_EN = $SMessage.Description_EN
     $Message.Category = $SMessage.Category
     $Message.Severity = $SMessage.Severity
     $Message.CustomField1 = $CustomField1
     $Message
     If ($Stop)
     {
         Exit $Message.ID
     }
 }
 Function Get-CatalogUid(
     [Parameter(Mandatory=$True)]
     [String] $CatalogName
 )
 {
     (Get-BrokerCatalog | Where-Object {$_.Name -eq $CatalogName}).UId
 }
  
 Function Get-DesktopGroupUid(
     [Parameter(Mandatory=$True)]
     [String] $DesktopGroupName
 )
 {
     (Get-BrokerDesktopGroup | Where-Object {$_.Name -eq $DesktopGroupName}).UId
 }
  
 Function Get-Domain(
     [Switch] $NetBIOS
 )
 {
     $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
     If ($NetBIOS)
     {
         [Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols") | Out-Null
         $RootDSE = [ADSI]"LDAP://RootDSE"
         $Config = $RootDSE.Get("configurationNamingContext")
         $ADSearchRoot = New-object System.DirectoryServices.DirectoryEntry("LDAP://CN=Partitions," + $Config)
         $SearchString = "(
         $DirectorySearcher = New-Object DirectoryServices.DirectorySearcher($ADSearchRoot, $SearchString)
         ($DirectorySearcher.FindOne()).Properties["netbiosname"]
     }
     Else
     {
         $Domain.Name
     }
 }
  
 Function global:Get-vClient(
     [Parameter(Mandatory=$True)]
     [String] $ClientName
 )
 {
     $OFS=""
     $Location = [string]($ClientName -replace "([^\d]{0,5})([0-9]*)",'$2')[0..2]
     $vClient = ($ConfigXML.ZR.Locations.Location | Where-Object {$_.Name -eq $Location}).VirtualClients.VirtualClient | Where-Object {$_.Name -eq $ClientName}
     If ($vClient)
     {
         $vClient
     }
     Else
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not find Client `"$ClientName`" in the XML-file."
         Create-Message -MessageID $([math]::pow(2,20)+1) -Stop
     }
     Remove-Variable OFS
 }
  
 Function Check-Catalog(
     [Parameter(Mandatory=$True)]
     [String] $CatalogName
 )
 {
     If(Get-BrokerCatalog | Where-Object {$_.Name -eq $CatalogName})
     {
         $True
     }
     Else
     {
         $False
     }
 }
  
 Function Check-Client(
     [Parameter(Mandatory=$True)]
     [String] $ClientName
 )
 {
     If(Get-BrokerMachine | Where-Object {$_.MachineName -eq $ClientName})
     {
         $True
     }
     Else
     {
         $False
     }
 }
  
 Function Check-ADUser(
     [Parameter(Mandatory=$True)]
     [String] $SAMAccountName
 )
 {
     $Searcher = [ADSISearcher]"(sAMAccountName=$SAMAccountName)"
     $SearchResults = $Searcher.FindOne()
     If ($SearchResults -ne $Null)
     {
         $True
     }
     Else
     {
         $False
     }
     $True
 }
  
 ###
 ### Script starts here
 ###
  
  
 $PsScriptRoot = Split-Path $MyInvocation.MyCommand.Path
 $LogFileName = Join-Path $PsScriptRoot "XenDesktopControl.log"
 Write-Message Info "$($MyInvocation.MyCommand.Name) - Starting..."
 Write-Message Info "$($MyInvocation.MyCommand.Name) - Trying to load message file..."
 If (-not $(Test-Path $(Join-Path $PsScriptRoot Messages.xml)))
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Can not load Message file `"$(Join-Path $PsScriptRoot Messages.xml)`", can not continue and must exit."
     Exit 1
 }
 Else
 {
     [XML]$Global:Messages = Get-Content $(Join-Path $PsScriptRoot Messages.xml)
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Successfuly loaded message file from `"$(Join-Path $PsScriptRoot Messages.xml)`"."
 }
 Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding required PsSnapIns if needed."
 Add-PsSnapIn "Citrix.Broker.Admin.V1" -ErrorAction SilentlyContinue
 If (-not (Get-PSSnapin "Citrix.Broker.Admin.V1" -ErrorAction SilentlyContinue))
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not load all PSSnapins needed, can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+2) -Stop
 }
  
 #Check, if the BrokerService is OK
 $BrokerServiceStatus = Get-BrokerServiceStatus
 If ($BrokerServiceStatus -eq "OK")
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - BrokerService status is: $($BrokerServiceStatus)"
 }
 Else
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not reach BrokerService, the status is: $($BrokerServiceStatus). Can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+15) -Stop -CustomField1 $BrokerServiceStatus
 }
  
 #Check, if the global XML file XDConfig.xml is reachable.
 If (-not $(Test-Path $(Join-Path $PsScriptRoot "XDConfig.xml")))
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not reach XML config file `"$(Join-Path $PsScriptRoot "XDConfig.xml")`", can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+12) -Stop
 }
 Else
 {
     [XML]$Global:ConfigXML = Get-Content $(Join-Path $PsScriptRoot "XDConfig.xml")
 }
 $CurrentDomainNBName = Get-Domain -NetBIOS
 $OFS=""
 $Location = [string]($ClientName -replace "([^\d]{0,5})([0-9]*)",'$2')[0..2]
 Remove-Variable OFS
 $Global:vClient = Get-vClient -ClientName $ClientName
 #Get the type of desktop
 If ($vClient.XDPoolType -eq "Random")
 {
     $DesktopKind = "Shared"
 }
 Else
 {
     $DesktopKind = "Private"
 }
 #Check, if User should be removed from private Desktop
 If ($RemoveUser)
 {
     Get-BrokerUser -MachineUid (Get-BrokerMachine | Where-Object {$_.MachineName -eq $(Join-Path $CurrentDomainNBName $ClientName)}).Uid | Foreach-object {Remove-BrokerUser -InputObject $_ -Machine $(Join-Path $CurrentDomainNBName $ClientName) -ErrorAction SilentlyContinue}
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Error removing users from Client `"$ClientName`"."
         Create-Message -MessageID $([math]::pow(2,20)+13) -Stop
     }
     Else
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - Successfuly removed users from Client `"$ClientName`"."
         Create-Message -MessageID 0 -Stop
     }
 }
  
 # If desktop is private, a username must be provided
 If ($DesktopKind -eq "Private")
 {
     If (-not $UserName)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - The desktop kind is `"$DesktopKind`" and no username was provided. Can not continue an must exit."
         Create-Message -MessageID $([math]::pow(2,20)+3) -Stop
     }
 }
 #Check, if Catalog exists -> Create Catalog
 If (-not (Check-Catalog -CatalogName $vClient.XDPoolName))
 {
     New-BrokerCatalog -Name $vClient.XDPoolName -AllocationType $vClient.XDPoolType -CatalogKind Unmanaged | Out-Null
 }
 #Check, if Client is in Catalog -> Add to Catalog
 If (-not (Check-Client -ClientName $(Join-Path $CurrentDomainNBName $ClientName)))
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding Client `"$ClientName`" to catalog `"$($vClient.XDPoolName)`"..."
     $NewBrokerMachine = New-BrokerMachine -MachineName $ClientName -CatalogUid $(Get-CatalogUid -CatalogName $vClient.XDPoolName)
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not add client `"$ClientName`" to Catalog `"$($vClient.XDPoolName)`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+5) -Stop
     }
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Client `"$($ClientName)`" is already in Catalog `"$($vClient.XDPoolName)`"."
 }
  
 #Check, if DesktopGroup exists -> Create DesktopGroup
 $DesktopGroupName = "$($Location)_$($vClient.XDPoolName)"
 If (-not (Get-BrokerDesktopGroup | Where-Object {$_.Name -eq $DesktopGroupName}))
 {
     $CurrentDesktopGroup = New-BrokerDesktopGroup -Name $DesktopGroupName -DesktopKind $DesktopKind -PublishedName $DesktopGroupName
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not create DesktopGroup `"$DesktopGroupName`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+6) -Stop
     }
     New-BrokerAccessPolicyRule -AllowedConnections "NotViaAG" -AllowedProtocols @("RDP","HDX") -AllowedUsers "AnyAuthenticated" -AllowRestart $True -Enabled $True -IncludedDesktopGroupFilterEnabled $True -IncludedDesktopGroups @("$DesktopGroupName") -IncludedSmartAccessFilterEnabled $True -IncludedUserFilterEnabled $True -Name "$($DesktopGroupName)_Direct_1"
     New-BrokerAccessPolicyRule -AllowedConnections "ViaAG" -AllowedProtocols @("RDP","HDX") -AllowedUsers "AnyAuthenticated" -AllowRestart $True -Enabled $True -IncludedDesktopGroupFilterEnabled $True -IncludedDesktopGroups @("$DesktopGroupName") -IncludedSmartAccessFilterEnabled $True -IncludedSmartAccessTags @() -IncludedUserFilterEnabled $True -Name "$($DesktopGroupName)_AG"
     New-BrokerPowerTimeScheme -DaysOfWeek "Weekdays" -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -DisplayName "Weekdays" -Name "$($DesktopGroupName)_Weekdays_1" -PeakHours @($False,$False,$False,$False,$False,$False,$False,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$False,$False,$False,$False,$False) -PoolSize @(0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0)
     New-BrokerPowerTimeScheme -DaysOfWeek "Weekend" -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -DisplayName "Weekend" -Name "$($DesktopGroupName)_Weekend_1" -PeakHours @($False,$False,$False,$False,$False,$False,$False,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$True,$False,$False,$False,$False,$False) -PoolSize @(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
     If ($DesktopKind -eq "Shared")
     {
         #Select a AD Group for Entitlement Policy
         Switch ($vClient.XDPoolName)
         {
             "ZR" {$EntitlementGroup = "$($Location)_G_ZR"}
             "ExternalSupport" {$EntitlementGroup = "$($Location)_G_ExternalSupport"}
             "Developer" {$EntitlementGroup = $(Join-Path $CurrentDomainNBName "$($Location)_G_Developer")}
         }
         Write-Message Info "$($MyInvocation.MyCommand.Name) - DesktopGroup is shared, so assigning an apropriate AD Group `"`" to it..."
         New-BrokerEntitlementPolicyRule -DesktopGroupUid $(Get-DesktopGroupUid -DesktopGroupName $DesktopGroupName) -IncludedUsers $EntitlementGroup -Name "$($DesktopGroupName)_EntitlementPolicyRule_1"
         If (-not $?)
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not assign AD Group `"`" to the DekstopGroup `"$DesktopGroupName`", could not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+7) -Stop
         }
     }
     # Update filter for Citrix Policy SessionLimits_Auskunft
     If ($vClient.XDPoolName -eq "Developer")
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - New DesktopGroup for `"Developer`" was created, so the Citrix Policy `"SessionLimits_Developer`" needs to be adjusted."
         # Load Group Policy SnapIn
         Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding Citrix PsSnapIn to manage Citrix Policies if needed."
         Add-PsSnapIn "Citrix.Common.GroupPolicy" -ErrorAction SilentlyContinue
         If (-not (Get-PSSnapin "Citrix.Common.GroupPolicy" -ErrorAction SilentlyContinue))
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not load PSSnapin `"Citrix.Common.GroupPolicy`", can not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+2) -Stop
         }
         # Create PSDrive for Citrix Policies
         New-PSDrive -Name LocalFarmGPO -PSProvider CitrixGroupPolicy -Root \ -Controller $Env:Computername
         If (Get-PSDrive -Name LocalFarmGPO -ErrorAction SilentlyContinue)
         {
             Write-Message Info "$($MyInvocation.MyCommand.Name) - PS Drive `"LocalFarmGPO`" successfuly created."
         }
         Else
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not create PS Drive `"LocalFarmGPO`", can not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+16) -Stop
         }
         Get-ChildItem -Path LocalFarmGPO:\User\SessionLimits_Developer\Filters\DesktopGroup | Remove-Item -Force
         Get-BrokerDesktopGroup | Foreach-Object {$_.Name} | Where-Object {$_ -match "_Developer"} | Foreach-Object {New-Item -Path "LocalFarmGPO:\User\SessionLimits_Developer\Filters\DesktopGroup" -Name "DGF_$($_)" -FilterValue $_ -ErrorAction SilentlyContinue}
         If ((Compare-Object $(Get-BrokerDesktopGroup | ForEach-Object {$_.Name} | Where-Object {$_ -match "Developer"}) $(Get-ChildItem "LocalFarmGPO:\user\SessionLimits_Developer\Filters\DesktopGroup" | ForEach-Object {$_.FilterValue})) -eq $Null)
         {
             Write-Message Info "$($MyInvocation.MyCommand.Name) - The Citrix Policy `"SessionLimits_Developer`" successfuly updated."
         }
         else
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - The Citrix Policy `"SessionLimits_Developer`" was not successfuly updated, can not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+17) -Stop
         }
     }
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - DesktopGroup `"$($DesktopGroupName)`" already exists."
 }
  
 #Check, if Client is in DesktopGroup -> Add Client to DesktopGroup. If DG is Permanennt, assign user
 $CurrentDesktop = Get-BrokerDesktop -DesktopGroupName $DesktopGroupName -MachineName $(Join-Path $CurrentDomainNBName $ClientName) -AdminAddress $Env:ComputerName -ErrorAction SilentlyContinue
 If ((-not $?) -and ($Error[0].ToString() -ne "Object does not exist"))
 {
     Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not determine, if the client `"$($ClientName)`" is in desktop group `"$($DesktopGroupName)`", can not continue and must exit."
     Create-Message -MessageID $([math]::pow(2,20)+14) -Stop
 }
 If (-not $CurrentDesktop)
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding client `"$ClientName`" to DesktopGroup `"$DesktopGroupName`"..."
     $AddResult = Add-BrokerMachinesToDesktopGroup -Catalog $vClient.XDPoolName -DesktopGroup $DesktopGroupName -Count 1
     If (-not $?)
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not add client `"$ClientName`" to the DekstopGroup `"$DesktopGroupName`", could not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+8) -Stop
     }        
 }
 Else
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Client `"$ClientName`" is already in DesktopGroup `"$DesktopGroupName`"."
 }
  
 #Check, if a user should be assigned
 If ($vClient.XDPoolType -eq "Permanent")
 {
 #Check, if a user is already assigned
     $CurrentlyAssignedUser = $(Get-BrokerMachine -MachineName $(Join-Path $CurrentDomainNBName $ClientName)).AssociatedUserNames
     If (-not $CurrentlyAssignedUser)
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - Assigning user `"$UserName`" to client `"$ClientName`"..."
         $AddedUser = Add-BrokerUser -Name $(Join-Path $CurrentDomainNBName $UserName) -Machine $(Join-Path $CurrentDomainNBName $ClientName)
         If (-not $?)
         {
             Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not assign user `"$UserName`" to the client `"$ClientName`", could not continue and must exit."
             Create-Message -MessageID $([math]::pow(2,20)+9) -Stop -CustomField1 $UserName
         }        
     }
     Else
     {
         Write-Message Warning "$($MyInvocation.MyCommand.Name) - The user `"$CurrentlyAssignedUser`" is already associated with the client `"$ClientName`"."
         Create-Message -MessageID $([math]::pow(2,20)+11) -CustomField1 "$($CurrentlyAssignedUser)" -Stop
     }
 }
  
 Create-Message -MessageID 0
 Write-Message Info "$($MyInvocation.MyCommand.Name) - Finished."

Rebootmanagement der VDA´s.

Die fehlende Verwaltbarkeit des Hypervizors manifestiert sich unter anderem darin, dass z.B. das Rebootmanagement der VDA´s in eigener Regie entwickelt werden musste. Hier hat auch der Kunde selbst tatkräftig unterstützt. Die Rebootzeiten sind über die zentrale Konfigurationsdatei definiert. Die Realisierung erfolgte dann auf der Hyper-V Ebene. Dazu wurde ein Scheduled Task erstellt, welcher durch den Start des Hosts getriggert wird. Darüber hinaus existiert ein weiterer Scheduled Task, welcher ein mal in der Nacht den erwähnten Scheduled Task ausführt. Darin werden pro Client zwei weitere Scheduled Tasks angelegt, für das Herunterfahren und Starten. Die Informationen für die Scheduled Tasks werden der zentralen Konfigurationsdatei XDConfig.xml unter Berücksichtigung der Standort- bzw. der clientspezifischen Zeiten entnommen. Die Clients, welche die Eigenschaft “Locked” auf den Wert 1 eingestellt haben, werden anders behandelt. Da sie nicht verwendet werden, werden sie nur in einem kleinen Zeitfenster in der Nacht hochgefahren, um eventuelle Updates via ESD zu bekommen. Dadurch, dass der Task, welcher die einzelnen Tasks erstellt, regelmäßig ausgeführt wird, wird dafür gesorgt, dass die Änderungen in der zentralen XDConfig.xml berücksichtigt werden. Das Starten und Herunterfahren der Clients erfolgt mit Hilfe von einzelnen Powershellskripten, welche lokal auf den Hyper-V Hosts liegen.

Dynamische Konfiguration von Citrix Policies.

Die Citrix Policies werden grundsätzlich mit Hilfe von sepagoLogiX behandelt. Sie wurden anfänglich in der GUI konfiguriert und anschließend in Form von XML Dateien exportiert. Die Exportdateien wurden wiederum in die Installationsskripte integriert. Im Falle einer kompletten Neuinstallation der Umgebung werden die XML Dateien bei der Installation des ersten XenDesktop Controllers importiert.

Für eine Desktop Group (Developer) bestand die Forderung, dass der Idle-Timeout extrem kurz gewählt werden sollte. Das wurde mit Hilfe einer Citrix Policy realisiert. Diese Policy wurde mit einem auf Desktop Groups basierenden Filter versehen. Das Problem dabei war, dass die Desktop Groups bei Bedarf erzeugt werden. So musste dafür gesorgt werden, dass jedes mal, wenn eine Desktop Group für Developer für einen bestimmten Standort erzeugt wurde, der Policyfilter aktualisiert werden musste. Das geschieht im folgenden Abschnitt des Skriptes:

 # Update filter for Citrix Policy SessionLimits_Auskunft
 If ($vClient.XDPoolName -eq "Developer")
 {
     Write-Message Info "$($MyInvocation.MyCommand.Name) - New DesktopGroup for `"Developer`" was created, so the Citrix Policy `"SessionLimits_Developer`" needs to be adjusted."
     # Load Group Policy SnapIn
     Write-Message Info "$($MyInvocation.MyCommand.Name) - Adding Citrix PsSnapIn to manage Citrix Policies if needed."
     Add-PsSnapIn "Citrix.Common.GroupPolicy" -ErrorAction SilentlyContinue
     If (-not (Get-PSSnapin "Citrix.Common.GroupPolicy" -ErrorAction SilentlyContinue))
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not load PSSnapin `"Citrix.Common.GroupPolicy`", can not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+2) -Stop
     }
     # Create PSDrive for Citrix Policies
     New-PSDrive -Name LocalFarmGPO -PSProvider CitrixGroupPolicy -Root \ -Controller $Env:Computername
     If (Get-PSDrive -Name LocalFarmGPO -ErrorAction SilentlyContinue)
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - PS Drive `"LocalFarmGPO`" successfuly created."
     }
     Else
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - Could not create PS Drive `"LocalFarmGPO`", can not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+16) -Stop
     }
     Get-ChildItem -Path LocalFarmGPO:\User\SessionLimits_Developer\Filters\DesktopGroup | Remove-Item -Force
     Get-BrokerDesktopGroup | Foreach-Object {$_.Name} | Where-Object {$_ -match "_Developer"} | Foreach-Object {New-Item -Path "LocalFarmGPO:\User\SessionLimits_Developer\Filters\DesktopGroup" -Name "DGF_$($_)" -FilterValue $_ -ErrorAction SilentlyContinue}
     If ((Compare-Object $(Get-BrokerDesktopGroup | ForEach-Object {$_.Name} | Where-Object {$_ -match "Developer"}) $(Get-ChildItem "LocalFarmGPO:\user\SessionLimits_Developer\Filters\DesktopGroup" | ForEach-Object {$_.FilterValue})) -eq $Null)
     {
         Write-Message Info "$($MyInvocation.MyCommand.Name) - The Citrix Policy `"SessionLimits_Developer`" successfuly updated."
     }
     else
     {
         Write-Message Error "$($MyInvocation.MyCommand.Name) - The Citrix Policy `"SessionLimits_Developer`" was not successfuly updated, can not continue and must exit."
         Create-Message -MessageID $([math]::pow(2,20)+17) -Stop
     }
 }

Hier wird zunächst das Snap-In für Citrix Policies geladen und ein PSDrive mit dem Namen “LocalFarmGPO” erzeugt. In der Zeile 24 werden alle Filtereinträge zunächst entfernt. In Zeile 25 werden alle Desktop Groups für Developer aufgelistet und basierend auf den einzelnen Namen die Filter für die Citrix Policy erzeugt (New-Item). Anschließend wird mit Compare-Object überprüft, ob alle relevanten Desktop Groups berücksichtigt wurden.