Using Luggage to Repackage Crappy Installers

Every Mac admin has received crappy packages from software vendors.  Sometimes they don’t follow Apple standards at all.  This include stashing files inside the pkg and placing them with a perl script, having worthless receipts, etc.

It turns out that Luggage can save the day.  Luggage is a Makefile-based tool that leverages PackageMaker’s CLI functionality.  More info about Luggage here: https://github.com/unixorn/luggage .  This writeup will only cover fixing bad apple packages.  For more general information regarding Luggage, check out their github wiki page.

So how does one fix crappy installers?  Using some nifty Luggage tricks of course!

One of the most common Apple hack-pkg methods I’ve come across is simply copying the files into place using a postflight and fixing the permissions in that very same script.  Luckily, this gives us a cheat sheet for fixing the package because we know where the files need to be as well as what permissions they need to have.

Some general rules for fixing bad packages using Luggage:

1.  Always fix the package in an automated way.  The goal is to be able to drop the bad package into the directory with the Makefile and just run “make pkg” to create a fixed package.

2.  Always use the built in Luggage tools and shortcuts whenever possible. Check /usr/local/share/luggage/luggage.make to see what is available

3.  Split up your stanzas in the Makefile logically.  Try not to throw everything into one stanza unless it makes sense to do so.

Labstats is a great example of the postflight-installed payload.  By looking at the postflight, it gives up all the secrets that we need to know.  In steps #3 and #4 it clearly lists which files get copied where and step #5 shows which permissions these files need:

#Step 3: Create necessary directories
sudo mkdir "/Applications/LabStats"
sudo mkdir "/Library/Application Support/LabStats"

#Step 4: Copy files to correct location
sudo cp -r "$1/Contents/Executable/" "/Applications/LabStats"
sudo cp -r "$1/Contents/Bundles/" "/Applications/LabStats"
sudo cp "$1/Contents/Launch Daemon/LabStats.plist" "/Library/LaunchDaemons"
sudo cp "$1/Contents/Launch Agent/LabStats.plist" "/Library/LaunchAgents"
sudo cp "$1/host.txt" "/Library/Application Support/LabStats"

#Step 5: Set Permissions
sudo chmod 644 /Library/LaunchAgents/LabStats.plist
sudo chmod 644 /Library/LaunchDaemons/LabStats.plist
sudo chmod -R 755 /Applications/LabStats
sudo chmod 755 "/Library/Application Support/LabStats"
sudo chmod -R 644 "/Library/Application Support/LabStats/*"
sudo chmod +x /Applications/LabStats/updater.sh
sudo chmod -R +x /Applications/LabStats/LabStatsUserSpace.app/

Lets translate that to what we need Luggage to do:

LABSTATS_PACKAGE_NAME="LabStats 5 Client.pkg"</pre>
prepare-files: l_Applications l_Library_Application_Support
 # Created necessary directories and copies the files into them
 @sudo mkdir -p ${WORK_D}/Applications/LabStats
 @sudo mkdir -p "${WORK_D}/Library/Application Support/LabStats"
 @sudo ${CP} -R ./${LABSTATS_PACKAGE_NAME}/Contents/Executable/ ${WORK_D}/Applications/LabStats/
 @sudo ${CP} -R ./${LABSTATS_PACKAGE_NAME}/Contents/Bundles/ ${WORK_D}/Applications/LabStats/

pack-labstats: prepare-files l_Library_LaunchDaemons l_Library_LaunchAgents
 # Fix permissions on the installed files
 @sudo chmod -R 755 ${WORK_D}/Applications/LabStats
 @sudo chmod +x ${WORK_D}/Applications/LabStats/updater.sh
 @sudo chmod -R +x ${WORK_D}/Applications/LabStats/LabStatsUserSpace.app

 # Installs LaunchDaemon and applies proper permissions
 @sudo ${CP} "./"${LABSTATS_PACKAGE_NAME}"/Contents/Launch Daemon/LabStats.plist" ${WORK_D}/Library/LaunchDaemons
 @sudo chmod 755 ${WORK_D}/Library/LaunchDaemons/LabStats.plist

 # Installs LaunchAgent and applies proper Permissions
 @sudo ${CP} "./"${LABSTATS_PACKAGE_NAME}"/Contents/Launch Agent/LabStats.plist" ${WORK_D}/Library/LaunchAgents
 @sudo chmod 755 ${WORK_D}/Library/LaunchAgents/LabStats.plist

 # Installs host.txt config file and corrects permissions on Application Support folder
 @sudo ${CP} "./"${LABSTATS_PACKAGE_NAME}"/host.txt" "${WORK_D}/Library/Application Support/Labstats"
 @sudo chmod -R 644 "${WORK_D}/Library/Application Support/LabStats"
 @sudo chmod 755 "${WORK_D}/Library/Application Support/LabStats"
