Citrix Cloud Blog - October 2019

Automating Citrix Cloud & Windows Virtual Desktop

NOTE: This blog was also posted on MyCUGC.org (link) on October 16th, 2019.

(See the script in action! Watch the webinar recording (YouTube) for a demo and discussion around the script.)

Introduction

The year 2019 has been all about Windows Virtual Desktop. If you are even slightly active in the IT circles on social media, you have definitely read about it. The most interesting part about it is the fact that it finally turns Windows 10 into a multi-user OS. Of course, there are other benefits (access to FSLogix!), but that’s not what this blog is about, nor is it about Citrix’s reaction to it in the form of Citrix Managed Desktop. As the title suggests, it’s about automation. It’s about another challenge to automate something that isn’t automated (yet) out of the box.

This time, we will be trying to automate the Azure deployment of a Citrix Cloud Connector machine together with a Windows 10 multi-user VM, all the way until it is ready to accept user sessions. We will walk you through the challenges we had, the issues we ran into and why we are even doing this (short answer: because we can).

Rules of engagement

Last year we did a similar thing but with App-layering. We created a goal for ourselves (automate App Layer creation in VMware App Volumes and Citrix App Layering) and setup some rules to which the resulting scripts should adhere.

This time we had the same approach: We created another goal for ourselves (automate the deployment of a basic Citrix Cloud and Windows 10 MU environment) and set up some rules of engagement which were as follows:

  • The script can be run from any Windows device as long as there is an internet connection
  • The scripting language is PowerShell
  • Credentials should be stored as much as possible in parameters
  • The script must run with minimum user input
  • Clean up any resources that are no longer needed
  • Proof of concept

What should we automate?

When beginning something like this, it’s usually a good idea to sum up all the actions that need to be automated. This we did and it resulted in the following list:

  • Citrix Cloud – Sign in
  • Citrix Cloud – Create resource location
  • Azure – Sign in
  • Azure – Create Cloud Connector VM
  • Azure – Download and install Cloud Connector software on Cloud Connector VM
  • Azure – Create Windows 10 MU (WVD) VM
  • Azure – Download and install VDA on W10MU VM
  • Citrix Cloud – Create Machine Catalog in Citrix Cloud CVAD
  • Citrix Cloud – Create Delivery Group in Citrix Cloud CVAD
  • Citrix Cloud – Assign users to Delivery Group

We divided each action between each other and started scripting and after a couple of weeks of tinkering we ended up with the script you see at the bottom of this blogpost.

Issues

Of course we ran into some problems and we will go into some of them.

Automation of Citrix Cloud Secure Client creation
To do anything with Citrix Cloud through PowerShell you need a Secure Client. This is a combination of a Client ID and a Client Secret (basically two strings). This Secure Client needs to be created on the Citrix Cloud website. In essence, this creation process is just your browser sending some commands to a web server. The developer mode of your browser is able to show these commands (POST, GET, etc.) and you can then reproduce these with a Invoke-WebRequest command and add them to your script. While this isn’t that hard for some websites, we had a hard time getting this Secure Client part to work and could not get it going in a reasonable amount of time.

So, this created a prerequisite for using the script: Create a Secure Client manually and add the ID and Secret to the script variables.

Remotely run scripts on Azure hosted virtual machines
We needed to find a way to remotely download and install the Cloud Connector and Virtual Desktop Agent software on the Azure hosted machines. We basically had three options for this. The first was giving the machines a public IP address and use a remote PowerShell session. But this was not what we wanted because of obvious security-related reasons.

The second is using AzureAutomation, which requires a so-called hybrid worker. This would make the whole script much more complex because we first would need to configure AzureAutomation, install the hybrid worker, prepare the runbooks, etc.

The third option is using custom script extensions. These extensions are collections of one or more files and have a dedicated run command. That means you specify how the extension should be executed and when you apply it to a VM it will do exactly that. The contents can be a single script file or an elaborate collection of installation files and scripts. You basically create a storage container, put any file/script/software you like in it. After that, you can create the custom script extension in which you specify the storage container, the appropriate run command and the name of the VM (and some other parameters like resource location etc.) and it will run it shortly after.

This third option is the way we went because we could put it all in the same script and keep it manageable.

Setup a hosting connection from Citrix Cloud to Azure
When you want to use managed machines in Citrix Virtual Apps and Desktops, you need a hosting connection. This hosting connection allows you to use Machine Creation Services and power management, for example. The same holds for CVAD in Citrix Cloud. In our automation-project, we would need to create this connection between our CVAD environment and the Azure tenant. However, we weren’t able to automate this. This is because it requires setting up the appropriate API rights and as far as we could find out, you cannot fully automate this part. This would result in more prerequisites for the script which we didn’t like and therefore we decided to skip the entire hosting connection. So session hosts created by this script will not be power managed by CVAD.

