Citrix images using Citrix Cloud RestAPI and Azure DevOps

Intro

In this blog post, I want to show you how to create Citrix images using Azure DevOps and publish them to Citrix Cloud via the RestAPI. I will show you that Citrix images can run both on-premises and in Azure (or any other cloud, but outside this scope). I am using Citrix Cloud, Azure DevOps, Azure IaaS, VMware ESXi, and Microsoft Deployment Toolkit (MDT) for this blog post. Now I won’t get much into the MDT configuration and use, but I am using it in both the on-premises and Azure deployments. I will be using Azure DevOps Pipelines to execute my code to deploy new images without touching a GUI. Below I have shown the basic workflow for both on-premises and in Microsoft Azure.

The solution outline is shown below.

Citrix Cloud setup

In Citrix Cloud, I have two things that are important to this guide. The first is the API access that I need to create. Azure DevOps use this to update the Machine Catalogs in Citrix Cloud. The other part is the Machine Catalogs in Citrix Cloud. These are the containers for the virtual machines that are running my Citrix workers. Let’s have a quick look at the API access creation.
The first step is to go to “Identity and Access management.”

Then go to “API Access,” provide a name for the new access and click on “Create Client.”

Note down the “ID” and “Secret,” after you close this view, you can’t get to this information again.

The next step is to create your machine catalogs, now I won’t go into details about that here, but if you need assistance setting-up Citrix Cloud, you can read my blog series on this subject here. For now, you can see that I have two machine catalogs in my environment. “MTH-OnPrem-Win10” is my on-premises, and “MTH-Azure-Win10” is in Azure.


The last thing I want to highlight in the Citrix Cloud setup, for now, is that each catalog uses a template. In the two pictures below, notice the “Disk Image” section, where we have the version of the current image. The first picture shows my on-premises environment, and the second is my Azure environment.

Azure DevOps setup

I use Azure DevOps to source control my code and run the workflows to update the Citrix images. I have created a basic setup for this blog post where I use my repository directly in my release pipeline, so I do not have any unit testing involved. I also use the graphical release pipeline to make the process clearer in this blog post.

First, let us have a look at the repository. The repository has a folder structure that I feel fits well with a small setup like this. I am using an ARM template to deploy and join a VM to a domain in Azure. I am also using a custom script extension to start my MDT workflow on the new VM in Azure. The modules folder contains a PowerShell module that I have created. The module makes the RestAPI calls from the main script easier to read and use. I will show you code from both the main script and some examples from the module to clarify how I update my images. Under scripts is my main deployment script used by the pipeline.

To not make this a long blog post with a lot of code, I have chosen to describe some code snippets in the “Update-Image.ps1” script. First, I have all the parameters I need to create a new VM on-premises and in Azure. For instance, vCenter address on-premises and resource group in Azure. I have both deployment types in one script, so not all parameters need to be filled out, but a few are mandatory, like choosing if the deployment is on-premises or in Azure. This parameter is mandatory to validate which other parameters are needed before executing any code. Both my on-premises and Azure deployment will shut down the VM when MDT has completed its work. For this reason, I am running a loop to check when the VM is in a stopped state. This check is to ensure that I create the snapshot or capture it at the right time.

Let us look at the code to create a snapshot and update the machine catalog in Citrix Cloud with the new version.

