Jump to content
Welcome to our new Citrix community!
  • Automating Citrix Cloud & Windows Virtual Desktop


    cugcblogs

    chrisjeuckenrnd.png by Chris Jeucken, CTA christwiestrnd.png Chris Twiest, CTA

    (See the script in action! Watch 

     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.

    jeucken1019-01.jpeg.2f6a21e51982e974c1446b610e5b0d15.jpeg

    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.

    jeucken1019-02.jpeg.3c86bd47f184637df00f1a416740cd76.jpeg

    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.

    jeucken1019-03.jpeg.3ea8d1505f97f1be1567997f5204bc7b.jpeg

    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 

     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 

     for a demo and discussion around the script.)

    User Feedback

    Recommended Comments

    Guest November 2019 – Chris Jeucken & Chris Twiest – BLOGS

    Posted

    […] created a script and presented it to the Dutch CUGC as well as to the entire CUGC community via blog and webinar. Talk about your experience in taking an idea and going from the local to the global […]
    Link to comment
    Share on other sites

    Guest Automating Citrix Cloud & Windows Virtual Desktop - ChrisJeucken.com

    Posted

    […] This blog was also posted on MyCUGC.org (link) on October 16th, […]
    Link to comment
    Share on other sites



    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

×
×
  • Create New...