Friday, October 18, 2019

WVD and VDI automation: Change in language pack installation on Windows 10 1809 and higher

Currently I’m working on a Windows 10 Enterprise 1903 CAD-VDI project now. One goal of this project is to deploy the golden master image with Automation.

My customer has Spanish, French, English, Dutch, Romanian and Russian speaking employees and needs the Windows User Interface in this language. As of Windows 1809 and higher, Microsoft changed things with the language pack installation. They call this a Local Experience Pack (LXP in short).

Prior to Windows 10 1809 an installation of a Language Interface Pack (LIP in short) was easy. Just download the lp.cab of the language pack and install it with DISM or lpksetup.exe. Example old commands:

  • dism.exe /online /add-package /packagepath:D:\langpacks\de-de\lp.cab
  • lpksetup /i es-ES /r /s /p D:\langpacks\es-es\lp.cab
Installation of LXP are different then good old LIPs. So, this is what I want to share with you. More information here: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/add-language-packs-to-windows

The User Experience changed when using LXP in Windows 10 >=1809. With LXP, things are more user focused from the modern settings panel. And more user input is needed, which is difficult for automation. If an end-user wants to select their “Windows Display Language”. The user needs to add a preferred language first (even if the LXP is already installed on the machine level). After that, he’s able to select the Windows Display Language of choice (which is a subset of the user defined preferred language).


If you want to automate this process in a pooled VDI or Windows Virtual Desktop environment all the workers need to have the LXP installed upfront or your users need admin rights. If you want to automate the installation of LXP to a golden master or persistent images, the process is as follows:
  1. Download the “Windows 10 Language Pack” ISO for your Windows build from the Windows Volume Licensing or MSDN website. (i.e. “Windows 10 Language Pack, version 1903”) 
  2. Install the required Language Pack cab language files to the system. (i.e. “Microsoft-Windows-Client-Language-Pack_x64_es-es.cab”)
  3. Install the corresponding LXP APPX file to the system. (i.e. “LanguageExperiencePack.es-ES.Neutral.appx”)
  4. Add the language to the “preferred language” per user!

I’ve created a powershell script, which automates the steps 2 to 4. The script can be downloaded here: https://vandenbornit-my.sharepoint.com/:u:/g/personal/patrick_vandenborn_it/EdzXPl6eDB5Fj-uDxgRZsf4BkyMyGjRDZ050r62PoVs3eA?e=7yMJIK or copy paste the code at the bottom of this blog.


Use the powershell script as follows:
1.    Create a folder of choice and create two subfolders (LangPacks and LXP)


2.    Copy the cab files from the ISO (x86 or x64 folder) to the LangPacks directory


3.    Copy the corresponding LXP folders form the ISO (LocalExperiencePacks) to the LXP directory


4.    Edit the Install_Win10_LXP.ps1 script and change the global settings:

 

5.    Add the Install_Win10_LXP.ps1 to your automation software or run as admin from PowerShell

6.    The scripts detect missing files


7.    When the script finishes, a powershell command is generated. Use this as a RunOnce login script for each user in your WVD or VDI environment:


8.    Run the powershell script in user context

###########################################
# PowerShell Script for users or login script
###########################################
$prefered_list = Get-WinUserLanguageList
$prefered_list.Add("es-es")
$prefered_list.Add("fr-fr")
$prefered_list.Add("nl-nl")
Set-WinUserLanguageList($prefered_list) -Force


9.    This adds the installed languages to the “preferred language list” automatically:

 The powershell script:
