CUGC Blogs

Automating Citrix Cloud & Windows Virtual Desktop

By Chris Jeucken posted 10-16-2019 07:03 AM

  

Automating Citrix Cloud & Windows Virtual Desktop

by Chris Jeucken and Chris Twiest



(See the script in action! Watch the webinar recording 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.

K90iCeR0CuuqcTAuyRAA_Screenshot1.jpg



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.
Have questions? Ask them in this forum thread!


# 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/a530vdfmj3eq/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*
        # -------------------------------

        # 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 for a demo and discussion around the script.)


#CitrixCloud
#Automation
#PowerShell
#Virtual_Desktops
#WindowsVirtualDesktops