The JSS REST API for Everyone

In this post I’m going to be exploring the JAMF Software Server (JSS) REST API. In the opening we’ll be covering some of the basics behind HTTP requests and responses. Then there will be examples of making these calls for both Bash and Python that you can use as starting points. Lastly I’ll outline an example of using the API for performing a search and pulling out the data we want.

In this post I’m going to be exploring the JAMF Software Server (JSS) REST API. In the opening we’ll be covering some of the basics behind HTTP requests and responses. Then there will be examples of making these calls for both Bash and Python that you can use as starting points. Lastly I’ll outline an example of using the API for performing a search and pulling out the data we want.

To better follow along with this post check out your “JSS REST API Resource Documentation”:

https://myjss.com/api (adapt to your JSS address)

The documentation page for your JSS contains a list of all of the available resources, their objects, the parameters those objects can use and what methods you can use to interact with them. Especially useful on the documentation page is the “Try it out!” button for every GET example. This will display the XML of any object in yours JSS for you to view and copy as you being to explore the API (Note: any use of the documentation page to view resources will require authentication).

 

HTTP methods:

All of the interactions with the JSS REST API are done using HTTP methods. There are four methods you may use:

GET retrieves data from a JSS resource for us to parse. A successful GET returns a status code of 200 and the XML of the requested resource/object.

POST takes input XML from a variable or a file and creates a new JSS object for a resource. A successful POST returns a status code of 201 and XML containing the ID of the object we just created, allowing us to further interact with it.

PUT takes input XML from a variable or a file and updates a JSS object. A successful PUT returns a status code of 201 and XML containing the ID of the object we updated.

DELETE will delete a JSS object. A successful DELETE will return a status code of 200 and XML containing the ID of the object we deleted including the tag: “<successful>True</successful>”.

 

Understanding status codes for HTTP responses:

Successful and failed API calls will return a variety of status codes. 2XX codes represent a successful response. 4XX and 5XX codes are errors. The status code you receive can help you troubleshoot your API call and determine what is incorrect or needs to be changed.

200: Your request was successful.

201: Your request to create or update an object was successful.

400: There was something wrong with your request. You should recheck the request and/or XML and reattempt the request.

401: Your authentication for the request failed. Check your credentials or check how they are being processed/handled.

403: You have made a valid request, but you lack permissions to the object you are trying to interract with. You will need to check the permissions of the account being used in the JSS interface under “System Settings > JSS User Accounts & Groups”.

404: The JSS could not find the resource you were requesting. Check the URL to the resource you are using.

409: There was a conflict when your request was processed. Normally this is due to your XML not including all of the required data, having invalid data or there is a conflict between what your resource and another one (e.g. some resources require a unique <name>). Check your XML and reattempt the request.

500: This is a generic internal server error. A 500 status code usually indicates something has gone wrong on the server end and is unrelated to your request.

 

Using cURL in Shell Scripts:

Working with shell scripts you will be using the cURL command. The variables below for the XML can be either XML string variables or files. If you are using a variables instead of a file you will need to be careful that you have properly escaped double-quotes within the string.

GET:
curl https://myjss.com/JSSResource/.. --user "$user:$pass"

POST:
curl https://myjss.com/JSSResource/.. --user "$user:$pass" -H "Content-Type: text/xml" -X POST -d $POSTxml

PUT:
curl https://myjss.com/JSSResource/.. --user "$user:$pass" -H "Content-Type: text/xml" -X PUT -d $PUTxml

DELETE:
curl https://myjss.com/JSSResource/.. --user "$user:$pass" -X DELETE

If you want to get both the XML response as well as the status code in the same call, you will need to amend the above commands to include “–write-out \\n%{http_code} –output -“ so the status code is written at the bottom of the output.

curl https://myjss.com/JSSResource/.. --user "$user:$pass" --write-out \\n%{http_code} --output -

If you go to parse the XML you may need to remove the status code line.

 

Using urllib2 in Python:

In Python 2.X the standard library for HTTP requests is urllib2. While there are more lines of code involved than the above cURL examples, Python has a few advantages. The response that we create below contains separate attributes for the returned XML and the status code.

