30 Jul Searching the JSS with API calls and Python
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
asset_tag
bar_code_1
bar_code_2
id
name
alt_mac_address
mac_address
serial_number
udid
building
building_name
department
department_name
email_address
position
realname
room
username
Mobile Devices
id
name
mac_address
serial_number
udid
wifi_mac_address
building
building_name
department
department_name
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
#!/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