Wednesday, October 30, 2024

No REST for the RESTful: Python And The Horizon Server API

Expanding upon earlier articles in this No REST for the RESTful series, today's post explores Horizon automation using Python.  These previous articles detailed how to automate Horizon administration through a sequence of calls executed from Postman. With our logic and syntax for the Horizon Server API already sorted out via this prior work, the path forward to a Python script is relatively straight forward. We can adopt logic from Postman into Python using the Requests module, "an elegant and simple HTTP library for Python, built for human beings." 


The Requests module is an ideal mechanism for making HTTP requests from Python, providing an easy way to retrieve and process data from the Horizon Server API.  Adopting Postman logic into Python revolves around translating Postman requests to calls made from Python's Requests module.  My sample scripts are available here at GitHubThis article will provide a a quick primer on the Requests module that drives these scripts, then walk through some of the functions I've created to simplify interactions between Python and the Horizon Server API. 

Python's Requests Module 

The Requests module is a widely adopted and well documented mechanism for making HTTP requests from Python.  It allows you to make requests with as little as a single line of code.  Further, with simple to use parameters for building complex requests, along with features like it's JSON decoder, the requests module makes it easy to retrieve and parse data provided from a REST API. 

https://requests.readthedocs.io/en/latest/







Once you have the request module installed, making HTTP requests is a snap.  To make a simple request against GitHub, you can type these two lines within the Python interpreter: 

import requests
r = requests.get('https://api.github.com/events')


The response is stored in an object called r you can start to pull information from.  For example, to confirm the status of the call you can leverage the status_code attribute of the r object. 

print(r.status_code) 

Or, to get the JSON response body of the request you can leverage the JSON decoder:

print(r.json()) 

For Python interactions with the Horizon Server API I lean heavily on the GET and POST methods of the Requests module.  The more complex calls are made using Requests's parameters like params, headers and JSON.  These parameters allowed me to replicate the calls made from the Horizon Server API Shenanigans Postman collection.  

https://requests.readthedocs.io/en/latest/api/

To simplify the adoption of Horizon Server Shenanigans to Python I've created four different functions. The remainder of this article will review how these functions help automate tasks against the Horizon Server API.  


Handling Authentication With The get_access_token Function

All the sample Python scripts lean on a function for handling authentication/authorization called get_access_token.  This function replicates the login call used in the Horizon Server API Shenanigans Postman collection.  For context, here's what the Login request looks like in Postman:









To replicate this process in Python the get_access_token function gathers the ingredients required to make an identical call using the Requests module.  It's first argument, the target variable, is the full address to the login endpoint in the target Horizon environment.  The following 3 variables are the AD login credentials for an admin account in the Horizon environment.  These make their way into the JSON request body through the Requests's module JSON parameter.  

def get_access_token(target, username, password, domain):
    json_data = {
    "username": username,
    "password": password,
    "domain": domain
    }

    response = requests.post(
        target,
        json=json_data,
        verify=True
    )

    return response.json()["access_token"]

If all goes well the request module will return the request response back in the response object.  We can then use the Requests module's .json() decoder method to get the desired JSON value of access_token. This access token is an absolute requirement for any other calls to the Horizon Server API.  All the scripts start off by obtaining the access token with this function and assigning it to the variable JWtoken:

JWtoken = get_access_token(target, username, password, domain)

This JWtoken variable is then referenced by every other function that makes calls to the Horizon Server API, inserted in the headers of these calls: 

headers = {
    'authorization': 'Bearer ' + JWtoken
}


So the successful execution of this get_access_token function is absolutely required for any of the other functions to work.   


Filtering Out JSON Responses With getR_got_w_filter

A critical requirement for these scripts is to leverage filter query parameters to narrow down JSON response results. Calls to endpoints like external/v1/ad-users-or-groups can yield thousands of results depending on the size of a production environment.  Fortunately, many Horizon Server API endpoints support filtering and we can access this capability from Python scripts in a similar manner we've done already with Postman.  If you haven't already read No REST for The RESTful: Chaining Together Calls To The Horizon Server API, halfway through the article I provide a primer of filter query parameters.  

One of the most prominent uses of filtering within the Horizon Server API Shenanigans Postman collection is to narrow down a JSON array response of AD users to a single user.   Here's an example from that collection:







Yes, leveraging filter objects leads to some messy visuals in Postman, but well worth it when you consider the benefits. Thankfully, it's little less messy to use filters with Python's Requests module.   To begin with, here's what the function looks like:

def getR_got_api_w_filter(target, filter_param):
    headers = {
        'authorization': 'Bearer ' + JWtoken
    }

    params = {
        'filter': filter_param
    }

    response = requests.get(
        target,
        params=params,
        headers=headers,
        verify=True,
    )

    return response.json()


Fortunately, creating the filter object for use with the Requests module is a little less involved.  Instead of having to minify AND encode the object filter - see ugly Postman screenshot above - we only have to minify the object filter.  The Requests object handles the encoding for us by default.  So, when creating the filter object argument for getR_got_w_filter, the scripts have this relatively simple line of code: 

ad_users_or_groups_filter = '{"type":"Equals","name":"login_name","value":"' + login_name + '"}'

This add_user_or_groups variable is then passed on as an argument to getR_got_w_filter, along with a URL to the target endpoint:

responseBodyJSON = getR_got_api_w_filter(target_url, ad_users_or_groups_filter)

If all goes well we're returned a single JSON object within an array.  We can grab the internal Horizon ID from this object and assign it to target_user_id.   

target_user_id = responseBodyJSON[0]["id"]

Translating a short AD login name to this internal ID used by Horizon is a task performed in all 4 sample scripts. A lot of the more interesting tasks to address with the Horizon Server API require this process.


Making Posts To The Horizon Server API Using getR_posted_w_JSON

The culmination of these sample Horizon automations is a Post request of some sort.  All four of the automations start with the gathering of prerequisite information that eventually finds it's way in a climatic Post request.  Typically, these Post methods with the Horizon Server API require a JSON body request with specific parameters depending on the endpoint being called.  For example, there's this request that sends a message to an active Horizon session:








Calls like these are simplified within my Python scripts using the gerR_posted_w_JSON function. It crafts a POST request by taking in a target URL and JSON body as an argument.   Here's the function:

def getR_posted_w_JSON(target, json_4_request):
    headers = {
        'authorization': 'Bearer ' + JWtoken
    }

    response = requests.post(
        target,
        json=json_4_request,
        headers=headers,
        verify=True
    )

    return response

Note that, unlike the previous function, in this one the Requests module leverages the POST method, request.post,  when making it's call.   It's critical 2nd argument is a JSON body response that does most of the heavy lifting.  For example, in the Message_N_Disconnect.py script, the JSON body is delivered via a json_data variable configured like this:   

json_data = {
    "message": "Your session will be disconnected in about 8 seconds.",
    "message_type": "INFO",
    "session_ids": [ target_session_id ]
}

If you look back at the Postman screenshot directly above this variable configuration should look familiar.  

While this getR_posted_w_json method is simple enough, it plays a star role in 3 of the 4 Python scripts.

 

Searching Through Returned Arrays Of JSON Objects With getR_got_w_object_search

No all endpoints of the Horizon Server API support filters.  In those situations there's sometimes a need to do some filtering on your own by looping through an array of objects in search of a key with a unique value.  The Kill Specific App In Session automation is a great example.  The last call in this sequence returns an array of JSON objects, with each object representing a separate application actively running within a Horizon session. To narrow in on the target app within this array we can loop through each object, checking for a match between one of the objects keys called, "name," that has a value matching the target application we wish to terminate. Within Postman we accomplished this using a forEach method against the JSON response data.  If you look at the post-response script used by this call you'll see the JavaScript in action: 









To adapt this Postman request to Python I came up with the getR_got_w_object_search.  Note the foreach function leveraged at the tail end of this function.  It mimics the JavaScript logic above, looping through the entire array in search of an object with a specific key holding a searched for unique value.  Take a look:

def getR_got_w_object_search(target, params, object_key_name, object_key_value):
    headers = {
        'authorization': 'Bearer ' + JWtoken
    }

    response = requests.get(
        target,
        params=params,
        headers=headers,
        verify=True
    )

    object_array = response.json()

    for json_object in object_array:
        if json_object[ object_key_name ] == object_key_value:
            return json_object


With this function there are 4 required arguments, two of which specify the name and value to search against.   The object_key_name variable specifies the specific key of each object to search against, while object_key_value represents a value to search for.   For example, in the Python adaptation of the Kill Specific App In Session automation, the object_key_name is, "name," and the value getting searched for is, "Calculator."  When an object with the value of, "Calculator," for it's name is located, it's returned by the function.  

application_object = getR_got_w_object_search(target, params, "name", target_app)

Then the value of it's remote_application_id is pulled from the application_object returned from getR_got_w_object.

remote_app_id = application_object[ "remote_application_id" ]

This remote_app_id is then leveraged later on in a call to the end-remote-application endpoint.  