#############################
# Install Language pack Win10 1809 and higher
#
# Created by Patrick van den Born (vandenborn.it)
#
# MS Switched from lp.cab to APPX also called Local Experience Packs
# More info: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/add-language-packs-to-windows
#
# This tool installs the needed CAB and APPX packages
#
# You can download the Language Pack from your volume licensing channel website
#
# In my case: "Windows 10 Language Pack, version 1903"
#
# Create a folder structure like this
#
# \LangPacks         i.e. D:\Win10-1903\LangPacks --> Root folder for LangPacks
# \LXP               i.e. D:\Win10-1903\LXP --> Root folder for Local Experience Pack
#
# Then copy required cab from ISO x64 or x86 folder, for example
# \LangPacks\Microsoft-Windows-Client-Language-Pack_x64_es-es.cab
# \LangPacks\Microsoft-Windows-Client-Language-Pack_x64_fr-fr.cab
# \LangPacks\Microsoft-Windows-Client-Language-Pack_x64_nl-nl.cab
#
# Then copy corresponding Local Experience Pack folders from ISO LocalExperiencePack folder, for example
# \LXP\es-es\LanguageExperiencePack.es-ES.Neutral.appx
# \LXP\es-es\License.xml
# \LXP\fr-fr\LanguageExperiencePack.fr-FR.Neutral.appx
# \LXP\fr-fr\License.xml
# \LXP\nl-nl\LanguageExperiencePack.nl-NL.Neutral.appx
# \LXP\nl-nl\License.xml
#
# The cab and appx files needs to be installed as Admin
#
# To activate the languages, this should be started as user and is marked at the end of this script
#
###############################################

#####################
# Global script vars
#####################
$lp_root_folder = "D:\Win10-1903" #Root folder where the copied sourcefiles are
$architecture = "x64" #Architecture of cab files
$systemlocale = "nl-NL" #System local when script finishes

#####################
# Start installation of language pack on Win10 1809 and higher
#####################
$installed_lp = New-Object System.Collections.ArrayList
foreach ($language in Get-ChildItem -Path "$lp_root_folder\LXP") {
    #check if files exist

    $appxfile = $lp_root_folder + "\LXP\" + $language.Name + "\LanguageExperiencePack." + $language.Name + ".Neutral.appx"
    $licensefile = $lp_root_folder + "\LXP\" + $language.Name + "\License.xml"
    $cabfile = $lp_root_folder + "\LangPacks\Microsoft-Windows-Client-Language-Pack_" + $architecture + "_" + $language.Name + ".cab"
   
    if (!(Test-Path $appxfile)) {
        Write-Host $language.Name " - File missing: $appxfile" -ForegroundColor Red
        Write-Host "Skipping installation of "  + $language.Name
    } elseif (!(Test-Path $licensefile)) {
        Write-Host $language.Name " - File missing: $licensefile" -ForegroundColor Red
        Write-Host "Skipping installation of "  + $language.Name
    } elseif (!(Test-Path $cabfile)) {
        Write-Host $language.Name " - File missing: $cabfile" -ForegroundColor Red
        Write-Host "Skipping installation of "  + $language.Name
    } else {
        Write-Host $language.Name " - Installing $cabfile" -ForegroundColor Green
        Start-Process -FilePath "dism.exe" -WorkingDirectory "C:\Windows\System32" -ArgumentList "/online /Add-Package /PackagePath=$cabfile /NoRestart" -Wait

        Write-Host $language.Name " - Installing $appxfile" -ForegroundColor Green
        Start-Process -FilePath "dism.exe" -WorkingDirectory "C:\Windows\System32" -ArgumentList "/online /Add-ProvisionedAppxPackage /PackagePath=$appxfile /LicensePath=$licensefile /NoRestart" -Wait

        Write-Host $language.Name " - CURRENT USER - Add language to preffered languages (User level)" -ForegroundColor Green
        $prefered_list = Get-WinUserLanguageList
        $prefered_list.Add($language.Name)
        Set-WinUserLanguageList($prefered_list) -Force

        $installed_lp.Add($language.Name)
    }
}

Write-Host "$systemlocale - Setting the system locale" -ForegroundColor Green
Set-WinSystemLocale -SystemLocale $systemlocale

#Generating output for script

Write-Host " "
Write-Host "###########################################" -ForegroundColor Blue
Write-Host "PowerShell Script for users or login script" -ForegroundColor Blue
Write-Host "###########################################" -ForegroundColor Blue
Write-Host '$prefered_list = Get-WinUserLanguageList' -ForegroundColor Blue

For ($i = 0; $i -ne $installed_lp.Count; $i++) {
    Write-Host '$prefered_list.Add(' $installed_lp[$i] ')' -ForegroundColor Blue
}