# Import these resources into your script
import urllib2
import base64

# Initiate each request with these two lines
request = urllib2.Request('https://myjss.com/JSSResource/..')
request.add_header('Authorization', 'Basic ' + base64.b64encode(UsernameVar + ':' + PasswordVar))

#Use the following lines for each method when performing that request
GET:
response = urllib2.urlopen(request)

POST:
request.add_header('Content-Type', 'text/xml')
request.get_method = lambda: 'POST'
response = urllib2.urlopen(request, POSTxml)

PUT:
request.add_header('Content-Type', 'text/xml')
request.get_method = lambda: 'PUT'
response = urllib2.urlopen(request, PUTxml)

DELETE:
request.get_method = lambda: 'DELETE'
response = urllib2.urlopen(request)

We can take everything shown above and write a multi-purpose function to handle each of the methods. Depending upon what you are writing you can use this as a starting point and adapt it by removing some methods, changing the value returned (in this case the entire response) or introduce error handling so failures trigger actions or display specific output instead of Python’s Traceback.

Example of a Python API function:

def call(resource, username, password, method = '', data = None):
    request = urllib2.Request(resource)
    request.add_header('Authorization', 'Basic ' + base64.b64encode(username + ':' + password))
    if method.upper() in ('POST', 'PUT', 'DELETE'):
        request.get_method = lambda: method
     
    if method.upper() in ('POST', 'PUT') and data:
        request.add_header('Content-Type', 'text/xml')
        return urllib2.urlopen(request, data)
    else:
        return urllib2.urlopen(request)

Usage:

response = call('https://myjss.com/JSSResource/..', 'myname', 'mypass')
response = call('https://myjss.com/JSSResource/..', 'myname', 'mypass', 'post', myXML)
response = call('https://myjss.com/JSSResource/..', 'myname', 'mypass', 'put', myXML)
response = call('https://myjss.com/JSSResource/..', 'myname', 'mypass', 'delete')

In any case, we can get our status code and the XML by typing:

response.code
response.read()

 

Parsing results:

In Bash you have a few ways of retrieving data from your XML. Many Unix wizards out there can dazzle you with complex looking Awk and Sed one-liners. I have a simple one that I have used before here:

response=$(curl https://myjss.com/JSSResource/computers/id/1/subset/location --user "$user:$pass")
email=$(echo $response | /usr/bin/awk -F'<email_address>|</email_address>' '{print $2}')

Those two lines (which can also be made just one) return the email address of the user assigned to the Mac which can be handy in configuring software. The above method works because I know there is only one “<email_address>” tag in the XML.

For a more ubiquitous tag such as “<name>” we need to pipe our XML to a powerful command-line tool called Xpath. Xpath is a Perl binary that parses XML (but cannot modify it!). To retrieve the value of the tag you pipe the XML to Xpath with the path to the tag as an argument and then we can use Awk or Sed to remove the tags.

response=$(curl https://myjss.com/JSSResource/computers/id/1 --user "$user:$pass")
username=$(echo $response | xpath '//general/name' 2>&1 | awk -F'<name>|</name>' '{print $2}')

The “2>&1” for the Xpath command is redirrecting STDERR to STDOUT so we can suppress some of the verbose output from Xpath.

In Python we will be using the “xml.etree.ElementTree” library to working with the returned XML from the JSS. While there are a number of options avialable in Python, xml.etree.ElementTree is a part of the standard library and, from my experience, handles the job very well.

response = call('https://myjss.com/JSSResource/computers/id/1', 'myname', 'mypass')

# You'll see this in almost all online xml.etree.ElementTree examples where the import has
# been shortened to 'etree' or 'eTree' for easier writing.
import xml.etree.ElementTree as etree

xml = etree.fromstring(response.read())

We now have an object we can interact with. You can use xml.etree.ElementTree to not only read data out of of the XML, but modify it, output the changes into an XML string that can be used for a POST or PUT request, and also create new XML data on its own or that can be inserted into another XML object. The library is very powerful, but we’ll cover just reading out data. You might notice that the “.find()” method here is similar to Xpath where we type out the path to our tags.

# ID of the Mac's assigned Site
xml.find('general/site/id').text

# The Mac's assigned user's email address
xml.find('location/email_address').text

# The Mac's model
xml.find('hardware/model').text

# Here's a more advanced example where we're extracting part of the XML and parse it.
# In this example we're getting the name, version and path for every application that
# is in the Mac's inventory record.
applications = xml.find('software/applications')
for app in applications.iter('application'):
    print app.find('name').text
    print app.find('version').text
    print app.find('path').text

 

Working with the JSS API:

We’re now going to take all of the above knowledge and apply it to an example flow using the JSS REST API. In this scenario, we have a script or an app that is going to:

  1. Create an advanced computer search looking for Macs assigned to a specific user.
  2. Update that search with new criteria and display fields.
  3. Find and parse data on those results.
  4. Delete the advanced search once we are done.

Something that you won’t find in the JSS API documentation are criteria for when you’re creating an object. This can end up being a trial and error process, but the best practice I can recommend is going into a JSS, creating an empty object and then reading that object’s XML either through the API or the API documentation page. You should be able to get a good idea of what the base requirements for the XML you’ll be writing must be.

In our POST XML below we have the basic outline of an advanced computer search. Here we only have the name and a single criteria which we’ll pass a variable into (the triple quotes at the beginning and the end are a Python feature for multi-line variables, but you can write source XML files to use in Bash too). Something I would like to make a note of is the object’s “<name>” tag is only required when you are POSTing a new object. You will not need it again unless you intend to rename the object.

POSTxml = '''<advanced_computer_search>
    <name>Example</name>
    <criteria>
        <criterion>
            <name>Username</name>
            <priority>0</priority>
            <and_or>and</and_or>
            <search_type>is</search_type>
            <value>{VARIABLE}</value>
        </criterion>
    </criteria>
</advanced_computer_search>'''
POST https://myjss.com/JSSResource/advancedcomputersearches/id/0 POSTxml
    Returns 201 status code and XML with the "<id>" of created resource.

The URL in my POST example ends with “/id/0”. Every object for each resource in the JSS is assigned a unique ID in sequential order starting at 1. Sending our POST to 0 tells the JSS we are creating a new object and it should be assigned the next available ID number. That ID is then returned with our response. Once we capture that ID and store it in ours script we will be able to pass it to the rest of our functions.

Let’s say the ID of our newly created object was 100.

GET https://myjss.com/JSSResource/advancedcomputersearches/id/100
    Returns 200 status code and XML of the object.

Even though we only provided the name and a single criteria for our advanced search the response XML now includes many more elements now including “<computers>” and “<display_fields>”. As soon as we POST the advanced computer search the JSS populates it with matching results.

We’re now going to update the critera for the advanced search. Our XML is going to look nearly identical except we have removed the “<name>” tag as we don’t want to make any changes there. Any PUT made to the JSS will replace the existing fields those contained in the XML. A PUT action is not an additive function. This is something to be careful about. If you were to perform a PUT of a single computer into a static computer group you would end up wiping the membership!

We’re going to switch this search from searching by assigned user to computer name.

PUTxml1 = '''<advanced_computer_search>
    <criteria>
        <criterion>
            <name>Computer Name</name>
            <priority>0</priority>
            <and_or>and</and_or>
            <search_type>is</search_type>
            <value>${VARIABLE}</value>
        </criterion>
    </criteria>
</advanced_computer_search>'''

PUT https://myjss.com/JSSResource/advancedcomputersearches/id/100 PUTxml1
    Returns 201 status code and XML with the "<id>" of the updated resource.

GET https://myjss.com/JSSResource/advancedcomputersearches/id/100
    Returns 200 status code and XML of the resource.

Performing another GET on the advanced search will return new results. If you scanned through either of the lists inside “<computers>” you would find that the XML contains the JSS IDs, names, and UUIDs (or UDIDs…) of all the Macs that meet the criteria. While we could begin making API calls on each of these computer objects to retrieve and parse data, there’s a more efficient method.

We’re going to update the advanced search with criteria for both the username and the computer name to narrow the results even further, but we’re also going to add in display fields we want returned values on. The criteria for the advanced search have priorities associated so we need to make sure the second criterion is incremented.

PUTxml2 = '''<advanced_computer_search>
    <criteria>
        <criterion>
            <name>Username</name>
            <priority>0</priority>
            <and_or>and</and_or>
            <search_type>is</search_type>
            <value>{VARIABLE}</value>
        </criterion>
        <criterion>
            <name>Computer Name</name>
            <priority>1</priority>
            <and_or>and</and_or>
            <search_type>like</search_type>
            <value>{VARIABLE}</value>
        </criterion>
    </criteria>
    <display_fields>
        <display_field>
            <name>JSS Computer ID</name>
        </display_field>
        <display_field>
            <name>Asset Tag</name>
        </display_field>
        <display_field>
            <name>Computer Name</name>
        </display_field>
        <display_field>
            <name>Username</name>
        </display_field>
        <display_field>
            <name>Operating System</name>
        </display_field>
        <display_field>
            <name>Model</name>
        </display_field>
        <display_field>
           <name>Serial Number</name>
        </display_field>
    </display_fields>
</advanced_computer_search>'''

PUT https://myjss.com/JSSResource/advancedcomputersearches/id/100 PUTxml2
    Returns 201 status code and XML with the "<id>" of the updated resource.

If you ran the advanced search in the JSS web app you would see each of those “<display_fields>” as their own column with the matching computer records as rows. In the API we can access the data in the same way by making a GET request to “../JSSResource/computerreports/id/100”. The “<computer_reports>” resource provides the data for the selected “<display_fields>” of an advanced computer search. We can use the same ID for both resources.

GET https://myjss.com/JSSResource/computerreports/id/100
    Returns 200 status code and XML of the resource containing the values for each "<display_field>" of the matching advanced computer search.

You can rinse and repeat the above steps and number of times, parse the data and pipe it out to another system or in a more human readable format. Once we are done with the advanced search and computer report we can clean up by deleting the resource from the JSS.

DELETE https://myjss.com/JSSResource/advancedcomputersearches/id/100
 Returns 200 status code and XML with the "<id>" of the deleted resource and a "<successful>" tag.

 

Wrap Up

Hopefully the example we walked through and the code snippets will help you start experimenting more the the JSS REST API and start leveraging it to automate more of what you do. While I chose to do an example based up advanced computer searches, I’ve heard of amazing workflows involving:

  • Automated license management (../JSSResource/licensedsoftware)
  • Package deployment (../JSSResource/packages, ../JSSResource/policies)
  • Security (../JSSResource/computercommands, ../JSSResource/mobiledevicecommmands).

If you have any feedback on this post, or feel that something hasn’t been explained clearly, please reach out to me.

Author: Bryson Tyrrell

AWS serverless developer from the Twin Cities. Former benevolent Casper Admin at Jamf, helped cofound Twin Cities Mac Admins @MspMacAdmns,, avid Python coder.

17 thoughts on “The JSS REST API for Everyone”

  1. I’m not seeing where you “Create an advanced computer search looking for Macs assigned to a specific user.” as described above. What’s the GET?

    Thanks,

    RL

    Like

    1. A little further down we perform a PUT adding in a criteria for username:

      –My XML got rendered and lost, but the part where we PUT in a criteria containing a USERNAME is where we’re performing a match.

      Like

      1. Great! If you capture the output into a variable I don’t think you’ll need the pipe to tail:

        $ response=$(curl -u ‘username':’password’ https://my.jss.com/JSSResource/activationcode –write-out %{http_code} –silent –output /dev/null)
        $ if [ $response -ne 0 ]; then
        > echo "Request error: $response"
        > fi

        Like

  2. Hi is there a python script to query all mobile devices (i.e. iPads) and then display the battery percentage? The objective is to use Nagios monitoring to alert any iPads that are low on battery thus needs to be plugged in for recharge. Currently we have to manually check every iPads battery level in the morning.

    Like

Leave a comment