The ability to loop through an array of objects like this comes up several times throughout the four automations.  It's a requirement that's bound to show up when an endpoint doesn't support filtering or there's a situation when multiple objects need to be extracted from a response.  


Communicating Securely With The REST API

By default, Python's Requests module leverages the same trusted root authorities as the Mozilla browser.  Specifically, it uses a bundle of trusted CA certificates from the certify library, which uses a curated collection of Root Certificates from Mozilla. So if you have a publicly trusted SSL on your Connection server there's a good chance you'll be fine running requests against it.  Quite often though customers user an SSL cert issued from their on internal certificate authority, not a publicly trusted authority.  In those situations the Requests module will fail to make a secure connection and these scripts will fail.  To get around that challenge, there's two options.  The first one is rather easy and suitable if you're working within a lab environment.   You can just set Verify to false when making the requests.   For example:

    response = requests.post(
        target,
        json=json_data,
        verify=True
    )

This will disable SSL verification and allow the request to be made against an untrusted URL.   That's certainly fine for a lab environment, but isn't suitable for a production deployment.  To make secure requests against a Connection server using a SSL cert from an internal CA, you can instruct the Requests module to trust the internal certificate authority by specifying a path to a custom CA bundle to use.   In my environment I've pointed the requests module to an export of my internal certificate authority.  

    response = requests.post(
        target,
        json=json_data,
        verify="root_cert.cer"
    )

While it's easier to just set verify to false and move on to the automation fun, given you're sending AD credentials over these connections, in any situations outside a lab environment you'll need to get this sorted out properly. 

What About Error Handling? 

These sample Python scripts are designed to demonstrate bare bones functionality, so they're missing a few features required for a production environment.  Specifically, there's no error handling.  For example if you run the Message_N_Disconnect.py and the target user doesn't have an active Horizon session, the script just breaks with no useful output.  Same goes for the Kill_App_In_Session.py script.  If the target app isn't running, the script just breaks with no useful output.   The main intent of these scripts is to demonstrate as simply as possible how Python can be leveraged for the Horizon Server API, a MVP for educational purposes.   If someone intends to leverage these for production they are going to need to add some tweaks for error handling and flow control.  In the future I might update these scripts accordingly, but dadda's tired and needs a rest. 

If you want to see an example of Python scripts for the Horizon Server API that are more fine tuned for production, check out the Python module developed by Wouter Kursten.  If you look through his code you'll see he has some pretty extensive error handling in place.  You might also notice he's leaning on the Requests module to do a lot of the heavy lifting. You can view his demonstration in this VMworld 2021 video


Conclusion

My primary motivation for writing this overview is that Python is currently the programing language I'm most comfortable with.   About a year ago I leveraged Python to create troubleshoot.evengooder.com, so it's relatively fresh in my head and easy to work with.  That said, I'm no expert. Simply a geek that used to get quite frisky with VBScript in the oughts and now has developed some familiarity with Python.  So, this article isn't intended to be a tutorial on elegant Python programing.  It's a demonstration of how we can easily pivot to whatever language or mechanisms we like once we've gotten our Horizon Server API syntax down.  That's the beauty of the Horizon Server API's  REST based architecture: folks can use their programing language of choice.

Friday, September 20, 2024

No REST for The RESTful: Chaining Together Calls To The Horizon Server API

My previous post focused on getting familiar with the Horizon Server API using Swagger and Postman.  This post will review how to chain together Horizon Server API calls to automate Horizon tasks.  Using Postman collections we can configure sequences of calls that build upon each other, passing variables from request to request.  More specifically, there's JavaScript wrapped around each request through pre-request and post-requests scripts that share variables across the collection.



Postman has a rich offering for testing out API responses including a Java Script API, code snippets, and custom AI generated code.  For the purposes of automating Horizon tasks we can get an awful lot done with very little JavaScript, in some instances just two lines of code.  So, while this post will get into some JavaScript weeds, it's more about understanding how to interface with the Horizon Server API, how to interpret it's JSON responses, and chaining multiple requests together to achieve a complex outcome.  These lessons will be applicable regardless of what programing language you go on to leverage for the Horizon Server API. 

Pulling Values From Horizon Server API Responses

Responses from the Horizon Server API will include a JSON object or array of JSON objects. JSON objects are collections of key\value pairs, separated by colons and  commas, wrapped in brackets.   The values of these key\value pairs can be strings, other objects, arrays or even arrays of other objects.  To access specific data in these responses you can leverage Postman's JavaScript API within the post-response script.  For example, pm.response.json() is used to parse the response body into a JavaScript object:

const jsonData = pm.response.json();

At this point, we can leverage the jsonData object to explore the values of the JSON object programmatically with JavaScript, pulling any data off of it we need.  The Login call included in the Horizon Server API Shenanigans collection does just that.  After a successful call to the login endpoint a JSON response is returned that includes an access_token key/value pair.  The post-response script then assigns this JSON response to jsonData, an object we can harvest the access_token value from by referencing jsonData.access_token.  













The access_token value is assigned to the global JWtoken variable using pm.globals.set("JWtoken", jsonData.access_token), another example of Postman's JavaScript API.  The rest of the calls throughout the collection are configured to insert the value of this JWtoken variable within the header of their requests.   If you navigate to the authorization tab of the root of this collection you can see how Postman is configured to automatically leverage this token for authentication.   
















To illustrate further here's an example of the JSON object returned by the external/v1/ad-users-or-groups/{id}, a mechanism for retrieving a specific Active Directory user:    



















Now, say we want to grab the id value from this JSON object.  Similar to the  post-resonse script for Login, we can add the following two lines of code: 

const jsonData = pm.response.json();
pm.collectionVariables.set("ad_user_id", jsonData.id);

Once again pm.response.json() converts the JSON response into a JavaScript object, jsonData, that we can use to extract our desired data from.   Then we access the id value using jsonData.id, assigning it to the collection variable ad_user_id.  We could just as easily grab other values from the object as needed.  Say we wanted to retrieve not only the id value, but also the login name and email.  No problem:  

pm.collectionVariables.set("ad_user_id", jsonData.id);
pm.collectionVariables.set("ad_login_name", jsonData.login_name);
pm.collectionVariables.set("ad_email", jsonData.email);

Now, we've yanked 3 values off the JSON object, assigning them to the collection variables ad_user_id, ad_login_name and ad_email.  


Pulling Values From JSON Objects With Objects For Values

Now, what about situations where you have a JSON object with other JSON objects as values?  For example, the output of the inventory/v2/sessions/{id} endpoint.  Note the assigned values for client_data and security_gateway_data are JSON objects. 
















Say for instance you want to extract the IP address of the client.   Similar to the examples above, you use pm.response.json() to parse the JSON response into a JavaScript object, jsonData.  Then, to access the client ip address you go with jsonData.client_data.addressYou can easily confirm it's value using console.log()










If you look at the console after execution you'll see the value of the address yielded:







While this should all seem fairly straight forward so far, something to keep in mind is that often responses yield arrays of JSON objects to sort through, in some cases, very, very large numbers of them.  Fortunately, some of the Horizon Server API endpoints support filtering, a way to narrow down the JSON objects returned within a response.  


Filter Query Params - Using Objects To Filter Objects

A subset of Horizon Server API endpoint include support for filter parameters to narrow down results when multiple JSON objects are returned. Examples are inventory/v2/sessions or external/v1/ad-users-or-groups, endpoints that could easily return thousands of objects. In a nutshell, these endpoints accept filter query params, JSON objects that define how to filter and what value to filter by.   Here's an example: 

{
    "type": "Equals",
    “name": “login_name",
    "value": “cmoltisanti”
}


The, "name," value represents the name attribute we're we're focusing the filter on.   In the case above we're talking about the login_name.   The, "value," represents the value for the login name we're focusing on.  In the example above it's cmoltisanti.  Finally, the type represents the type of filter we're using.  In the object above it's, "equal."  (Possible values for filter types are Equals, NotEquals, Contains, StartsWith, Between, And, Not and Or.)  So the filter object example above is targeting objects with a login_name that is equal to cmoltisanti.  

In order to pass a filter query object to an endpoint we have to do two things to it.  We have minify it and then encode it.  Minifying amounts to removing all the unnecessary white space.  You could do it manually or use a utility online.  After using the first utility I found outline, I ended up with this: 

{"type":"Equals","name":"login_name","value":"cmoltisanti"}

Then, to encode it I went to https://www.urlencoder.org/.   I ended up with this: 

%7B%22type%22%3A%22Equals%22%2C%22name%22%3A%22login_name%22%2C%22value%22%3A%22cmoltisanti%22%7D

Going back to the ad-user-or-groups call in Postman, you add a new query param called, "filter," with the encoded query object as a value.   










Now, instead of getting every single user and group from my AD environment, I get the specific record for the login_name cmoltisanti. 
























It's an absolutely ugly URL, but the results are gorgeous.  Instead of having possibly thousands of objects to sort through, I've got the single AD account I'm looking for.  Also, I can easily reuse this ugly filter string to target any other user by simply replacing cmoltisanti with another valid login name.  