if ($FinishAction -eq "Update-Cloud-MCS") {             
        Write-Verbose "Creating Snapshot"
        $VMSnapshot = New-Snapshot -VM $VM -Name ($VMPrefix + (Get-date -Format ddMMyyyy-HHmm)) -Verbose:$False
        Write-Verbose "Creating Citrix Cloud acccess token"
        $AccessToken = Get-CCAccessToken -ClientID $ClientID -ClientSecret $ClientSecret
        Write-Verbose "Getting Citrix Cloud Site ID"
        $SiteID = Get-CCSiteID -CustomerID $CustomerID -AccessToken $AccessToken 
        Write-Verbose "Getting machine catalog information"
        $MachineCatalogInfo = Get-CCMachineCatalog -CustomerID $CustomerID -SiteID $SiteID -AccessToken $AccessToken | Where-Object {$_.Name -eq "$MCSCatalogName"}
 
        $SnapShotPath = "$($MachineCatalogInfo.ProvisioningScheme.ResourcePool.xdpath)\$($VM).vm\$($VMSnapshot).snapshot" 
        $SnapShotPath =  $SnapShotPath.Replace('\','\\')
        Write-Verbose "Getting Citrix Cloud Site ID"
        $SiteID = Get-CCSiteID -CustomerID $CustomerID -AccessToken $AccessToken 
        Write-Verbose "Getting machine catalog information"
        Update-CCMachineCatalog -MachineCatalogId $MachineCatalogInfo.id -Snapshot $SnapShotPath -SiteID $SiteID -CustomerID $CustomerID -AccessToken $AccessToken -Verbose:$False                                                                                      
 }

If you want to do the same with the Azure image, the code below will do the trick.

if ($FinishAction -eq "Update-Cloud-Azure-MCS") {        
    Write-Verbose "Creating Citrix Cloud acccess token"
    $AccessToken = Get-CCAccessToken -ClientID $ClientID -ClientSecret $ClientSecret
    Start-Sleep -Seconds 5
    Write-Verbose "Getting Citrix Cloud Site ID"
    $SiteID = Get-CCSiteID -CustomerID $CustomerID -AccessToken $AccessToken 
    Write-Verbose "Getting machine catalog information"
    $MachineCatalogInfo = Get-CCMachineCatalog -CustomerID $CustomerID -SiteID $SiteID -AccessToken $AccessToken | Where-Object {$_.Name -eq "$MCSCatalogName"}
    $NewImageVersion = Get-AzGalleryImageVersion -GalleryName $SharedImageGalleryName -ResourceGroup $ShareImageGalleryResourceGroup -GalleryImageDefinitionName $imageName | Sort-Object Name -Descending | Select-Object -First 1        
    $NewVersionXDPath = "XDHyp:\HostingUnits\MTH-Azure\image.folder\$($ShareImageGalleryResourceGroup).resourcegroup\$($SharedImageGalleryName).gallery\$($imageName).imagedefinition\$($NewImageVersion.Name).imageversion"
    $NewVersionXDPath = $NewVersionXDPath.Replace('\','\\')
    Write-Host $NewVersionXDPath
    Update-CCMachineCatalog -MachineCatalogId $MachineCatalogInfo.id -Snapshot $NewVersionXDPath -SiteID $SiteID -CustomerID $CustomerID -AccessToken $AccessToken -Verbose:$False                                                                                      
}

As I mentioned previously, I created a PowerShell module to ease the use of the RestAPI calls from the main script, so let us look at these functions.

When talking to Citrix Cloud, the first step is to get an access token to authenticate to the APIs hosting all of our machine catalog commands. Below is the code to create an access token.

function Get-CCAccessToken {
    param (
        [string]$ClientID,
        [string]$ClientSecret
    )
    $TokenURL = "https://api-us.cloud.com/cctrustoauth2/root/tokens/clients"
    $Body = @{
        grant_type = "client_credentials"
        client_id = $ClientID
        client_secret = $ClientSecret
    }
    $Response = Invoke-WebRequest $tokenUrl -Method POST -Body $Body
    $AccessToken = $Response.Content | ConvertFrom-Json
    return $AccessToken.access_token
}

I can then use the access token to get the site ID which is needed to call the machine catalog APIs. Below is the function to get the site ID.

function Get-CCSiteID {
    param (
        [Parameter(Mandatory=$true)]
        [string] $AccessToken,
        [Parameter(Mandatory=$true)]
        [string] $CustomerID
    )
    $RequestUri = "https://api-us.cloud.com/cvadapis/me"
    $Headers = @{
        "Accept" = "application/json";
        "Authorization" = "CWSAuth Bearer=$AccessToken";
        "Citrix-CustomerId" = $CustomerID;
    }
    $Response = Invoke-RestMethod -Uri $RequestUri -Method GET -Headers $Headers
    return $Response.Customers.Sites.Id
}

I can then use the site id and access token to get the information from my machine catalog. Below is the function to get the machine catalog.

function Get-CCMachineCatalog {
    param (
        [Parameter(Mandatory=$true)]
        [string] $CustomerID,
        [Parameter(Mandatory=$true)]
        [string] $SiteID,
        [Parameter(Mandatory=$true)]
        [string] $AccessToken
    )
    $RequestUri = "https://api-us.cloud.com/cvadapis/$SiteID/MachineCatalogs"
    $Headers = @{
        "Accept" = "application/json";
        "Authorization" = "CWSAuth Bearer=$AccessToken";
        "Citrix-CustomerId" = $CustomerID;
    }
    $Response = Invoke-RestMethod -Uri $RequestUri -Method GET -Headers $Headers 
    return $Response.items
}

The last function I need to run is the update machine catalog function. The code for that is listed below.

function Update-CCMachineCatalog {
    param (
        [Parameter(Mandatory=$true)]
        [string] $CustomerID,
        [Parameter(Mandatory=$true)]
        [string] $SiteId,
        [Parameter(Mandatory=$true)]
        [string] $AccessToken,        
	      [string]$MachineCatalogId,        
        [string]$SnapShot
    )
    $RequestUri = "https://api.cloud.com/cvadapis/$SiteId/MachineCatalogs/$MachineCatalogId/`$UpdateProvisioningScheme"    
    $Headers = @{
        "Accept" = "application/json";
        "Authorization" = "CWSAuth Bearer=$AccessToken";
        "Citrix-CustomerId" = $CustomerID;
    }
 $body = @"
 {
   "MasterImagePath":"$SnapShot",
   "StoreOldImage":true,
   "CpuCount":2,
   "MemoryMB":8192,
   "RebootOptions":{
     "RebootDuration":0,
     "WarningDuration":0,
     "SendMessage":false
    },
    "RebootOptions":{
      "RebootDuration":0,
      "WarningDuration":0,
      "SendMessage":false
    }
  }
"@    
    $Response = Invoke-WebRequest -Uri $requestUri -Method Post -Headers $headers -Body $body  -ContentType "application/json"
    return $Response
}

To create the module and functions, I used the Citrix developer site, modified it to what I wanted, and then transformed it into a PowerShell module. Calling each RestAPI with the module is cleaner to read and more accessible for system administrators to use. The link to the developer site is here.

Azure DevOps Pipeline
In Azure DevOps, I have created two pipelines, one for on-premises and one for Azure image updates. The two pipelines could be combined into one, but to keep it simple, I have created two. Both are running the same script but with different parameters. Let us have a look at how the pipelines configurations.
The first step is not creating the pipeline but creating variables groups in the “Library” under “Pipelines.” As shown below, I have three variable groups, one shared which is the “Citrix Cloud Variables,” this contains my client id, client secret, and customer id for Citrix Cloud. The other two are one for on-premises variables and one for Azure variables.

If we look at the “Citrix Cloud Variables” group, we can see the following variables. Notice that I have hidden all the values. Hiding the values in the group will also hide the value from any log generated inside of Azure DevOps. I also cannot unhide the value. If I have forgotten what it was, I have to reenter it, which would probably mean I need to create a new API client in Citrix Cloud.

Next, I have the on-premises variables that I use for MDT and VMware items.

The last group I have is for Azure, and this contains quite a few items since a lot of information is needed to deploy into Azure.

Next, I have created two pipelines. Looking at the pipelines, we can see the how-to insert the script and link variable groups.

In my pipeline, I use the repository as my artifacts.

For stage 1 in the Azure deployment, I have chosen the Azure DevOps provided agents and running it on Windows Server 2022.

For stage 1 in my on-premises deployment, I have chosen the self-hosted agent, a small agent running on my MDT server at home. The settings are pretty similar to the Azure-hosted agent but using my hardware for the agent.

I am using a service connection into Azure. If you need assistance with that setup, have a look at the guide from Microsoft here. You can see I reference my script, and all the parameters reference the variables that I have created in my variable group. Using one-to-one mapping makes it easy to understand and troubleshoot. Also, I chose to run the latest installed version of Azure PowerShell. One difference between the Azure and on-premises deployment is that Azure uses an “Azure PowerShell Script” task, and on-premises uses a standard “PowerShell script” task.

The last part of the pipeline is to link my variable groups. Adding a variable group is done quickly under the “Variables” tap and “Link variable group.” As you can see below, I have two groups linked, one for Citrix Cloud and one for Azure. For my on-premises deployment, I have Citrix Cloud and VMware groups linked.

The result
The result is that I now have two release pipelines in Azure DevOps that execute an on-premises image update or an Azure image update. Both will run a full image deployment and create a snapshot or a shared image gallery version before updating the machine catalog in Citrix Cloud.

Citrix Cloud also shows that our machine catalog is using the updated version.

There are multiple options for running the pipelines, but most will use the Azure DevOps GUI or schedule the pipeline to run weekly. It is possible to create a PowerApp frontend or maybe a PowerShell GUI. Azure DevOps has its own RestAPI, so you can execute any pipeline where you feel it makes the most sense for the users.

I hope this post is helpful and if you have any feedback, please reach out on Twitter or LinkedIn.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.