Graham Gilbert bio photo

Graham Gilbert

A Mac admin, automating the hell out of things in London.

The opinions on this site are my own, and are not necessarily shared by my employer.

Twitter LinkedIn Github

In my last post I promised a tool I’ve been working on to automate the building of a package for crankd.

buildCrankPkg is a small script that will:

  • Pull down the latest version of crankd (or use a local or remote repository if you specify one)
  • Build a package that includes crankd and your custom settings and scripts.

I’ve included two examples, one that implements calling Munki and Puppet as detailed in the last post, and one to run a Casper policy.

If you’re happy with what crankd does and using the command line, head on over to the repository and enjoy. If you need a bit more help to get started, read on.


First off, you’re going to need to get the buildCrankPkg repository.

cd ~/src
git clone

You’re left with three directories that you need to fill:

  • crankd: You will be putting your custom code in here.
  • Preferences: Just a plist that will call our custom code.
  • LaunchDaemons: A LaunchDaemon to run crankd - an example that should be fine is already there.

Assuming you cloned the buildCrankPkg repository to ~/src/buildCrankPkg, save the following as ~/src/buildCrankPkg/crankd/ (or copy the example). The only change between this one and the from last time is that we’re calling the JAMF binary to run a Casper policy (I know, the horror, I do actually use Casper occasionally). Our trigger’s name is NetworkTrigger - the line you’d need to customise to change this is 28.
#!/usr/bin/env python
# The OnNetworkLoad method is called from crankd on a network state change, all other
# methods assist it. Modified from Gary Larizza's script (
# Last Revised - 10/07/2013
__author__ = 'Graham Gilbert ([email protected])'
__version__ = '0.2'
import syslog
import subprocess
from time import sleep
class CrankTools():
"""The main CrankTools class needed for our crankd config plist"""
def policyRun(self):
"""Checks for an active network connection and calls the jamf binary if it finds one.
If the network is NOT active, it logs an error and exits
Arguments: None
Returns: Nothing
command = ['jamf','policy','-trigger','NetworkTrigger']
if not self.LinkState('en1'):
elif not self.LinkState('en0'):
syslog.syslog(syslog.LOG_ALERT, "Internet Connection Not Found, Puppet Run Exiting...")
def callCmd(self, command):
"""Simple utility function that calls a command via subprocess
Arguments: command - A list of arguments for the command
Returns: Nothing
task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def LinkState(self, interface):
"""This utility returns the status of the passed interface.
interface - Either en0 or en1, the BSD interface name of a Network Adapter
status - The return code of the subprocess call
return["ipconfig", "getifaddr", interface])
def OnNetworkLoad(self, *args, **kwargs):
"""Called from crankd directly on a Network State Change. We sleep for 10 seconds to ensure that
an IP address has been cleared or attained, and then perform a Puppet run and a Munki run.
*args and **kwargs - Catchall arguments coming from crankd
Returns: Nothing
def main():
crank = CrankTools()
if __name__ == '__main__':

Now for the preferences - no change from last time here, as we’ve not changed the name of our class or method. This goes into ~/src/buildCrankPkg/Preferences/com.googlecode.pymacadmin.crankd.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
<plist version="1.0">

One last step until we can build our package is the Launch Daemon - we’re going to use the one that’s included in the repository, as 99% of people won’t need to change it.

Prepare the build!

Our package needs to have the version number of 2.1 and we’re going to set the package’s identifier to com.example.crankd

cd ~/src/buildCrankPkg
sudo ./ --version 2.1 --identifier com.example.crankd

Your package will be in ~/src/buildCrankPkg waiting for you.