Using Citrix Cloud APIs from the developer site
When creating this script we went through a lot of trial and error. A small part of this was due to the Citrix Cloud API not being documented that great. Now don’t take this the wrong way, because there is a lot of information about the API on the Citrix developer site (https://developer.cloud.com) and you can definitely do some cool stuff with it. But some commands didn’t behave as described and we couldn’t get it to work to our liking. Therefore we scripted the creation of the Machine Catalog and the Delivery Group in the same way as the installation of the VDA and the Cloud Connector software (through a custom script extension).

That was it for the issues that had the most impact on the final result. Of course, we had more issues, but the cause for most of them was us being idiots.

Setting up the script for your environment

As for the final result, how can you use/try this for yourselves? Of course, you need to define your own variables. We divided them into ‘user variables’ and ‘non-user variables’. The user variables are specific for your own Citrix Cloud and Azure environments so these need to be changed for your situation.

The non-user variables do not have to be changed (the script will work with the current values), although it might be a good idea to change them anyway, because they probably are not to your liking.

When running the script, it will ask you to input some information:

  • Azure credentials
    It will show a popup where you can enter your Azure credentials.
    Note: When using Visual Studio Code (which is awesome) the popup might be shown behind the VSC window.
  • MyCitrix credentials
    These are for the Citrix website to download the VDA software.
    Note: These will be tested and the script will exit if it produces a sign-in failure.
  • Local administrator name and password
    This is for creation of a local administrator account on the Azure hosted virtual machines that will be created.
  • Domain join credentials
    It will need the credentials for an account that has the permissions to add the virtual machines to your Azure domain.

After this, the script will run uninterrupted. It will install PowerShell modules if needed. It will create resource groups and storage accounts if needed and remove anything that is no longer needed after the script. The script should take about 30 to 40 minutes and the end result will be a VM that is setup as a Citrix Cloud Connector and a VM that has the VDA software installed and registers itself with this Cloud Connector. There will also be a Machine Catalog and a Delivery Group and you should be able to start an HDX session through your Citrix Cloud store.

At the end, it will display a short list of the things that were created. Along with that it will also retrieve the access URL from the Citrix Cloud Workspace Configuration. This can be used to start on the deployed desktop.

Final words

Now, we know this is not a script that is ready for everyone and it certainly isn’t perfect. Feel free to use and abuse it, change it or take anything from it for your own Automation projects. We actually did the same with the code that downloads the VDA software (stolen from CTP Ryan Butler) and the code that retrieves the bearer token from Citrix Cloud (stolen from fellow CTA Eltjo van Gulik). Thanks a lot for this guys!

So that’s about it. We would very much like to hear your feedback and are open for any suggestions (or complaints) that we could use to make this script even better. Until the next blog and happy automating!

– Chris Jeucken & Chris Twiest

See the script in action! Watch the webinar recording for a demo and discussion around the script.

# SCRIPT INFO -------------------
# --- Create Virtual Desktop in Citrix Cloud ---
# By Chris²
# v0.1
# -------------------------------
# Run on management machine
# Requires -RunAsAdministrator (or elevated PowerShell session)
# Requires existing domain controller (powered on!)
# Requires a Citrix Cloud API key see --> https://docs.citrix.com/en-us/citrix-cloud/citrix-cloud-management/identity-access-management.html
# -------------------------------

# USER VARIABLES ----------------
# Set Citrix Cloud credentials
$CTXCloudCustomerID = '?' # <-- Found in CSV file when creating a Citrix Cloud Secure Client
$CTXCloudClientID = "?" # <-- Found in CSV file when creating a Citrix Cloud Secure Client
$CTXCloudClientSecret = "?" # <-- Found in CSV file when creating a Citrix Cloud Secure Client
# Set Azure specifics - Must be valid
$AzureResourceGroupLocation = "westeurope"
$AzureVNetName = "ChrisLab-WestEurope-vnet" # <<-- must have domain controller on network
$AzureSubnetName = "default"
# Set Azure specifics - Will be created if needed
$AzureResourceGroupName = "Chrislab-WestEurope"
$AzureVNetResourceGroupName = "Chrislab-WestEurope"
$AzureDiagnosticsStorageAccountName = "chrislabwesteuropediag" # <<-- Must be all lower case
$AzureDiagnosticResourceGroupName = "Chrislab-WestEurope"
# Miscellaneous
$DomainName = "christraining.nl"
# -------------------------------

# NON-USER VARIABLES ------------
# Set Citrix Cloud credentials
$CTXCloudResourceLocation = "SCRIPT-ResourceLocation"
$CTXCloudMachineCatalogName = "SCRIPT - Machine Catalog - WVD"
$CTXCloudDeliveryGroupName = "SCRIPT - Delivery Group - WVD"
$CTXCloudDesktopName = "Desktop W10 MU"
# Set Azure specifics
$AzureStorageAccountName = "citrixdeploymentauto" # <<-- Must be all lower case
$AzureVMCCDeploymentTemplateFile = "https://pastebin.com/raw/D56BpnY1"
$AzureVMW10MUDeploymentTemplateFile = "https://pastebin.com/raw/ewviUySp"
# Set virtual machine specifics
$CloudConnectorMachineName = "Script-cc01"
$CloudConnectorMachineType = "Standard_DS1_v2"
$CloudConnectorDiskType = "Premium_LRS"
$W10MUMachineName = "Script-VDA01"
$W10MUMachineType = "Standard_D2s_v3"
$W10MUDiskType = "Premium_LRS"
# Miscellaneous
$LocalTempFolder = "C:\Temp"
$UsersGroupName = "Domain Users"
# -------------------------------

# PREREQUISITES -----------------
# Setup script running time
$ScriptStopWatch = [System.Diagnostics.StopWatch]::StartNew()

# Check if user is admin and script is running elevated
$CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (!($CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
Write-Host "User does not have admin rights. Are you running this in an elevated session?" -ForegroundColor Red
Write-Host "Stopping script." -ForegroundColor Red
Return
}

# Enable TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# -------------------------------

# FUNCTIONS ---------------------
Function Add-JDAzureRMVMToDomain {
param(
[Parameter(Mandatory = $true)]
[string]$DomainName,
[Parameter(Mandatory = $false)]
[System.Management.Automation.PSCredential]$Credentials = $ADCredentials,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('VMName')]
[string]$Name,
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateScript( { Get-AzureRmResourceGroup -Name $_ })]
[string]$ResourceGroupName
)
begin {
# Define domain join settings (username/domain/password)
$Settings = @{
Name = $DomainName
User = $Credentials.UserName
Restart = "true"
Options = 3
}
$ProtectedSettings = @{
Password = $Credentials.GetNetworkCredential().Password
}
Write-Verbose -Message "Domainname is: $DomainName"
}
process {
try {
$RG = Get-AzureRmResourceGroup -Name $ResourceGroupName
$JoinDomainHt = @{
ResourceGroupName = $RG.ResourceGroupName
ExtensionType = 'JsonADDomainExtension'
Name = 'joindomain'
Publisher = 'Microsoft.Compute'
TypeHandlerVersion = '1.0'
Settings = $Settings
VMName = $Name
ProtectedSettings = $ProtectedSettings
Location = $RG.Location
}
Write-Verbose -Message "Joining $Name to $DomainName"
Set-AzureRMVMExtension @JoinDomainHt
}
catch {
Write-Warning $_
}
}
end { }
}