For more guidance on filtering, check out the Horizon Server REST, Pagination and Sorting Guide


Nailing Down The Filter Syntax With Swagger

To illustrate how handy this process can get, I'm going to review a challenge I had developing a call to inventory/v5/machines.  I needed to leverage advanced filters and for one reason or another I just couldn't get the syntax right within Postman.  So I headed over to the Swagger interface and navigated to inventory/v5/machines, then clicked the option, "Try it out." The interface already had a template in place for a filtering option.  







This preconfigured suggestion for filtering looks like this: 

{ "type": "And", "filters": [ { "type": "Equals", "name": "", "value": "<>" } ]}

To get it configured I populated it with the name of the value I wanted to match against and the actual value. So the object's value I wanted to match against was, "name" and the value was Win10-22H2-Manual-Pool. So, I edited the filter accordingly.

{ "type": "And", "filters": [ { "type": "Equals", "name": "name", "value": "Win10-22H2-Manual-Pool" } ]}

After making the edits, not only did I get successful execution against the local Horizon Connection server, but I was provided with syntax for making the call with the proper filter.   Note the long nasty request URL: 












When copying this request URL directly into Postman it automatically recognized the filter parameter and organized it in the interface as such.  













How was I messing up originally?  I don't care anymore. I used Swagger to get the answer I needed and was able to move on to life's other mysteries. 


Pulling Values From Filtered JSON Object Arrays

Earlier in this article we reviewed parsing a json response with pm.response.json(), creating a JavaScript object called jsonData that we could use to access different values of the parsed object.   Specifically, we used jsonData.access_token to harvest the access token from a successful response from the login endpoint.  We performed a similar process to pull data from a successful response from inventory/v2/sessions/{id}, using jsonData.client_data.address to retrieve the ip address of a client connection.   There's a similar but significantly different process for yanking info off our object retrieved from the filtered query above.  Take a second look at the output of that filter against the cmontisanti login name.  While there's a single JSON object displayed, it's actually an array with a single JSON object in it.  Make a note of brackets:  

























To more clearly illustrate this nuance lets leverage console.log() again, taking jsonData from our filtered query and printing it to the console.  Using console.log() in this manner provides a way to explore the structure of our JSON response.  















After executing our call we can see the results in the console.   Here we can clearly see that jsonData is actually an array with a single element in it, the JSON object representing cmoltisanti.



Fortunately, because this filter does it's search based on a unique attribute, the AD login_name, we're only getting a single object array.  Getting access to it's data is simply a matter of calling out the index of this array object, 0 because there's only a single object to choose from.  So we specify jsonData[0].what_json_object_name_we_want_the_value_of.  In this example, we're looking for the user id, so it's jsonData[0].id.  












Pulling Requests Together For Desktop Pool Entitlement And Session Management

Using the lessons we've reviewed about parsing and filtering JSON responses we can chain four separate calls together to entitle a user to a desktop pool.   After importing the Horizon Server API Shenanigans collection, navigate to the Assign User To Desktop Pool folder within the Horizon Automation folder: 











The Login request uses the login endpoint to authenticate and then retrieve an access token.  Then a filtered call to external/v1/ad-users-or-groups is used to obtain the ID of the target user.  In a similar fashion, a filtered call to inventory/v4/desktop-pools obtains the ID of the target desktop pool.  Finally, both IDs are passed as variables into the body request of a call to entitlements/v1/desktop-pools, where the final magic happens.  

To get started using this sequence, along with the variables for login credentials, you need to set two collection scoped variables, target_user and target_pool.














Populate these variables and then begin sending these calls in sequence one a time.  You'll end up with a new entitlement.  





















Or, you can run the entire folder at once by right clicking on it and selecting, "Run Folder."  Postman's Collection Runner will spring into action, executing the sequence in order, then provide a summary of the execution. 





























Whether you run the sequence through Collection Runner or manually execute the preconfigured calls one at a time the general process is the same.  We start with some preconfigured collection scope variables, then each call retrieves additional required variables, building upon each other till we achieve the final outcome of entitling the user to the target pool. The Message User And Disconnect task sequence within the Horizon Automation folder works very similarly, except you need only set the login credentials and target_user collection variable before kicking it off.  


Looping Through Arrays Of JSON Objects

When dealing with responses that are arrays of JSON objects there's times where it's necessary to iterate over every object of the array. For that we can turn to JavaScripts forEach() method. An example of this is the Get target_app_id for target_app call within the Kill Specific App In Session automation.