<pre>

With Luggage, ${WORK_D} represents a working directory where we stash all our files in the hierarchy that they would be found after being installed.  For instance, if there is a file in ${WORK_D}/Library/Preferences/com.company.some.plist, when the package is built using Luggage, it will deliver a file named com.company.some.plist into /Library/Preferences/ on the target that you are install the package to.

If you step through this chunk of Makefile, you can see that I am doing exactly the same things the postflight is doing, but in a way that Luggage can work with it.  Instead of copying files into position using a postflight, we are copying them into the ${WORK_D} so that Luggage can package them up into a proper Apple Package.

Now that we have the proper directory structure, the files are in place and we know the permissions are correct, the only thing left to do is take the other bits of the postflight from the crappy installer and place them in the proper location.

Here are rest of the steps from the postflight as provided by ComputerLabSolutions:


#!/bin/bash

#Step 1: Kill any exising processes
sudo killall mono
sudo killall LabStatsClient

#step 1.5 Stop any agents.
launchctl unload /Library/LaunchDaemons/LabStats.plist
#launchctl unload -D all /Library/LaunchAgents/LabStats.plist
#launchctl unload /Library/LaunchAgents/LabStats.plist

#Step 2: Delete any existing LabStats resources
if [ -d "/Applications/LabStats" ]; then
 sudo rm -rf "/Applications/LabStats"
fi

if [ -d "/Library/StartupItems/CLSService" ]; then
 sudo rm -rf "/Library/StartupItems/CLSService"
fi

if [ -d "/Library/Application Support/LabStats" ]; then
 sudo rm -rf "/Library/Application Support/LabStats"
fi

if [ -d "/Library/StartupItems/CLSService" ]; then
 sudo rm -rf "/Library/StartupItems/CLSService"
fi

if [ "/Library/LaunchDaemons/LabStats.plist" ]; then
 sudo rm -rf "/Library/LaunchDaemons/LabStats.plist"
fi

if [ "/Library/LaunchAgents/LabStats.plist" ]; then
 sudo rm -rf "/Library/LaunchAgents/LabStats.plist"
fi

...

#Step 5: Reload agents
launchctl load /Library/LaunchDaemons/LabStats.plist
# RJS- Commented this out; it runs the UserSpace as root, which is odd
# The other option is to find out a way to
# launch this for each running user
# launchctl load /Library/LaunchAgents/LabStats.plist

It becomes clear that Steps 1, 1.5 and 2 should be done before we install anything. I’ve cut and pasted those sections into a new file named ‘preinstall’ which sits in the same directory as the Makefile. Likewise, Step 5 shouldn’t be done before the install, so we will put it into a file named ‘postflight’ also in the same directory as the Makefile.

At the end of all this, we end up with an Apple Package that installs files using the proper mechanism as well as preinstall and postflight scripts that are being used in their proper capacity.

For the Labstats example, all this work is posted and ready to be used here: https://github.com/unixorn/luggage-examples .  I would caution against blindly using this Makefile and expecting it to work, but you can clearly see how it is meant to function and tweak as is necessary.

Cisco AnyConnect is another example of an Apple Package gone wrong.  You can see an example of how I handled repacking it here: https://github.com/unixorn/luggage-examples/tree/master/ciscoanyconnect

Special thanks to Gary Larizza and Allister Banks.  They had great Makefiles for Puppet that helped me immensely with these repack Makefiles.

DeployStudio Branching Workflows and iLife

One of the biggest annoyances with imaging machines of various ages is that they all require a different version of iLife.  I know you can get a site license for iLife, but for many companies and universities, these apps cannot be justified for work use.  In the past, I’ve always made DeployStudio workflows available that would install the proper version of iLife and the techs would simply look up when the machine was purchased, compare that to when iLife was released and run the proper one after running the main imaging workflow.  No longer!

As of RC129, DeployStudio supports Workflow Branching.  This means you can use a Meta Workflow task, point it at a script and have the script return either the name of the ID of the workflow you want to run.  This allows you to dynamically install various pieces of software based upon any information you can glean from the system.