Function RegisterRP {
Param(
[string]$ResourceProviderNamespace
)

Write-Host "Registering Azure resource provider '$ResourceProviderNamespace'";
Register-AzureRmResourceProvider -ProviderNamespace $ResourceProviderNamespace;
}
# -------------------------------

# MODULES-1 ---------------------
# Azure - Import necessary modules
Write-Host "1. Import necessary PowerShell modules - Part 1" -ForegroundColor Green

# Azure Resource Manager module
if (Get-Module -ListAvailable -Name AzureRM) {
Write-Host "Azure RM module already available, importing..." -ForegroundColor Yellow
Import-Module AzureRM | Out-Null
} else {
Write-Host "Azure RM module not yet available, installing..." -ForegroundColor Yellow
Install-Module -Name AzureRM -scope AllUsers -Confirm:$false -force
Import-Module AzureRM | Out-Null
}
# -------------------------------

# AUTHENTICATION ----------------
Write-Host "2. Ask user for credentials" -ForegroundColor Green

# Azure
Write-Host "*** Azure login ***" -ForegroundColor Yellow
Login-AzureRmAccount

# Citrix
Write-Host "*** Citrix ***" -ForegroundColor Yellow
Write-Host "MyCitrix credentials (for downloading the VDA)"
$MyCitrixUserName = Read-Host "Please supply your MyCitrix username"
$MyCitrixPassword1 = Read-Host "Please supply your MyCitrix password" -AsSecureString
$MyCitrixPassword2 = Read-Host "Please supply your MyCitrix password once more" -AsSecureString
$MyCitrixPassword1Temp = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($MyCitrixPassword1))
$MyCitrixPassword2Temp = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($MyCitrixPassword2))

if ($MyCitrixPassword1Temp -ne $MyCitrixPassword2Temp) {
Write-Host "The supplied MyCitrix passwords are not the same" -ForegroundColor Red
Return
}
Remove-Variable -Name MyCitrixPassword1Temp,MyCitrixPassword2Temp

$CitrixCredentials = New-Object System.Management.Automation.PSCredential ($MyCitrixUserName, $MyCitrixPassword1)
#$CitrixCredentials = Get-Credential -Message "Please supply your MyCitrix credentials (for downloading the VDA)"

# Verify Citrix credentials
# Ryan Butler TechDrabble.com @ryan_c_butler 07/19/2019
$CitrixUserName = $CitrixCredentials.UserName
$CitrixPassword = $CitrixCredentials.GetNetworkCredential().Password

# Initialize Session
Invoke-WebRequest "https://identity.citrix.com/Utility/STS/Sign-In?ReturnUrl=%2fUtility%2fSTS%2fsaml20%2fpost-binding-response" -SessionVariable CTXWebSession -UseBasicParsing | Out-Null

# Authenticate
$WebFormAuth = @{
"persistent" = "on"
"userName" = $CitrixUserName
"password" = $CitrixPassword
}

$LoginWebRequest = Invoke-WebRequest -Uri ("https://identity.citrix.com/Utility/STS/Sign-In?ReturnUrl=%2fUtility%2fSTS%2fsaml20%2fpost-binding-response") -WebSession $CTXWebSession -Method POST -Body $WebFormAuth -ContentType "application/x-www-form-urlencoded" -UseBasicParsing

if (!($LoginWebRequest.Content.Contains("You are signed in as $CitrixUserName"))) {
Write-Host "MyCitrix credentials not correct. Please rerun the script." -ForegroundColor Red
Return
}

# Virtual machines - Local administrator
Write-Host "*** Virtual machine - Local administrator ***" -ForegroundColor Yellow
Write-Host "Please enter the Windows administrator credentials to be set on the Cloud Connector" -ForegroundColor Yellow
$CloudConnectorAdminUsername = Read-Host "Username"
$CloudConnectorAdminPassword = Read-Host "Password" -AsSecureString
Write-Host "Please enter the Windows administrator credentials to be set on the Windows 10 multi-user virtual machine" -ForegroundColor Yellow
$W10MUAdminUsername = Read-Host "Username"
$W10MUAdminPassword = Read-Host "Password" -AsSecureString

# Virtual machines - Domain join
Write-Host "*** Virtual machine - Domain join ***" -ForegroundColor Yellow
Write-Host "Enter the credentials for a user that is allowed to join machines to the domain" -ForegroundColor Yellow
$ADCredentials = Get-Credential

# -------------------------------

# MODULES-2 ---------------------
# Azure - Import necessary modules
Write-Host "3. Import necessary PowerShell modules - Part 2" -ForegroundColor Green

# Azure Active Directory module
if (Get-Module -ListAvailable -Name AzureAD) {
Write-Host "Azure AD module already available, importing..." -ForegroundColor Yellow
Import-Module AzureAD | Out-Null
} else {
Write-Host "Azure AD module not yet available, installing..." -ForegroundColor Yellow
Install-Module -Name AzureAD -Scope AllUsers -Confirm:$false -Force
Import-Module AzureAD | Out-Null
}

# Remote Desktop Infrastructure module
if (Get-Module -ListAvailable -Name Microsoft.RDInfra.RDPowerShell) {
Write-Host "WVD RDInfra module already available, importing..." -ForegroundColor Yellow
Import-Module Microsoft.RDInfra.RDPowerShell | Out-Null
} else {
Write-Host "WVD RDInfra module not yet available, installing..." -ForegroundColor Yellow
Install-Module -Name Microsoft.RDInfra.RDPowerShell -scope AllUsers -Confirm:$false -force
Import-Module Microsoft.RDInfra.RDPowerShell | Out-Null
}
# -------------------------------
# SCRIPT ------------------------
# CTXCloud - Get bearer token
Write-Host "4. CTXCloud - Get bearer token" -ForegroundColor Green
$Body = @{
"ClientId" = $CTXCloudClientID;
"ClientSecret" = $CTXCloudClientSecret
}
$PostHeaders = @{
"Content-Type" = "application/json"
}

$TrustURL = "https://trust.citrixworkspacesapi.net/root/tokens/clients"
$Response = Invoke-RestMethod -Uri $TrustURL -Method POST -Body (ConvertTo-Json -InputObject $Body) -Headers $PostHeaders
$BearerToken = $Response.token
$Token = "CwsAuth Bearer=" + $BearerToken

