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 comments:

Post a Comment