Build Your Own Jamf Pro Integrations: A Tutorial Series

byo-jpi-logo
Enter a caption

During Penn State MacAdmins 2017, I delivered my first ever workshop style session entitled “Build Your Own Jamf Pro Integrations.” Going in I felt well prepared and convinced that people would walk away feeling empowered to build new kinds of functionality atop their Jamf Pro environments.

The result was not what I envisioned.

Information about the level of experience with Python that was required coming into the workshop was lost in the posting. My slide pacing was too fast. People weren’t given enough time to write down the examples on screen before I would move on to lab sections which did not contain any code examples. Due to a lot of the group not being at the needed experience level with Python I burned through extra time moving around to help them troubleshoot issues and reach the next step. I ended up not finishing all of the content I had prepared and the workshop was left with an unfinished air about it.

Overall, I wasn’t too happy with how the workshop turned out. Some of the attendees afterwards gave me some feedback about what could have been done to improve for a future version (which I was also encouraged to submit for 2018). Make sure the prerequisite experience for the workshop is clearly communicated. The full code being available prior to the workshop would have made transitioning between sections easier. Volunteer assistants to help others with issues and errors as they arise to allow me to focus on delivery. Do a full day and not a half: I had more than enough content to fill it.

Later, when the form submitted feedback was compiled and provided, I found that on a whole the sentiments above were shared by many. They enjoyed the content, they liked the hands-on approach, but structure and timing prevented them from getting the most out of it. The reception was better than I had expected (coming from an IT background, I know it’s more likely that people will submit complaints than compliments).

While I still intend to submit this workshop again for MacAdmins 2018, I have decided to adapt the slide deck into a multi-part tutorial series that will cover building your first Flask (Python) based integration with Jamf Pro and Slack.

Once this post has gone live, each part of the tutorial will go up once a day until the series has been completed. The full code for the complete sample project will be available on my GitHub on day of the last posting.

Requirements

You can use either Python 2 or Python 3, but you will need to create a virtual environment and install the required modules from the Python Package Index (PyPI).

If you are going to use the Python installation that came with your Mac (Python 2) you can install pip and virtualenv using the following commands:

~$ sudo easy_install pip
~$ sudo pip install virtualenv

If you have installed another copy of Python (either 2 or 3) on your Mac, or you are installing Python on Windows or Linux, these commands should be available as a part of that.

Create the virtual environment for the project somewhere in your user directory (the root or your Documents folder would both work):

~$ virtualenv byojamf

To use your new virtual environment call it’s activate script and you will see the environment’s name appear in parenthesis in the terminal session:

~$ source /path/to/byojamf/bin/activate
(byojamf) ~$

Now install the three PyPI modules that we will be using for this sample project:

(byojamf) ~$ pip install flask jook requests

Flask is a microframework for writing web applications.

GitHub: https://github.com/pallets/flask
Documentation: http://flask.pocoo.org/docs/latest/

Requests is an HTTP client library that we will be using to make API requests.

GitHub: https://github.com/requests/requests
Documentation: http://docs.python-requests.org/en/master/

Jook is a Jamf Pro Webhooks simulator to allow you to test your integration.

GitHub: https://github.com/brysontyrrell/Jook
Documentation: http://jook.readthedocs.io/en/latest/

Next up…

An Intro to Webhooks and Flask

I’ll see you in Part I tomorrow!

Advertisements

Casper-HC: the friendly HipChat plugin

Hello all.

The last time I posted on this blog I was still working in the IT department at Jamf. I happen to still be working at Jamf, but after five years of watching the IT team grow up I was approached with an opportunity to make a difference within our Cloud & Delivery team (Jamf Cloud) as a System Administrator.  You’ll likely hear more about that as time goes on.

Now, I wouldn’t have taken any job that didn’t involve me writing lots, and lots, of Python code, and as it so happens I’m currently in the middle of building an API using my all-time favorite microframework: Flask.

In the month leading up to my transition I tried to burn through code-completion (in my book that means the application was in a fully usable state if not feature complete) on several web apps that I had rolling. I’ve been given permission to push all of these to the Jamf IT GitHub page so you’ll see a series of blog posts detailing each of them.