In this example I use the forEach() method to iterate over every object within the jsonData object returned by pm.response.json() after a call to helpdesk/v1/performance/remote-application.  In this scenario jsonData is an array of JSON objects that represent different applications running within an active session.  The forEach() method allows me to walk through each one of these objects, comparing the apps name to the target app I'm looking for. 
 
    jsonData.forEach(function(da_apps){
        if (da_apps.name.toLowerCase()  == targetApp.toLowerCase() ) {
        pm.collectionVariables.set("target_app_id", da_apps.remote_application_id);
        }
    })

If there's a match the collection variable target_app_id is assigned the remote_application_id associated with the matching JSON object.  This variable is consumed by the next call, Kill target_app, to terminate the application within the session.  

In this example it was necessary to iterate through every JSON object partially because their was no filter supported by the helpdesk/v1/performance/remote-application endpoint.   So I had to roll up my sleeves and walk through the entire array of returned objects to find what I'm looking for.  I dealt with a similar challenge in my Add VMs To Manual Pool automation.  In that scenario I had to locate a target VM from an array of JSON objects returned from a call to external/v1/virtual-machines. Again, there was no filter option supported by the API to narrow my search down to a single JSON object response.  So I used the forEach() method to iterate over each object till I found a matching VM name, then harvested it's vCenter VM id.  



This is probably as technical as I've had to get in terms of my understanding of JavaScript coding. When working with Postman the forEach() method is something you can fall back to pin point a single object or harvest information from multiple objects in an array of JSON objects.   It's a strategy I'll explore in different languages in future posts. 


Now Doowutchalike

While Postman is an excellent way to explore the Horizon Server API and share requests with other admins, you're free to choose pretty much any programing language, OS, or endpoint your wish to develop your solution on.  That's a major benefit of the modernization of Horizon's API to a REST based architecture.  Though Postman can make for a very compelling option in some use cases, it's been used in these last two posts mainly for teaching and illustration purposes.  You should have an easy time adapting these lessons to any language of your choice with a few adjustments.  



No REST For The RESTful: Omnissa's Horizon Server API

The Horizon Server API is a RESTful API that supersedes the legacy View API for Horizon.  It's the outcome of a modernization effort years in the making, a REST based API at full parity with it's predecessor as of the Horizon 2312 release. While Horizon currently supports both the newer and legacy API, the writing is on the wall.  Going forward new Horizon automations should be created using the Horizon Server API and any previous automations based on the View API should be considered for migration. 

https://developer.omnissa.com/horizon-apis/horizon-server/
https://developer.omnissa.com/horizon-apis/horizon-server/














Customers have the flexibility to access the RESTful Horizon Server API from pretty much any programing language, OS or endpoint imaginable, a major advantage over the PowerShell constricted View API.   However, many Horizon admins are unfamiliar with RESTful APIs and the principals that guide their use, so I've put together this primer.  After a brief review of RESTful concepts I'll focus on Swagger and Postman as tools to get familiar with the Horizon Server API.  


RESTful APIs 

When making calls to the Horizon Server API and it's various endpoints, the base url will be https://<fqdn_of_connection_server>/rest/For example, my Horizon Connection Server is horizon.evengooder.com, so the base URL is https://horizon.evengooder.com/rest/.  Then there's the various endpoints offering different functionality, such as monitor/v3/connection-servers, inventory/v2/sessions, or external/v1/ad-users-or-groups.













Leveraging various endpoints is all about sending HTTP requests to these URLs and in turn receiving HTTP responses.  The exact requirements for the requests vary from endpoint to endpoint.  Some require special parameters to the URL or path variables, while others require specific JSON attributes within the body of the request.  The responses vary as well, though they all involve returning data in the JSON format within the body of the responses. 

Intro To APIs Part 3: HTTP Protocol Explained - https://www.youtube.com/watch?v=FAnuh0_BU4c&list=PLM-7VG-sgbtBBnWb2Jc5kufgtWYEmiMAw&index=3























The JSON objects returned are collections of key\value pairs, separated by colons and commas, wrapped in brackets.   The values of these key\value pairs can be strings, other objects, arrays or even arrays of other objects.   






















This standardized format makes the Horizon Server API output easy to work with across languages.  As json.org puts it, "JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate."  

An ideal way to begin exploring the various Horizon Server API endpoints is through the Swagger implementation available by default on your Horizon Connection Servers.  Along with providing documentation, this Swagger implementation is essentially an HTTP client with training wheels, guiding admins in the execution of calls against the local instance of the Horizon Server API. 


Swagger Like Us

