Using Box as a Distribution Point – The 9.x Version

I never updated my original post on using Box.com as a Casper Share after the Casper Suite 9.0 was released  The process is still essentially the same as in 8.x, but instead of only posting updated screenshots I thought I would add in a couple of alternatives to relying upon Box Sync and Casper Admin as I had described before.

But first, something pretty important I’ve learned for anyone considering this workflow…

Single Sign On Considerations

WebDAV doesn’t work with SSO credentials.  If you check out Box’s article here you can follow the instructions for setting up an ‘external password’ that will use the account’s email address and that password for authentication.  If you choose to go this route consider setting password requirements that force the password to match or exceed what is used for SSO.

Now for putting it all together.

Setup the Casper Share Folder in Box

Before putting all of the settings into your JSS, make sure the Box folder you want to use is in place with the correct permissions.

Create a user account in Box that is just for read-only (viewer) access and invite it to that folder.  By inviting the user you can have the ownership of the Casper Share rest with any other user in your Box account, located anywhere in their folder hierarchy, and it will still be a root-level folder for the read-only user.

Remember, you can invite as many other users are you need to for managing your packages, or you can get real crafty and do some fun stuff using Box’s automation tools they’ve introduced.  Example:

  1. You have a folder in Box called “Package Staging”.
  2. Your IT staff upload a package here and it triggers a task to the party responsible for reviewing packages prior to distribution.
  3. If the task is marked as completed the package is then automatically moved to the Casper Share.

Nifty stuff.  I won’t really dive too much into it beyond that, but you get the gist.

Now, inside the Casper Share create a new “Packages” folder.  Because this is being treated as a traditional File Share Distribution Point the JSS will take the address and path we input below and then look for a “Packages” folder for the files to download.

The Box based Casper Share is now ready for use.

File Share Distribution Point Settings

In 9.x there was a change where the File Sharing tab for your File Share Distribution Point could not have empty values.  In 8.x as I had previously described you could get around the File Sharing settings by never clicking on it.  Even so, these settings are meaningless as we will never interact with this distribution point using AFP/SMB, so inputting junk values is acceptable.

The following screenshots detail the settings to use.  Here is also a quick summary:

  • General > Server Address: dav.box.com
  • HTTP/HTTPS > “Use HTTP downloads” is checked
  • HTTP/HTTPS > “Use SSL” is checked
  • HTTP/HTTPS > Context: /dav/CasperShare — This can also be any folder you want instead of labeling it “CasperShare”
  • HTTP/HTTPS > Authentication Type: Username and Password

 

 

Configure Your Network Segment

You may be in a position where you want to have specific network segments being directed to this distribution point, but generally you would want to use Box as the external catch-all for anyone who is not inside one of your offices.  You Network Segment settings will look something like this to direct all external clients to download from Box:

  • Starting IP Address: 1.1.1.1
  • Ending IP Address: 255.255.255.255
  • Default Distribution Point: Specific file share distribution point
  • Default File Share Distribution Point: Your Box.com distribution point from above

Skip Casper Admin, Use a Browser and the API

In the previous article I had details how to setup Box Sync on a Mac, make the directory mountable allowing you to continue to leverage Casper Admin for package management.

You don’t really need to do that.  Using the Box web interface works great for uploading even large files that are multiple gigabytes in size.

The one thing that was nice about Casper Admin was it created the package object for you in the JSS after you dragged it in and the app copied the file out to your master distribution point.  You can easily do this yourself through the API and build out a workflow that works best for your staff.  If you’re not familiar with the JSS REST API you can read my introduction post here.  There are code example for how to interact with it.

The minimal XML you would use to post your package’s information into the JSS would look like this:

<package>
    <name>Twitter</name>
    <filename>Twitter.pkg</filename>
</package>

That’s it.  POST that to ../JSSResource/packages/id/0 and you’ll have a package you can assign to your policies.  Of course, there are a lot of options you can set in the XML.  You only need to include the elements that you want to specify in the JSS.  Otherwise, everything not included in the XML will be set at their defaults (disabled checkboxes, no category, priority 10).

<package>
    <name>Twitter.pkg</name>
    <category>Social</category>
    <filename>Twitter.pkg</filename>
    <info>Repackaged on 2014-09-12</info>
    <notes>Packaged by Bryson</notes>
    <priority>15</priority>
    <boot_volume_required>false</boot_volume_required>
    <os_requirements>10.7, 10.8, 10.9, 10.10</os_requirements>
</package>

There are also elements that are very specific to the type of package you’re working with.

If the installer requires a reboot, set that flag with this element:

<reboot_required>false</reboot_required>

If you’re using a DMG instead of a PKG you can set the “Fill User Template” options:

<fill_user_template>true</fill_user_template>
<fill_existing_users>true</fill_existing_users>

If you are still working in an environment with PowerPC Macs in the mix you can set the restrictions for the architecture and an alternative package to run:

<required_processor>x86</required_processor>
<switch_with_package>Twitter_PPC.pkg</switch_with_package>

Lastly, if you’re dealing with distributing software updates and want them installable only if they are listed in the Mac’s available software updates, you can enable that:

<install_if_reported_available>true</install_if_reported_available>

 

Using OS X VMs 
for FileVault 2 Testing

This post details how to create a FileVault 2-ready base OS X virtual machine using Parallels Desktop 10 and then create either a virtual machine template or make use of the new ‘linked clones’ feature to quickly create new virtual machines to test FileVault 2 workflows.

Using a virtual machine for FileVault 2 testing has a number of benefits.

  • A virtual machine is much smaller than most Macs (without repartitioning) and it takes less time to fully encrypt.
  • Instead of having to decrypt a Mac to run a test again, or wipe it to reinstall OS X, you can clone virtual machines endlessly and delete the encrypted ones when you are finished with them.
  • You can also snapshot virtual machines and them revert to previous states.

Create a New Base OS X 10.9 VM

You can easily create a new virtual machine by dragging the “Install OS X Mavericks.app” onto the Parallels icon in your dock.

Choose the default options for CPU (2), RAM (2 GB) and Disk space (64 GB).

Click Continue to create the virtual machine.

Repartition the VM to Conserve Disk Space

When a virtual machine is FileVault 2 encrypted it will encrypt all of the disk space that it is assigned.  If left at the default of 64 GB the virtual disk file will expand to the full size and take up unneeded space on your Mac.

To prevent this, repartition the disk drive so the boot volume only takes up what is required to install OS X.

Once you are in the OS X Installer, go to the Utilities menu and open Disk Utility.

Split the drive into two partitions with the first/boot partition being set to 16 GB.

Click Apply to create the new partitions and then quit Disk Utility.

Install OS X to the 16 GB boot partition.

Reinstall OS X to Create a Recovery Partition

Once the install of OS X has finished and you have created your account open Terminal and run ‘diskutil list’.  You will note there is no Recovery HD partition present.

Per Parallel’s KB article, copying the “Install OS X Mavericks.app” to the virtual machine and running it will correctly create the Recovery HD partition and allow FileVault 2 encryption.

http://kb.parallels.com/en/122661

Mount your host Mac in the virtual machine and copy the “Install OS X Mavericks.app” to your second partition.  Then run it and re-install OS X Mavericks.

Upon completion of the second install, run ‘diskutil list’ again and you will find the Recovery HD is present.

The final virtual machine takes up less than ~15 GB of disk space on your Mac.

DO NOT FILEVAULT 2 ENCRYPT THIS!

Close all open applications and shut down the virtual machine for the next steps.

Create a New VM for FileVault 2 Testing

Option 1) Template the Base OS X 10.9 VM

You can convert your base OS X virtual machine into a VM Template.  Select the virtual machine and then from the File menu select “Clone to Template…” to create a copy that will be the template, or select “Convert to Template” to convert the virtual machine and not create a copy.

Once you have your template, double-click on it and you will receive a prompt to create a new virtual machine from it.  This will create a new virtual machine

You can share a virtual machine template with other Parallels users or move it to other Macs.  Each virtual machine created from a template will have unique identifiers to avoid conflicts.

Option 2) Create a Linked Clone of the Base OS X 10.9 VM

“Linked Clones” are a new feature of Parallels Desktop 10.  A linked clone is a virtual machine created from a snapshot of a “parent” virtual machine.  This method is faster than the above virtual machine templates for spinning up instances.

http://kb.parallels.com/en/122669

By selecting “New Linked Clone…” from the virtual machine’s context menu a snapshot will be  automatically created from the parent virtual machine and the cloned virtual machine window will immediately appear and be ready to launch.

Enable FileVault 2 on the Cloned VM

With your clone of the base virtual machine you can now enroll it to a JSS (or whatever) to test whatever FileVault 2 workflows are required.

In the below screenshots you can see this virtual machine was enrolled, a configuration profile was deployed requiring FileVault 2 and included key redirection to the JSS.  Once logged out, FileVault 2 was triggered.

Done Migrating

Very please with the performance boost. I’m vastly under-utilizing the total available capacity of the Pool, but that’s OK.  I can increase the size of any of these Spaces at any time.

Once I had removed all of the drives for the old Pool it no longer appeared when I booted up.  I can now safely store those drives and reattach them in the future should I ever need to recover data from one of the old Spaces.

Migrating Storage Spaces

A while back I posted this shaky-cam YouTube video demonstrating the Storage Spaces feature of Windows 8.  It’s essentially a software-level RAID that allows you to group a bunch of physical disks together and then thin provision logical volumes with different levels of resiliency.  Very cool tech.

When I originally set this up I was using three aging HDDs from different vendors and of different sizes (400 GB, 250 GB and 250 GB respectively) that were brought over from my previous system.  Storage Spaces enabled me to consolidate them into simpler logical volumes and I ended up with two spaces: a Media Space with two-way redundancy (equivalent to RAID1) and a Programs Space with no resiliency (equivalent to RAID0 for better read/write performance).  Both were NTFS formatted as I had made them under Windows 8.0.

I now have four higher performance 4 TB disks that will take their places in the tower.  My original plan for migrating over the existing Spaces was to add these new disks to the Storage Pool and then begin removing the older three disks one-by-one and allowing the data to be redistributed.  Storage Spaces does support this, but only if the disk contains data for Spaces with resilience.  My Programs Space, being non-resilient, prevents me from doing this.

(If I had intended on keeping the original three disks this would not be an issue, I would add the new drives into the existing Storage Pool and they would be utilized without any further action on my part.)

The backup plan is to build a new Storage Pool, recreate the two existing Spaces and then copy the contents over.  I still plan on keeping the Spaces just as they are (the performance for the Programs space should be much greater in a four disk pool), but the initial allocation to both spaces will be set to 1 TB each (Storage Spaces allows me to increase this value at any time).  I will also  be creating a File History Space this time around to enable Windows’ File History feature for local backups of my user files.

All four 4 TB disks are attached to the PC via a USB 3.0 enclosure.  Storage Spaces doesn’t care how a disk is connected to my PC.  The disks themselves contain all of the configuration information so no matter how I choose to attach them (USB, eSATA, internal SATA, etc.) Windows will correctly recognize what Storage Pool a disk belongs to.

With all that said, my step-by-step plan is:

  1. Create the new Storage Pool with the matching Storage Spaces
  2. Copy all content of the old Spaces to the new Spaces
  3. Reassign the corresponding drive letters of the original Storage Spaces to the new ones (i.e. drives D: and E:)
  4. Delete the original Storage Pool (optional: I could leave it and Windows would alert me the disks are missing once I remove them, but if I had to reattach them the Pool’s data would all be intact – CORRECTION: Removing all of the disks associate to a Pool removes the Pool from Windows. If at any time any one of the disks are reattached the Pool will appear with an error message about missing disks, but attaching all of the disks for a pool will restore it just as it was before)
  5. Power off the PC, remove the old physical disks and install the four new disks

 

Original Storage Space Setup:

(Both of the Media and File History spaces will be using the new ReFS format included in Windows 8.1.  Among many of the cool new things this file system sports, it is capable of self-healing and does not require maintenance via chkdsk.  Neither Simple or Parity storage space types support ReFS; only mirrors.)

Creating the New Storage Space:

The Two Storage Pools:

From here I’ll be doing some simple copy operations from the command line to copy everything over:

C:\> robocopy E:\ G:\ /E

I’ll probably set both copy operations to run overnight the day before I have some time to do the physical swaps.  I’ll leave an update here once that’s done just so you can know how it went.

A few quick updates: scripts, Promoter, jsslib

I’ve been absent from the blog for a while with my time being pretty split between work and family.  I thought I’d drop a quick update on some things I’m meaning to get posted and also get some small ones out of the way.  I also wanted to switch out of my minimalist theme and into something a bit easier on the eyes.

Updated script for listing Mac App Store apps  in Self Service

The original version of this script I posted here attempted to capture inventory data from a user installing an App Store app just as thought they were installing software published by the IT department.  Back in 10.8 the script worked, but as of 10.9 timeouts and errors with various AppleScript actions happened randomly and it was no longer reliable.

Also, come VPP v2 and I had a philosophical shift when it came to having accurate inventory.  As I no longer could be sure when users were claiming the apps we were provisioning from the VPP portal it made no sense to rewrite my original App Store workflow  I’m treating my Macs more and more like I would iPads when it comes to provisioning.

I trimmed out most of the code and was left with this:

#!/bin/bash

# The 'loggertag' is used as a tag for all entries in the system.log
loggertag="selfservice-macappstore"

log() {
# This log() function writes messages to the system.log and STDOUT
echo "${1}"
/usr/bin/logger -t "${loggertag}: ${policy}" "${1}"
}

# The iTunes address for the App (can be grabbed from its App Store page) is passed
# from the JSS into the 'App Store URL (itunes.apple.com/...)' variable from parameter 4.
# Example: itunes.apple.com/app/ibooks-author/id490152466
appAddress="${4}"
log "Mac App Store URL: ${appAddress}"

# The App Store is opened to the specified app.
log "Opening the Mac App Store"
/usr/bin/osascript -e "tell application \"System Events\" to open location \"macappstore://${appAddress}\""
if [ $? != 0 ]; then
    log "Script error $?: There was en error opening the Mac App Store"
    exit 1
fi

exit 0

The script takes only the iTunes/App Store URL and opens the Mac App Store to that item and nothing more.  Its simpler, an overall better experience, and I am still able to present a quick shortcut to common Mac App Store apps in Self Service just as I would on any of my users’ iOS devices.

SelfService_MacAppStore

New script for downloading Box Sync 4 via Self Service

Prior to the release of Box Sync 4 I had written a new stand-alone Self Service install script for the app.  The script at this earlier post still works for loading the Box Edit plugin, and I still use it, but the script below downloads the Box Sync 4 DMG, mounts it, copies the app and then remove all traces of the process.

#!/bin/bash

# The 'policytag' is used for log entries
policytag="BoxSync4"
# The 'loggertag' is used as a tag for all entries in the system.log
loggertag="boxsync4install"

log() {
# This log() function writes messages to the system.log and STDOUT
/usr/bin/logger -t "${loggertag}: ${policytag}" "${1}"
echo "${1}"
}

mountCheck() {
if [ -d /Volumes/boxsync4 ]; then
    log "/Volumes/boxsync4/ directory exists"
    if [[ $(/sbin/mount | /usr/bin/awk '/boxsync4/ {print $3}') == "/Volumes/boxsync4" ]]; then
        log "/Volumes/boxsync4/ is a mounted volume: unmounting"
        /usr/bin/hdiutil detach /Volumes/boxsync4
        if [ $? == 0 ]; then
            log "/Volumes/boxsync4/ successfully unmounted"
        else
            log "hdiutil error $?: /Volumes/boxsync4/ failed to unmount"
            exit 1
        fi
    fi
    log "Deleting /Volumes/boxsync4/ directory"
    /bin/rm -rf /Volumes/boxsync4
fi
}

cleanup() {
# The cleanup() function handles clean up tasks when 'exit' is called
log "Cleanup: Starting cleanup items"
mountCheck
if [ -f /tmp/boxsync4.dmg ]; then
    log "Deleting /tmp/boxsync4.dmg"
    /bin/rm -rf /tmp/boxsync4.dmg
fi
log "Cleanup complete."
}

# This 'trap' statement will execute cleanup() once 'exit' is called
trap cleanup exit

log "Beginning installation of ${policytag}"

# Check for the expected size of the downloaded DMG
webfilesize=$(/usr/bin/curl box.com/sync4mac -ILs | /usr/bin/tr -d '\r' | /usr/bin/awk '/Content-Length:/ {print $2}')
log "The expected size of the downloaded file is ${webfilesize}"

# Download the Box Sync Installer DMG
log "Downloading the Box Sync Installer DMG"
if [ -f /tmp/boxsync4.dmg ]; then
    # If there's another copy of the DMG inside /tmp/ it is deleted prior to download
    /bin/rm /tmp/boxsync4.dmg
    log "Deleted an existing copy of /tmp/boxsync4.dmg"
fi

/usr/bin/curl -Ls box.com/sync4mac -o /tmp/boxsync4.dmg
if [ $? == 0 ]; then
    log "The Box Sync Installer DMG successfully downloaded"
else
    log "curl error $?: The Box Sync Installer DMG did not successfully download"
    exit 1
fi

# Check the size of the downloaded DMG
dlfilesize=$(/usr/bin/cksum /tmp/boxsync4.dmg | /usr/bin/awk '{print $2}')
log "The size of the downloaded file is ${dlfilesize}"

# Compare the expected size against the downloaded size
if [[ $webfilesize -ne $dlfilesize ]]; then
    echo "The file did not download properly"
    exit 1
fi

# Check if the /Volumes/boxsync4 directory exists and is a mounted volume
mountCheck

# Mount the /tmp/boxsync4.dmg file
/usr/bin/hdiutil attach /tmp/boxsync4.dmg -mountpoint /Volumes/boxsync4 -nobrowse -noverify
if [ $? == 0 ]; then
    log "/tmp/boxsync4.dmg successfully mounted"
else
    log "hdiutil error $?: /tmp/boxsync4.dmg failed to mount"
    exit 1
fi

if [ -e /Applications/Box\ Sync.app ]; then
    /bin/rm -rf /Applications/Box\ Sync.app
    log "Deleted an existing copy of /Applications/Box\ Sync.app"
fi

log "Copying /Volumes/boxsync4/Box\ Sync.app to /Applications"
/bin/cp -R /Volumes/boxsync4/Box\ Sync.app /Applications/Box\ Sync.app
if [ $? == 0 ]; then
    log "The file copied successfully"
else
    log "cp error $?: The file did not copy successfully"
    exit 1
fi

# Open /Applications/Box\ Sync.app
# This will also begin the migration from Box Sync 3 to Box Sync 4
#/usr/bin/open /Applications/Box\ Sync.app

# Run a recon to update the JSS inventory
log "Postinstall for ${policytag} complete. Running Recon."
/usr/sbin/jamf recon
if [ $? == 0 ]; then
    log "Recon successful."
else
    log "jamf error $?: There was an error running Recon."
fi

exit 0

Promoter

I haven’t abandoned Promoter.  Work is still progressing, but I stopped during my reorganization of the code into classes/modules when I broke off into a tangent that ended up becoming a full blow JSS REST API Python library.  I’ll dive into that next, but I have made a couple of changes to my goals with Promoter:

  • I’m dropping file migration support – for now
    There are a number of reasons behind this.  The bigger piece of this is there’s no API to the JSS for uploading packages/scripts.  The other part is that because I have been writing Promoter as a tool to be integrated into scripted workflows it occurs to me that most IT admins will already have their own methods of transferring files between different shares and such functionality would be redundant and possibly less robust.  In the end, upon a successful migration, you would want to write in the code that would migrate your files into your production environment (if that would even be required).
  • Including more controls
    Based upon some feedback from Tobias and others, I’m going to be expanding the command line options to allow some limited transformation of the XML before it is posted to the destination JSS mainly in the form of swapping out values (e.g. setting a Site, adding in computer groups for scope that are present on the destination, forcing the policy to be enabled by default).
  • Support for importing as a Python module
    Once I have finished cleaning up the code I’ll be posting the raw source online so everyone can grab it and modify it freely, but also so the code can be easily imported into other Python scripts instead of being a command line utility.  That said, I’ll still post compiled versions of the binaries as I did before so they can be run on systems their either do not have the third-partly libraries or don’t have Python installed.

jsslib – A Python library for the JSS REST API

As I mentioned above, I’ve been working on a Python library for making scripts interacting with the JSS REST API easy and quick to write.  The project took off when I decided to rewrite the existing JSS class I had written for Promoter and then began expanding it to cover the full API.

The syntax for the new library looks like this:

>>> import jsslib
>>> myjss = jsslib.JSS('https://myjss.com', 'myuser', 'mypass')
>>> results = myjss.computers()
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/computers

The above line would return the list of all computers in the JSS (you can see the URL in the output).  While that’s easy to write its not very special.  Any kind of library should be doing work for you and making things easier for you.  That’s why I decided to write the new API to handle a lot of tasks for the end user.  The returned objects from these calls all result in “auto-parsed” attributes:

>>> results.id_list
[1, 2]
>>> results.size
'2'
>>> results.data
'<?xml version="1.0" encoding="UTF-8"?><computers><size>2</size><computer><id>1</id><name>USS-Voyager</name></computer><computer><id>2</id><name>Starship Enterprise</name></computer></computers>'

In addition to auto-parsing, the library also allows for any and all ‘simple’ objects in the JSS to be created or updated using parameters instead of passing XML:

>>> results = myjss.buildings_create("Minneapolis")
<Response [201]> https://jss.jamfcloud.com/bryson/JSSResource/buildings/id/0
>>> results.id
'5'
>>> myjss.buildings_update(5, "St Paul")
<Response [201]> https://jss.jamfcloud.com/bryson/JSSResource/buildings/id/5
<jsslib.JSSObject instance at 0x103629dd0>
>>> myjss.buildings(5)
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/buildings/id/5
>>> myjss.buildings(5).name
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/buildings/id/5
'St Paul'

The API will also be ‘smart’ in that it will use order of priority to test your input for certain API calls.  The best example of this is with computers and mobile devices.  Here is a series of API calls to look up a computer.  Take a note of the URLs in each of the outputs.

>>> results = myjss.computers(1)
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/computers/id/1
>>> results.udid
'ZZZZ0000-ZZZZ-1000-8000-001B639ABA8A'
>>> myjss.computers(results.udid).name
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/computers/udid/ZZZZ0000-ZZZZ-1000-8000-001B639ABA8A
'USS-Voyager'
>>> myjss.computers("USS-Voyager").serial_number
<Response [200]> https://jss.jamfcloud.com/bryson/JSSResource/computers/name/USS-Voyager
'QP8ZZZZHX85'

You’ll notice again I’m making use of the auto-parsing features that are built into the library.  Computer and mobile device records all return the following attributes already parsed and readable: UDID, serial number, MAC address, ID, model and computer name.

You might be wondering how you’re going to figure out how all of this works as the JSS API returns a wide variety of objects with different sets of data.  A huge point of this to me was to make sure anyone could start working with the library without hand holding, and so I’m making sure all of the documentation if baked into the code just like any other Python library:

>>> help(jsslib.JSS.advancedcomputersearches)

Help on method advancedcomputersearches in module jsslib:

advancedcomputersearches(self, identifier=None) unbound jsslib.JSS method
    Returns a JSSObject for the /advancedcomputersearches resource
 
    Example Usage:
    myjss.advancedcomputersearches() -- Returns all advanced computer searches ('None' request)
    myjss.advancedcomputersearches(1) -- Returns an advanced computer search by ID
    myjss.advancedcomputersearches('name') -- Returns an advanced computer search by name
 
    Keyword Arguments:
    identifier -- The ID or name of the advanced computer search
 
    Returned Values for a 'None' request:
    JSSObject.data -- XML from response
    JSSObject.size -- Total number of all advanced computer searches
    JSSObject.id_list -- List of all advanced computer search IDs (int) from the response
 
    Returned Values for ID or name requests:
    JSSObject.data -- XML from response
    JSSObject.id -- The ID of the advanced computer search
(END)

I presented jsslib at an internal company event not long ago and I was about 50% done at that point.  This library will end up being used by Promoter for all of its interactions with the JSS, and I will be posted the library somewhere once it is ready.  I’m also still continuing to flesh out some of the features (I’m considering adding a .search() method to returned objects for searching the XML data without having to pipe it into an XML parser).

You can expect to see jsslib popping up on here again in the near future.

As always, if you have any comments or questions just reach out to me here or anywhere else I linger.

The JSS REST API for Everyone, part 2

I had a really positive response to the original article I posted on the JSS API, and the good people over at Macbrained.org even asked for a copy to be posted up on their site.  It was a lot of fun to write up and I decided to expand upon the first post with items I skipped or did not go into detail about.  I’m also compiling both articles into a single document with a more cohesive order and fully scripted examples of the examples that are covered.

 

JSS User Accounts

If you plan to leverage the JSS API for automation or other tasks, you will want to setup unique user accounts for each script/service/app that needs access.  In version 8.x of the JSS this was a little more straightforward than in version 9.x.  In both cases, the JSS objects that you can interact with have CRUD permissions to enable or disable for the user account you are working with.

CRUD means: Create, Read, Update and Delete.  They are analogous to the API methods we have already covered: POST, GET, PUT and DELETE.

In 8.x, API permissions were separate from the web interface permissions.  You could have API accounts with limited access to certain JSS objects and no access to the web interface.  The 8.x account information API page also only displays the CRUD permissions that applied to that object.

JSS 8x API Settings

In 9.x, the permissions for all JSS objects are unified.  Setting the CRUD permissions for an object grants both API and web interface access for the account.  Any account you create for API access will also be able to log into the JSS web interface.  Another effect is that the full CRUD list is shown for every object.  You will need to rely on the API Documentation to determine what permissions are applicable to the objects you are interacting with.

JSS 9x API Settings

 

Modifying and Building XML using ElementTree (Python)

If your workflow requires modifying JSS objects you will need to drop Bash for, or at least augment it with, another scripting language that has a solid XML parser available.  In my examples here we will be using Python and the ‘xml.etree.ElementTree’ library.

This example will be modifying a static computer group’s memberships.  As I mentioned in our earlier example with advanced computer searches a PUT is not an additive action (or subtractive for that matter).  If you make a PUT request of only one item to a JSS object that contained a list of ten you will end up replacing them them all.  To add to the existing list you must include your new item in a new list already containing the existing items and then submit it.

The basic workflow for updating a JSS object is to GET the original XML, parse out the section(s) we will be updating, insert/remove the XML the elements we want and then PUT this back into the JSS to the same JSS ID.

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

<?xml version="1.0" encoding="UTF-8"?>
<computer_group>
    <id>123</id>
    <name>The Fleet</name>
    <is_smart>false</is_smart>
    <site>
        <id>-1</id>
        <name>None</name>
    </site>
    <criteria>
        <size>0</size>
    </criteria>
    <computers>
        <size>3</size>
        <computer>
            <id>1</id>
            <name>USS-Enterprise</name>
            <mac_address>NC:C1:70:1A:C1:1A</mac_address>
            <alt_mac_address/>
            <serial_number>Z00AB1XYZ2QR</serial_number>
        </computer>
        <computer>
            <id>2</id>
            <name>USS-Excelsior</name>
            <mac_address>NC:C2:00:01:1A:2B</mac_address>
            <alt_mac_address/>
            <serial_number>Z00CD2XYZ3QR</serial_number>
        </computer>
        <computer>
            <id>3</id>
            <name>USS-Defiant</name>
            <mac_address>NC:C1:76:41:B2:B3</mac_address>
            <alt_mac_address/>
            <serial_number>Z00EF3XYZ4QR</serial_number>
        </computer>
    </computers>
</computer_group>

The computer group object in the JSS contains a lot of information we won’t need for the update we will be performing. We need to convert the XML string we received in the response into an ElementTree object that we can work with.

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

To make some of our interactions easier we’re going to make another ElementTree object that is just the ‘computers’ node of the ‘computergroup’ object we just created.

computers = computergroup.find('computers')

To better visualize what that did, here is how the ‘computers’ object would print out:

<computers>
    <computer>
        <id>1</id>
        <name>USS-Enterprise</name>
        <mac_address>NC:C1:70:1A:C1:1A</mac_address>
        <alt_mac_address/>
        <serial_number>Z00AB1XYZ2QR</serial_number>
    </computer>
    <computer>
        <id>2</id>
        <name>USS-Excelsior</name>
        <mac_address>NC:C2:00:01:1A:2B</mac_address>
        <alt_mac_address/>
        <serial_number>Z00CD2XYZ3QR</serial_number>
    </computer>
    <computer>
        <id>3</id>
        <name>USS-Defiant</name>
        <mac_address>NC:C1:76:41:B2:B3</mac_address>
        <alt_mac_address/>
        <serial_number>Z00EF3XYZ4QR</serial_number>
    </computer>
</computers>

As you can see, we are now only interacting with the ‘computers’ node and its children. One of the great things about using the ElmenetTree library is that we can divide up a large XML source into multiple parts, like breaking out the computers above, but all of the changes we make will be reflected in the root object.

For example, if we wanted to find and delete the computer named ‘USS-Enterprise’ we could use the following code on the ‘computers’ object:

for computer in computers.findall('computer'):
    if computer.find('name').text == 'USS-Enterprise':
        computers.remove(computer)

Now if we print the output of the ‘computergroup’ object it will not include any computers named ‘USS-Enterprise’:

<computer_group>
    <id>123</id>
    <name>The Fleet</name>
    <is_smart>false</is_smart>
    <site>
        <id>-1</id>
        <name>None</name>
    </site>
    <criteria>
        <size>0</size>
    </criteria>
    <computers>
        <size>3</size>
        <computer>
            <id>2</id>
            <name>USS-Excelsior</name>
            <mac_address>NC:C2:00:01:1A:2B</mac_address>
            <alt_mac_address/>
            <serial_number>Z00CD2XYZ3QR</serial_number>
        </computer>
        <computer>
            <id>3</id>
            <name>USS-Defiant</name>
            <mac_address>NC:C1:76:41:B2:B3</mac_address>
            <alt_mac_address/>
            <serial_number>Z00EF3XYZ4QR</serial_number>
        </computer>
    </computers>
</computer_group>

We can use the same logic with the other key identifiers in our static group: JSS ID, serial number and MAC address.

Let’s take the truncated membership and PUT it back into the JSS; removing the computer ‘USS-Enterprise’ from the static group. We won’t be using the XML retrieved by our GET for this. Instead, we will use ElementTree to build a new XML object and then copy the computers over.

NewXML = etree.Element('computer_group')
NewXML_computers = etree.SubElement(newXML, 'computers')

We created the root element, ‘computer_group’, in the first line and then created a node, ‘computers’, in the second line. If we print out the ‘NewXML’ object it will look like this:

<computer_group>
    <computers/>
<computer_group>

Now that we have our XML structure we can use a simple for loop to iterate over each computer in the source XML and copy them over. The result will be an XML object containing only the computers we want to update the JSS computer group with.

for computer in computers.iter('computer'):
    NewXML_computers.append(computer)

<computer_group>
    <computers>
        <computer>
            <id>2</id>
            <name>USS-Excelsior</name>
            <mac_address>NC:C2:00:01:1A:2B</mac_address>
            <alt_mac_address/>
            <serial_number>Z00CD2XYZ3QR</serial_number>
        </computer>
        <computer>
            <id>3</id>
            <name>USS-Defiant</name>
            <mac_address>NC:C1:76:41:B2:B3</mac_address>
            <alt_mac_address/>
            <serial_number>Z00EF3XYZ4QR</serial_number>
        </computer>
    </computers>
</computer_group>

Now we can output this into a string and make a PUT request to the JSS. Unless writing to a file on the disk, ElementTree will not include our XML declaration line. As a workaround, we can have the XML declaration read in another string variable and then concatenate it to the output XML string.

xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>'
PUTxml = xmlDeclaration + etree.tostring(NewXML)

And here is our final XML:

<?xml version="1.0" encoding="UTF-8"?>
<computer_group>
    <computers>
        <computer>
            <id>2</id>
            <name>USS-Excelsior</name>
            <mac_address>NC:C2:00:01:1A:2B</mac_address>
            <alt_mac_address/>
            <serial_number>Z00CD2XYZ3QR</serial_number>
        </computer>
        <computer>
            <id>3</id>
            <name>USS-Defiant</name>
            <mac_address>NC:C1:76:41:B2:B3</mac_address>
            <alt_mac_address/>
            <serial_number>Z00EF3XYZ4QR</serial_number>
        </computer>
    </computers>
</computer_group>

Now we’re going to make the PUT request and update the static computer group’s membership.

PUT https://myjss.com/JSSResource/computergroups/id/123 PUTxml
    Returns 201 status code and XML with the "<id>" of the updated resource.

This code allowed us to remove a computer from a static group. What if you wanted to add a computer? The concept is the same, but now we’ll build a second XML object to insert into our XML for the PUT. To make this a little more interesting we’ll get the informaion about the computer via the API as well.

GET https://myjss.com/JSSResource/computergroups/id/123
    Returns 200 status code and XML of the resource.computergroup = etree.fromstring(response.read())

computers = computergroup.find('computers')

NewXML = etree.Element('computer_group')
NewXML_computers = etree.SubElement(NewXML, 'computers')

for computer in computers.iter('computer'):
    NewXML_computers.append(computer)

Now we will retrieve the information of the computer to add to the group and parse that into an XML object to add into our ‘NewXML’. The computer object we’re creating is just like the ‘NewXML’ object but with more sub-elements. We are also assigning values to the elements that we’re creating, parsed from the returned JSS computer XML.

GET https://myjss.com/JSSResource/computers/id/5
    Returns 200 status code and XML of the resource.
Mac = etree.fromstring(response.read())
NewMember = etree.Element('computer')
NewMember_id = etree.SubElement(NewMember, 'id')
NewMember_id.text = Mac.find('general/id').text
NewMember_name = etree.SubElement(NewMember, 'name')
NewMember_name.text = Mac.find('general/name').text
NewMember_macadd = etree.SubElement(NewMember, 'mac_address')
NewMember_macadd.text = Mac.find('general/mac_address').text
NewMember_serial = etree.SubElement(NewMember, 'serial_number')
NewMember_serial.text = Mac.find('general/serial_number').text

NewXML_computers.append(NewMember)

PUTxml = xmlDeclaration + etree.tostring(NewXML)

<?xml version="1.0" encoding="UTF-8"?>
<computer_group>
    <computers>
        <computer>
            <id>1</id>
            <name>USS-Enterprise</name>
            <mac_address>NC:C1:70:1A:C1:1A</mac_address>
            <alt_mac_address/>
            <serial_number>Z00AB1XYZ2QR</serial_number>
        </computer>
        <computer>
            <id>2</id>
            <name>USS-Excelsior</name>
            <mac_address>NC:C2:00:01:1A:2B</mac_address>
            <alt_mac_address/>
            <serial_number>Z00CD2XYZ3QR</serial_number>
        </computer>
        <computer>
            <id>3</id>
            <name>USS-Defiant</name>
            <mac_address>NC:C1:76:41:B2:B3</mac_address>
            <alt_mac_address/>
            <serial_number>Z00EF3XYZ4QR</serial_number>
        </computer>
        <computer>
            <id>5</id>
            <name>USS-Constitution</name>
            <mac_address>NC:C1:70:0C:00:00</mac_address>
            <serial_number>Z00FE4XYZ5QR</serial_number>
        </computer>
    </computers>
</computer_group>

PUT https://myjss.com/JSSResource/computergroups/id/123 PUTxml
    Returns 201 status code and XML with the "<id>" of the updated resource.

 

Extension Attributes for Power Admins

While we spent a good amount of time describing how to update a static computer group using the API, I will say this is probably a workflow you should avoid (though you may find situations where maintaining a static group this way is the correct solution).  It makes for a great example, and good practice, but there are better ways to achieve the same goal.

Consider the extension attribute.  An extension attribute is capable of returning or storing additional data beyond the standard inventory report.  You can create extension attributes to be populated by LDAP attribute, pop-up menu, script or simple text input.  These values become a part of a device’s inventory record and can be used for criteria in smart groups and advanced searches.

Let’s look at a possible scenario to apply this to.  We have an extension attribute labeled “App Store” that is displayed with the location information for a mobile device.  There are smart mobile device groups for each country your organization has a VPP account in and the extension attribute is a pop-up menu with the criteria for populating those groups.  Tried to those smart groups are country specific apps and ebooks for redemption.

As a part of the on-boarding process your users may select the App Store they will be receiving these VPP codes from.  Whichever way you wish to present this you can leverage the JSS API to dynamically populate the available options be based upon what is defined by the extension attribute you created:

GET https://myjss.com/JSSResource/mobiledviceextensionattributes/id/1
    Returns 200 status code and XML of the resource.
<mobile_device_extension_attribute>
    <id>1</id>
    <name>App Store</name>
    <description>Used for scoping content from a specific country's App Store.</description>
    <data_type>String</data_type>
    <input_type>
        <type>Pop-up Menu</type>
        <popup_choices>
            <choice>United States</choice>
            <choice>Canada</choice>
            <choice>Great Britain</choice>
            <choice>Germany</choice>
            <choice>Hong Kong</choice>
            <choice>Australia</choice>
        </popup_choices>
    </input_type>
    <inventory_display>User and Location</inventory_display>
</mobile_device_extension_attribute>

Parse out the extension attribute’s ID, name, data type and the section for the ‘popup_choices’.  Make the choices a drop list for your user to select from.  When they go to submit you can take their selection and the other details and pipe them into XML that will update their device’s record.

PUTxml = '''<mobile_device>
    <extension_attributes>
        <extension_attribute>
            <id>1</id>
            <name>Mobile Device Type</name>
            <type>String</type>
            <value>United States</value
        </extension_attribute>
    </extension_attributes>
</mobile_device>'''
PUT https://myjss.com/JSSResource/mobiledevices/id/1 PUTxml
    Returns 201 status code and XML with the "<id>" of the updated resource.

This PUT will update the mobile device’s “Last Inventory Update” timestamp causing the smart groups to recalculate their memberships.  The end result is through an API action you have made tailored content immediately available to a user’s device without waiting for the 24 hour mobile device update cycle.  The same is true for computers.

 

Wrap Up, part 2

I hope to have a neatly formatted copy of the two posts together in PDF format soon.  It was after publishing the first post and chatting with people who had questions that I decided to write a follow-up that went into more advanced territory.  Before I wrap up the PDF I may yet have more to throw in.  If you have any feedback, as before, please reach out to me!  You’ll find I’m pretty active on Twitter.

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.

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.