There are some who said this day would never come…
This has been on my wish list for a very long time, and on the wishlists of several other people in the community that I’ve talked to about it. With the v9.93 update released yesterday we finally have webhooks for the JSS: real time outbound event notifications.
This is a big deal for those of us who work on building integrations into the Casper Suite. If you’ve wanted a service to run and take action on changes happening in the JSS you were normally forced to have an API script run on a schedule to pull in mass amounts of data to parse through. That’s not real time and computationally expensive if you’re an admin with a large environment.
How the Events API relates to Webhooks
There has been an alternative route to using the REST API and that was the Events API. If you haven’t heard of it that may be because it isn’t advertised too loudly. It was shown off at the 2012 JNUC by some of the JAMF development team:
The Events API is Java based. You write a plugin that registers for certain events and then it receives data to process. This all happens on the JSS itself as the plugin must be installed into the application. It is also Java which not many of us are all too fluent in. Plus if you use JAMF Cloud you don’t have access to the server so plugins aren’t likely to be in the cards anyway.
This new outbound event notification feature is actually built ON TOP of the existing Events API. Webhooks translate the Events API event into an HTTP POST request in JSON or XML format. HTTP, JSON and XML. Those are all things that the majority of us not only understand but work with on an almost daily basis. They’re languages we know, and they’re agnostic to what you use to process them. You can use shell scripting, Python, Ruby, Swift; it doesn’t matter now!
How a webhook integration work
If you want to start taking advantage of webhooks for an integration or automation you’re working on, the first thing to understand is that webhooks needs to be received on an external web server to the JSS. This diagram shows the basic idea behind what this infrastructure looks like:
Wehbooks trigger as events occur within the JSS. The primary driver behind the majority of these events will be the check-in or inventory submission of your computers and mobile devices. When a change occurs the JSS will fire off the event to the web server hosting the integration you’ve created.
At that point your integration is going to do something with the data that it receives. Likely, you’ll want to parse the data for key values and match them to criteria before executing an action. Those actions could be anything. A few starting examples are:
- Send emails
- Send chat app notifications
- Write changes to the JSS via the REST API
- Write changes to a third-party service
Create a webhook in the JSS
There are a number of events from the Events API you can enable as an outbound webhook. They are:
For a full reference on these webhook events you can visit the unofficial docs located at https://unofficial-jss-api-docs.atlassian.net/wiki/display/JRA/Webhooks+API
When you create the outbound webhook in the JSS you give it a descriptive name, the URL of your server that is going to receive the webhook, the format you want it sent int (XML or JSON) and then the webhook event that should be sent.
Once saved you can see all of your webhooks in a simple at-a-glance summary on the main page:
That’s it. Now every time this event occurs it will be sent as an HTTP POST request to the URL you provided. Note that you can have multiple webhooks for the same event going to different URLs, but you can’t create webhooks that send multiple events to a single URL. At this time you need to create each one individually.
Create your integration
On the web server you specified in the webhook settings on the JSS you will need something to receive that HTTP POST and process the incoming data.
There are a number of examples for you to check out on my GitHub located here: https://github.com/brysontyrrell/Example-JSS-Webhooks
As a Python user I’m very comfortable using a microframework called Flask (http://flask.pocoo.org/). It’s simple to start with, powerful to use and allows you to scale your application easily.
Here’s the basic one-file example to getting started:
import flask app = flask.Flask('my-app') @app.route('/') def index(): return "<h1>Hello World!</h1>" if __name__ == '__main__': app.run()
On line 1 we’re import the flask module. On line 3 we’re instantiating our flask app object.
On line 5 this is a decorator that says “when a user goes to this location on the web server run this function”. The ‘/’ location would be the equivalent to http://localhost/ – the root or index of our server. Our decorated function is only returning a simple HTML string for a browser to render in this example
Line 9 will execute the application if we’re running it from the command line as so:
~$ python my_app.py
I will say at this point that this works just fine for testing out your Flask app locally, but don’t do this in production.
There are many, many guides all over the internet for setting up a server to run a Flask application properly (I use Nginx as my web server and uWSGI to execute the Python code). There are some articles on the Unofficial JSS API Docs site that will cover some of the basics.
With that being said, to make our Flask app receive data, we will modify our root endpoint to accept POST requests instead of GET requests (when using the @app.route() decorator it defaults to accepting only GET). We also want to process the incoming data.
This code block has the app printing the incoming data to the console as it is received:
import flask app = flask.Flask('my-app') @app.route('/', methods=['POST']) def index(): data = flask.request.get_json() # returned JSON data as a Python dict() # Do something with the data here return '', 204 if __name__ == '__main__': app.run()
For processing XML you’ll need to import a module to handle that:
import flask import xml.etree.ElementTree as Et app = flask.Flask('my-app') @app.route('/', methods=['POST']) def index(): data = Et.fromstring(flask.request.data) # Do something with the data here return '', 204 if __name__ == '__main__': app.run()
You can see that we changed the return at the end of the function to return two values: an empty string and an integer. This is an empty response with a status code of 204 (No Content). 2XX status code signal to the origin of the request that it was successful.
This is just a technical point. The JSS will not do anything or act upon different success status codes or error codes. 200 would be used if some data were being returned to the requestor. 201 if an object were being created. Because neither of those are occurring, and we won’t be sending back any data, we’re using 204.
With this basic starting point you can begin writing additional code and functions to handle processing the inbound requests and taking action upon them.
Here is a list of the links that I had provided in this post:
- JAMF Nation Annoucement Post – Please join the conversation here!
- Unofficial JSS API Docs – Webhooks API Documentation
- Example JSS Webhook Integrations on GitHub
- Join the MacAdmins Slack and visit the #jamfnation and #jss-api channels