# CTXCloud - Create Resource Location and get StoreFront configuration
Write-Host "5. CTXCloud - Create Resource Location" -ForegroundColor Green
$Body = @{
"Name" = $CTXCloudResourceLocation
}
$Headers = @{
"Accept" = "application/json";
"Authorization" = $Token;
"Content-Type" = "application/json"
}
$Json = ConvertTo-Json -InputObject $Body

$ResourceURL = "https://registry-westeurope-release-a.citrixworkspacesapi.net/" + $CTXCloudCustomerID + "/resourcelocations"
$Resource = Invoke-WebRequest -Method POST -uri $ResourceURL -body $json -Headers $headers -UseBasicParsing

$CTXCloudResourceID = ($Resource.Content | ConvertFrom-Json).ID

$WorkspaceConfigurationURL = "https://storefrontconfiguration-westeurope-release-a.citrixworkspacesapi.net/" + $CTXCloudCustomerID + "/storeconfigs"
$WorkspaceConfiguration = Invoke-WebRequest -Method GET -Uri $WorkspaceConfigurationURL -Headers $Headers -UseBasicParsing -ErrorAction SilentlyContinue | ConvertFrom-Json

if ($WorkspaceConfiguration.Items.StoreFrontDomains) {
$CTXCloudAccessURL = $WorkspaceConfiguration.Items.StoreFrontDomains
} else {
$CTXCloudAccessURL = "Not set (yet?)"
}
# Create Azure storage account
Write-Host "6. Azure - Create Azure resource groups (if needed)" -ForegroundColor Green

# Check for existing resource group and create new one if needed
$AzureResourceGroup = Get-AzureRmResourceGroup -Name $AzureResourceGroupName -ErrorAction SilentlyContinue
if (!$AzureResourceGroup) {
Write-Host "Resource group '$AzureResourceGroupName' does not exist yet" -ForegroundColor Yellow
Write-Host "Creating resource group '$AzureResourceGroupName' in location '$AzureResourceGroupLocation'" -ForegroundColor Yellow
New-AzureRmResourceGroup -Name $AzureResourceGroupName -Location $AzureResourceGroupLocation
} else {
Write-Host "Using existing resource group '$AzureResourceGroupName'" -ForegroundColor Yellow
}

if ($AzureVNetResourceGroupName -ne $AzureResourceGroupName) {
Write-Host "Different resource group specified for Virtual Networks" -ForegroundColor Yellow
$AzureVNetResourceGroup = Get-AzureRmResourceGroup -Name $AzureVNetResourceGroupName -ErrorAction SilentlyContinue
if (!$AzureVNetResourcegroup) {
Write-Host "Virtual network resource group '$AzureVNetResourceGroupName' does not exist yet" -ForegroundColor Yellow
Write-Host "Creating virtual network resource group '$AzureVNetResourceGroupName' in location '$AzureResourceGroupLocation'" -ForegroundColor Yellow
New-AzureRmResourceGroup -Name $AzureVNetResourceGroupName -Location $AzureResourceGroupLocation
} else {
Write-Host "Using existing virtual network resource group '$AzureVNetResourceGroupName'" -ForegroundColor Yellow
}
} else {
Write-Host "Specified virtual network resource group is identical to the VM resource group" -ForegroundColor Yellow
}
if ($AzureDiagnosticResourceGroupName -ne $AzureResourceGroupName) {
Write-Host "Different resource group specified for diagnostic information" -ForegroundColor Yellow
$AzureDiagnosticResourceGroup = Get-AzureRmResourceGroup -Name $AzureDiagnosticResourceGroupName -ErrorAction SilentlyContinue
if (!$AzureDiagnosticResourceGroup) {
Write-Host "Diagnostic resource group '$AzureDiagnosticResourceGroupName' does not exist yet" -ForegroundColor Yellow
Write-Host "Creating diagnostic resource group '$AzureDiagnosticResourceGroupName' in location '$AzureResourceGroupLocation'" -ForegroundColor Yellow
New-AzureRmResourceGroup -Name $AzureDiagnosticResourceGroupName -Location $AzureResourceGroupLocation
} else {
Write-Host "Using existing diagnostic virtual network resource group '$AzureDiagnosticResourceGroupName'" -ForegroundColor Yellow
}
} else {
Write-Host "Specified diagnostic resource group is identical to the VM resource group" -ForegroundColor Yellow
}

# Create Azure storage account
Write-Host "7. Azure - Create Azure storage accounts (if needed)" -ForegroundColor Green

# Check for existing storage accounts and create new ones if needed
if ($AzureStorageAccount = (Get-AzureRmStorageAccount -ResourceGroupName $AzureResourceGroupName -Name $AzureStorageAccountName -ErrorAction SilentlyContinue).StorageAccountName) {
Write-Host "Azure storage account already exists" -ForegroundColor Yellow
} else {
Write-Host "Azure storage account does not exist yet, creating..." -ForegroundColor Yellow
$AzureStorageAccount = (New-AzureRmStorageAccount -ResourceGroupName $AzureResourceGroupName -Name $AzureStorageAccountName -SkuName Standard_GRS -Location $AzureResourceGroupLocation).StorageAccountName
}

if (Get-AzureRmStorageAccount -ResourceGroupName $AzureResourceGroupName -Name $AzureDiagnosticsStorageAccountName -ErrorAction SilentlyContinue) {
Write-Host "Azure diagnostics storage account already exists" -ForegroundColor Yellow
} else {
Write-Host "Azure diagnostics storage account does not exist yet, creating..." -ForegroundColor Yellow
New-AzureRmStorageAccount -ResourceGroupName $AzureResourceGroupName -Name $AzureDiagnosticsStorageAccountName -SkuName Standard_LRS -Location $AzureResourceGroupLocation
}