While there's on-line documentation available for the Horizon Server API, the Connection Server's built-in Swagger implementation provides a readily accessible and more useful alternative.  It's accessed by pointing your browser to https://<fqdn-of-connectionserver>/rest/swagger-ui/index.html.  























As you explore the various endpoints you'll see there's an option to execute against them directly from within the documentation using a built-in client.  The client prompts for any required parameters, path variables or body responses needed for successful execution of a specific endpoint, helping nudge customers towards proper syntax.  For example, there's a call you can make to inventory/v8/desktop-pools to get information regarding a specific desktop pool.  Since it requires a desktop pool's ID as a path variable the Swagger interface prompts for the ID, indicating it as required. 













When you provide the necessary ID and execute you'll see the client automatically appends the ID to your call for you. 



















Along with required parameters or path variables Swagger also provides guidance on any attributes required in the request body.  Take for example the entitlements/v1/desktop-pools endpoint.  Swagger provides a sample of the name/value attributes required to execute this call successfully.  Replace them with valid ad_user_or_group_ids and a proper desktop pool ID and you'll get your entitlement created. 





























Before executing calls through Swagger it's necessary to obtain a valid bearer token for authentication.  The most common way to do this is through the login endpoint under Auth.

















After clicking on the option to, "Try it out,"  you'll see a preconfigured response body with variables to fill out.   Enter in the domain name, short AD username, and password for an account with admin privileges. 


































Hit the execute button and if things go well you should see a body response that includes an access_token name and value.  













Copy the access_token value, navigate back up to the top of the Swagger interface and click the authorize button.   Paste the access token in as your Bearer value.  
















At this point you can fire at will, leveraging all the different endpoints available from the Swagger interface for the default 30 minutes the access_token takes to expire. 

Overall, Swagger offers a solid process for nailing down your syntax and getting your hands dirty quick with the JSON output.  As far as I'm concerned Swagger on the local Connection Server should be the first stop for anyone looking to explore a specific endpoint.   However, if you're looking to explore more complex tasks that involve multiple calls working together Postman is definitely worth checking out. 


Getting Further Acquainted With The Horizon Server API Through Postman

Postman is an HTTP client designed to assist with the development and testing of APIs.  As such, it's an ideal vehicle for getting familiar with the Horizon Server API and it's REST architecture.   A free version of Postman is available after registering at Postman.com.   Once registered you can log into Postman and easily import my own Postman collection, Horizon Server API Shenanigans, by clicking on this button:


After importing the collection you'll have access to handful of requests, including some advanced sequences within the Horizon Automation folder. 














This collection is an adaptation of a collection originally shared by Chris Halstead in 2019.  While his shared collections attempted to provide preconfigured calls for every supported endpoint, Horizon Server API Shenanigans is a more stripped down collection of calls for the purpose of demonstrating how to chain Horizon REST API calls together.  It shamelessly borrows the authentication scheme introduced in Chris's original work.  The process starts by entering in your admin AD credentials in the collection's Variables tab.






















After setting those variables navigate to the Login call within the root folder and execute it.   If the call is successful a JWtoken global variable is assigned a fresh access_token.   The collection is configured so that this access token is automatically inserted in the header of other calls executed by the collection.  If you look at the authorization tab at the root of the Postman collection you can see how this is configured:



















All the other calls are configured to emulate this authentication scheme by leveraging the option, "Inherit auth of parent."  So, once you've successfully executed this first call you can fire at will with all the other ones, at least until your access token expires in 30 minutes.  An easy way to take things for an initial test spin is to poke around the Horizon Monitoring folder.  For example, here's the status for the Horizon Connection Server:


























All these Infrastructure monitoring calls can be executed once the Login call has been run.   Other sequences, such as Message User And Disconnect, rely on additional variables getting set ahead of time.  I'll address this sequence in more detail next.  


Chaining Multiple Calls For More Advanced Procedures 

The main objective of Horizon Server API Shenanigans is to demonstrate how multiple calls to the Horizon Server API are chained together to perform complex automations.  For example, the Message User And Disconnect folder includes 5 different calls that build upon each other.  The first 3 calls have a post-response script configured to parse responses and retrieve variables that our leveraged for future requests.  For example, the call to the login endpoint is used to retrieve an access token that's used for the next 4 calls.  The external/v1/ad-users-and-groups endpoint is used to locate the unique ID associated with a specific user's login account.  The inventory/v1/sessions endpoint is used to locate a session ID associated with this AD account.  Finally. the send message and disconnect endpoints are used to send a message and disconnect a session.