When I first read about this functionality, my iLife predicament was the first thing that popped into my head.  A simple problem, but one that is annoying none the less.  The solution for me turned out to be the excellent warranty2.sh that Rusty Myers has been updating and improving along with the contribution of others.  I made a custom format (./warranty2.sh -f DSProperties) for use specifically with DeployStudio.  Essentially, instead of outputting the warranty info using STDOUT, it uses DS RC129′s “echo “RuntimeSetCustomProperty: MY_PROPERTY=MyValue” command to set the Properties in the DS Runtime.  The end result is that you have the following Properties in your DeployStudio Runtime:

SERIAL_NUMBER
PURCHASE_DATE
WARRANTY_EXPIRES
WARRANTY_STATUS
MODEL_TYPE
ASD

Obviously, SERIAL_NUMBER is already an available variable within the DS Runtime, but the others are not normally available and are quite handy to have.  The information we are using to branch the workflow is the PURCHASE_DATE variable.

The PURCHASE_DATE variable has the dashes removed so it can be used in conditionals in bash.  Since it is in the YYYYMMDD format, it makes comparisons trivial.  We then compare the PURCHASE_DATE with the known release dates for the various iLife versions and that tells us which version the machine should get.  We then use the Workflow Branching command to tell DeployStudio which version to install and off it goes.

For a full step-by-step writeup on this topic, please see www.osxdeployment.com

The scripts that make this happen can be found here and here.

Updated – Force MCX to refresh during DeployStudio’s firstboot

Now that Lion is out and DirectoryService is no longer used on it, I had to modify my firstboot script to kill the right service based upon OS version.  With help from the guys in ##osx-server on irc.freenode.net, I came up with this:

echo "Forcing MCX Settings Refresh..."
if [ "`uname -r | cut -d "." -f 1`" -lt "11" ]
	then
		echo "Killing DirectoryService..."
		killall DirectoryService
	else
		echo "Killing opendirectoryD..."
		killall opendirectoryD
fi

Lets step through this. We have an if statement that evaluates two items. First, we are returning the Darwin version of the current OS using the uname command and cleaning it up a bit. We are then comparing this to the number “11″ using -lt, which is “Less Then”. In plain english, this is saying “If the darwin version of this machine is less then 11, do this.” In this case, if the version is less then 11, it kills DirectoryService. If it is equal or greater then 11, it will kill opendirectoryD. Presumably opendirectoryD will be used in future versions of of Mac OS X, so this litle if statement should be valid for a while.

You can use this ‘if statement’ for any commands that changed between Snow Leopard (Darwin 10) and Lion (Darwin 11). I plan on making more conditionals in my firstboot script and this will be something I can recycle as needed.

Good stuff coming soon…

I recently got Final Cut Studio 3 deploying properly via Munki and by the end of this week I hope to have Pro Tools 9, Cinema 4D and Avid Media Composer deployed using munki as well.  Can’t wait to get that done and complete some good write-ups.  Stay posted…

Force MCX to refresh during DeployStudio’s firstboot

I’ve always had mixed luck with MCX settings being applied in time during DeployStudio’s ds_finalize phase (first boot after imaging with DS).  Mike Boylan suggest doing a killall DirectoryService as part of the ds_finalize and sure enough it caused MCX settings to get placed almost right away.

Here are the lines I added to my firstboot.sh which is a “Run a Script” task.  Make sure it is marked for “Postponed execution” so that it runs during ds_finalize on first boot.

echo "Setting munki to bootstrap mode..."
touch /Users/Shared/.com.googlecode.munki.checkandinstallatstartup

echo "Forcing MCX Settings Refresh..."
killall DirectoryService

echo "Finished applying firstboot settings."

echo "Sleeping for 60 seconds..."
sleep 60

In my particular case, I needed to make sure that my ManagedInstalls.plist was in place before the machine rebooted and munki tried to do its bootstrap run.  These three commands made sure that the MCX settings were in place before DS rebooted.

Thin Imaging with DeployStudio, MCX and Munki

I finally got around to documenting my current thin imaging workflow.  It works really well for standard images, but I’m not sure how well it would do for huge computer lab images.

Essentially, this follows the Thin Imaging methodology that Rich Trouton talked about at PSU MacAdmins 2011, but with a environment specific twist.  Also, it is worth noting that you could substitute Puppet for MCX if you’d like for the settings management side of things.

Check out the new wiki page here.

If there are any glaring errors, feel free to correct it on the wiki (Just grab an account and feel free to contribute).  If anyone has suggestions, please use the “Discussion” tab to present an alternate/better way of doing it.

Welcome to my Blog

Thanks for stopping by my new blog!  I plan on posting various helpful bits of information for Managing OS X.  I hope you’ll stop by in the future as I add more content.