Searching the JSS with API calls and Python

Searching the JSS with API calls and Python

r2d2_search

Like a loyal droid, your Jamf Pro server wants to help! (Episode III)


Jamf Software’s Jamf Pro server (JPS) provides an Application Programming Interface, or API, to interact with the JPS database. This allows an enterprise to customize specific areas of the JPS as needed. This post builds on the other articles in the series: Fetching Data From the JSS using API calls & Python and Publishing information to the Jamf Pro server using API calls and Python. We’ll discuss searching for specific data within the JPS.

The inspiration for this series of articles came from internal inventory application that began as an experiment I described in a previous post: Using GIF images in Python GUI scripts. Since that post was written the small experiment turned into a cross-platform, automated, text- and GUI-based application that talks to our JPS, campus LDAP as well as multiple MySQL databases. The core activity of the application is maintaining the User and Location area of the Inventory pane for each computer in the JPS. It’s called Tugboat.

Here’s an example of Tugboat’s search function is action:

Searching the JPS


Each type of device has a similar GET method for searching inside its space:

/computers/match/{search term}

/mobiledevices/match/{search term}

The following table shows slightly different fields each device categories search encompasses, there are also the fields returned with each match:

Computers


General
asset_tag
bar_code_1
bar_code_2
id
name
Hardware
alt_mac_address
mac_address
serial_number
udid
User and Location
building
building_name
department
department_name
email
email_address
position
realname
room
username

Mobile Devices


General
id
name
Hardware
mac_address
serial_number
udid
wifi_mac_address
User and Location
building
building_name
department
department_name
email
email_address
position
realname
room
username

Breaking down the sample script


Building the search URL

I’ll assemble the search URL in two stages for ease of reading, the base API URL and the full search URL. The jss_base_url  is built using the jss_host string the user enters at the beginning of script. The search_url  is constructed by combining the base API URL with the jss_device_space  (computer or mobiledevice), with the API call (match ) and the jss_search_term .

The search can be wildcarded by wrapping the search team with asterisks (* ). This will return any record that includes with search term within any of the supported fields. Note that hitting return in the script on the JSS search string?  prompt will match everything, since it will be an empty wildcard. Removing the asterisks will search only for instances of that specific search term.

# assemble root API url to your Jamf pro server
jss_base_url = 'https://' + jss_host + ':8443/JSSResource'


# assemble search space url, wildcarding the search string
# entering a blank string will return everything
search_url = jss_base_url + '/' + jss_device_space + '/match/*' + jss_search_term + '*'
Executing the search URL

I pass the search_url to the JPS using the urllib2 module. This module is included in the standard library of Python. Other developers find the Requests module easier to use. If your production environment allows you to install it, I suggest taking a look at it.

# build the request we'll send to the JSS
request = urllib2.Request(search_url)
request.add_header('Accept', 'application/json')
request.add_header('Authorization', 'Basic ' + base64.b64encode(jss_user + ':' + jss_pwrd))

# send request to jss
response = urllib2.urlopen(request)
Handling the results

Reading the payload of the response involves using the JSON module to organize an easily parse-able data structure.

response_json = json.loads(response.read())
Displaying the results

The JPS API gets a little odd here, so hang with me for a second. While we may search the mobiledevices  space, the root node of the results is labeled mobile_devices . So we’ll have to use the appropriate label here when we ask for the number of sub-items.

We continue to use the JSON module to pretty print the entire data structure.

if jss_device_space == 'mobiledevices':
    print('\n{:,d} matches returned from {} search.'.format(len(response_json['mobile_devices']), jss_device_space))
else:
    print('\n{:,d} matches returned from {} search.'.format(len(response_json[jss_device_space]), jss_device_space))

# pretty print the data returned from search
print(json.dumps(response_json, indent=4, sort_keys=True))

Complete sample script


Here is the complete sample script that show’s how to connect to your JPS, request a search and display the results:
#!/usr/bin/python
"""
An example script describing a simple search in a JAMF Pro server.
"""

from __future__ import print_function
import base64
import getpass
import inspect
import json
import re
import urllib2


def simple_jss_search(jss_user, jss_pwrd, search_url, jss_device_space):
    """
    perform search on JSS, handle errors and display results
    """

    try:
        # build the request we'll send to the JSS
        request = urllib2.Request(search_url)
        request.add_header('Accept', 'application/json')
        request.add_header('Authorization', 'Basic ' + base64.b64encode(jss_user + ':' + jss_pwrd))

        # send request to jss
        response = urllib2.urlopen(request)

        # attempt to parse returned data into a JSON object
        try:
            response_json = json.loads(response.read())
        except Exception as exception_message:
            print("issue parsing JSON: {}".format(exception_message))
            return

        # a non-200 response is bad, report and return
        if response.code != 200:
            print("{}: error from jss".format(inspect.stack()[0][3]))
            return

    # handle various network communication errors
    except urllib2.HTTPError, error:
        if error.code == 400:
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Request error."))
        elif error.code == 401:
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Authorization error."))
        elif error.code == 403:
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Permissions error."))
        elif error.code == 404:
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Resource not found."))
        elif error.code == 409:
            contents = error.read()
            error_message = re.findall(r"Error: (.*)<", contents)
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Resource conflict. " + error_message[0]))
        else:
            print("{}: HTTP code {}: {}".format(inspect.stack()[0][3], error.code, "Generic error."))
        return
    except urllib2.URLError, error:
        print("{}: Error contacting JSS.".format(inspect.stack()[0][3]))
        return
    except Exception as exception_message:
        print("{}: Generic error. [{}]".format(inspect.stack()[0][3], exception_message))
        return

    # print the number of returned records
    # FYI: the API is a little odd here, while we're requesting 'mobiledevices', the data is stored in 'mobile_devices'.
    if jss_device_space == 'mobiledevices':
        print('\n{:,d} matches returned from {} search.'.format(len(response_json['mobile_devices']), jss_device_space))
    else:
        print('\n{:,d} matches returned from {} search.'.format(len(response_json[jss_device_space]), jss_device_space))

    # pretty print the data returned from search
    print(json.dumps(response_json, indent=4, sort_keys=True))


def main():
    """
    Handle user input of required data.
    """

    jss_user = raw_input("JSS username? ")              # request appropriate JSS user
    jss_pwrd = getpass.getpass("JSS password? ")        # request JSS users password
    jss_host = raw_input("JSS server address? ")        # request JSS server address
    jss_search_term = raw_input("JSS search string? ")  # request string to search

    # assemble root API url to your Jamf pro server
    jss_base_url = 'https://' + jss_host + ':8443/JSSResource'

    # search for string in both device spaces
    for jss_device_space in ['mobiledevices', 'computers']:

        # assemble search space url, wildcarding the search string
        # entering a blank string will return everything
        search_url = jss_base_url + '/' + jss_device_space + '/match/*' + jss_search_term + '*'

        # call search funtion
        simple_jss_search(jss_user, jss_pwrd, search_url, jss_device_space)


if __name__ == '__main__':
    main()
Sample output of the running script:
JSS username? your_api_username	
JSS password? 
JSS server address? your.jamf.server.address
JSS search string? vmware

0 matches returned from mobiledevices search.
{
    "mobile_devices": []
}

1 matches returned from computers search.
{
    "computers": [
        {
            "alt_mac_address": "78:31:C1:D0:6A:E1", 
            "asset_tag": "n/a", 
            "bar_code_1": "", 
            "bar_code_2": "", 
            "building": "Marriott Library", 
            "building_name": "Marriott Library", 
            "department": "IT and Digital Library Services", 
            "department_name": "IT and Digital Library Services", 
            "email": "todd.mcdaniel@utah.edu", 
            "email_address": "todd.mcdaniel@utah.edu", 
            "id": 691, 
            "mac_address": "00:0C:29:16:3F:CE", 
            "name": "[DUPL]-VIRTUAL-PC", 
            "position": "User Support and Computing Services", 
            "realname": "Todd McDaniel", 
            "room": "5300A MLIB", 
            "serial_number": "VMware-56 4d bb 55 e0 58 4d 9f-0e 16 d5 b2 1e 16 3f ce", 
            "udid": "55BB4D56-58E0-9F4D-0E16-D5B21E163FCE", 
            "username": "todd.mcdaniel@utah.edu"
        }
    ]
}

Note

There is an additional computer search /computers/match/name/{matchname} documented in the API. It doesn’t appear to return any results. If anyone has any further info about this call, please let me know in the comments below!

No Comments

Leave a Reply