Write-Host 'Set-WinUserLanguageList($prefered_list) -Force' -ForegroundColor Blue

Wednesday, March 14, 2018

How to: Configure Citrix XenMobile to use Microsoft SQL multi-subnet (Basic) Availability Groups

In the previous blogs I’ve explained you “How to: Install and configure Microsoft SQL Server 2016 Standard multi-subnet Basic Availability Groups for Citrix XenDesktop and XenMobile” and “Microsoft SQL and Microsoft SQL AlwaysOn basics for Citrix Admins”.

This blog is a guide for configuring Citrix XenMobile with a multi-subnet SQL AlwaysOn database. When using a multi-subnet SQL AlwaysOn cluster, we need to reconfigure the Windows Failover Cluster Network parameters RegisterAllProvidersIP=0 and HostRecordTTL=60 for the XenMobile AlwaysOn listener. Special thanks for the details delivered by @JanPaulPlaisier! Details will be explained later in this blog.


During a customer’s project we’ve build the following XenMobile backend:

  • One XenMobile cluster 
    • One XenMobile server in Amersfoort: PBO-XMS01.pbo.lan 
    • One XenMobile server in Nijkerk: PBO-XMS02.pbo.lan 
  • Both sites have a NetScaler with a GSLB configuration for XenMobile. The NetScaler configuration is not covered in this blog. 
    • The XenMobile service in Amersfoort is the primary site. When the XenMobile service in Amersfoort becomes unresponsive, GSLB will failover the XenMobile services to Nijkerk.
Logical overview

Note: Site-to-site network latency is 4ms.

Create SQL Login for XenMobile service account

Create an Active Directory Service Account for the XenMobile database connection as follows:

1.    Create a Domain User in Active Directory, this is your service account (i.e. XMSvc)


2.    Open SQL Management Studio
3.    Create a SQL Server login for the XenMobile service account on all the SQL nodes in your AlwaysOn configuration. Starting with node 1:


4.    Select Windows Authentication and select your Service Account (i.e. PBO\XMSvc). Then click Server Roles:


5.    Select dbcreator and click OK:


6.    Create this login on the remaining SQL servers:

Creating the initial database

Before we can join the Citrix XenMobile database to a (Basic) Availability Group, we need to create the database on one of the SQL servers directly using the XenMobile appliance. This how-to is describing an initial setup using the XenMobile appliance. If you need to move an existing XenMobile database to an availability group, please skip this chapter and start with the next chapter.

1.    Import the XenMobile appliance you’ve downloaded from download.citrix.com
2.    The “First time Use mode” is started, I will not discuss default configuration. I will only discuss database configuration


3.    Configure the database connection to connect to one of your SQL nodes directly (i.e. PBO-SQL01.pbo.lan). Connect using the XenMobile service account (i.e. PBO\XMSvc)


4.    The database is created by the “First Time Use mode


5.    Finish the “First Time Use mode” configuration with the specific options for your XenMobile design.

Joining the Citrix XenMobile database to a (Basic) AlwaysOn Availability Group

At this point, we want to join the “CTX-XM” database to a new Microsoft SQL multi-subnet Basic AlwaysOn Availability Group (BAG). When you have a Citrix XenMobile environment running without BAG, you can also use these steps to move to a BAG setup.

1.    Open SQL Management Studio
2.    Right click the CTX-XM database and click Properties


3.    Click Options and ensure Recovery model is Full. Click OK


4.    Right click CTX-XM and click Tasks --> Back Up..


5.    Create a Full backup of the CTX-XM database:


6.    Right click Availability Groups and click New Availability Group Wizard…


7.    Click Next


8.    Enter a name for the Availability Group (i.e. BAG-CTX-XM) and click Next


9.    Select the CTX-XM database and click Next


10.    Click Add Replica…


11.    Login to the second SQL node:


12.    Select Automatic Failover and ensure Availability Mode is Synchronous commit. Click Listener tab:


13.    Configure DNS-name and static IPs for the SQL AlwaysOn listener, click Next:


14.    Use a SMB share for initial seeding, click next:


15.    Availability Group validation. Click Next


16.    Verify Summary and click Finish


17.    Creating the AlwaysOn Availability group


18.    When everything is OK. Click Close:


19.    The availability group is created:



Reconfigure Windows Failover Cluster network parameters for BAG-CTX-XM Availability group

As I’ve mentioned in the Citrix XenApp/XenDesktop multi-subnet AlwaysOn blog, Windows Failover Cluster is creating a round-robin DNS A-record for the multi-subnet Availability group. This is also the default after creating a multi-subnet Availability group for XenMobile:


Since XenMobile is supporting (Basic) Availability groups https://docs.citrix.com/en-us/xenmobile/server/system-requirements.html


The SQL driver in the XenMobile appliance isn’t aware or configurable with the MultiSubnetFailover=true for the SQL connection string. Due to this, the XenMobile appliance will resolve the IP-addresses of the BAG with a round robin mechanism. Result: connection issues to the XenMobile database when the off-line IP off the SQL listener is resolved. Other Citrix products like Provisioning Services, XenApp or XenDesktop are configurable with the MultiSubnetFailover=true connection string.
 

To fix this issue for XenMobile, we can reconfigure the network parameters for the AlwaysOn XenMobile Availability group listener to only register the on-line IP to the DNS A-record. To force a recheck of the operating system, we reconfigure the network parameters to register a short TTL to the DNS record. This will force the XenMobile appliance to check if the IP is changed on the DNS every minute.

Reconfigure the network parameters for the SQL AlwaysOn XenMobile listener as follows:


1.    Login to one SQL nodes of the Windows Failover Cluster (i.e. PBO-SQL01)
2.    Start Powershell elevated


3.    Type Import-Module FailoverClusters to import the powershell modules:


4.    Type Get-ClusterResource and find the Cluster Resource Network Name for your XenMobile BAG. In this example BAG-CTX-XM_BAG-CTX-XM


5.    Reconfigure Cluster parameters as follows (replace BAG-CTX-XM_BAG-CTX-XM with the name of your WFC configuration):


Import-Module FailoverClusters 

Get-ClusterResource BAG-CTX-XM_BAG-CTX-XM | Set-ClusterParameter RegisterAllProvidersIP 0  

Get-ClusterResource BAG-CTX-XM_BAG-CTX-XM |Set-ClusterParameter HostRecordTTL 60 

Stop-ClusterResource BAG-CTX-XM_BAG-CTX-XM

Start-ClusterResource BAG-CTX-XM_BAG-CTX-XM

Then wait or force DNS replication to all your DNS-servers. After that flush DNS information on all the WFC nodes of the SQL AlwaysOn cluster and the XenMobile appliances.


6.    You will now have only one A-record with the active IP for the BAG:



7.    And after a failover to the other SQL server in the other subnet:


8.    The DNS record is adjusted:


As described in this blog I have a DNS-server PBO-DC01 in Amersfoort and a PBO-DC02 in Nijkerk. It is important that the XenMobile appliance (PBO-XM01) and SQL Server (PBO-SQL01) are using the DNS-server (PBO-DC01) as primary DNS-server in Amersfoort. For the Nijkerk site it is important that the XenMobile Appliance (PBO-XM02) and SQL Server (PBO-SQL02) are using DNS-server (PBO-DC02) as the primary DNS-server. The Windows Failover Cluster will contact the primary DNS-server to reconfigure the DNS-records in case of a failover. So if SQL and XenMobile appliances are configured the same way as WFC/SQL, XenMobile will notice the A-record change within 1 minute since it is contacting the same primary DNS-server.

Reconfigure Citrix XenMobile to use the multi-subnet Basic Availability Group

You can reconfigure XenMobile as follows:

1.    Login to the console of your XenMobile appliance and select 0 (Configuration) from the menu:


2.    Select option 3 (Database) from the menu:


3.    Reconfigure the server property to connect to the BAG FQDN:


4.    Reboot the appliance:


5.    Rebooting


6.    XenMobile is now reconfigured using the BAG: