When Heath Robinson met Rube Goldberg

13 Jul 2017

or - building a ridiculous automation so you don't have to.

Preamble

So, I was roped into doing a talk at London Apple Admins - which is fine, as I did have a topic in mind... so, you can watch the video - or get the general gist from the post below.

We're running Fuze for Rooms as a meeting room solution at ThoughtWorks (edit - actually, no we're not any more, now we've embraced our new Zoom powered future)

The Fuze for Rooms client is just a regular desktop client - that behaves in a different way when fed a special login. After some head scratching, we finally worked out where the credentials to turn a Fuze client into a Fuze room were stored.

Here:

~/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/

in a format I'd never some across called Level DB. This promptly invalidated all the work I'd done with Jinja in a previous post...

You can package up the contents of the auth directory on one machine, and then deploy that package to another machine running Fuze to turn a Fuze client into a Fuze room. But how do you generate that authentication data in an automated way?

In testing - we'd manually login to a client - and then capture the files we needed. But manually logging into over 100 rooms?

Can't someone else do it?

In the dim and distant past I have memories of Windows admin colleagues using a tool called Auto IT which could automate filling in dialog boxes and clicking buttons - to help automate the installation of software that lacked a silent install option. Is there something similar for the Mac?

Kind of...

Hat tip to Jon Artzen - who suggested SikuliX - a macOS port of the Java powered sikuli project. SikuliX can automate GUI items on your computers. I watched a couple of tutorial videos - and then got stuck in...

SikuliX uses image recognition to find items on the screen. So you create screenshots in the application - and then tell SikuliX what to do when it finds that item on screen (click a button, enter text etc) - which makes it very easy to get started - and almost makes the code entirely self documenting - as you can see what you're asking it to click on, or interact with.

In the IDE - the code looks like this:

Like this

SikuliX is looking for the username field - enters a room_id - uses \t to tab to the next field - types our generic rooms password, and clicks the SIGN IN button which logs us into the room.

Jython???

SikuliX is a Jython application - which as far as I can see means it supports python syntax and packages. In the code above I run

    import sys.argv
    import time

sys.argv allows me to pass arguments on the command line when I call this script - and time allows for time.sleep(number of seconds) calls.

Relying on UI items and waiting for Fuze to respond isn't always very reliable - so the sleep commands are there to allow the app to get into a known state. Whilst SikuliX supports a wait command - it didn't seem to work very well for me, so I defaulted to sleep commands.

Automation

So we've built a thing that can be called on the command line (and fed a room name on the command line) that automates logging into the Fuze client and turning it into a Fuze room. Now what?

I've already written about using the python csv libraries to loop though a csv of room details to create configuration profiles. We can use something similar here -

MunkiRooms = open(file_name, "r")
csvReader = csv.reader(MunkiRooms)
header = csvReader.next()

RoomName = header.index("USER ID")
PackageName = header.index("NEW ID")

for row in csvReader:
    room_id = row[RoomName]
    package_name = row[PackageName]
    munki_safe_package_name = package_name.replace("-","_")

And now we send our room details to a function that calls Sikulix and include some error handling (baby steps - I'm still learning!) so that if my flaky script falls over - at least it'll let me know where, so I can edit my root csv file and start again.

try:
    automagic(sikulix_location, sikulix_config_location, room_id)
except:
    print "Something's gone wrong with the Sikulix run, build failed on room " + package_name
    break

So the SikuliX script launches and logs a room in. Now we have a some room credentials. How do we package them?

Enter the Dragon, er, Luggage...

The Luggage is a command line tool based on the venerable Unix make command. Because it's command line based - it becomes nicely scriptable. Make supports variables passed at the command line. I keep meaning to blog about how the Luggage works, but everytime I think I can work it - it surprises me... my packages break, and I spend hours de-bugging things that should be simple...

At a simple level, a Mac package is simply instructions as to where to unpack and place files and folders - whilst also supporting pre and post install shell scripts.

In the luggage, you define where you want files to go using shell commands. Here's my code (called a Makefile) to package up Fuze authentication files.

USE_PKGBUILD=1
include /usr/local/share/luggage/luggage.make
TITLE=$(pkg_name)
REVERSE_DOMAIN=com.thoughtworks
PAYLOAD=\
pack-fuze-files\
pack-script-postinstall

pack-fuze-files: Fuze_auth_data
    @sudo ${CP} ./Fuze_Auth/* ${WORK_D}/Users/Shared/Fuze/

Fuze_auth_data: l_Users_Shared
    @sudo mkdir -p ${WORK_D}/Users/Shared/Fuze
    @sudo chown root:wheel ${WORK_D}/Users/Shared/Fuze
    @sudo chmod 755 ${WORK_D}/Users/Shared/Fuze

It accepts a package name from the command line (the TITLE) item and then creates a package that installs the Fuze authentication files to a temporary location - in this case /Users/Shared/Fuze.

time for a quick intermission - it's some Luggage 101

(feel free to skip this bit - it's a little more detail about how the luggage does what it does)

At the top of the makefile there's a reference to /usr/local/share/luggage/luggage.make which contains rules (or presets if you will) for common locations you might want to place a file. If you want to create a new packaging location - you can take an existing location - and modify it. So, l_Users_Shared is a luggage preset that lets you install files to /Users/Shared/ - we're creating a new location Fuze=_auth_data based on that existing rule - and defining it at the bottom of our makefile.

The PAYLOAD item shows what's being packaged. pack-script-postinstall is a special rule to package postinstall scripts (and is really handy for making payloadless packages - packages that just run a script, rather than delivering files/folders)

pack-fuze-files is a rule we've defined. So then we have to state what that rule does. It uses the Fuze_auth_data location and then copies files to a location, using ${CP} to copy things, and defining $WORK_D which is the working directory where our Makefile and other items live. Hopefully between this and the code I've posted that makes some sense? One day I'll write this up properly...

Right - back to business

Then we invoke a postinstall script to copy those files into the correct location. These need to go into the homedirectory of the logged in user. So there's some code to check someone's actually logged in...

and if someone is logged in, away we go...

#!/bin/bash

set -euo pipefail

CURRENT_USER=`/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }'`

#This may make it hard for the installer to run - will need testing...

if [ "$CURRENT_USER" == 'root' ]; then
# this can't run at the login window, we need the current user
exit 1
fi

delete any current Fuze configuration

/bin/rm -rf /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/

Make a new directory and copy our packaged up Fuze auth data into place

/bin/mkdir -p /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/https_web.fuze.com_0.indexeddb.leveldb/

/bin/cp /Users/Shared/Fuze/* /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/https_web.fuze.com_0.indexeddb.leveldb/

Then set permissions accordingly.

#Because I can't work Chown :(

/usr/sbin/chown -R $CURRENT_USER:staff /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/https_web.fuze.com_0.indexeddb.leveldb/*

/usr/sbin/chown -R $CURRENT_USER:staff /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/https_web.fuze.com_0.indexeddb.leveldb/

/usr/sbin/chown -R $CURRENT_USER:staff /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/IndexedDB/

/usr/sbin/chown -R $CURRENT_USER:staff /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/Cache/

/usr/sbin/chown -R $CURRENT_USER:staff /Users/$CURRENT_USER/Library/Application\ Support/com.fuzebox.fuze.Fuze/

So as part of our loop - once we've logged the Fuze client in, we can copy the auth data to Fuze_Auth folder in our packaging location - and package it up using the luggage. It clocks it at 30 seconds to a minute per-package. Time for some bacon pancakes whilst we wait.

Interestingly, when I ran this workflow against real data, the Sikuli automation didn't know how to deal with failed logins - the new version of Fuze required user accounts to be migrated into a new Fuze portal - and as this was a phased rollout, some clients didn't get migrated successfully... And some just had incorrect login details. The luggage then dutifully packaged that incomplete authentication data up. Disaster? Actually, no - as it turned out a successful login generated a package size of 9k - whereas an unsuccessful login generated a filesize of 5k. So I was able to separate out the bits that worked - and had the 5k packages to show the rooms that hadn't been migrated. Handy!

Munki time

Now we can import all of these packages into Munki. I've found a bulk import script that works quite nicely. Because Munki will keep trying to install a package until it succeeds, if it does try to install the package at the login window - it'll fail (remember we check for a logged in user) but will try again until a user is logged in.

And the title of this post?

I've heard the term Rube Goldberg machine banded about - without really knowing what it meant. So I asked the internet. Rube Goldberg was an American illustrator who drew crazy and complication contraptions to do (often) simple tasks. His images reminded me of another British author and illustrator - W Heath Robinson, whose Professor Brainstawm books and illustrations are very similar in design and conception.

Goldberg tends to be applied to crazy contraptions, whereas to say something is a little Heath Robinson, tends to mean a little ramshackle and perhaps a little unreliable. looks at the code I've just written...

Anyway, I dedicate this blog post and associated python/sikulix/luggage script to those two great illustrators.

Published on 13 Jul 2017 Find me on Twitter!