Those projects include:

  • Casper-HC
    (This post)
  • QuickDNS 2
    A front-end to a DNS (bind9) that allows the creation of randomized and chosen names and managing those names using a RESTful API.
  • Apple School Manager Photo Server
    A side-project for Jamf that allowed numerous Jamf Pro servers to demo Apple School Manager’s photo integration (a bit of a niche).
  • Avatar Server (Employee Photos)
    An internal employee photo service that mimics the way photos are delivered from Gravatar with options for scaling and auto-cropping. This was originally written for us to be able to use Facewall in our Minneapolis office lobby.

So, about Casper-HC…

When webhooks finally, finally, came into Jamf Pro with v9.93 I was very excited about what that meant for the next generations of plugins/integrations that could start to be developed. I had openly talked about a bare-bones HipChat plugin that I had been working on and my desire to build real time notifications into its features. I had also talked about doing the same with Slack.

We’re an Atlassian shop so HipChat got my attention first (sorry…).

casper-hc-installed.png

Casper-HC is the friendly HipChat plugin to Jamf Pro:

https://github.com/jamfit/Casper-HC

The original (internal) plugin I wrote for HipChat and Jamf Pro was purely a search interface in chat format. Type some commands into a room with some search terms and you’ll get nicely rendered results back.

The all-new plugin is a Flask application that is installed per-room (not globally) and makes much better use of HipChat’s API framework. All of the original search functionality has been preserved, improved, and I’ve integrated the new webhooks to provide notifications.

Get started.

When you install the plugin into a room you’ll get the nice notification you see above providing a randomized endpoint for you to send webhook events to. This is partly security through obscurity as there are no authentication options from the Jamf Pro server to the destination. It also links the inbound webhook to the appropriate chatroom.

If authentication makes its way into the product I can add in support.

An extra step of configuring a service account for accessing the REST API is required before using the search features, but notifications are immediately available. This allows you to install the plugin into rooms that will purely display notifications without any other features.

Follow the suggestion to type casper help to learn more.

casper-hc-help.png

Not all the help text has been implemented at this time, but we can see that enabling a service account is done on the configuration page for the plugin for this room. Heading there gives us a very bare bones screen for entering a URL, username, and a password for the JSS.

 

casper-hc-configure.png

Clicking Save will perform a quick test to the Jamf Pro server to verify the account can actually authenticate against it. Upon success the username and password are encrypted and saved in the database and you will receive two notifications.

 

casper-hc-configured.png

casper-hc-configured-notify.png

A future feature would be to verify the service account has all the required permissions for the search functions. That currently isn’t being handled.

What all does it do?

While not a huge deal, you can always grab your current version of the Jamf Pro server.

casper-hc-version.png

Right away notifications were available to setup in the new room. You might have noticed that all of the system notifications from the plugin are shown in purple. Different types of notifications will have different colors to help them stand out in high traffic rooms and in some cases provide a little context to what occurred.

casper-hc-notifications.png

The following webhooks are supported:

  • ComputerAdded
  • ComputerCheckIn
  • ComputerInventoryCompleted
  • JSSShutdown
  • JSSStartup
  • MobileDeviceCheckIn
  • MobileDeviceEnrolled
  • MobileDeviceUnEnrolled
  • PatchSoftwareTitleUpdated
  • RestAPIOperation

Some of those could easily flood a chat room which is where installing across multiple rooms comes in handy. One trick with RestAPIOperation notifications is that the plugin, for the installed room, will ignore API notifications triggered by the service account for that room, but those API calls would appear in another room also receiving API notifications.

The search functionality covers computers, mobile devices, and users. As a design choice I skipped the “slash” command convention and made the plugin listen for room messages beginning with “casper” and then the appropriate keyword. The regular expressions support shortnames for each of them so you can type quicker:

  • c|omputer|s
  • m|obile|s
  • u|ser|s

casper-hc-computers1.png

All of the search commands follow the same syntax of:

casper [command] (search string)

