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.

Author: Bryson Tyrrell

AWS serverless developer from the Twin Cities. Former benevolent Casper Admin at Jamf, helped cofound Twin Cities Mac Admins @MspMacAdmns,, avid Python coder.

One thought on “A few quick updates: scripts, Promoter, jsslib”

Leave a comment