You can see all this in action by first setting the target_user variable, under the collection's Variables tab, to the short AD login name for the user you wish to target.  Then navigate to the Message User And Disconnect folder and begin executing the calls one by one.   With the first call to the login endpoint the global JWtoken variable gets set.  After running Fetch target_user ID successfully the collection variable ad_user_id is set to the target user's ID.  After running Fetch target_user Session ID the collection variable SessionHunt gets set to the target user's session ID.  You can confirm how these collection variables have been set by navigating to the Variables tab of the collection.





















Finally, the Send Message To User and Disconnect session calls will leverage the SessionHunt variable to send the target_user a message and disconnect them.






























Within the Horizon Automation folder of Horizon Server API Shenanigans, similar flows are available for adding user entitlements to desktop pools, adding desktops to manual pools or killing specific applications within VDI sessions.   Each of these flows requires the setting of specific collection variables, like target_pool or machine_name.   






















To get deeper on this topic of chaining calls together through variables and post-request scripts, check up my follow up article, No REST For The RESTful: Chaining Together Calls To The Horizon Server API.


Adding Additional Calls To Postman Via Swagger Exploration

You can add additional calls to your Postman collection by right clicking and selecting Add Request.



Rather than stressing about getting proper syntax and URLs lined up, you can get everything sorted out ahead of time through the Swagger interface.  For example, to add a new request to a monitoring API, navigate to saml-autheneticators under monitor within Swagger.  Hit the Try it out button and click execute.  You'll get a response like this: 























Note, not only have you successfully executed a call against the local Connection Server, but you have the request URL clearly spelled out for you.  You can copy and then paste this request URL directly into Postman and your off to the races:





















That is a very simple example of how we can pull a query from Swagger into the Postman interface.  There are definitely more complex calls to adopt with this strategy.  These will be demonstrated in the follow up No REST For The RESTful: Chaining Together Calls To The Horizon Server API


Original Collection Put Out By Chris Halstead

Still available at GitHub today is a preconfigured Postman Collection for VMware Horizon REST API put out by Chris Halstead.  While this collection is a bit dated, last updated for Horizon 2111, I think it's still highly relevant and worthwhile for anyone getting started with the Horizon Server API today.   It includes 100's of preconfigured calls for folks to explore and tinker with, all organized according to endpoint type and versioning.  

While I'm a big fan of this collection, and Chris Halstead in general, it is slightly dated and starting to show it's age.  For one thing, I had challenges leveraging the collection variables for authentication purposes.  To get it working properly I had to change Username, Password and Domain to lowercase.  



















Another challenge I ran into was that the authentication method for all the individual calls were defaulting to bearer token, rather than, "Inherit auth from parent."  So for each call I was interested in running I had to make this adjustment.   It's not rocket science once you know to do it, but it can lead to confusion.  






















Finally, some of the logic includes deprecated code.  It still works, but can be a bit confusing for the uninformed as you see lines cutting across the screen.  













All that said, if you're looking to explore the Horizon Sever API it's silly not to take advantage of all these preconfigured and organized calls.   You can go ahead and create your own collection, but also import this collection and start copying it's most relevant calls into your own.  That's certainly what I did.  Stand on the shoulders of giants baby!  

Speaking of giants, along with these Postman Collections, Chris also once authored an article in TechZone called, "Using The VMware Horizon Server REST API." Like his collections, it was getting a little long in the tooth which is why I imagine it was removed from TechZone November of last year.  However, it was a beautiful piece of documentation, providing many a Horizon admin their first introduction to REST architecture and the JSON standard.  So I'd highly recommend taking a look at the original article using the internet archive.  Here's a link to a version of the document that was published as of November of 2023.  


"I got bills to pay 
 I got mouths to feed
 There ain't nothing in this world for free" - Cage The Elephant

For a lot of grizzled Horizon admin the trickiest thing about Horizon Server API adoption is it's REST architecture.  Sure, as windows admins they've had all the motivation in the world to learn PowerShell, but up till now they may never have had  reason to access a REST based API.  So for some folks there's a bit of a learning curve as they get up to speed with REST architecture and JSON format.  But once the Horizon admin is THERE, they're THERE and they can certainly do anything they've been doing with PowerCLI using the more modern Horizon Server API.  As an added bonus, getting up to speed on REST architecture leads to deeper understanding of how much of the internet runs today, and sets a foundation for automating against SaaS solutions that enterprises have been fully embracing.   

To get deeper into the Horizon Server API and REST principles in general, check out my follow up article, No REST For The RESTful: Chaining Together Calls To The Horizon Server API.