Computer and mobile device searches take advantage of the */match/* endpoints in the API. This effectively replicates the functionality of being logged into Jamf Pro and using the search boxes. When searching these devices you can:

  • Use wildcards (*)
  • Matches on most device identifiers and location data
  • Return single and list results

Users are a little different in that they have no similar feature in the API. Instead, you can match a user by passing either the matching username or email address for their record (no wildcards here).

Nearly every notification contains a weblink back to the original object in the Jamf Pro web interface. This makes the plugin extremely handy for techs by eliminating a large number of clicks in order to get to the device records of interest. If you have a chat room setup to receive notifications for support tickets you can search that user immediately without leaving the window (this is what the buzzword “chat-ops” refers to).

How do you start using it?

As I mentioned earlier, Casper-HC is just a Flask app. It requires a MySQL database for the backend and a web server for the front end, but it isn’t dependent upon any specific platform. You can find some more instructions on getting up and running in both a test and production environment in the README file for the project’s repository.

The running plugin that is being used for chat rooms at Jamf is deployed as a Docker container. You can find the setup for this here:

https://github.com/jamfit/Casper-HC-Docker

The docker-compose setup launches three containers: Nginx, Casper-HC + uWSGI, and MySQL. A data volume is created to persist the database between container tear-downs. You can find out more in the README at the link.

If you decide to follow suit with deploying Casper-HC as a containerized app you will want to create a service to periodically run a mysqldump and backup your database.

Are there future plans?

With my move out of IT I won’t be directly working on improving the plugin for my own use any longer, but if people were to begin using it and open issues on the GitHub page for bug fixes and feature requests I can pick up the work or others can contribute to improving the plugin.

A few of the features I had planned on getting to:

  • Get computer/mobile/user group memberships in chat
  • View advanced search results in chat
  • Non-destructive MDM commands from HipChat cards (card actions)
  • File uploads to Jamf Pro (via chat attachments)

If your org uses HipChat and Jamf Pro, I’d like to encourage you to try it out and send some feedback my way.

Build Your Own Jamf Pro Integrations: Part I An Intro to Webhooks and Flask

Welcome back to the BYO Jamf Pro Integrations tutorial! In Part I we will be giving an introduction to both the webhooks feature of Jamf Pro and the Flask microframework that you installed into your virtual environment in the introduction post.

Webhooks in Jamf Pro

Webhooks are a framework introduced in Jamf Pro v9.93. A webhook itself is an HTTP callback: an HTTP POST that occurs when something happens.

A webhook itself is an HTTP request made by a server to a destination, with a payload, in response to an event. Jamf Pro’s webhooks are built directly on top of a pre-existing Java API called the Events API.

The Events API allowed for Java plugins as .jar files to be installed on the Jamf Pro server. While not an option for Jamf Cloud customers, self-hosted users can take advantage of this. Learn more here: https://github.com/jamf/JSSEventsAPI

While not 100%, the available events for Jamf Pro Webhooks closely matches the list for the Events API.

  • ComputerAdded
  • ComputerCheckIn
  • ComputerInventoryCompleted
  • ComputerPolicyFinished
  • ComputerPushCapabilityChanged
  • JSSShutdown
  • JSSStartup
  • MobileDeviceCheckIn
  • MobileDeviceCommandCompleted
  • MobileDeviceEnrolled
  • MobileDevicePushSent
  • MobileDeviceUnEnrolled
  • PatchSoftwareTitleUpdated
  • PushSent
  • RestAPIOperation
  • SCEPChallenge
  • SmartGroupComputerMembershipChange
  • SmartGroupMobileDeviceMembershipChange

To setup a webhook for one of these events, log into your Jamf Pro Server and navigate to Settings -> Global Management -> Webhooks. Click the + New button and you will be taken to a screen to set and select the following:

  • Name
    A description.
  • URL
    The address that you want Jamf Pro to send the event data to.
  • Content Type
    Choose whether that data in in XML or JSON format.
  • Event
    The event from the list above that you want to send on.

When you setup your webhook, Jamf Pro will send an HTTP POST with a payload of the content type you selected containing contextual data on the event. These payloads are broken into two parts: the webhook and eventObject/event keys.

Here is an example in XML:

<JSSEvent>
    <webhook>
        <id>1</id>
        <name></name>
        <webhookEvent>JSSShutdown</webhookEvent>
    </webhook>
    <eventObject>
        <institution></institution>
        <hostAddress></hostAddress>
        <webApplicationPath></webApplicationPath>
        <isClusterMaster>false</isClusterMaster>
        <jssUrl></jssUrl>
    </eventObject>
</JSSEvent>

Here is that same example as JSON:

{
    "webhook": {
        "id": 1,
        "name": "",
        "webhookEvent": "JSSShutdown"
    },
    "event": {
        "institution": "",
        "hostAddress": "",
        "webApplicationPath": "",
        "isClusterMaster": false,
        "jssUrl": ""
    }
}

The webhook key is about the Jamf Pro Webhook itself. The database ID, name you set and the type of event are contained here. This is data that, later in the tutorials, can be used to identify which events you are receiving from Jamf Pro.

The eventObject (XML) or event (JSON) key contains the contextual data of what triggered the event, or contextual data about the event depending upon which event was triggered. Many of the events all send the exact same data under this key with the difference will be what is contained under the webhook key. This is, for examples, true for Computer* and MobileDevice* events.

You can dive into full examples of every webhook event in XML and JSON formats at the Unofficial JSS API Docs site: https://unofficial-jss-api-docs.atlassian.net/wiki/spaces/JRA/pages/14450694/Webhooks+API

Flask

There needs to be something on the receiving end, the destination URL of the Jamf Pro webhook, to receive the payload and process it / take action on. This is where web development enters the picture. The type of integration that works with webhooks is a web app with endpoints that can accept POST requests.

There are many web technologies out there for all kinds of programming languages. The tutorial series will focus on Python (very popular in the Mac admin community for scripting alongside Ruby) and a microframework called Flask.

The “micro” portion means that Flask does not contains many elements of larger frameworks like a pre-defined database interface. Instead, Flask relies on extensions that plug into the framework and extend the functionality of your code. Flask also does not dictate design choices. Flask apps can be hundreds of files in size, structured in nearly any way, or just one single file. The size and complexity of the project is determined by the scope of your work.

Here is the absolute smallest Flask app that you could write (and you can use this as the boilerplate code to start any of your projects from):

# my-jamf-app.py
import flask

app = flask.Flask(__name__)


@app.route('/')
def root():
    return "Hello Penn State MacAdmins!"

if __name__ == '__main__':
    app.run()

Seven lines.

You should have Flask installed and available for your project in the virtual environment created in the first post. At the top of our file we are importing the package.

Then we create an app object that is an instance of the flask.Flask() class:

app = flask.Flask(__name__)

This object will represent the web app throughout our code. To add endpoints, or routes, to the web app we will use the route() decorator.

Decorators are a special kind of Python syntax that “wrap” a function around another function.

In this case, the route() decorator will register an endpoint based upon the path we give as it’s first argument and then execute the wrapped function below it whenever that endpoint is requested! You’ll be able to see clearly this in a moment.

@app.route('/')
def root():
    return "Hello Penn State MacAdmins!"

The“/” path means the root of the web server. Once this app is running you will be able to reach it in your web browser by navigating to http://localhost:5000. The “/” path resolves to that address.

Flask has a built in development server you can start by calling the run() method on the app object. When you call a Python file as a script from the command line the __name__ dunder becomes set to a value of __main__.

If you are a little confused by the word “dunder” at this point don’t worry, you can continue on without understanding some of these concepts, but you may want to brush up on your Python with some online resources.

By checking if the __name__ dunder is __main__ you can control what your Python scripts do based on whether they have been called from the command line or, later on, imported into other Python code. When imported, the __name__ dunder takes on the name of the file!

So, the last two lines of this single file Flask app mean it will only run the app using the development server if it has been called as a script from the command line:

if __name__ == '__main__':
    app.run()
(byojamf) ~$ python /path/to/my-jamf-app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Your web app is now available when you navigate to http://localhost:5000 in your web browser. Give it a try and you should see the text message in the code! See how when you request the “/” or root of the web app the root() function is executing?

Decorators in action.

Next up…

Accepting Webhooks and Testing with Jook

I’ll see you in Part II tomorrow!

 

 

 

Open Distribution Server and JNUC 2017

Two posts in one day! I wanted to do a quick JNUC update and promote a session that I’m really excited for.

This year, as with years past, I will be pretty involved with the conference. Aside from finding me roaming the halls of the Hyatt; I am on the committee for the first ever JNUC Hachathon, participating in the API Scripting and Webhooks labs, and delivering the Webhooks Part Deux! presentation with Oliver Lindsey from our Pro Services team.

But the session I am most excited about is a very late addition that was put onto the JNUC App’s schedule this morning.

The Open Distribution Server

Around July (Penn State), I began work on an alternative distribution server to the JDS. As the community recently learned, the JDS has been discontinued and will no longer be supported by Jamf as cloud-centric options are being focused on. Prior to that announcement, I was involved in some talks with Product Management at Jamf about the JDS, and I took the opportunity to show them what I was working on.

Joe Bloom, our Jamf Pro Product Manager who you will hear talk at several product sessions this year, was very excited about this and urged me to continue working on my distribution server and release it as a free, open source solution.

Joe has secured an additional session slot on Tuesday at 4:00 PM dedicated to the Open Distribution Server. You can find it at the link or in the JNUC App (it is not listed on the website).

During this session I’m going to talk about the goals of this project, what it aims to solve, what features I have implemented and plan to implement, but then turn the rest of the time over to you so we can talk about the key things that will make this a successful solution:

  • What features don’t work as described or need changed to fit your workflows?
  • What features are missing that you need?
  • How can the community contribute to this project?

The current code base for this project was posted to GitHub a couple weeks ago:

https://github.com/brysontyrrell/ODST/tree/develop

The Open Distribution Server (ODS) is an open-source package distribution and syncing solution for IT administrators to serve as a potential alternative for the Jamf Distribution Server.

For those looking for an on-premise, automated distribution point solution, and those who are in need of a replacement for their JDS infrastructure, please attend and be a part of the discussion.

I hope to see you there!

Scripting the stuff that you think is only in the JSS GUI

(Or Jamf Pro – I may own Dean a dollar now…)

The JSS APIs are the first and best solution to writing automations or integrations with the data that’s in your JSS and taking action on them.

Still, those APIs sometimes have gaps in them. Things that you have access to in the GUI but not otherwise. Sometimes you will be staring at a button and asking yourself, “Why can’t I do this with the API?”

Well, perhaps you can.

In this post I am going to detail how you can replicate actions you see in the JSS GUI via scripting and open up new options to automating some normally manual processes.

It’s not really reverse engineering

Screen Shot 2016-11-15 at 10.57.44 AM.pngIt’s easier to figure out what’s happening in a web interface than you might think. I’ll be using Chrome here to dig in and find out what is happening in the background. In Chrome, you will want to use a handy feature called “Inspect” which opens a console for you to view all sorts of data about the page you are on and what it is doing.

You can open that by right/control-clicking on the browser and selecting the option from the context menu.

To observe the various requests that are happening as you click around you will want to use the “Network” tab. This section details every action the current page is making as it loads. That includes page resources, images, HTML content and various other requests.

Screen Shot 2016-11-15 at 10.58.09 AM.png

As you can see there is a lot of stuff that gets loaded. Most of it you can ignore because it isn’t relevant to what we’re trying to accomplish. Keep this open and watch carefully though as you begin clicking on actions in the pages your are on.

Let’s use OS X Configuration Profiles as an example. Wouldn’t it be nice if you could trigger a download of a signed configuration profile from the JSS without having to go to the GUI? Let’s see what happens when the ‘Download’ button is clicked.

Screen Shot 2016-11-15 at 2.47.58 PM.png

An HTTP POST request was made to the current page! POST requests usually contain data, so if we scroll down to the bottom of the Headers tab we see that the browser sent the following form-encoded data.

Screen Shot 2016-11-15 at 2.53.11 PM.png

There’s a lot of stuff being submitted here, but we can rationally ignore most of it and focus on just two key values: action and session-token.

Performing a POST to the configuration profile’s page in the JSS with those two values as form data will result in us being able to get a signed configuration profile returned!

Now, about that session-token…

You will find as you inspect actions in the JSS GUI the value called the session-token is used almost everywhere, but what is it?  The value isn’t in the cookies for our browser session, but we know it is being submitted as a part of the form data. If the data isn’t in the session then it must be stored somewhere else, and because we know it is being sent with the form…

Screen Shot 2016-11-15 at 11.22.27 AM.png

The token is in the HTML as a hidden form field! The session-token has an expiration of 30 minutes (1800 seconds) at the time it is created and is contained within the page itself. We need only to get a page that contains this token, parse it and then use it until the expiration point has been reached and then obtain another one (this is a process the JSS session handles for you and you never have to think about when in the GUI, but it’s a bit different when you’re manually obtaining these tokens and need to keep track of time).

You knew Python was going to be in here somewhere

Let’s look at some Python code using the requests library to obtain one of these session-tokens. This is a little different than how you would interact with the REST API because we need to be logged into a session and obtain a cookie for our requests.

That’s a simple task with requests:

import requests

session = requests.Session()

data = {'username': 'your.name', 'password': 'your.pass'}
session.post('https://your.jss.org', data=data)

With the code above you have now obtained a cookie that will be used for all further interactions between you and the JSS. To parse out the session-token from a page we can use this tiny function to quickly handle the task:

def get_session_token(html_text):
    for line in html_text.splitlines():
        if 'session-token' in line:
            return line.encode('utf-8').translate(None, '<>"').split('=')[-1]

You would pass the returned HTML from a GET request into the function like so:

session_token = get_session_token(session.get('https://your.jss.org/OSXConfigurationProfiles.html?id=XXX').text)

That tackles the most complicated piece about replicating GUI functions. Now that we can easily obtain session-tokens we can pass them with form data for anything we capture using the Chrome console.

Here’s the code to download a signed configuration profile and save it onto the Mac:

data = {'session-token': session_token, 'action': 'Download'}
r = session.post('https://your.jss.org/OSXConfigurationProfiles.html?id=XXX&o=r', data=data)

with open('/Users/me/Desktop/MyConfig.mobileconfig', 'wb') as f:
    f.write(r.content)

The r.content method returns the data from the response as binary instead of text like you saw above with r.text being passed to our get_session_token() function.

Double-click that .mobileconfig file and you’ll see a nice green Verified message along with the signing source being the JSS Built-In Signing Certificate.

Screen Shot 2016-11-15 at 3.23.23 PM.png

Now apply that EVERYWHERE

As you can see we were able to take a download action in the JSS and script it to pull down the desired file and save it locally without using a browser. Our process was:

  1. Perform the desired action once and observe the HTTP request and required data
  2. Start a session using an HTTP library or binary (in this example we used requests)
  3. Get a session-token from a JSS page
  4. Recreate the HTTP request using the library/binary passing the required form data with the session-token as expected

That sums it up. The key is you will need to perform the action you want to automate at least once so you can capture the request’s headers and determine what data you need to submit and how that data is going to be returned or what the response is expected to be.

Not everything int he JSS GUI will perform posts back to the exact same URI of the object you’re looking at, and the form data between these actions is likely to be different all over the place save for the presence of the session-token (from what I have observed so far).

And of course…

TEST TEST TEST TEST!!! That can never be stressed enough for anything you are doing. Be sure you’re not going to accidentally cause data loss or pull sensitive information and store it insecurely outside of the JSS. There are already plenty of ways to shoot yourself in the foot with the JSS, don’t add to it with a poorly written script.

Webhooks come to the JSS

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.

Enter webhooks.

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:

basic_webhook_integration_diagram.png

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:

  • ComputerAdded
  • ComputerCheckIn
  • ComputerInventoryCompleted
  • ComputerPolicyFinished
  • ComputerPushCapabilityChanged
  • JSSShutdown
  • JSSStartup
  • MobileDeviceCheckIn
  • MobileDeviceCommandCompleted
  • MobileDeviceEnrolled
  • MobileDevicePushSent
  • MobileDeviceUnEnrolled
  • PatchSoftwareTitleUpdated
  • PushSent
  • RestAPIOperation
  • SCEPChallenge
  • SmartGroupComputerMembershipChange
  • SmartGroupMobileDeviceMembershipChange

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.

Webhooks_AddNew.png

Once saved you can see all of your webhooks in a simple at-a-glance summary on the main page:

Webhooks_List.png

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.

Resources

Here is a list of the links that I had provided in this post:

 

HipSlack – because we can all be friends (and promoter…)

Channeling my inner David Hayer, “Kept you waiting, huh?”

But really, it has been a while. I’ve had a pretty tumultuous 2015 that pulled me away from projects both at JAMF and personally, and also took time away from the Mac admin communities, but now I’m starting to get back into writing code and just doing stuff like the good old days.

And the best way to do that is throw something out there!

Meet HipSlack

0

Someone, who shall remain nameless, joined JAMF and quipped about using Slack and HipChat side by side. It begged the question, “What, you mean like a bridge?”

45 minutes of my life late than night yielded the initial Flask app that accomplishes the bare minimum functionality that I can build off of: all messages from one HipChat room get piped into a Slack channel, and all the messages from that Slack channel get sent back to the HipChat room.

I’ve posted the initial source code to my GitHub. Right now it only supports one room to one channel and only sends text messages between them. My plans will include supporting multiple room/channel installs, transferring uploaded files between services, mapping emoji/emoticons to display correctly (where possible) and… maybe… see if @mentions could be made to work.

https://github.com/brysontyrrell/HipSlack

Check out the README for instructions on firing this up and playing around with it on your own. Also feel free to improve upon and submit pull requests if you want to take a crack at it before I get around to implementing more features/improvements.

And about promoter…

I threw that up too. Didn’t want people to believe it was vaporware.

https://github.com/brysontyrrell/promoter

Please, please don’t run that as production yet (there’s even a warning!).