# Check for existing storage keys and create new one if needed
if ($AzureStorageKeys = Get-AzureRMStorageAccountKey -ResourceGroupName $AzureResourceGroupName -Name $AzureStorageAccount -ErrorAction SilentlyContinue | Where-Object{$_.KeyName -eq "Key1"}) {
Write-Host "Azure storage key already exists" -ForegroundColor Yellow
} else {
Write-Host "Azure storage key does not exist yet, creating..." -ForegroundColor Yellow
$AzureStorageKeys = New-AzureRmStorageAccountKey -ResourceGroupName $AzureResourceGroupName -Name $AzureStorageAccount -KeyName "Key1"
}

$AzureStorageSAKey = ($AzureStorageKeys | Where-Object {$_.KeyName -eq "Key1"}).Value

# Various
$ResourceProviders = @("microsoft.resources", "microsoft.compute");
if ($ResourceProviders.Length) {
Write-Host "Registering Resource Providers" -ForegroundColor Yellow
foreach ($ResourceProvider in $ResourceProviders) {
RegisterRP($ResourceProvider);
}
}

if (!(Test-Path -Path $LocalTempFolder -ErrorAction SilentlyContinue)) {
New-Item -ItemType Directory -Path $LocalTempFolder -Force
}
$AzureSubscription = Get-AzureRmSubscription | Select-Object -First 1
$AzureSubscriptionId = $AzureSubscription.Id

# Azure - Create Cloud Connector virtual machine
Write-Host "8. Azure - Create Cloud Connector Virtual Machine" -ForegroundColor Green

# Various
$CloudConnectorNIName = $CloudConnectorMachineName + "901"

# Start the deployment
Write-Host "Starting virtual machine deployment. This can take some time (~5min)..." -ForegroundColor Yellow
New-AzureRmResourceGroupDeployment -ResourceGroupName $AzureResourceGroupName -Name "CloudConnector" -TemplateUri $AzureVMCCDeploymentTemplateFile `
-Location $AzureResourceGroupLocation `
-NetworkInterfaceName $CloudConnectorNIName `
-SubnetName $AzureSubnetName `
-VirtualNetworkId "/subscriptions/$AzureSubscriptionId/resourceGroups/$AzureVNetResourceGroupName/providers/Microsoft.Network/virtualNetworks/$AzureVNetName" `
-VirtualMachineName $CloudConnectorMachineName `
-VirtualMachineRG $AzureResourceGroupName `
-OSDiskType $CloudConnectorDiskType `
-VirtualMachineSize $CloudConnectorMachineType `
-AdminUsername $CloudConnectorAdminUsername `
-AdminPassword $CloudConnectorAdminPassword `
-DiagnosticsStorageAccountName $AzureDiagnosticsStorageAccountName `
-DiagnosticsStorageAccountId "/subscriptions/$AzureSubscriptionId/resourceGroups/$AzureDiagnosticResourceGroupName/providers/Microsoft.Storage/storageAccounts/$AzureDiagnosticsStorageAccountName"

# Domain join
Write-Host "Cloud connector VM created, joining machine to domain and restarting" -ForegroundColor Yellow
Start-Sleep -Seconds 30
Get-AzureRmVM -ResourceGroupName $AzureResourceGroupName | Where-Object { $_.Name -like $CloudConnectorMachineName } | Add-JDAzureRMVMToDomain -DomainName $DomainName -Verbose
Start-Sleep -Seconds 30
Restart-AzureRmVM -ResourceGroupName $AzureResourceGroupName -Name $CloudConnectorMachineName -Verbose

# CTXCloud/Azure - Deploy Citrix Cloud Connector software
Write-Host "9. CTXCloud/Azure - Deploy Cloud Connector software" -ForegroundColor Green
$AzureStorageContainerName = "cloudconinstaller"

# Create Cloud Connector deployment script
$DeployCloudConnectorScriptContent = "
`$CTXCloudCustomerID = `"$CTXCloudCustomerID`"
`$CTXCloudClientId = `"$CTXCloudClientID`"
`$CTXCloudClientSecret = `"$CTXCloudClientSecret`"
`$CTXCloudResourceID = `"$CTXCloudResourceID`"

