Python wrapper for JSS API.
This project provides a python wrapper for the Jamf JSS API.
Designing a wrapper for the JSS API allowed me to solve some of the issues I was facing in administering our macs in more efficient ways. Increased use of autopkg to automate our software deployment also led to an interest in the API that Jamf provides. While this project aims to offer simple, elegant, pythonic access to the JSS, the level of data construction and validation that may be required for tasks beyond policy, package, and computer management may be lacking.
A concrete example is the template system. While I rely heavily on automating policy creation, I will not need implementations for MobileDeviceInvitations. However, based on the code here, it should be easy for anyone wishing to do so to implement a new() method for those objects, and I would be happy to include them. Send me your pull requests!
The easiest method is to use pip to grab python-jss:
$ pip install python-jss
Alternately, the python-jss package can be put wherever you normally install your modules.
Behind the scenes, python-jss uses requests and Greg Neagle’s FoundationPlist. Check them out at: requests: http://docs.python-requests.org/en/latest/ FoundationPlist is part of Munki: https://code.google.com/p/munki/
The wrapper prevents you from trying to delete object types that can’t be deleted, and from POSTing to objects that can’t be created. It does zero validation on any xml prior to POST or PUT operations. However, the JSS handles all of this nicely, and ElementTree should keep you from creating improperly formatted XML. If you get an exception, chances are good the structure of the XML is off a bit.
The JSS also handles filling in missing information pretty well. For example, in a policy scope, if you provide the id of a computer to scope to, it will add the name.
Basics-Connecting to the JSS:
Prior to doing anything else, you need a JSS object, representing one server. Note, it’s quite possible to have active connections to multiple servers for transferring data between them!
# Connect to the JSS >>> import jss >>> jss_prefs = jss.JSSPrefs() >>> j = jss.JSS(jss_prefs)
Supplying Credentials to the JSSPrefs object:
You need a user account with API privileges on your JSS to connect and do anything useful. It is recommended that you create a user specifically for API access, with only the privileges required for the task at hand. For testing purposes, a fully-enabled admin account is fine, but for production, permissions should be finely controlled.
These settings are in the JSS System Settings=>JSS User Accounts & Groups panel.
The preferred method for specifying credentials is to create a preferences file at “~/Library/Preferences/com.github.sheagcraig.python-jss.plist”. Required keys include: - jss_user - jss_pass - jss_url (Should be full URL with port, e.g. “https://myjss.domain.org:8443” and can be set with:
defaults write ~/Library/Preferences/com.github.sheagcraig.python-jss.plist jss_user <username> defaults write ~/Library/Preferences/com.github.sheagcraig.python-jss.plist jss_pass <password> defaults write ~/Library/Preferences/com.github.sheagcraig.python-jss.plist jss_url <url>
If you are working on a non-OS X machine, the JSSPrefs object falls back to using plistlib, although it’s up to you to create the proper xml file.
Interacting with the JSS:
In general, you should use the following workflow: - To query for existing objects, use the factory methods on the server instance. For each object type listed in the API reference, there is a corresponding method: JSS.Package, JSS.MobileDeviceConfigurationProfile, etc. If the query is successful, it will return an object of the appropriate class, or for listing operations (no argument), it will return a list of objects.
After careful consideration, I decided to do it this way rather than by using the composite pattern to treat lists and single objects similarly. In thinking about what operations I would want to perform, deleting ALL computers at once, or updating all policies at once, for example, seemed both dangerous and unnecessary.
Also, the JSS returns different data structures for an object type depending on the context. A “full” object listing is not the same thing as the greatly abbreviated data returned by a listing operation or a “match” search. Likewise, trying to PUT a new object by just editing the full XML retrieved from an already existing object would fail. (For example, the ID property is assigned by the JSS, not you.)
Therefore, there are a couple of object classes for each type of data: * JSSObject: All data on a single object, like a single computer, the activation code, or a policy. * JSSObjectList: A list of JSSListData objects, containing only the most important information on an object.
>>> # To query, provide a search argument: >>> existing_policy = j.Policy("Install Adobe Flash Player-220.127.116.11") >>> # To list, don't provide any arguments: >>> all_policies = j.Policy()
- For creating new objects (of classes which allow it) instantiate an object of the desired type directly. I.e.:
>>> # This creates a new Policy object with the basic required XML. >>> new_policy = jss.Policy(j, "Al Pastor") >>> # When you are ready to add it to the server, perform a save... >>> new_policy.save()
Notice that with new objects you have to pass a reference to the server object to the object constructor. Think of this as associating this new object, in this case a Policy, with a server.
- Any time you want to save changes to an existing policy or upload a new one, call the .save() method on it. Continuing from above…
>>> existing_policy.find('general/name').text = "Install Adobe Flash Player 18.104.22.168" >>> existing_policy.save()
- Deleting an object is a method on the object for those types which support it.
Querying for Objects:
Different objects allow different kinds of searches. Most objects allow you to search by ID or by name.
>>> # Find a computer (returns a Computer object, which prints itself if not >>> # assigned >>> j.Computer('my-computer') <computer> <general> <id>42</id> <name>my-computer</name> ... </general> ... # Tons of information removed for example's sake </computer >>> # Most JSSObjects have a name and id property. >>> mycomputer = j.Computer('my-computer') >>> mycomputer.name 'my-computer' >>> mycomputer.id '42' >>> # ...as well as some extra properties on devices >>> mycomputer.serial_number 'WXXXXXXXXXXX' >>> mycomputer.udid '1F38EB0B-XXXX-XXXX-XXXX-XXXXXXXXXXXX' >>> # Computers have a list of addresses, since you can't be sure >>> # what network devices they have >>> mycomputer.mac_addresses ['3C:07:54:XX:XX:XX', '04:54:53:XX:XX:XX'] >>> # Mobile devices have wifi and bluetooth mac properties: >>> myipad = j.MobileDevice('my-ipad') >>> myipad.wifi_mac_address 'C3:PO:XX:XX:XX:X1' >>> myipad.bluetooth_mac_address 'C3:PO:XX:XX:XX:X2' >>> # Providing no arguments to the factory method returns a list. >>> # (Some object types return only a set of data, like ActivationCode). >>> computers = j.Computer() >>> computers -------------------------------------------------- List index: 437 id: 453 name: my-mbp -------------------------------------------------- List index: 438 id: 454 name: my-imac -------------------------------------------------- List index: 439 id: 455 name: USLab-test -------------------------------------------------- ... # Results go on...
Working with JSSObjectList(s):
You can sort lists of objects, which by default uses the ID property. You can also sort by name. Also, objects referenced in a list can be “converted” to full objects by using the retrieve method.
Again, listing operations don’t retrieve full information. A list of computers returns only their names and ID’s. A list of mobile devices returns a bit more info: Serial number, mac addresses, UDID, and a few others. Obviously, the JSS stores a lot more information on these devices, and indeed, pulling the “full” object allows you to access that information.
>>> # Objects can be retrieved from this list by specifying an id or list index: >>> myimac = computers.retrieve(438) # same as computers.retrieve_by_id(454) >>> # The entire list can be "convertd" into a list of objects, although this >>> # can be slow. >>> full_computers_list = computers.retrieve_all()
The available object types can be found in the JSS API documentation. They are named in the singular, with CamelCase, e.g. MobileDeviceConfigurationProfiles for mobiledeviceconfigurationprofiles.
Of course, you can get a list like this as well:
>>> help(jss) >>> help(jss.JSS) # For factory method names...
The JSS works with data as XML, and as such, python-jss’s objects all inherit from xml.etree.ElementTree. Users familiar with Elements will find manipulating the data very easy. Those unfamiliar with ElementTree should check out https://docs.python.org/2/library/xml.etree.elementtree.html and http://effbot.org/zone/element-index.htm for great introductions to this useful module.
python-jss adds a better repr method to its JSSObjects and, however. Simply print() or call an object in the interpretor to see a nicely indented representation of the Element. This aids in quickly experimenting with and manipulating data in the interpretor.
In addition to the various methods of Element, JSSObjects also provides helper methods to wrap some of the more common tasks. Policies, for example, includes methods for add_object_to_scope(), add_object_to_exclusions(), set_recon(), set_set_service(), etc.
To see a full list of methods available for an object type, as well as their signatures and docstrings:
Help on class Policy in module jss.jss: class Policy(JSSContainerObject) | Method resolution order: | Policy | JSSContainerObject | JSSObject | xml.etree.ElementTree.Element | __builtin__.object | | Methods defined here: | | add_object_to_exclusions(self, obj) | Add an object 'obj' to the appropriate scope exclusions block. | | obj should be an instance of Computer, ComputerGroup, Building, | or Department. | | add_object_to_scope(self, obj) | Add an object 'obj' to the appropriate scope block. | | add_package(self, pkg) | Add a jss.Package object to the policy with action=install. | | clear_scope(self) | Clear all objects from the scope, including exclusions. #...more methods and properties
Note: All data in the objects are strings! True/False values, int values, etc, are all string unless you cast them yourself. The id properties of the various objects are strings!
Example: Creating, Updating, and Deleting Objects:
To create a new object, you need to instantiate the desired object type with a reference to the JSS server you plan to upload to, and a name. Some object types include extra keyword arguments to speed up initial setup.
Next, modify the object to your needs and then call the save() method.
>>> new_policy = jss.Policy(j, "New Policy") >>> # Manipulate with Element methods >>> new_policy.find('enabled').text = 'false' >>> # Add a computer to the scope (accepts Computer objects, or ID or name) >>> # First, let's grab a computer to scope to... >>> myIIGS = j.Computer("myIIGS") >>> # ...and add it to our policy's scope: >>> new_policy.add_object_to_scope(myIIGS) >>> # Up to this point, the object is not on the server. To upload it... >>> new_policy.save() >>> # Subsequent changes must also be saved: >>> new_policy.find('general/name').text = 'Install Taco Software' >>> new_policy.save() >>> # ...and to delete it: >>> new_policy.delete()
The JSS stores all of the information about your file share “Distribution Points” in an API object named, appropriately “DistributionPoints”. These repositories contain the packages and scripts that are deployed with policies, and are normally managed with the Casper Admin application.
python-jss includes objects to help handle these repositories. When you create a JSS object, it includes a DistributionPoint object to delegate copying to as a property, named .distribution_points. e.g. my_jss.distribution_points. For this to be useful, you’ll have to include some extra information in your com.github.sheagcraig.python-jss.plist file. Add a key repos, with an array as its value. The array should contain dictionaries containing name (which corresponds to the name field on the JSS Computer Management->File Share Distribution Points->Display Name field), and password. It should look like this:
<key>repos</key> <array> <dict> <key>name</key> <string>Repo1</string> <key>password</key> <string>xyzzy</string> </dict> <dict> <key>name</key> <string>Repo2</string> <key>password</key> <string>abc123</string> </dict> </array>
Once this is in place, the JSS object can be used to copy files to the distribution points with the copy methods. In general, copy() should be used, as it will enforce putting pkg and dmg files into Packages, and everything else into Scripts automatically. There are copy_pkg() and copy_script() methods too, however.
If the DP isn’t mounted, the copy operation will mount it automatically. If it’s important to keep the mount from appearing in the GUI, you can use the nobrowse=True parameter to the mount methods on the individual DP’s.
Please note: Copying a file to the distribution points does not create a Package or Script object! You must also use the python-jss Package.new() and Script.new() to create the objects in the database. The Packages and Scripts directories must be flat, meaning no subdirectories (although technically, bundle-style packages are directories, but this is not an issue). When specifying the filename, the JSS will assume a package is in the Packages directory, and a script in the Scripts directory, so only specify the basename of the file (i.e. Correct: ‘my_package.pkg’ Incorrect: ‘jamf/Packages/my_package.pkg’).
It’s not really important which order you do this in, with the only realy side effect being that Casper Admin will report missing files if the Package/Script object has been created before it has been copied to the file shares.
Configuring the permissions correctly on your file share can be important to prevent unexpected freakouts!
As soon as the ability to mount using the hashed passwords from the DistributionPoints works, the need to specify the passwords in the preferences file will go away.
Finally, you can always ignore the delegate JSS.distribution_points and just set up your own, or even instantiate individual repositories and copy/mount/etc to them manually.
Requests is in the process of integrating changes to urllib3 to support Server Name Indication (‘SNI’) for python 2.x versions. If you are requesting SSL verification (which is on by default in python-jss), and your JSS uses SNI, you will probably get Tracebacks that look like this:
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "requests/api.py", line 55, in get return request('get', url, **kwargs) File "requests/api.py", line 44, in request return session.request(method=method, url=url, **kwargs) File "requests/sessions.py", line 461, in request resp = self.send(prep, **send_kwargs) File "requests/sessions.py", line 567, in send r = adapter.send(request, **kwargs) File "requests/adapters.py", line 399, in send raise SSLError(e, request=request) requests.exceptions.SSLError: hostname 'testssl-expire.disig.sk' doesn't match 'testssl-valid.disig.sk'
Installing and/or upgrading the following packages should solve the problem: - pyOpenSSL - ndg-httpsclient - pyasn1
Supposedly, requests with py3.x does not have this problem, so developing with that environment may be a possibility for you as well.
Hopefully this is temporary, although requests’ changelog does claim to have “Fix(ed) previously broken SNI support.” at version 2.1.0 (Current included version is 2.3.0).
FoundationPlist, binary plists, and Python:
python-jss should handle all plist operations correctly. However, you may see a warning about FoundationPlist not importing.
OS X converts plists to binary these days, which will make the standard library plistlib fail, claiming that the plist is “badly formed.” Thus, python-jss includes FoundationPlist. However, if you have installed python from a non-Apple source (i.e. python.org), FoundationPlist’s dependencies will not be met, and python-jss will fall back to using plistlib. This will also happen on non-OS X machines, where it should not be a problem, since they shouldn’t be secertly converting preferences to binary when you aren’t looking.
To include binary plist support, you will need to ensure that python-jss/FoundationPlist have access to the PyObjC package, and specifically the Foundation module. In some circumstances, it can be as easy as adding the path to the Apple-installed PyObjC to your PYTHONPATH. On my machine:
This won’t work for Python3.x, and may not work for some setups of 2.x. You should either try to install PyObjC sudo pip install pyobjc, create a plist file by hand rather than by using defaults (you could create the file as described above and then plutil -convert xml1 plist_filename , or just use the username and password arguments to the JSS constructor and avoid using the JSSPrefs object.