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.)