`$DownloadLocCloudConnector = `"https://downloads.cloud.com/`" + `$CTXCloudCustomerID + `"/connector/cwcconnector.exe`"
`$TargetLocCloudConnector = `"C:\cwcconnector.exe`"
# Download Citrix Cloud Connector
if (!(Test-Path -Path `$TargetLocCloudConnector)) {
Write-Host `"Download Citrix Cloud Connector`" -ForegroundColor Yellow
`$StartTimeDownloadCloudConnector = Get-Date
(New-Object System.Net.WebClient).DownloadFile(`$DownloadLocCloudConnector, `$TargetLocCloudConnector)
Write-Host `"Time taken: `$((Get-Date).Subtract(`$StartTimeDownloadCloudConnector).Seconds) second(s)`"
}

`$Arguments = `"/q /customername:`$CTXCloudCustomerID /clientid:`$CTXCloudClientid /clientsecret:`$CTXCloudClientSecret /location:`$CTXCloudResourceID /acceptTermsofservice:true`"
Start-Process `$TargetLocCloudConnector `$Arguments -Wait"

$ScriptFile = "InstallCloudCon.ps1"
$LocalScriptFile = "$LocalTempFolder\$ScriptFile"
Set-Content -Path $LocalScriptFile -Value $DeployCloudConnectorScriptContent -Force

$TempScriptContent = Get-Content -Path $LocalTempFolder\$ScriptFile
$TempScriptContent = $TempScriptContent -Replace "\?", ""
Set-Content -Path $LocalScriptFile -Value $TempScriptContent -Force

# Upload Cloud Connector deployment script
$AzureStorageContext = New-AzureStorageContext -StorageAccountName $AzureStorageAccountname -StorageAccountKey $AzureStorageSAKey
Set-AzureRmCurrentStorageAccount -Context $AzureStorageContext
New-AzureStorageContainer -Name $AzureStorageContainerName
Set-AzureStorageBlobContent -File $LocalScriptFile -container $AzureStorageContainerName -Force

Set-AzureRmVMCustomScriptExtension -Name 'Cloudcon-Installer' -ContainerName $AzureStorageContainerName -FileName $ScriptFile -StorageAccountName $AzureStorageAccountName -ResourceGroupName $AzureResourceGroupName -VMName $CloudConnectorMachineName -Run "installcloudcon.ps1" -Location $AzureResourceGroupLocation
Start-Sleep -Seconds 10

Write-Host "Citrix Cloud Connector installation succesful, cleaning up..." -ForegroundColor Yellow
# Remove Extension and Script
Remove-AzureRmVMCustomScriptExtension -ResourceGroupName $AzureResourceGroupName -VMName $CloudConnectorMachineName -Name 'Cloudcon-Installer' -Force

# Delete storage container
Remove-AzureStorageContainer -name $AzureStorageContainerName -Force
# WVD/Azure - Create Windows 10 multi-user virtual machine
Write-Host "10. WVD/Azure - Create Windows 10 multi-user virtual machine" -ForegroundColor Green

# Various
$W10MUNIName = $W10MUMachineName + "901"

# Start the deployment
Write-Host "Starting virtual machine deployment. This can take some time (~5min)..." -ForegroundColor Yellow
New-AzureRmResourceGroupDeployment -ResourceGroupName $AzureResourceGroupName -Name "VDA01" -TemplateUri $AzureVMW10MUDeploymentTemplateFile `
-Location $AzureResourceGroupLocation `
-NetworkInterfaceName $W10MUNIName `
-SubnetName $AzureSubnetName `
-VirtualNetworkId "/subscriptions/$AzureSubscriptionId/resourceGroups/$AzureVNetResourceGroupName/providers/Microsoft.Network/virtualNetworks/$AzureVNetName" `
-VirtualMachineName $W10MUMachineName `
-VirtualMachineRG $AzureResourceGroupName `
-OSDiskType $W10MUDiskType `
-VirtualMachineSize $W10MUMachineType `
-AdminUsername $W10MUAdminUsername `
-AdminPassword $W10MUAdminPassword `
-DiagnosticsStorageAccountName $AzureDiagnosticsStorageAccountName `
-DiagnosticsStorageAccountId "/subscriptions/$AzureSubscriptionId/resourceGroups/$AzureDiagnosticResourceGroupName/providers/Microsoft.Storage/storageAccounts/$AzureDiagnosticsStorageAccountName" `
-ErrorAction Stop
# Domain join
Write-Host "Virtual Desktop VM created, joining machine to domain and restarting" -ForegroundColor Yellow
Start-Sleep -Seconds 30
Get-AzureRmVM -ResourceGroupName $AzureResourceGroupName | Where-Object { $_.Name -like $W10MUMachineName } | Add-JDAzureRMVMToDomain -DomainName $DomainName -Verbose
Start-Sleep -Seconds 30
Restart-AzureRmVM -ResourceGroupName $AzureResourceGroupName -Name $W10MUMachineName -Verbose

# WVD/Citrix - Deploy Citrix Virtual Desktop Agent
Write-Host "11. WVD/Citrix - Deploy Citrix Virtual Desktop Agent" -ForegroundColor Green

Write-Host "Download VDA software from Citrix" -ForegroundColor Yellow
# Download and install VDA
# Ryan Butler TechDrabble.com @ryan_c_butler 07/19/2019
$CitrixUserName = $CitrixCredentials.UserName
$CitrixPassword = $CitrixCredentials.GetNetworkCredential().Password

$VDADownloadPath = $LocalTempFolder + "\VDAServerSetup_1906.exe"

# Initialize Session
Invoke-WebRequest "https://identity.citrix.com/Utility/STS/Sign-In?ReturnUrl=%2fUtility%2fSTS%2fsaml20%2fpost-binding-response" -SessionVariable CTXWebSession -UseBasicParsing

# Authenticate
$WebFormAuth = @{
"persistent" = "on"
"userName" = $CitrixUserName
"password" = $CitrixPassword
}
Invoke-WebRequest -Uri ("https://identity.citrix.com/Utility/STS/Sign-In?ReturnUrl=%2fUtility%2fSTS%2fsaml20%2fpost-binding-response") -WebSession $CTXWebSession -Method POST -Body $WebFormAuth -ContentType "application/x-www-form-urlencoded" -UseBasicParsing

$DownloadVDA = Invoke-WebRequest -Uri ('https://secureportal.citrix.com/Licensing/Downloads/UnrestrictedDL.aspx?DLID=16110&URL=https://downloads.citrix.com/16110/VDAServerSetup_1906.exe') -WebSession $CTXWebSession -UseBasicParsing -Verbose -Method GET
$WebFormDownload = @{
"chkAccept" = "on"
"__EVENTTARGET" = "clbAccept_0"
"__EVENTARGUMENT" = "clbAccept_0_Click"
"__VIEWSTATE" = ($DownloadVDA.InputFields | Where-Object { $_.id -eq "__VIEWSTATE" }).value
"__EVENTVALIDATION" = ($DownloadVDA.InputFields | Where-Object { $_.id -eq "__EVENTVALIDATION" }).value
}

# Download
Invoke-WebRequest -Uri ("https://secureportal.citrix.com/Licensing/Downloads/UnrestrictedDL.aspx?DLID=16110&URL=https%3a%2f%2fdownloads.citrix.com%2f16110%2fVDAServerSetup_1906.exe") -WebSession $CTXWebSession -Method POST -Body $WebFormDownload -ContentType "application/x-www-form-urlencoded" -UseBasicParsing -OutFile $VDADownloadPath -Verbose

# Upload VDA and install Script to Azure Container
$CTXCloudConnector = $CloudConnectorMachineName + "." + $DomainName
$AzureStorageContainerName = "vdainstaller" # <<-- Must be all lower case

# Create VDA deployment script
$DeployVDAScriptContent = "
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force

`$AzureStorageAccountName = `"$AzureStorageAccountName`"
`$AzureStorageSAKey = `"$AzureStorageSAKey`"
`$CTXCloudConnector = `"$CTXCloudConnector`"
`$AzureStorageContainerName = `"$AzureStorageContainerName`"

Install-PackageProvider -Name NuGet -Confirm:`$false -Force
Install-Module -Name AzureRM -Scope AllUsers -Confirm:`$false -Force
Import-Module AzureRM | Out-Null

`$AzureStorageContext = New-AzureStorageContext -StorageAccountName `$AzureStorageAccountname -StorageAccountKey `$AzureStorageSAKey
`$AzureStorageBlob = Get-AzureStorageBlob -Container `$AzureStorageContainerName -Context `$AzureStorageContext
Get-AzureStorageBlobContent -Container `$AzureStorageContainerName -Blob `"VDAServerSetup_1906.exe`" -Destination `"C:\`" -Context `$AzureStorageContext

`$VDAArguments = `'/quiet /components VDA /controllers `"temp`" /masterimage /noreboot /optimize /disableexperiencemetrics /install_mcsio_driver /enable_hdx_ports /enable_hdx_udp_ports /enable_remote_assistance /exclude `"Citrix User Profile Manager`",`"Citrix User Profile Manager WMI Plugin`",`"Personal vDisk`",`"Citrix Personalization for App-V - VDA`"`'
Start-Process `"C:\VDAServerSetup_1906.exe`" `$VDAArguments -Wait

Set-Itemproperty -Path `'HKLM:\SOFTWARE\Citrix\VirtualDesktopAgent`' -Name 'ListOfDDCs' -value `$CTXCloudConnector"

$LocalScriptFile = $LocalTempFolder + "\InstallVDA.ps1"
Set-Content -Path $LocalScriptFile -Value $DeployVDAScriptContent -Force
$TempContent = Get-Content -Path $LocalScriptFile
$TempContent = $TempContent -Replace "\?", ""
Set-Content -Path $LocalScriptFile -Value $TempContent

# Upload VDA deployment script
$AzureStorageContext = New-AzureStorageContext -StorageAccountName $AzureStorageAccountname -StorageAccountKey $AzureStorageSAKey
Set-AzureRmCurrentStorageAccount -Context $AzureStorageContext
New-AzureStorageContainer -Name $AzureStorageContainerName
Set-AzureStorageBlobContent -File $LocalScriptFile -container $AzureStorageContainerName -Force
Set-AzureStorageBlobContent -File $VDADownloadPath -container $AzureStorageContainerName -Force
# Create custom script extension for installation of VDA and apply it to W10 MU VM
Set-AzureRmVMCustomScriptExtension -Name 'VDA-Installer' -ContainerName $AzureStorageContainerName -FileName "InstallVDA.ps1" -StorageAccountName $AzureStorageAccountName -ResourceGroupName $AzureResourceGroupName -VMName $W10MUMachineName -Run "InstallVDA.ps1" -Location $AzureResourceGroupLocation

Start-Sleep -Seconds 30
Restart-AzureRmVM -ResourceGroupName $AzureResourceGroupName -Name $W10MUMachineName

Write-Host "Citrix VDA installation succesful, cleaning up..." -ForegroundColor Yellow
# Remove script and extension
Remove-AzureRmVMCustomScriptExtension -ResourceGroupName $AzureResourceGroupName -VMName $W10MUMachineName -Name 'VDA-Installer' -Force

# Delete storage container
Remove-AzureStorageContainer -name $AzureStorageContainerName -Force

# WVD/Citrix - Deploy Citrix Virtual Desktop Agent
Write-Host "12. WVD/Citrix - Deploy Citrix Virtual Desktop Agent" -ForegroundColor Green

#Create MC and DG as extension on Cloud Connector
$AzureStorageContainerName = "machinecatalogscript" # <<-- Must be all lower case

$MCName = $CTXCloudMachineCatalogName
$DGName = $CTXCloudDeliveryGroupName
$DesktopName = $CTXCloudDesktopName
# Create machine catalog and delivery group deployment script
$CreateMCandDGscript = "
# VARIABLES ---------------------
# Citrix Cloud
`$DownloadLocPoshSDK = `"https://download.apps.cloud.com/CitrixPoshSdk.exe`"
`$TargetLocPoshSDK = `"C:\CitrixPoshSdk.exe`"
`$PoshSDKSilentArgs = `"/q`"
`$PoshSDKUninstallQuery = `"Citrix Broker PowerShell Snap-In`"
`$CTXCloudCustomerID = `"$CTXCloudCustomerID`"
`$CTXCloudClientID = `"$CTXCloudClientID`"
`$CTXCloudClientSecret = `"$CTXCloudClientSecret`"
`$CTXCloudConnector = `"$CTXCloudConnector`"
`$CTXCloudResourceLocation = `"$CTXCloudResourceLocation`"
`$DGName = `"$DGName`"
`$MCName = `"$MCName`"
`$DesktopName = `"$DesktopName`"
`$DomainName = `"$DomainName`"
`$VDAName = `"$W10MUMachineName`"
`$Users = `"$DomainName`" + `"\`" + `"$UsersGroupName`"
# -------------------------------

# MODULES -----------------------
# Download Citrix Remote PowerShell SDK
if (!(Test-Path -Path `$TargetLocPoshSDK)) {
Write-Host `"Download Citrix Remote PowerShell SDK`" -ForegroundColor Yellow
`$StartTimeDownloadPoshSDK = Get-Date
if ((Get-Service -Name BITS).Status -eq `"Stopped`") {
Write-Host `"BITS service not running. Starting service.`"
Start-Service -Name BITS
}
Import-Module BitsTransfer
Start-BitsTransfer -Source `$DownloadLocPoshSDK -Destination `$TargetLocPoshSDK
Write-Host `"Time taken: `$((Get-Date).Subtract(`$StartTimeDownloadPoshSDK).Seconds) second(s)`"
}
# Install Citrix Remote PowerShell SDK
if (((Get-ItemProperty -Path HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Contains `$PoshSDKUninstallQuery) -ne `$true) {
Write-Host `"Install Citrix Remote PowerShell SDK`" -ForegroundColor Yellow
`$Installation = (Start-Process -FilePath `$TargetLocPoshSDK `$PoshSDKSilentArgs -Wait -PassThru).ExitCode
if (`$Installation -ne `"0`") {
Write-Host `"Installation failed.`" -ForegroundColor Red
Return
}
else {
Write-Host `"Done`"
}
}
# Clean up Citrix Remote PowerShell SDK
if (Test-Path -Path `$TargetLocPoshSDK -ErrorAction SilentlyContinue) {
Write-Host `"Remove Citrix Remote PowerShell SDK installation file`" -ForegroundColor Yellow
Remove-Item -Path `$TargetLocPoshSDK -Force
Write-Host `"Done`"
}
# Import Citrix PowerShell Snap-ins
Write-Host `"Import Citrix PowerShell Snap-ins`" -ForegroundColor Yellow
Add-PSSnapin -Name Citrix*
Add-PSSnapin -Name Citrix*
# -------------------------------

# SCRIPT ------------------------
# Sign into Citrix Cloud

Set-XDCredentials -APIKey `$CTXCloudClientID -SecretKey `$CTXCloudClientSecret -CustomerId `$CTXCloudCustomerID -StoreAs `"CitrixCloud`" -ProfileTyp CloudApi
Get-XDCredentials -ProfileName CitrixCloud
Get-XDAuthentication -ProfileName CitrixCloud

# Get Zone ID
`$zoneid = (get-configzone | Where-Object {`$_.name -eq `$CTXCloudResourceLocation}).Uid.Guid

# Create MC
`$MC = New-BrokerCatalog -AdminAddress `$Cloudconnector -AllocationType `"Random`" -IsRemotePC `$False -MachinesArePhysical `$True -MinimumFunctionalLevel `"L7_20`" -Name `$MCname -PersistUserChanges `"OnLocal`" -ProvisioningType `"Manual`" -Scope @() -SessionSupport `"MultiSession`" -ZoneUid `$zoneid

# Add VDA to MC
`$vda = New-BrokerMachine -AdminAddress `$Cloudconnector -CatalogUid `$MC.Uid -IsReserved `$False -MachineName `"`$DomainName\`$VDAName`"

# Create DG
`$DG = New-BrokerDesktopGroup -Name `$DGName -DeliveryType DesktopsOnly -PublishedName `$DesktopName -AdminAddress `$Cloudconnector -DesktopKind Shared -SessionSupport MultiSession

# Add VDA
Add-BrokerMachinesToDesktopGroup -Catalog `$MC -DesktopGroup `$DG -Count 1 -AdminAddress `$Cloudconnector
# Add Users to DG
New-BrokerEntitlementPolicyRule -Name `$DGName -DesktopGroupUid `$DG.Uid -IncludedUsers `$Users -description `$DGName
New-BrokerAccessPolicyRule -Name `$DGName -IncludedUserFilterEnabled `$true -IncludedUsers `$Users -DesktopGroupUid `$DG.Uid -AllowedProtocols @(`"HDX`",`"RDP`")
# -------------------------------
"

$ScriptFile = "CreateMCandDG.ps1"
$LocalScriptFile = "$LocalTempFolder\$ScriptFile"
Set-Content -Path $LocalScriptFile -Value $CreateMCandDGscript -Force
$TempScriptContent = Get-Content -Path $LocalTempFolder\$ScriptFile
$TempScriptContent = $TempScriptContent -Replace "\?", ""
Set-Content -Path $LocalScriptFile -Value $TempScriptContent -Force

# Upload machine catalog and delivery group deployment script
$AzureStorageContext = New-AzureStorageContext -StorageAccountName $AzureStorageAccountname -StorageAccountKey $AzureStorageSAKey
Set-AzureRmCurrentStorageAccount -Context $AzureStorageContext
New-AzureStorageContainer -Name $AzureStorageContainerName
Set-AzureStorageBlobContent -File $LocalScriptFile -container $AzureStorageContainerName -Force

# Create custom script extenstion
Set-AzureRmVMCustomScriptExtension -Name "MC-and-DG-creation" -ContainerName $AzureStorageContainerName -FileName "CreateMCandDG.ps1" -StorageAccountName $AzureStorageAccountName -ResourceGroupName $AzureResourceGroupName -VMName $CloudConnectorMachineName -Run "CreateMCandDG.ps1" -Location $AzureResourceGroupLocation
Start-Sleep -Seconds 10

Write-Host "Machine catalog and delivery group created succesfully, cleaning up..." -ForegroundColor Yellow
# Remove custom script extension
Remove-AzureRmVMCustomScriptExtension -ResourceGroupName $AzureResourceGroupName -VMName $CloudConnectorMachineName -Name 'MC-and-DG-creation' -Force

# Delete storage container
Remove-AzureStorageContainer -name $AzureStorageContainerName -Force
# Delete storage account
Remove-AzureRmStorageAccount -ResourceGroupName $AzureResourceGroupName -Name $AzureStorageAccountName -force
# -------------------------------

# RESULTS -----------------------
# Present results
Write-Host "13. Present results" -ForegroundColor Green

Write-Host "`n-- AZURE --" -ForegroundColor Cyan
Write-Host "Cloud Connector VM name: " -NoNewline
Write-Host $CloudConnectorMachineName -ForegroundColor Yellow
Write-Host "Windows 10 MU VM name: " -NoNewline
Write-Host $W10MUMachineName -ForegroundColor Yellow

Write-Host "`n-- CITRIX CLOUD --" -ForegroundColor Cyan
Write-Host "Machine Catalog name: " -NoNewline
Write-Host $CTXCloudMachineCatalogName -ForegroundColor Yellow
Write-Host "Delivery Group name: " -NoNewline
Write-Host $CTXCloudDeliveryGroupName -ForegroundColor Yellow

Write-Host "`n-- USER INFORMATION --" -ForegroundColor Cyan
Write-Host "Virtual Desktop access group: " -NoNewline
Write-Host $DomainName\$UsersGroupName -ForegroundColor Yellow
Write-Host "Virtual Desktop access site: " -NoNewline
Write-Host https://$CTXCloudAccessURL -ForegroundColor Yellow
Write-Host "Virtual Desktop name: " -NoNewline
Write-Host $CTXCloudDesktopName -ForegroundColor Yellow
# Present timing
$ScriptStopWatch.Stop()
$ScriptRunningTime = [math]::Round($ScriptStopWatch.Elapsed.TotalMinutes,1)
Write-Host "Script ran for" $ScriptRunningTime "Minutes" -ForegroundColor Magenta
# -------------------------------

(See the script in action! Watch the webinar recording (YouTube) for a demo and discussion around the script.)

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *