Using Munki to revert or downgrade software

Introduction

It might come as little surprise to find out that I use Munki in my organization to manage software installations on macOS.

Munki is really good at keeping software up-to-date. Every time it runs, it compares the versions it has on the server against the versions installed on the local machine and updates any software at a lower version than it has on the server.

Its default behavior when an item on the local machine has a higher version than that on the server is to leave it alone. This is great when you have users that for whatever reason need to test newer versions (or perhaps they are actually developing the newer version of the software).

I also use AutoPkg to automate finding new software updates and to import them into my Munki repo. For us, AutoPkg checks on approximately 50 items each day, importing anything new into my Munki repo into a testing catalog.

On Tuesday of this week, Mozilla released Firefox 59. AutoPkg found the new release and imported it into Munki as expected. On Wednesday, I noticed that AutoPkg had imported Firefox 60! I looked at the installed application, and its version was actually 60.0b3. Someone at Mozilla had goofed and pointed the “latest firefox release” link at the 60 beta. Later in the day this goof was remedied and the link once again returned Firefox 59.

But my AutoPkg run had occurred while the Mozilla site was offering 60.0b3, and so it was downloaded and added to my Munki’s repo’s testing catalog. 25 Macs in my organization (including my own laptop) now had Firefox 60.0b3 installed.

(Side note: because of the way Munki does version comparisons, when the final release of Firefox 60 comes out, if it is versioned as “60.0”,  Munki would not “upgrade” from 60.0b3 to 60.0 – “60.0b3” compares as higher than “60.0”.)

I wanted to configure Munki to downgrade any install of Firefox 60.0b3 to Firefox 59. Since by default Munki leaves higher versions alone, this is not exactly obvious how to do.

Munki Behaviors

It’s extremely useful for Munki admins to understand how Munki decides what needs to be installed. There’s even a wiki page describing the behavior: How Munki Decides What Needs To Be Installed. Most of the mechanisms involve comparing versions of receipts or bundles, but there are a couple that don’t.

If an item in an installs array is a file (instead of application, bundle, or plist), Munki has no version information to work with. It uses either the mere existence of the file, or, if available, compares md5checksums to verify the file contents are the same.

This will be useful for this task, because instead of Munki being able to tell that an older version is installed and therefore it needs to be updated, it can only tell that the wrong version is installed and therefore the “right” version needs to be installed.

In order to make use of this, you’ll need to find a file (or files) that consistently changes for every single release of the software. For application bundles, a good candidate for this is usually the binary executable located in Some.app/Contents/MacOS.

I decided to use this mechanism to downgrade machines that had Firefox 60.0b3 to Firefox 59.

Pkginfo edits

I removed the erroneous Firefox 60 import from my Munki repo. I then needed to modify the pkginfo for Firefox 59. (in my case Firefox is installed via a package, but the idea is similar if Firefox is installed via copy_from_dmg). I manually installed Firefox 59, then used makepkginfo to create an installs item for the binary inside the application bundle:

bash-3.2$ makepkginfo -f /Applications/Firefox.app/Contents/MacOS/firefox-bin 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>_metadata</key>
 <dict>
 <key>created_by</key>
 <string>gneagle</string>
 <key>creation_date</key>
 <date>2018-03-14T20:47:24Z</date>
 <key>munki_version</key>
 <string>3.2.0.3476</string>
 <key>os_version</key>
 <string>10.12.6</string>
 </dict>
 <key>autoremove</key>
 <false/>
 <key>catalogs</key>
 <array>
 <string>testing</string>
 </array>
 <key>installs</key>
 <array>
 <dict>
 <key>md5checksum</key>
 <string>e1a17544fc4366987790666ad88475ad</string>
 <key>path</key>
 <string>/Applications/Firefox.app/Contents/MacOS/firefox-bin</string>
 <key>type</key>
 <string>file</string>
 </dict>
 </array>
 <key>version</key>
 <string>1.0.0.0.0 (Please edit me!)</string>
</dict>
</plist>

That’s a lot of output, but we only need the installs array:

 <key>installs</key>
 <array>
 <dict>
 <key>md5checksum</key>
 <string>e1a17544fc4366987790666ad88475ad</string>
 <key>path</key>
 <string>/Applications/Firefox.app/Contents/MacOS/firefox-bin</string>
 <key>type</key>
 <string>file</string>
 </dict>
 </array>

This becomes the replacement (or new) installs array for Firefox 59. When Munki runs, it finds Firefox 59 as the newest version on the server, then compares the md5checksum on disk against the md5checksum stored in the repo’s pkginfo for Firefox 59. The ultimate effect is that it doesn’t matter _what_ version of Firefox is installed at /Applications/Firefox.app; if the checksum for /Applications/Firefox.app/Contents/MacOS/firefox-bin doesn’t match, Munki will decide to install this version.

Testing and debugging

I made these changes and then periodically checked my Sal server to see if the number of machines with Firefox 60 installed was going down. After a few hours had passed and the number didn’t go down, I suspected something wasn’t quite working as I expected. I ssh-ed into a machine with Firefox 60 installed and did a little investigation:

tail /Library/Managed\ Installs/Logs/Install.log
Mar 14 2018 10:04:45 -0700 Install of Mozilla Firefox-60.0: SUCCESSFUL
Mar 14 2018 14:44:11 -0700 Install of Mozilla Firefox-59.0: SUCCESSFUL
Mar 14 2018 15:24:49 -0700 Install of Mozilla Firefox-59.0: SUCCESSFUL
Mar 14 2018 16:51:03 -0700 Install of Mozilla Firefox-59.0: SUCCESSFUL

Munki was at least attempting to reinstall Firefox 59, but when I looked at the version info in /Applications/Firefox.app/Contents/Info.plist, I could see Firefox 60.0b3 was still installed. And so, the next time Munki ran, it attempted to install Firefox 59 again.

I looked in /var/log/install.log to see if I could figure out what was going wrong during the package install, and found this:

Mar 14 14:44:08 some_hostname installer[61528]: PackageKit: Skipping component "org.mozilla.firefox" (59.0.0-5918.3.10-*-*) because the version 60.0.0-6018.3.12-*-* is already installed at /Applications/Firefox.app.

Apple’s installer is too smart for us here! It’s using logic very similar to Munki’s default logic and deciding to skip the install of the Firefox application bundle since the installed version is newer.

Another Tweak

There’s probably some tweaking I could have done to modify the installer package so that didn’t happen, but I went for a simpler, faster (for me at least) workaround. I added a preinstall_script to the pkginfo:

 <key>preinstall_script</key>
 <string>#!/bin/sh
/bin/rm -r /Applications/Firefox.app
exit 0</string>

With this addition, when Munki does the install, it first removes the existing Firefox.app before installing the Firefox pkg. Thus /usr/sbin/installer doesn’t find a newer application bundle, and the older bundle gets installed.

Success

After a few hours, I checked, and Firefox 60.0b3 was disappearing from machines, and replaced with Firefox 59. Success at last!

Once Firefox 60.0b3 is eradicated, I will probably revert the changes (installs array and preinstall_script) to the Firefox 59 pkginfo so that it reverts to the default behavior. That probably isn’t strictly necessary, though.

Using Munki to revert or downgrade software