28 Jun Fetching Data From the JSS using API calls & Python
Like a loyal droid, your JSS wants to help!
JAMF Software’s Casper Suite provides an Application Programming Interface, or API, to communicate with the JAMF Software Server (JSS) database. This allows an enterprise to customize specific areas of the JSS as needed. This is the first in a series of articles that will discuss these APIs and walk through building Python scripts that take advantage of the data available in the JSS. Future articles will show how to upload data to the JSS, building Extension Attributes in the JSS to react to that data and more.
These articles are based on an 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 experiment turned into a cross-platform, automated, text- and GUI-based application that talks to the JSS, campus LDAP as well as two MySQL databases. The core of the application is maintaining the User and Location area of the Inventory pane for each computer in the JSS.
Requesting information from the JSS
There are a number of ways to get computer info from the JSS. The computer’s hostname or JSS ID will work. I elected to use the UUID, or Universally Unique IDentifier because it’s a cross-platform solution. Here are examples for obtaining UUIDs on macOS and Windows:
Getting the Macintosh UUID
uuid_request_raw = subprocess.check_output(["system_profiler", "SPHardwareDataType"]) my_uuid = re.findall('Hardware UUID: (.*)', uuid_request_raw)[0] print "uuid: %s" % my_uuid
Getting the Windows UUID
uuid_request_raw = subprocess.check_output("wmic CsProduct Get UUID") uuid_request_raw = uuid_request_raw.split("\r\r\n")[1] my_uuid = uuid_request_raw.split(" ")[0]
Making the connection
Once you’ve selected a method of identifying the machine, you will then need to request the data from the JSS. This is done by submitting a specific URL to the JSS. Python has a number of modules available to perform this action. I’ve included examples using the included urllib2 and the very popular Requests module. If you choose the requests method, you will need to make sure the module is installed on any machine you attempt to use your script on.
The code breaks down to the following steps:
- Assembling the URL
- Submitting the URL
- Storing the reply
- Creating a JSON object to contain the organized contents of the reply
Assembling the URL
# JSS API credentials UsernameVar = "your_api_user" PasswordVar = "your_api_user_password" # base URL of JSS jss_url = 'https://your_api_server:8443/JSSResource' computer_url = jss_url + '/computers/udid/' + local_uuid
urllib2:
request = urllib2.Request(computer_url) request.add_header('Accept', 'application/json') request.add_header('Authorization', 'Basic ' + base64.b64encode(UsernameVar + ':' + PasswordVar)) response = urllib2.urlopen(request) print "Status code from request: %s\n" % response.code response_json = json.loads(response.read())
requests:
response = requests.get(computer_url, headers={'Accept': 'application/json'}, auth=(UsernameVar,PasswordVar)) print "Status code from request: %s" % response.status_code response_json = json.loads(response.text)
Handling the data
The JSS will reply to your request in a JSON-formatted message. The JavaScript Object Notation is a common, easy to manipulate, human readable data format. The message will be arranged in something like a tree, requiring you to navigate various branches (keys) to find the leaf (data) you require. Fortunately, the json module makes it very easy to find the information you’re looking for.
Once we’ve stored the reply in a json object, we can use the following command to print the entire object. The quantity of data isn’t something that would be useful to an end user, but you as the developer, will need to see it to familiarize yourself with the structure. A computer record from our JSS is nearly 5,000 lines.
print json.dumps(r_json, sort_keys=True, indent=4)
The following code snippet shows direct navigation and processing the extension attributes of a machine to find a specific value. Each extension attribute contains the JSS ID for the EA and the name of the EA, this example utilizes the name. Again, refer to the entire JSON object to build the specific path to the information you need.
General User and Location data
print "JSS assigned ID: %s" % r_json['computer']['general']['id'] print "Asset Tag: %s" % r_json['computer']['general']['asset_tag'] print "User name (uNID): %s" % r_json['computer']['location']['username'] print "Email Address: %s" % r_json['computer']['location']['email_address'] print "Real name: %s" % r_json['computer']['location']['real_name'] print "Phone number: %s" % r_json['computer']['location']['phone'] print "Location: %s" % r_json['computer']['location']['room']
Parsing the Extended Attributes for specific information
jss_purpose_raw = r_json['computer']['extension_attributes'] for ea in jss_purpose_raw: if ea['name'] == 'Inventory purpose': jss_purpose = ea['value'] print "JSS purpose field: %s" % jss_purpose
More Data from the JSS
The JSS offers much, much more information. For example we can ask the JSS to give us all of the buildings or departments we’ve defined, this information could be used to build a menu offering the user a valid set of choices. This data is located in a different location so we’ll need to change to URL we will call to reflect it.
departments_url = jss_url + '/departments'
urllib2:
request = urllib2.Request(departments_url) request.add_header('Accept', 'application/json') request.add_header('Authorization', 'Basic ' + base64.b64encode(UsernameVar + ':' + PasswordVar)) response = urllib2.urlopen(request) print "Status code from request: %s\n" % response.code response_json = json.loads(response.read())
Requests:
response = requests.get(departments_url, headers={'Accept': 'application/json'}, auth=(UsernameVar,PasswordVar)) print "Status code from request: %s\n" % response.status_code response_json = json.loads(response.text)
Massaging the data
The JSON object containing the department information needs to be cleaned up as we’re only interested in the names of the departments.
{ "departments": [ { "id": 8, "name": "Administration" }, { "id": 12, "name": "Collections and Scholarly Communication" }, { "id": 11, "name": "IT and Digital Library Services" }, { "id": 9, "name": "Research and User Services" }, { "id": 10, "name": "Special Collections" } ] }
This section examines each branch and stores the name in the departments list.
print json.dumps(response_json, sort_keys=True, indent=4) departments = [] for a_department in response_json['departments']: departments.append(a_department.get('name')) print "%s" % departments
Your API User
You should seriously consider creating a dedicated “API User” with the minimum required privileges. Should this account be compromised, the damage would be minimized. In System Settings:JSS User Accounts & Groups. Creating a new user with a Privilege Set of Auditor is easiest. Then uncheck every box, in the JSS Settings and in every panel down to Casper Imaging. This gives your api user a limited amount of Read privileges.
Putting it all together
Here is the complete sample script, if you have the requests module available you can comment out or remove the urlllib2 sections. You will need to supply the correct URL to your JSS and the credentials for your “API User”.
#!/usr/bin/python import subprocess import platform import requests import urllib2 import base64 import json import xml.etree.cElementTree as ET import re def main(): current_platform = platform.system() if current_platform == 'Darwin': local_uuid_raw = subprocess.check_output(["system_profiler", "SPHardwareDataType"]) local_uuid = re.findall('Hardware UUID: (.*)', local_uuid_raw)[0] elif current_platform == 'Windows': local_uuid_raw = subprocess.check_output("wmic CsProduct Get UUID") local_uuid_raw = local_uuid_raw.split("\r\r\n")[1] local_uuid = local_uuid_raw.split(" ")[0] else: quit() # JSS API credentials UsernameVar = "your_api_user" PasswordVar = "your_api_user_password" # base URL of JSS jss_url = 'https://your_jss_server:8443/JSSResource' computer_url = jss_url + '/computers/udid/' + local_uuid print ("Fetching computer record from JSS...") print ("") # # urllib2 method request = urllib2.Request(computer_url) request.add_header('Accept', 'application/json') request.add_header('Authorization', 'Basic ' + base64.b64encode(UsernameVar + ':' + PasswordVar)) response = urllib2.urlopen(request) print "Status code from request: %s" % response.code print ("") response_json = json.loads(response.read()) # end of urllib2 method # # requests method. Try not to use, since we'd need to deploy another module # response = requests.get(computer_url, headers={'Accept': 'application/json'}, auth=(UsernameVar,PasswordVar)) # print "Status code from request: %s" % response.status_code # response_json = json.loads(response.text) # end of requests method # print full record # print json.dumps(response_json, sort_keys=True, indent=4) # print appropriate data print "JSS assigned ID: %s" % response_json['computer']['general']['id'] print "Asset Tag: %s" % response_json['computer']['general']['asset_tag'] print "User name (uNID): %s" % response_json['computer']['location']['username'] print "Email Address: %s" % response_json['computer']['location']['email_address'] print "Real name: %s" % response_json['computer']['location']['real_name'] print "Phone number: %s" % response_json['computer']['location']['phone'] print "Location: %s" % response_json['computer']['location']['room'] jss_ea_list = response_json['computer']['extension_attributes'] for ea in jss_ea_list: if ea['name'] == 'Inventory purpose': jss_purpose = ea['value'] print ("JSS purpose field: %s" % jss_purpose) print ("") print ("") print "Fetching Departments from JSS..." print ("") departments_url = jss_url + '/departments' # # urllib2 method request = urllib2.Request(departments_url) request.add_header('Accept', 'application/json') request.add_header('Authorization', 'Basic ' + base64.b64encode(UsernameVar + ':' + PasswordVar)) response = urllib2.urlopen(request) print "Status code from request: %s" % response.code print ("") response_json = json.loads(response.read()) # end of urllib2 method # # requests method. Try not to use, since we'd need to deploy another module # response = requests.get(departments_url, headers={'Accept': 'application/json'}, auth=(UsernameVar,PasswordVar)) # print "Status code from request: %s\n" % response.status_code # response_json = json.loads(response.text) # end of requests method # print full record print json.dumps(response_json, sort_keys=True, indent=4) departments = [] for a_department in response_json['departments']: departments.append(a_department.get('name')) print "%s" % departments if __name__ == '__main__': main()
Output of the script
Here’s the output of the sample script running on our JSS. If you haven’t defined any departments, your results will be different.
uuid: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn Fetching computer record from JSS... Status code from request: 200 JSS assigned ID: 35 Asset Tag: 123456 User name (uNID): u0000000 Email Address: lyanna.mormont@utah.edu Real name: Lyanna Mormont Phone number: (801) 555-1212 Location: 6001 MLIB JSS purpose field: Staff laptop Fetching Departments from JSS... Status code from request: 200 { "departments": [ { "id": 8, "name": "Administration" }, { "id": 12, "name": "Collections and Scholarly Communication" }, { "id": 11, "name": "IT and Digital Library Services" }, { "id": 9, "name": "Research and User Services" }, { "id": 10, "name": "Special Collections" } ] } [u'Administration', u'Collections and Scholarly Communication', u'IT and Digital Library Services', u'Research and User Services', u'Special Collections']
Acknowledgements
Without the following resources, my project wouldn’t have progressed as quickly as I did.
http://macbrained.org/the-jss-rest-api-for-everyone/
https://unofficial-jss-api-docs.atlassian.net/wiki
No Comments