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

English English Deutsch Deutsch

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

$VMs = @()

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

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

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

    }until($CSV)
    return $CSV
}

$Cluster = GetCluster

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

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

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

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

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

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

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

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

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

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

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

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

Gruppenrichtlinien mit PowerShell bearbeiten

English English Deutsch Deutsch

Mit der PowerShell lassen sich nicht nur Gruppenrichtlinien sichern und importieren sondern auch geziehlt einzelne Einstellungen mittels „Set-GPRegistryValue“ anpassen.

Wie im letzten Beitrag (https://jans.cloud/2019/08/microsoft-fslogix-profile-container/) bereits genutzt, hier ein wenig mehr zum Arbeiten mit / in Gruppenrichtlinien-Objekten und der PowerShell.

Warum nicht einfach einmalig Konfigurieren, dann sichern, und in der Zielumgebung importieren?

  1. Darum!
  2. Weil ichs kann!
  3. Wie im Beispiel unten zu sehen ist, ist sicherlich nicht in jeder Umgebung z.B. der Remotedesktop-Lizenzserver „rdslic01.jans.cloud“ ;). Ebenfalls könnten sich Domain-Namen, Profilspeicherorte, etc. pp. ändern, was eine „händische“ Anpassung (oder eine Anpassung per „Migration Table„) mit sich ziehen würde.
  4. Am Ende des Tages läuft es sicherlich auf eine Mischung von vorhandenen zu importierenden GPO-Sets raus, wo an entsprechender Stelle die „Dynamik“ per GPO und den Set- / Get-GPRegistryValue-CMDLets erfolgt.

Daher legen wir los: Im ersten Step lege ich einfach ein leeres GPO an und schaue einmal, getreu dem Motto „Doof darf man sein, man muss sich nur zu helfen wissen“, mit Get-GPRegistryValue nach, was ich da so finde, um es später einfach mit Set-GPRegistryValue zu ändern. Leider nein, leider gar nicht. Das endet im Fehler. Daher setzen wir doch einfach einmal drei „Test Keys“ in der neu erstellten Richtlinie und schauen erneut nach.

Get-GPRegistryValue : Die folgende Gruppenrichtlinien-Registrierungseinstellung wurde nicht gefunden: „HKEY_LOCAL_MACHINE\Software“.
Parametername: keyPath
In Zeile:1 Zeichen:1
Get-GPRegistryValue „C_Mein_TestGPO“ -Key „HKLM\Software“
~~~~~~~~~~~~~ CategoryInfo : InvalidArgument: (Microsoft.Group…tryValueCommand:GetGPRegistryValueCommand) [Get-GPRegistryValue], ArgumentException
FullyQualifiedErrorId : UnableToRetrievePolicyRegistryItem,Microsoft.GroupPolicy.Commands.GetGPRegistryValueCommand

PowerShell Fehler „Get-GPRegistryValue“ bei leerem GPO
$GPOObject = New-GPO "C_Mein_TestGPO"

# Endet im Fehler (Bild 1)
Get-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software"
# Die Remotedesktop-Lizenzierung wurde gesetzt (Bild 2 und 3)
Get-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software"
# Weiterhangel.. (Bild 4)
Get-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies"
# Bis zum Ende (Bild 5)
Get-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services"

Schaut man sich die Ergebnisse etwas „genauer“ an, stellt man sicherlich fest, dass das ganze ja der Registry ziemlich nahe kommt bzw. identisch mit dem ist, was später auf dem Client im Policy-Zweig ankommt *Magic*. 😉 Zum Setzen / Ändern der Einstellungen im GPO geht es direkt mit Set-GPRegistryValue weiter. Dazu wird benötigt:

  1. Name oder GuId des GPO (C_Mein_TestGPO)
  2. Key (KeyPath vom CMDlet Get-GPRegistryValue (oben Bild 5)
  3. ValueName (Ebenfalls Bild 5)
  4. Value (siehe Bild 5)
  5. Type (Bild 5)
# Das jeweilige "Gegenteil" wird konfiguriert:
Set-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -ValueName fDisableTerminalServerTooltip -Value 0 -Type DWord
Set-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -ValueName LicensingMode -Value 2 -Type DWord
Set-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -ValueName LicenseServers -Value "rdslica01.dertest.org" -Type String

# Eine Einstellung wird auf "Nicht konfiguriert" zurückgesetzt:
Set-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -ValueName fDisableTerminalServerTooltip -Disable

# Der ganze Zweig wird auf "Nicht konfiguriert" zurückgesetzt:
Set-GPRegistryValue "C_Mein_TestGPO" -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -Disable

Hier gehe ich noch auf Punkt 4 „Am Ende des Tages“ mit einem Besipielszenario ein: Es wird ein neues GPO erstellt, verlinkt, eine Sicherung* importiert und der RDS-Lizenzierungs-Host angepasst.

* Die Sicherung habe ich bereits vorbereitet und bei mir unter „C:\Install\GPOs\C_RDSLizenzierung“ abgelegt

$RDSLicHost = $("rdslic01" + "." + $env:USERDNSDOMAIN.ToLower())
do{
    $LinkGPOToOU = Get-ADOrganizationalUnit -Filter * | Select-Object Name, DistinguishedName | Sort-Object Name | Out-GridView -Title "Wo soll das GPO verlinkt werden?" -PassThru
}until($LinkGPOToOU)
$LinkGPOToOU = Get-ADOrganizationalUnit $LinkGPOToOU.DistinguishedName

$LatestBackupID = $(Get-ChildItem -Path "C:\Install\GPOs\C_RDSLizenzierung" | Sort-Object LastWriteTime -Descending | Select-Object -First 1).Name.Split("{")[1].Split("}")[0]

$NewGPO = New-GPO "C_RDSLizenzierung" | New-GPLink -Target $LinkGPOToOU.DistinguishedName -LinkEnabled Yes
Import-GPO -TargetGuid $NewGPO.GpoId -BackupId $LatestBackupID -Path "C:\Install\GPOs\C_RDSLizenzierung"

Set-GPRegistryValue -GuId $NewGPO.GpoId -Key "HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services" -ValueName LicenseServers -Value $RDSLicHost -Type String

Was passiert hier eigentlich?

  1. Der FQDN des Lizenzierungshosts wird mit dem Host „rdslic01“ und der „USERDNSDOMAIN“ erstellt
  2. Die OUs der aktuellen Domäne werden solange ausgelesen bis eine OU ausgewählt wird
  3. Da eine GPO-Vorlage durchaus schonmal angepasst werden kann, suche ich mir im Sicherungs-Ordner die neuste Version aus und schneide die führende „{“ und abschließende „}“ ab
  4. Das neue GPO wird erzeugt und direkt an die in Schritt 2 auerkorene OU verlinkt
  5. Der eigentliche Import des GPOs
  6. Anpassung des Lizenzierungshosts an die aktuelle Umgebung

Abschließende Anmerkung: Wer die Einstellungen nicht händisch durchklicken möchte, um diese dann mittels Get-GPRegistryValue auszulesen und später mit Set-GPRegistryValue zu setzen, der darf sich gerne bei der Group Policy Search oder GetADMX bedienen.

Microsoft FSLogix Profile Container

English English Deutsch Deutsch

Nach dem Aufkauf von FSLogix hier ein kleines How-To zum Einrichten der FSLogix Profile Container zur Nutzung in einer Microsoft Remotedesktop-Sammlung oder auch in einem Citrix Virtual Apps and Desktop Deployment.

Welche Betriebssysteme werden supported?

  • Desktop: ab Windows 7 (32- / 64-Bit)
  • Server: ab 2008 R2 (32- / 64-Bit)

Wer darf die Profile Container bzw. die FSLogix Suite nutzen?

  • Microsoft 365 E3/E5
  • Microsoft 365 A3/A5/ Student Use Benefits
  • Microsoft 365 F1
  • Microsoft 365 Business
  • Windows 10 Enterprise E3/E5
  • Windows 10 Education A3/A5
  • Windows 10 VDA per user
  • Remote Desktop Services (RDS) Client Access License (CAL)*
  • Remote Desktop Services (RDS) Subscriber Access License (SAL)

* Die RDS (User / Device) CAL dürfte für die On-Prem-Freunde von Interesse sein, um die Container in einer lokal gehosteten (RDSH / CVAD / Horizon) Umgebung zu nutzen.

Wo finde ich die FSLogix Apps? -> https://aka.ms/fslogix_download

Hier noch ein Hinweis: Ich versuche hier so viel wie möglich bzw. das meiste zu automatisieren / zu scripten. Das liegt zum Einen daran, dass ich diesen Task vermutlich des Öfteren wiederholen muss und zum Anderen plane ich ein „kleines“ Deployment-Script. 😉 Für die Freunde der GUI wäre evtl. das folgende Hersteller Video ein Ansatz: http://blog.fslogix.com/deployment-guide-video-for-fslogix-profile-containers-and-office-365-containers

An dieser Stelle fange ich damit an, eine entsprechende Freigabe zur Ablage der späteren Profile Container zu erstellen. Wenn möglich würde ich an dieser Stelle auf das ReFS Dateisystem setzen! Das kann man sich in der GUI zusammenklicken („Benutzer (S-1-5-32-545)“; Nur diesen Ordner; Ändern / „Ersteller Besitzer (S-1-3-0)“; Unterordner und Dateien; Ändern / Optional: „Administratoren (S-1-5-32-544)“; Dieser Ordner, Unterordner und Dateien; Vollzugriff) oder auch flott per PowerShell erledigen:

$FSLogixStore = "D:\FSLogixStore"
$FSLogixShare = "FSLogixStore"
$FslogixHost = "jans-dc01"
$FSLogixSetup = "\\" + $FslogixHost + "\Install\FSLogix_Apps_2.9.7117.27413"
$FSLogixO365Group = "grp_O365_Container"
$FSLogixProfileGroup = "grp_Profile_Container"
$FSLogixPolicy = "C_FSLogix_Config"

$IcaclsPath = "C:\Windows\System32\icacls.exe"

Enable-NetFirewallRule -Name FPS-SMB-In-TCP, FPS-SMB-Out-TCP

if(!(Test-Path $FSLogixStore)){
    New-Item -Path $FSLogixStore -ItemType Directory | Out-Null
} else{
    Write-Host "ACHTUNG: Berechtigungen für ""$FSLogixStore"" wird zurückgesetzt!" -ForegroundColor Red
}
Start-Process -FilePath $IcaclsPath -ArgumentList $($FSLogixStore + " /inheritance:d /T /C /Q") -Wait
Start-Process -FilePath $IcaclsPath -ArgumentList $($FSLogixStore + " /remove:g *S-1-3-0 /remove:g *S-1-5-18 /remove:g *S-1-5-32-545 /grant:r *S-1-5-32-544:(OI)(CI)(F) /grant:r *S-1-5-32-545:(M) /grant:r *S-1-3-0:(OI)(CI)(IO)(M) /T /C /Q") -Wait
if(!(Get-SmbShare | ? Name -eq $FSLogixShare)){
    New-SmbShare -Name $FSLogixShare -Path $FSLogixStore -FullAccess *S-1-5-11 -CachingMode None -Description "FSLogix Profildaten" | Out-Null
} else{
    Write-Host "ACHTUNG: Der Freigabe ""$FSLogixShare"" wird ""Authentifizerte Benutzer - Vollzugriff"" hinzugefügt!" -ForegroundColor Red
    Grant-SmbShareAccess -Name $FSLogixShare -AccountName *S-1-5-11 -AccessRight Full -Confirm:$false
}

Als weitere Vorbereitung ist es hilfreich entsprechende Gruppen und ein Gruppenrichtlinien-Objekt zur Nutzung der Profile Container vorzubereiten. Ebenfalls werden das ADMX / ADML File in den PolicyDefintions Ordner kopiert. Anbei ein PowerShell Script zum Erstellen der, in meinem Fall zwei, benötigten Gruppen („grp_O365_Container“ / „grp_Profile_Container“) sowie dem GPO („C_FSLogix_Config“) samt Konfiguration:

$FSLogixStore = "D:\FSLogixStore"
$FSLogixShare = "FSLogixStore"
$FslogixHost = "jans-dc01"
$FSLogixSetup = "\\" + $FslogixHost + "\Install\FSLogix_Apps_2.9.7117.27413"
$FSLogixO365Group = "grp_O365_Container"
$FSLogixProfileGroup = "grp_Profile_Container"
$FSLogixPolicy = "C_FSLogix_Config"

if(!(Test-Path \\$env:UserDNSDomain\sysvol\$env:UserDNSDomain\Policies\)){
    Copy-Item C:\Windows\PolicyDefinitions \\$env:UserDNSDomain\sysvol\$env:UserDNSDomain\Policies\ -Recurse
}
Copy-Item $($FSLogixSetup + "\fslogix.adml") \\$env:UserDNSDomain\sysvol\$env:UserDNSDomain\Policies\PolicyDefinitions\en-US\
Copy-Item $($FSLogixSetup + "\fslogix.adml") \\$env:UserDNSDomain\sysvol\$env:UserDNSDomain\Policies\PolicyDefinitions\de-DE\
Copy-Item $($FSLogixSetup + "\fslogix.admx") \\$env:UserDNSDomain\sysvol\$env:UserDNSDomain\Policies\PolicyDefinitions\

$GroupOU = Get-ADOrganizationalUnit -Filter * | Select-Object Name, DistinguishedName | Out-GridView -Title "In welcher OU sollen die beiden Gruppen angelegt werden?" -PassThru
$GroupOU = Get-ADOrganizationalUnit $GroupOU.DistinguishedName
$RDSHOU = Get-ADOrganizationalUnit -Filter * | Select-Object Name, DistinguishedName | Out-GridView -Title "Wo befinden sich die Session-Hosts / Desktops?" -PassThru
$RDSHOU = Get-ADOrganizationalUnit $RDSHOU.DistinguishedName

New-ADGroup -Name $FSLogixO365Group -DisplayName $FSLogixO365Group -Description "FSLogix Office 365 Container User" -Path $GroupOU.DistinguishedName -GroupCategory Security -GroupScope DomainLocal
New-ADGroup -Name $FSLogixProfileGroup -DisplayName $FSLogixProfileroup -Description "FSLogix Profile Container User" -Path $GroupOU.DistinguishedName -GroupCategory Security -GroupScope DomainLocal

$GPOObject = New-GPO $FSLogixPolicy | New-GPLink -Target $RDSHOU.DistinguishedName -LinkEnabled Yes

# Profile Container
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "Enabled" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "IsDynamic" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "SizeInMBs" -Value 5000 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "RoamSearch" -Value 2 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "VHDLocations" -Value $("\\" + $FslogixHost + "\" + $FSLogixShare) -Type String | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\FSLogix\Profiles" -ValueName "VolumeType" -Value "VHDX" -Type String | Out-Null

# Office Container
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "Enabled" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IsDynamic" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "SizeInMBs" -Value 5000 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeSkype" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeTeams" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOneNote" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOutlook" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOfficeActivation" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOneDrive" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeSharepoint" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOfficeFileCache" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOutlookPersonalization" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "IncludeOneNote_UWP" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "OutlookCachedMode" -Value 1 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "RoamSearch" -Value 2 -Type DWord | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "VHDLocations" -Value $("\\" + $FslogixHost + "\" + $FSLogixShare) -Type String | Out-Null
Set-GPRegistryValue -Name $FSLogixPolicy -Key "HKLM\Software\Policies\FSLogix\ODFC" -ValueName "VolumeType" -Value "VHDX" -Type String | Out-Null

# Group Policy Preferences Gruppen
New-Item $("\\" + $env:USERDNSDOMAIN + "\SYSVOL\" + $env:USERDNSDOMAIN + "\Policies\{" + $GPOObject.GpoId + "}\Machine\Preferences\Groups\Groups.xml") -ItemType File -Force | Out-Null
"<?xml version=""1.0"" encoding=""utf-8""?>" | Out-File $("\\" + $env:USERDNSDOMAIN + "\SYSVOL\" + $env:USERDNSDOMAIN + "\Policies\{" + $GPOObject.GpoId + "}\Machine\Preferences\Groups\Groups.xml") -Encoding utf8
"<Groups clsid=""{3125E937-EB16-4b4c-9934-544FC6D24D26}""><Group clsid=""{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}"" name=""FSLogix ODFC Include List"" image=""2"" changed=""$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")"" uid=""{7D296515-BA25-452D-85A5-B1DB2580B2D5}""><Properties action=""U"" newName="""" description=""Members of this group are on the include list for Outlook Data Folder Containers"" deleteAllUsers=""1"" deleteAllGroups=""1"" removeAccounts=""0"" groupSid="""" groupName=""FSLogix ODFC Include List""><Members><Member name=""$FSLogixO365Group"" action=""ADD"" sid=""""/></Members></Properties></Group>" | Out-File $("\\" + $env:USERDNSDOMAIN + "\SYSVOL\" + $env:USERDNSDOMAIN + "\Policies\{" + $GPOObject.GpoId + "}\Machine\Preferences\Groups\Groups.xml") -Encoding utf8 -Append
"	<Group clsid=""{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}"" name=""FSLogix Profile Include List"" image=""2"" changed=""$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")"" uid=""{6C2B4A05-A5F4-43E3-8F12-1C80538C2C6A}""><Properties action=""U"" newName="""" description=""Members of this group are on the include list for dynamic profiles"" deleteAllUsers=""1"" deleteAllGroups=""1"" removeAccounts=""0"" groupSid="""" groupName=""FSLogix Profile Include List""><Members><Member name=""$FSLogixProfileGroup"" action=""ADD"" sid=""""/></Members></Properties></Group>" | Out-File $("\\" + $env:USERDNSDOMAIN + "\SYSVOL\" + $env:USERDNSDOMAIN + "\Policies\{" + $GPOObject.GpoId + "}\Machine\Preferences\Groups\Groups.xml") -Encoding utf8 -Append
"</Groups>" | Out-File $("\\" + $env:USERDNSDOMAIN + "\SYSVOL\" + $env:USERDNSDOMAIN + "\Policies\{" + $GPOObject.GpoId + "}\Machine\Preferences\Groups\Groups.xml") -Encoding utf8 -Append

Kommen wir als nächstes zur Installation der FSLogix Suite auf dem / den Session-Hosts / Desktops bzw. im „Golden-Image“. Erneut bleibt die unspektakuläre Möglichkeit zum Klicken in der GUI (langweilig!) oder per PowerShell Script.

$FSLogixStore = "D:\FSLogixStore"
$FSLogixShare = "FSLogixStore"
$FslogixHost = "jans-dc01"
$FSLogixSetup = "\\" + $FslogixHost + "\Install\FSLogix_Apps_2.9.7117.27413"
$FSLogixO365Group = "grp_O365_Container"
$FSLogixProfileGroup = "grp_Profile_Container"
$FSLogixPolicy = "C_FSLogix_Config"

if(Test-Path $($FSLogixSetup + "\x64\Release\FSLogixAppsSetup.exe")){
    Start-Process -FilePath $($FSLogixSetup + "\x64\Release\FSLogixAppsSetup.exe") -ArgumentList $("/install /quiet /norestart /log " + $FSLogixSetup + "\FSLogix_Log.txt") -Wait
}

Nach einem ggfs. benötigten Reboot finden sich auf dem Host die folgenden Gruppen zur späteren Steuerung der Container wieder:

  • FSLogix ODFC Exclude List
  • FSLogix ODFC Include List (Standardmäßig ist hier „Jeder“ / „Everyone“ bzw. „S-1-1-0“ Mitglied)
  • FSLogix Profile Exclude List
  • FSLogix Profile Include List (Standardmäßig ist hier „Jeder“ / „Everyone“ bzw. „S-1-1-0“ Mitglied)

Sollte die Richtlinie wie oben in meinem Beispiel verwendet worden sein, befindet sich „\Jeder“ nicht mehr in den beiden Gruppen, sondern die, ebenfalls oben erstellten, Gruppen darin. Wenn bis hierher nichts vollkommen daneben gegangen ist, sollte eine Testanmeldung möglich sein. Bei der Anmeldung sollten dann auch die entsprechenden Container erstellt werden – Yay 🙂 !