Trial by Fire

« Back to blog

Using crankd to react to network events

I use Puppet (http://www.puppetlabs.com) as our in-house configuration management du jour.  Puppet is excellent, but one area that bothered me was with our district laptops.  I can commit a change to Puppet that will get pushed out whenever Puppet runs next (every hour for school machines), but if the laptop isn't on-network (or isn't on PERIOD) then the state will drift and the commit won't get applied until someone keeps the laptop on long enough for a puppet run.  

This is where crankd comes in.

Crankd is a cool utility that's part of the Pymacadmin (http://code.google.com/p/pymacadmin/) suite of tools co-authored by Chris Adams and Nigel Kersten.  In a nutshell, crankd is a Python script that lets you trigger shell scripts or access python modules based upon events like SystemConfiguration, NSWorkspace and FSEvents.  

 

Use Cases and Installation.

Why use crankd?  There are a couple of situations in which it would be handy to execute code in response to certain system events.  For example...

  1. You have laptops bound to OD that frequently leave your work/school/organization.  When these laptops go off-network and can't contact your corporation LDAP server, you want to be able to remove the LDAP Server from your search path  (ever notice how your laptop gets the spinning wheel of death when you authenticate off-network?  This could be why).  
  2. You run Puppet, Munki, or some other Configuration Management System and would like the laptops to check in whenever they get a network connection.  You'd also like them to NOT check in when they're OFF your corporate network
  3. You have a customized Firefox setup and would like it applied to ANY version of Firefox that users may download (or run - even if it's outside the Applications folder).
  4. You want to kill your VPN connection before you go to sleep and resume when you wake up.
  5. You would like to perform an action when you mount or unmount a specific volume.


To get crankd running, you'll need to download Pymacadmin (either from the google site at http://code.google.com/p/pymacadmin or from Github at http://github.com/acdha/pymacadmin) - I recommend downloading a .zip of the source from Github.  Running the bundled install-crankd.sh shell script will copy crankd.py into /usr/local/sbin and throw all of the support libraries into /Library/Application Support/crankd. 

If you've never used crankd, you can drop into Terminal and run /usr/local/sbin/crankd.py - this will alert you to the fact that you can run crankd.py with the --list-events argument to show all the events to which crankd will respond.  It will also create a sample /Library/Preferences/com.googlecode.pymacadmin.crankd.plist file that looks like this image:

 

 

As you can see, there are two main Keys in this plist - NSWorkspace and SystemConfiguration.  Below these keys are the specific notification events to which crankd will respond.  The entire list is here:

On this system SystemConfiguration supports these events:
Plugin:IPConfiguration
Plugin:InterfaceNamer
Setup:
Setup:/
Setup:/Network/BackToMyMac
Setup:/Network/Global/IPv4
Setup:/Network/HostNames
Setup:/Network/Interface/en1/AirPort
Setup:/System
State:/IOKit/PowerManagement/Assertions
State:/IOKit/PowerManagement/Assertions/ByProcess
State:/IOKit/PowerManagement/CurrentSettings
State:/IOKit/PowerManagement/SystemLoad
State:/IOKit/PowerManagement/SystemLoad/Detailed
State:/Network/Global/DNS
State:/Network/Global/IPv4
State:/Network/Global/Proxies
State:/Network/Interface
State:/Network/Interface/en0/IPv4
State:/Network/Interface/en0/IPv6
State:/Network/Interface/en0/Link
State:/Network/Interface/en1/AirPort
State:/Network/Interface/en1/Link
State:/Network/Interface/fw0/Link
State:/Network/Interface/lo0/IPv4
State:/Network/Interface/lo0/IPv6
State:/Network/Interface/vmnet1/IPv4
State:/Network/Interface/vmnet8/IPv4
State:/Network/MulticastDNS
State:/Network/PrivateDNS
State:/Users/ConsoleUser
com.apple.DirectoryService.NotifyTypeStandard:DirectoryNodeAdded
com.apple.network.identification

Standard NSWorkspace Notification messages:
NSWorkspaceDidLaunchApplicationNotification
NSWorkspaceDidMountNotification
NSWorkspaceDidPerformFileOperationNotification
NSWorkspaceDidTerminateApplicationNotification
NSWorkspaceDidUnmountNotification
NSWorkspaceDidWakeNotification
NSWorkspaceSessionDidBecomeActiveNotification
NSWorkspaceSessionDidResignActiveNotification
NSWorkspaceWillLaunchApplicationNotification
NSWorkspaceWillPowerOffNotification
NSWorkspaceWillSleepNotification
NSWorkspaceWillUnmountNotification

Many of these events are self-explanatory, but Google (and some experimentation) is your friend.  

 

Solving my Network Problem:

In the beginning I told you about my problem with laptops calling Puppet.  I'd like to setup crankd to call Puppet every time a laptop connects to our corporate network.  Just for fun, let's also setup crankd so that it removes our search path when the laptops go off-network and re-adds them when they come back on-network.  For that, we're going to need a couple of things:

  1. A crankd plist file that responds to network events
  2. Code to determine whether a network connection is being made or broken
  3. Code to determine whether we're on or off-network
  4. Code to call Puppet
  5. A LaunchDaemon to keep crankd running

 

1.  Crankd plist file creation

Crankd plist files are fairly easy to create.  Like I've shown above they're just XML with either an NSWorkspace or SystemConfiguration key, a specific notification event key, either a command, function, class, or method key, and finally a string with the path to either the command, function, class, or method.  Let's select the SystemConfiguration and State:/Network/Global/IPv4 keys since our scripts need to respond to network events.  Next, we need to consider whether we want to call a shell command in response to our IPv4 event, or whether we want to use a Python method.  Since maintaining wrapper scripts in shell is never much fun, I'm going to opt for using a Python method.  Finally, let's decide that our Python class should be called "CrankTools" and the specific method name triggered by crankd will be "OnNetworkLoad".  Our plist contains a block that now looks like this:

 

2.  Code to determine network connection

I've used two methods to determine whether our network connection is up or down.  The BEST method (in my opinion) is to use Apple's SystemConfiguration framework to extract the IP Address directly.  It requires a couple of lines of code, but it's reproducible.  The checkIP() method in the below gist provides sample code.

A quicker method was to use the exit code of the command "ipconfig getifaddr en0" on the Mac.  I wrote a method in Python to handle the return of the status code (see the gist below) OR you can use the simple one-liner I show in yellow that's before the gist.

retcode = subprocess.call(["ipconfig", "getifaddr", "en0"])

3.  Code to determine whether we're On or Off-Network

There are many ways to determine whether you're on your corporate network.  Sometimes you can ping to key equipment.  Maybe you curl a file off a secure web server.  For OUR simplistic network, I'm simply checking the IP Address to make sure it matches our corporate structure.  I've got a block of code that gathers the IP Address, splits it up, and checks each Octet to determine whether we're On or Off-Network.  This code ALSO contains links to the pymacds module that will do the Directory Service legwork that I'll talk about later.  For now, this Python method looks like this:

 

4.  Code to call Puppet

 Fortunately, I have my own Puppet wrapper script in /usr/bin/puppetd.rb, so THAT'S how I call puppet.  It shows up later in my files.  

5.  A LaunchDaemon to keep crankd running.

Crankd is a daemon, so it needs to be constantly running.  Launchd is great for this as creating a plist is pretty trivial.  The COOLEST part about crankd is that it recognizes changes in its plist files, and, if you're using a Python method to respond to events (like I am), it will also recognize changes to the Python script.  When it notices that one of these files has changed, it will automatically restart itself.  That's extremely handy.  Here's what my LaunchDaemon looks like (it will keep crankd running persistently unless the Launchd plist is unloaded):

Note that this plist is calling /usr/local/sbin/crankd.py and using the --config argument to point to our crankd plist.  

 

 

Putting it all Together

Now that you've explored all the steps you need to create a working crankd setup, let's piece it together and get something working.  This will involve the following:

  1. Install crankd.
  2. Install your custom Python libraries.
  3. Install your crankd plist.
  4. Run crankd and test it out.
  5. Install the LaunchDaemon to keep crankd running.
  6. Packaging and deployment

 

1.  Install crankd.

As I said before, I highly recommend visiting the Pymacadmin Github Repo (http://github.com/acdha/pymacadmin) and clicking the Downloads button to download a .zip of its source.  Once you've unzipped the files, you can run the install-crankd.sh script to install crankd to /usr/local/sbin and its libraries to /Libraries/Application Support/crankd.  

 

2.  Install your custom Python libraries.

In step 1 in the previous list, we created a crankd plist that calls code from a custom Python library.  This custom Python library (and any libraries called in your crankd plist) MUST be located in /Libraries/Application Support/crankd for crankd to be able to access it.  Also, pay close attention to naming and capitalization, as it's very important.  Let's look at the method call in my crankd plist:

 

 

<key>method</key>

<array>

<string>CrankTools</string>

<string>OnNetworkLoad</string>

</array>

 

 

The two strings in this method call refer to a Class name and then a Method name in our Python file.  It's important that the filename and the class name be identical (down to capitalization) for this to work.  As such, my file is called CrankTools.py and I've dropped a gist of it below.  The specific method that's being called by crankd is named OnNetworkLoad and takes arguments of "self" as well as two catch-all arguments of "*args" and "**kwargs".  When crankd notices a state change (in my case, it's the IPv4 change on the en0 and en1 interfaces), it will run all the code in the OnNetworkLoad method.  Take a look below:

 

 

3.  Install your crankd plist.

In step 1 on the previous list we created a plist file that crankd uses to respond to system events.  It makes sense that this file should be installed in a logical place - /Library/Preferences and NOT ~/Library/Preferences - as its going to be called by crankd.  I usually name this file com.huronhs.crankd.plist (substituting your organization name for huronhs), but beware as the launchd plist we create later will have the SAME name.  You can choose to name them differently or keep them the same (as they're going to be located in separate places on your hard drive), but be sure you can distinguish between the two.  

 

4.  Run crankd and test it out.

It's now time to make sure everything works!  Let's open up a Terminal window and type the following (should all be on one line, disregard Posterous' formatting):

sudo /usr/local/sbin/crankd.py --config=/Library/Preferences/com.huronhs.crankd.plist

If everything goes well, you should see that crankd is responding to all events you've specified in your plist.  The cool thing about crankd is that it will automatically reload if you change its plist or the method that it's watching in a custom Python library.  This is handy if you need to debug on-the-fly.  

Make sure you test EVERYTHING that will happen to these computers!  Pull the ethernet cable out, connect to an ethernet AND wireless network, put the machine to sleep, do a forced restart, connect with a non-working IP address, and etc...  You may find that you need to tweak your code to account for certain events.  

Once you're happy with your code and crankd setup, you can proceed to the next step...

 

5.  Install the LaunchDaemon to keep crankd running.

We created a launchd plist in step 5 on the previous list, but now we need to install and load it.  Install your launchd plist to /Library/LaunchDaemons - in this case mine is called com.huronhs.crankd.plist but you can rename it as you like.  To load the LaunchDaemon, drop into Terminal and run this:

sudo launchctl load /Library/LaunchDaemons/com.huronhs.crankd.plist

This will load and start the launchd plist which should keep crankd running.  There are MANY ways to test if crankd is currently running - a simple method is to open Activity Monitor.app and search for a Python process that was started by launchd.

 

6.  Packaging and Deployment

There are a couple of ways to bundle up the code and all your custom libraries.  The first method is to create a .pkg file with everything installed.  You can use Packagemaker, Iceberg, JAMF Composer, or any number of tools to do this.  I HIGHLY recommend using a piece of software called The Luggage (http://luggage.apesseekingknowledge.net/) which allows you to utilize makefiles for package creation.  It allows for code review and you can quickly change any mistakes if you need to.  It does require a bit of technical knowledge, but it's not terribly difficult at all (if you're writing Python code, you can/should use The Luggage).  

A second method is to use a Configuration Management tool like cfengine, Chef, Puppet, and etc... to deploy crankd and all your config files.  I DO use Puppet, so here's a copy of what my crankd manifest looks like:

 

 

 

In Closing...

There are many uses for crankd and not too many limits.  It DOES require you to be comfortable with Python, but as a sysadmin you should have that skill in your bag already.  I urge you to give it a shot and email me with any questions!