Local-Only Manifests in Munki

A while back, there was a discussion on Munki-Dev floating the idea of local-only manifests. After some long discussion, the final Pull Request was created and merged.

The idea behind local-only manifests is simple: if you specify a LocalOnlyManifest key in the preferences, Munki will look for that manifest name in /Library/Managed Installs/manifests. If it finds it, it’ll look for any managed_installs and managed_uninstalls specified inside, and concatenate those with whatever it gets from the Munki server. It’s an extra place to specify managed installs and uninstalls that is unique to the client.

Essentially, what it does is move the unique-client logic from the server to the client. As you scale upwards in client numbers, having huge numbers of unique server-side manifests induces significant overhead – potentially 10,000+ unique manifests in your Munki server’s manifests directory gets unwieldy. With the uniqueness moved client-side, the server only has to provide the common manifests.

There’s a lot of neat things you can do with this idea, so let’s explore some of them!

Hang Out With The Locals

While the basic idea of the local-only manifest is simple, the implementation has some fun details you can take advantage of.

Local-only manifests do not have any catalogs of their own. Instead, they inherit from whatever catalog is provided by the manifest given from the ClientIdentifier key. Thus, if your main manifest uses the catalog “release”, any items specified in the local-only manifest must also be in the “release” catalog (or they will simply be treated like adding any item to a manifest when it is not in a catalog – which is to say that you will receive warnings).

Local-only manifests also don’t have their own conditional items. This is where interaction with third-party tools really begins to shine, but we’ll explore that later.

Because this is a unique manifest, you get the benefits that “real” manifests get. You can specify items to be installed here that are not provided as optional items in the server-side manifest (as long as they’re in the catalog). You can still get the server’s provided list of optional installs, and use the local-only manifest to determine what items become managed installs or removals.

This doesn’t absolve the Munki admin of taking care, though. It’s still possible for an item to be specified as a managed install in one manifest and a managed uninstall in another manifest – and therefore trigger a collision. Local-only manifests are just as vulnerable to that as server-side manifests, and it’s easy for a client to contravene the server-side manifest and result in undefined (or undesireable) behavior.

It’s my recommendation, therefore, that you split the purposes and logic behind the server-side and local-only manifests into separate functions – optional vs. mandatory.

One Manifest To Rule Them All

Because of the slightly limited nature of local-only manifests, it’s important to think of them as addenda to server-side manifests. The way to mentally separate these functions is to also separate “mine” vs. “yours” – the things I, the Munki admin, want your machine to have vs. the things you, the client, want your machine to have (or not have).

The easiest way to accomplish this is to completely remove managed_installs and managed_uninstalls from your server-side manifest. The server-side manifest thus becomes the self-service list and gatekeeper to all optional software. The Munki admins determine what software is available because they control the optional installs list as well as the catalogs, but the clients now have essentially free customizability without needing any ability to modify the servers.

Because the unique aspects of clients are now done client-side and not server-side, this allows an external management mechanism, like Chef or Puppet, to control what Munki manages on a client, without needing the ability to make changes to the repo. If your repo is in source control (and it should be!), this means that the only commits to the repo’s manifests are done by the Munki admins, and will only involve changes that generally affect the whole fleet.

Whence Does This Mystical Manifest Come From?

The local-only manifest moves the work from maintaining the manifest relationships on the server to maintaining them on the client. This is really only beneficial if you already have a mechanism in place to manage these files – such as a config management tool (Chef, Puppet, etc.).

Facebook CPE handles this with our cpe_munki cookbook for Chef. In addition to managing the installation and configuration of Munki, we also create a local-only manifest on disk and tell clients to use it. Manifests are just plists, and plists are just structured-data representations of dictionaries/hashes.

Nearly every programming language offers a mechanism for interacting with dictionaries/hashes in relatively easy ways, and Ruby (in both Chef and Puppet) allows for simple abstractions here.

Abstracting Local Manifests Into Simple Variables

I’m going to use pseudo-Ruby via Chef as the base for this, but the same principles will apply to any scripting language or tool.

The Process in pseudocode:

The point of the pseudocode above is to show how simple it is to abstract out what amounts to a complex process – deciding what software is installed or removed on a machine – and reduce it to simply two arrays.

To add something to be installed on your client, you add to the local managed installs variable. Same for removals and its equivalent variable.

What you now have here is a mechanism by which you can use any kind of condition or trigger as a result of your config management engine to determine what gets installed on individual clients.

Use Some Conditioning, It Makes It All Smooth

Veteran Munki admins are very familiar with conditional items. Conditions can be used to place items in appropriate slots – managed installs/uninstalls, optionals, etc. They’re an extremely powerful aspect of manifests, and allows for amazing and complex logic and customization. You can also provide your own conditions using admin-provided conditionals, which essentially allow you to script any logic you want for this purpose.

Conditions in Munki are critical to success, but NSPredicates can be difficult and unintuitive. Admin-provided conditionals are a convenient way to get around complex NSPredicate logic by scripting what you want, but they require multiple steps:

  1. You have to write the scripting logic,
  2. You have to deploy the conditional scripts to the clients
  3. You still have to write the predicates into the manifest.

They’re powerful but require some work to utilize.

In the context of a local-only manifest, though, all of the logic for determining what goes in is determined entirely your management system. So there’s technically no client-side evaluation of predicates happening, because that logic is handled by the management engine whenever it runs. This unifies your logic into a single codebase which makes maintaining it easy, with less moving parts overall.

Some Code Examples

This is all implemented in Chef via IT CPE’s cpe_munki implementation, but here I’m going to give some examples of how to take this abstraction and use it.

In Chef, the local-only managed_installs is expressed as a node attribute, which is essentially a persistent variable throughout an entire Chef run. This array represents an array of strings – a list of all the item names from Munki that will be added to managed installs.

Thus, adding items in Chef is easy as pie:

node.default['cpe_munki']['local']['managed_installs'] << 'GoogleChrome'

Same goes for managed uninstalls:

node.default['cpe_munki']['local']['managed_uninstalls'] << 'MacKeeper'

Additionally, we specify in the Munki preferences that we have a local-only manifest called “extra_packages”:

 'DaysBetweenNotifications' => 90,
 'InstallAppleSoftwareUpdates' => true,
 'LocalOnlyManifest' => 'extra_packages',
 'UnattendedAppleUpdates' => true,
 }.each do |k, v|
   node.default['cpe_munki']['preferences'][k] = v

After a Chef run, you’ll see the file in /Library/Managed Installs/manifests:

$ ls -1 /Library/Managed\ Installs/manifests

If you look inside that file, you’ll see a plist with your managed installs and removals:


When managedsoftwareupdate runs, it will concatenate the server-side manifest with the local-manifest, as described above. The sample plist above will ensure that six items are always going to be installed by Munki on my machine, and that “Tableau8” will always attempt to uninstall if needed.

With a setup like this, anyone who can submit code to the Chef repo can easily configure their machine for whatever settings they want, and thus users have individual control over their own machines without needing the ability to access any of the server manifests.

Even If You Don’t Have Config Management

You can still benefit from local-only manifests without needing config management. Manifests, including local ones, are just plists, and there are lots of ways to manipulate plists already available.

You could also add items to your local manifest using defaults:

$ sudo defaults write /Library/Managed\ Installs/manifests/extra_packages managed_installs -array-add "GoogleChrome"

Note the issue mentioned above, though, which is that it’s trivial for someone to add an item name that doesn’t exist in the catalog. Should that happen, the Munki client would generate warnings to your reporting engine. The benefits of using an external config management is the ability to lint or filter out non-existent items and thus prevent such warnings.


Ultimately, the benefits here are obvious. Clients have the ability to configure themselves without needing any access to the Munki repo. In addition, your users and customers don’t even need to have an understanding of manifests or how they work in order to get results. The entire interaction they’ll have with Munki will be understanding that items added to managed_installs get installed, and items added to managed_uninstalls get removed.

Stay tuned for a follow-up blog post about how this fits into Facebook’s overall managed Munki strategy, and how source control plays an important role in this process.


A Grim Tableau

One of the perks of working at a huge enterprise tech company is that I get to play with expensive enterprise software. In a shining example of naive optimism, I walked into the doors of Facebook expecting relationships with great software vendors, who listen to feedback, work with companies to develop deployment methods, and do cool things to make it easy to use their software that I couldn’t even have imagined.

The horrible bitter truth is that enterprise vendors are just as terrible at large-scale deployment as educational software vendors, except they cost more and somehow listen less.

One such vendor here is Tableau, a data visualization and dashboard engine. The data scientists here love it, and many of its users tell me the software is great. It’s expensive software – $2000 a seat for the Professional version that connects to their Tableau Server product. I’ll trust them that the software does what they want and has many important features, but it’s not something I use personally. Since our users want it, however, we have to deploy it.

And that’s why I’m sad. Because Tableau doesn’t really make this easy.

Enough Editorializing

As of writing time, the version of Tableau Desktop we are deploying is 9.3.0.

We deploy Tableau Desktop to connect with Tableau Server. I’ve been told by other users that using Tableau Desktop without Server is much simpler, as users merely have to put in the license number and It Just Works™. This blog post will talk about the methods we use of deploying and licensing the Tableau Desktop software for Professional use with Server.


Installing Tableau

The Tableau Desktop installer itself can be publicly downloaded (and AutoPkg recipes exist). It’s a simple drag-and-drop app, which is easy to do.

If you are using Tableau Desktop with Tableau Server, the versions are important. The client and server versions must be in lockstep. Although I’m not on the team that maintains the Tableau Servers, the indication I get (and I could be wrong, so please correct me if so) is that backwards compatibility is problematic. Forward compatibility does not work – Tableau Desktop 9.1.8, for example, can’t be used with Tableau Server 9.3.0.

When a new version of Tableau comes out, we have to upgrade the server clusters, and then upgrade the clients. Until all the servers are upgraded, we often require two separate versions of Tableau to be maintained on clients simultaneously.

Our most recent upgrade of Tableau 9.1.8 to 9.3.0 involved this exact upgrade process. Since it’s just a drag-and-drop app, we move the default install location of Tableau into a subfolder in Applications. Rather than:


We place it in:


This allows easier use of simultaneous applications, and doesn’t pose any problem.

As we use Munki to deploy Tableau, it’s easy to install the Tableau dependencies / drivers, for connecting to different types of data sources, with the update_for relationship for things like the PostgresSQL libraries, SimbaSQL server ODBC drivers, Oracle Libraries, Vertica drivers, etc. Most of these come in simple package format, and are therefore easy to install. We have not noticed any problems running higher versions of the drivers with lower versions of the software – i.e. the latest Oracle Library package for 9.3 works with Tableau 9.1.8.

Since most of these packages are Oracle related, you get the usual crap that you’d expect. For example, the Oracle MySQL ODBC driver is hilariously broken. It does not work. At all. The package itself is broken. It installs a payload in one location, and then runs a postinstall script that assumes the files were installed somewhere else. It will never succeed.  The package is literally the same contents as the tar file, except packaged into /usr/local/bin/. It’s a complete train wreck, and it’s pretty par for what you’d expect from Oracle these days.

Licensing Tableau

Tableau’s licensing involves two things: a local-only install of FLEXnet Licensing Agent, and the License Number, which can be activated via the command line. Nearly all of the work for licensing Tableau can be scripted, which is the good part.

The first thing that needs to happen is the installation of the FLEXnet Licensing package, which is contained inside Tableau.app:

/usr/sbin/installer -pkg /Applications/Tableau9.3/Tableau.app/Contents/Installers/Tableau\ FLEXNet.pkg -target /

Licensing is done by executing a command line binary inside Tableau.app called custactutil.

You can check for existing licenses using the -view switch:

/Applications/Tableau9.3/Tableau.app/Contents/Frameworks/FlexNet/custactutil -view

To license the software using your license number:
/Applications/Tableau9.3/Tableau.app/Contents/Frameworks/FlexNet/custactutil -activate XXXX-XXXX-XXXX-XXXX-XXXX

The Struggle is Real

I want to provide some context as to the issues with Tableau licensing.

Tableau licensing depends on the FLEXnet Licensing Agent to store its licensing data, which it then validates with Tableau directly. It does not have a heartbeat check, which means it does not validate that it is still licensed after its initial licensing. When you license it, it uses up one of your counts of seats that you’ve purchased from Tableau.

The main problem, though, is that Tableau generates a computer-specific hash to store your license against. So your license is tied to a specific machine, but that hash is not readable nor reproducible against any hardware-specific value that humans can use. In other words, even though you have a unique hash for each license, there’s no easy way to tell which computer that hash actually represents. There’s no tie to the serial number, MAC address, system UUID, etc.

Uninstalling Tableau / Recovering Licenses

The second problem, related to the first, is that the only way to get your license back is to use the -return flag:

/Applications/Tableau9.3/Tableau.app/Contents/Frameworks/FlexNet/custactutil -return <license_number>

What happens to a machine that uses up a Tableau license and then gets hit by a meteor? It’s still using that license. Forever. Until you tell Tableau to release your license, it’s being used up. For $2000.

So what happens if a user installs Tableau, registers it, and then their laptop explodes? Well, the Tableau licensing team has no way to match that license to a specific laptop. All they see is a license hash being used up, and no identifiable information. $2000.

This makes it incredibly difficult to figure out which licenses actually are in use, and which are phantoms that are gone. Since the license is there forever until you remove it, this makes keeping track of who has what a Herculean task.  It also means you are potentially paying for licenses that are not being used, and it’s nearly impossible to figure out who is real and who isn’t.

One way to mitigate this issue is to provide some identifying information in the Registration form that is submitted the first time Tableau is launched.

Registering Tableau

With the software installed and licensed, there’s one more step. When a user first launches Tableau, they are asked to register the software and fill out the usual fields:

Screen Shot 2016-04-22 at 10.07.51 AM

This is an irritating unskippable step, BUT there is a way to save some time here.

The registration data is stored in a plist in the user’s Preferences folder:

The required fields can be easily pre-filled out by creating this plist by prepending the field name with “Data”, as in these keys:

 <string>Menlo Park</string>
 <string>Software &amp; Technology</string>

If those keys are pre-filled before launching Tableau, the fields are pre-filled out when you launch Tableau.

This saves some time for the user to avoid filling out the forms. All the user has to do is hit the “Register” button.

Once Registration has succeeded, Tableau writes a few more keys to this plist – all of which are hashed and unpredictable.

The Cool Part

In order to help solve the licensing problem mentioned before, we can put some identifying information into the registration fields. We can easily hijack, say, the “company” field as it’s pretty obvious what company these belong to. What if we put the username AND serial number in there?


Now we have a match-up of a license hash to its registration data, and that registration data gives us something useful – the user that registered it, and which machine they installed on. Thus, as long as we have useful inventory data, we can easily match up whether or not a license is still in use if someone’s machine is reported lost/stolen/damaged, etc.

The Post-Install Script

We can do all of this, and the licensing, in a Munki postinstall_script for Tableau itself:

Some Good News

The better news is that as of Tableau 9.3, by our request, there’s now a way to pre-register the user so they don’t have to do anything here and never see this screen (and thus never have an opportunity to change these fields, and remove or alter the identifying information we’ve pre-populated).

Registration can be done by passing the -register flag to the main binary:

/Applications/Tableau9.3/Tableau.app/Contents/MacOS/Tableau -register

There are some caveats here, though. This is not a silent register. It must be done from a logged-in user, and it must be done in the user context. It can’t be done by root, which means it can’t be done by Munki’s postinstall_script. It doesn’t really help much at all, sadly. Triggering this command actually launches Tableau briefly (it makes a call to open and copies something to the clipboard). It does pretty much everything we don’t want silent flags to do.

It can be done with a LaunchAgent, though, which runs completely in the user’s context.

Here’s the outline of what we need to accomplish:

  • Tableau must be installed (obviously)
  • The Registration plist should be filled out
  • A script that calls the -register switch
  • A LaunchAgent that runs that script
  • Something to install the Launch Agent, and then load it in the current logged-in user context
  • Clean up the LaunchAgent once successfully registered

The Registration Script, and LaunchAgent

The registration script and associated LaunchAgent are relatively easy to do.

The registration script in Python:

Assuming we place this script in, let’s say, /usr/local/libexec/tableau_register.py, here’s a LaunchAgent you could use to invoke it:

The LaunchAgent obviously goes in /Library/LaunchAgents/com.facebook.tableauregister.plist.

If you’re playing along at home, be sure to test the registration script itself, and then the associated LaunchAgent.

Loading the LaunchAgent as the logged in user

With the registration script and associated LaunchAgent ready to go, we now need to make sure it gets installed and loaded as the user.

Installing the two files is easy, we can simply package those up:

Import the tableau_register.pkg into Munki and mark it as an update_for for Tableau.

Now comes the careful question of how we load this for the logged in user. Thanks to the wonderful people of the Macadmins Slack, I learned about launchctl bootstrap (which exists in 10.10+ only). bootstrap allows you to load a launchd item in the context you specify – including the GUI user.

Our postinstall script needs to:

  1. Determine the UID of the logged in user
  2. Run launchctl bootstrap in the context of that user
  3. Wait for Tableau to register (which can take up to ~15 seconds)
  4. Verify Tableau has registered by looking at the plist
  5. Unload the LaunchAgent (if possible)
  6. Remove the LaunchAgent

Something like this should do:


Note that launchctl bootout only exists on 10.11, not 10.10. For Mavericks users, simply deleting the LaunchAgent will have to suffice. There’s no huge risk here, as it will disappear the next time the user logs out / reboots.

This process does make certain assumptions, though. For one thing, it assumes that there’s only one user who cares about Tableau. Generally speaking, it’s uncommon for us that multiple users will sign into the same machine, much less have multiple users with different software needs on the same machine, so that’s not really a worry for me.

Tableau themselves make this assumption. If one user installs and registers Tableau, it’s registered and installed for all user accounts on that machine. Whoever gets there first “wins.” Tableau considers this a “device” license, thankfully, not a per-user license. In a lab environment where devices aren’t attached to particular users, this may be a win because the admin need only register it to their own department / administrative account / whatever.

Another simple assumption made here is that the user’s home directory is in /Users. I did this for simplicity in the script, but if this isn’t true in your environment, you’ll need to either hard-code the usual path for your clients’ home directories in, or find a way to determine it at runtime.

Lastly, this all assumes this is happening while a user is logged in. This works out okay if you make Tableau an optional install only, which means users have to intentionally click it in Managed Software Center in order to install. If you plan to make Tableau a managed install in Munki, you’ll need to add some extra code to make sure this doesn’t happen while there’s no user logged in. If that’s the case, you might want to consider moving some of the postinstall script for Tableau into the registration script invoked by the LaunchAgent.

Putting It Together

The overall process will go like this:

  1. Install Tableau Desktop 9.3.
  2. Postinstall action for Tableau Desktop 9.3: pre-populate the Registration plist, install FLEXnet, and license Tableau.
  3. Update for Tableau Desktop 9.3: install all associated Tableau drivers.
  4. Update for Tableau Desktop 9.3: install the LaunchAgent and registration script.
  5. Postintall action for Tableau Registration: use launchctl bootstrap to load the LaunchAgent into the logged-in user’s context.
    1. Loading the LaunchAgent triggers Tableau to pre-register the contents of the Registration plist.
    2. Unload / remove the LaunchAgent.

Thus, when the user launches Tableau for the first time, it’s licensed and registered. Tableau now has a match between the license hash and a specific user / machine for easy accounting later, and the user has nothing in between installing and productivity.

What A Load of Crap

It’s frankly bananas that we have to do this.

I understand software development is hard, and enterprise software is hard, but for $2000 a copy, I kind of expect some sort of common sense when it comes to mass deployment and licensing.

Licensing that gets lost unless you uninstall it? No obvious human-readable match-up between hardware and the license number generated by hashing? Charging us year after year for licenses we can’t easily tell are being used, because there’s no heartbeat check in their implementation of FLEXNet?

Why do I have to write a script to license this software myself? Why do I have to write a separate script and a LaunchAgent to run it, because your attempt at silent registration was only ever tested in one single environment, where a logged in user manually types it into the Terminal?

Nothing about this makes sense, from a deployment perspective. It’s “silent” in the sense that I’ve worked around all the parts of it that aren’t silent and automated, by fixing the major gaps in Tableau’s implementation of automated licensing.  That still doesn’t fix the problem of matching up license counts to reality, for those who installed Tableau before we implemented the registration process. Tableau has been of no help trying to resolve these issues, and why would they? We pay them The Big Bucks™ for these licenses we may not be using. We used them at one point, though, so pay up!

This is sadly par for the course for the big enterprise software companies, who don’t seem to care that much about how hard they make it for admins. Users love the products and demand it, and therefore management coughs up the money, and that means us admins who have to spend the considerable time and energy figuring out how to make that happen are the ones who have to suffer. And nobody particularly cares.

Isn’t enterprise great?

Troubleshooting an Obscure DeployStudio Runtime Error

DeployStudio is an old hat classic ’round these parts, and many Mac admins are familiar with its foibles and idiosyncrasies. For those of you who haven’t moved on to Imagr yet, this sad story about troubleshooting DeployStudio may encourage you to hop onto the gravy train and off the failboat.

The story starts with a simple premise: my NetBoot clients would start up DeployStudio Runtime, but then would throw a repository access error when trying to mount the DS repository (which I have configured to be served via SMB):

DeployStudio doesn’t like when this happens. It also doesn’t give you very useful information about what happens, because the repository access error triggers the “timeout until restart” countdown. If your trigger is an unforgiving number, i.e. 0 or 1 seconds, this will result in an instant reboot without you being able to troubleshoot the environment at all.

There’s nothing really useful in the log about why it failed, or how, either. Not very helpful, there, DeployStudio.

I’m troubleshooting this remotely, so I don’t have physical access to these machines. I’m doing all this relayed through messages to the local field technicians.

What we know at this point: DS Runtime can’t mount the SMB repo share.

Step 1: Verify DS’s Repo share

Simplest thing: check the server to make sure SMB is running and that DS knows about it. That’s simple enough to do in the System Preferences’ DeployStudio pane, which will show the status of the service and the address of the DS repository it’s offering.

Just for kicks, let’s try restarting the DS service.

Did that work? Nope.

Step 2: Verify SMB sharing

Okay, DS thinks it’s fine. Maybe SMB itself is freaking out?
sudo serveradmin stop smb
sudo serveradmin start smb

Let’s try mounting the share directly on another client:
mkdir -p /Volumes/DS/
mount -t smbfs //username@serverIP/DeployStudio /Volumes/DS
ls /Volumes/DS/

Works fine. Well, gee, that’s both good news and disconcerting news, because if the share works fine on other clients, why are these DS clients not mounting it?


So at this point, we know the SMB share works on some clients, fails on other clients, but is otherwise configured correctly on the server. We approach Hour 3 of All Aboard The Fail Boat.

Okay, just in case, let’s try rebuilding the NBI using DS Assistant. Did that fix it? Nope.

Ping test from broken client to server. No packet loss. Connection looks solid.

Telnet test from broken client to server on SMB port. It connects. No firewall, no network ACLs, no change in VLAN, no weird stuff.

Packet capture. Spanning tree set up between ports to carefully monitor traffic. Why are 60% of these clients failing to mount the share, but 40% still working?

Tear your hair out in frustration. Move on to hour 4.

A Glimmer of Hope

Time to get ugly. We need more data to determine what’s happening, and part of that is figuring out the difference between successful SMB authentications and failed ones. To see that, we need log data.

Hat tip to Rich Trouton for helpfully pointing me to this link:

SMB logging sounds good. On 10.10, the above link is an easy solution – just unload the SMB launchd, edit the plist to add in the -debug and -stdout options, reload on the launchd, and watch the system log.

On 10.11, it’s a bit more work – your best bet would be to disable Apple’s launchd for SMB, make a copy of it with a different identifier, and load that (hat tip to @elios in MacAdmins Slack for this).

Once we’ve got logging enabled, let’s look very carefully at a success vs. a failure.


This seems to be the key indicator of success:
kdc: ok user=F5KP60PFF9VN\username proto=ntlmv2
Compare that to the failure log:
kdc failed with -1561745592 proto=ntlmv2

Hmm, what the heck error code is that?

Googling got me to one specific hint, which is what gave the solution away:

Linux cifs mount with ntlmssp against an Mac OS X (Yosemite
10.10.5) share fails in case the clocks differ more than +/-2h:

The clock!

Well, That Was Obvious In Hindsight

I needed to verify the clock on one of the affected machines. Sure enough, the technician confirmed that the date was December 31, 1969. Definitely a bit more than 2 hours difference to the server.

In my defense, I’d like to remind you that I was troubleshooting this remotely and therefore couldn’t have noticed this without someone telling me and yes I’m rationalizing my failures stop looking at me like that I’m hideous don’t even look at me

The real question, then, is why this was happening. DeployStudio NBIs, when built via DeployStudio Assistant or Per Oloffson’s excellent AutoDSNBI, use an NTP server to sync up the date and time to prevent precisely this problem. What went wrong here?

The next silly thing: it turns out we changed our NTP server, and I simply failed to notice. The old NTP server didn’t resolve anymore, and that’s why any client that happened to have an expired clock battery (and therefore set back to the default time) failed to sync back up.

So the 60% fail rate we were seeing was essentially random luck against a pile of old machines, some of whom had been powered off for so long the clock battery ran out and the system time was reset.

Rebuilding the NBIs with the correct NTP server fixed the problem immediately.

The lesson from all of this?

Check the damn clock.

Introducing Facebook’s AutoPkg Script

AutoPkg Wrapper Scripts

There are myriad AutoPkg wrapper scripts/tools available out there:

They all serve the same basic goal – run AutoPkg with a selection of recipes, and trigger some sort of notification / email / alert when an import succeeds, and when a recipe fails. This way, admins can know when something important has happened and make any appropriate changes to their deployment mechanism to incorporate new software.

Everything Goes In Git

Facebook is, unsurprisingly, big on software development. As such, Facebook has a strong need for source control in all things, so that code and changes can always be identified, reviewed, tested, and if necessary, reverted. Source control is an extremely powerful tool for managing differential changes among flat text files – which is essentially what AutoPkg is.

Munki also benefits strongly, as all of Munki configuration is based solely on flat XML-based files. Pkginfo files, catalogs, and manifests all benefit from source control, as any changes made to the Munki repo will involve differential changes in (typically) small batches of lines relative to the overall sizes of the catalogs.
Obvious note: binaries packages / files have a more awkward relationship with git and source control in general. Although it’s out of the scope of this blog post, I recommend reading up on Allister Banks’ article on git-fat on AFP548 and how to incorporate large binary files into a git repo.

Git + Munki

At Facebook, the entire Munki repo exists in git. When modifications are made or new packages are imported, someone on the Client Platform Engineering team makes the changes, and then puts up a differential commit for team review. Another member of the team must then review the changes, and approve. This way, nothing gets into the Munki repo that at least two people haven’t looked at. Since it’s all based on git, merging changes from separate engineers is relatively straightforward, and issuing reverts on individual packages can be done in a flash.

AutoPkg + Munki

AutoPkg itself already has a great relationship with git – generally all recipes are repos on GitHub, most within the AutoPkg GitHub organization, and adding a new repo generally amounts to a git clone.

My initial attempts to incorporate AutoPkg repos into a separate git repo were a bit awkward. “Git repo within a git repo” is a rather nasty rabbit hole to go down, and once you get into git submodules you can see the fabric of reality tearing and the nightmares at the edge of existence beginning to leak in. Although submodules are a really neat tactic, regulating the updating of a git repo within a git repo and successfully keeping this going on several end point machines quickly became too much work for too little benefit.

We really want to make sure that AutoPkg recipes we’re running are being properly source controlled. We need to be 100% certain that when we run a recipe, we know exactly what URL it’s pulling packages from and what’s happening to that package before it gets into our repo. We need to be able to track changes in recipes so that we can be alerted if a URL changes, or if more files are suddenly copied in, or any other unexpected developments occur. This step is easily done by rsyncing the various recipe repos into git, but this has the obvious downside of adding a ton of stuff to the repo that we may not ever use.

The Goal

The size and shape of the problem is clear:

  • We want to put only recipes that we care about into our repo.
  • We want to automate the updating of the recipes we care about.
  • We want code review for changes to the Munki repo, so each package should be a separate git commit.
  • We want to be alerted when an AutoPkg recipe successfully imports something into Munki.
  • We want to be alerted if a recipe fails for any reason (typically due to a bad URL).
  • We really don’t want to do any of this by hand.


Facebook’s Client Platform Engineering team has authored a Python script that performs these tasks: autopkg_runner.py.

The Setup

In order to make use of this script, AutoPkg needs to be configured slightly differently than usual.

The RECIPE_REPO_DIR key should be the path to where all the AutoPkg git repos are stored (when added via autopkg add).

The RECIPE_SEARCH_DIRS preference key should be reconfigured. Normally, it’s an array of all the git repos that are added with autopkg add (in addition to other built-in search paths). In this context, the RECIPE_SEARCH_DIRS key is going to be used to contain only two items – ‘.’ (the local directory), and a path to a directory inside your git repo that all recipes will be copied to (with rsync, specifically). As described earlier, this allows any changes in recipes to be incorporated into git differentials and put up for code review.

Although not necessary for operation, I also recommend that RECIPE_OVERRIDE_DIRS be inside a git repo as well, so that overrides can also be tracked with source control.

The entire Munki repo should also be within a git repo, obviously, in order to make use of source control for managing Munki imports.


In the public form of this script, the create_task() function is empty. This can be populated with any kind of notification system you want – such as sending an email, generating an OS X notification to Notification Center (such as Terminal Notifier or Yo), filing a ticket with your ticketing / helpdesk system, etc.

If run as is, no notifications of any kind will be generated. You’ll have to write some code to perform this task (or track me down in Slack or at a conference and badger me into doing it).

What It Does

The script has a list of recipes to execute inside (at line 33). These recipes are parsed for a list of parents, and all parent recipes necessary for executing these are then copied into the RECIPE_REPO_DIR from the AutoPkg preferences plist. This section is where you’ll want to put in the recipes that you want to run.

Each recipe in the list is then run in sequence, and catalogs are made each time. This allows each recipe to create a full working git commit that can be added to the Munki git repo without requiring any other intervention (obviously into a testing catalog only, unless you shout “YOLO” first).

Each recipe saves a report plist. This plist is parsed after each autopkg run to determine if any Munki imports were made, or if any recipes failed. The function create_task() is called to send the actual notification.

If any Munki imports were made, the script will automatically change directory to the Munki repo, and create a git feature branch for that update – named after the item and the version that was imported. The changes that were made (the package, the pkginfo, and the changes to the catalogs) are put into a git commit. Finally, the current branch is switched back to the Master branch, so that each commit is standalone and not dependent on other commits to land in sequence.
NOTE: the commits are NOT automatically pushed to git. Manual intervention is still necessary to push the commit to a git repo, as Facebook has a different internal workflow for doing this. An enterprising Python coder could easily add that functionality in, if so desired.

Execution & Automation

At this point, executing the script is simple. However, in most contexts, some automation may be desired. A straightforward launch daemon to run this script nightly could be used:

Some Caveats on Automation

Automation is great, and I’m a big fan of it. However, with any automated system, it’s important to fully understand the implications of each workflow.

With this particular workflow, there’s a specific issue that might arise based on timing. Since each item imported into Munki via AutoPkg is a separate feature branch, that means that the catalog technically hasn’t changed when you run the .munki recipe against the Master branch. If you run this recipe twice in a row, AutoPkg will try to re-import the packages again, because the Master branch hasn’t incorporated your changes yet.

In other words, you probably won’t want to run this script until your git commits are pushed into Master. This could be a potential timing issue if you are running this script on a constant time schedule and don’t get an opportunity to push the changes into master before the next iteration.

I Feel Powerful Today, Give Me More

If you are seeking even more automation (and feel up to doing some Python), you could add in a git push to make these changes happen right away. If you are only adding in items to a testing catalog with limited and known distribution, this may be reasonably safe way to keep track of all Munki changes in source control without requiring human intervention.

Such a change would be easy to implement, since there’s already a helper function to run git commands – git_run(). Here’s some sample code that could incorporate a git push, which involves making some minor changes to the end of create_commit():


Ultimately, the goal here is to remove manual work from a repetitive process, without giving up any control or the ability to isolate changes. Incorporating Munki and AutoPkg into source control is a very strong way of adding safety, sanity, and accountability to the Mac infrastructure. Although this blog post bases it entirely around git, you could accommodate a similar workflow to Mercurial, SVN, etc.

The full take-away from this is to be mindful of the state of your data at all times. With source control, it’s easier to manage multiple people working on your repo, and it’s (relatively) easy to fix a mistake before it becomes a catastrophe. Source control has the added benefit of acting as an ersatz backup of sorts, where it becomes much easier to reconstitute your repo in case of disaster because you now have a record for what the state of the repo was at any given point in its history.

Generating PBKDF2 Password Hashes In Python, Not Ruby

Chef offers a great many useful features, including the ability to manage and create user accounts. The password for a local user account can be specified either in clear text or as a password hash.

According to the documentation linked above, generating an appropriate password hash for 10.8+ requires the use of a specific Ruby function:


However, when trying to generate such a hash using this tool on 10.10.5, I discovered a problem:

irb(main):026:0> OpenSSL::PKCS5::pbkdf2_hmac(
irb(main):027:1* password,
irb(main):028:1* salt,
irb(main):029:1* iterations,
irb(main):030:1* 128,
irb(main):031:1* OpenSSL::Digest::SHA512.new
irb(main):032:1> )
NotImplementedError: pbkdf2_hmac() function is unimplemented on this machine
from (irb):26:in `pbkdf2_hmac'
from (irb):26
from /usr/bin/irb:12:in `<main>'

Well, that's not very nice.

The issue is that the version of OpenSSL on OS X for the last several years is still 0.9.8zg. That version simply doesn't have an implementation of the pbkdf2_hmac() that Ruby wants to use. However, Python does, thanks to hashlib.

To recreate the same process in Python that the Chef documentation recommends for generating a 10.8+ password hash, use the following steps:

import hashlib
import binascii
import os

password = b'password'
salt = os.urandom(32)
chef_salt = binascii.hexlify(salt)
iterations = 25000

hex = hashlib.pbkdf2_hmac('sha512', password, salt, 25000, 128)
chef_password_hash = binascii.hexlify(hex)

Let's break down what happened there. First, we set the password to our password string. In Python 2, the b before a string doesn't really do anything.

The salt is a random 32-bit string. In Python, this comes out in binary form:

>>> salt = os.urandom(32)
>>> salt

Chef, however, requires this in a hexadecimal form:

>>> binascii.hexlify(salt)

We use 25,000 iterations as a nice arbitrary number, but you should use anything above 10,000 to be at least minimally safe. Of course this is a local user account on a service machine in my context, so I’m not entirely worried about its security as much.

Once we have all the variables, we can use the actual pbkdf2_hmac() function. In the example above, we’re using the SHA-512 digest, with a derived key length of 128 as the Chef documentation suggests. Once again, the result of that command is binary data, and Chef requires a hexadecimal representation, so we turn to our trusty friend binascii.hexlify again.

This allows us to create the Chef user block we need:

user 'myuser' do
gid 'staff'
home '/Users/myuser'
shell '/bin/bash'
password 'e6a8a452c0a9edb7f80703657b91fae74191d3b83982687ca00b83741ad775410178542ffc176abe6db9dc46053bc7ed36c91c1f43f82ba1dedc12de929f81cca868e223a25f3f16728e9f92c02e4421e9f73d73edb5e23e5d0cf1784243e8c79307ee5e61b411c9f116c450af8112e519fa15cfb50f5e7a8c1e6a78fb7cbc0e'
salt 'eb30e9c1946f086b4cd84679c1ee81235edea080b28b1ce4d39341794fad1ccd'
iterations 25_000
supports manage_home: true
action :create

I’m told this same technique can also generate password hashes to be used with Puppet as well, although I haven’t tested it personally.

Using iOSConsole to Query Information About iOS Devices

When Apple discontinued iPhone Configuration Utility, we lost the ability to access certain kinds of information easily from iOS devices that had not yet completed the Setup Assistant. With iPCU, we could attach a device to a computer and then get information such as WiFi MAC Address and Bluetooth MAC Address (which are not written on the box, nor on the outside of the device). Anyone who requires any kind of MAC-address authentication for wireless (like me) will need this information before being able to activate iOS devices on our network.

Unfortunately, Apple Configurator doesn’t give this kind of information for devices that have not yet been Prepared. Xcode’s Devices window also doesn’t provide this information for attached devices. iPCU was the only tool that provided this info right off the bat.

Thankfully, Sam Marshall has provided us with some tools to get this information, by building a framework called iOSConsole. This does require compiling this project in Xcode, so there are a few steps that must be done first.

Major, major credit to Mike Lynn for his instructions on how to build this project – I wouldn’t have been able to do this without his help.

Obtain the Project Files

Update: Sam Marshall has provided a pre-built version of iOSConsole that is already signed, so you can simply download the release and skip to the “Using iOSConsole” section below. Thanks, Sam!

  1. Go to https://github.com/samdmarshall/SDMMobileDevice and download the Master as a zip.
  2. Go to https://github.com/samdmarshall/Core-Lib/tree/62b93fa94fbfde421ea8bb1513f5e935191e755d, which is a specific commit in time, and download the Master as a zip.
  3. Extract both archives.
  4. Place Core-Lib-62b93fa94fbfde421ea8bb1513f5e935191e755d/Core into SDMMobileDevice-master/Core/. The end result should look like SDMMobileDevice-master/Core/Core/.

Alternatively, you can do it via git:
git clone https://github.com/samdmarshall/SDMMobileDevice.git
cd SDMMobileDevice;
git submodule update --init --recursive

Build the Project

  1. Open SDMMobileDevice.xcworkspace
  2. Click on the scheme (to the right of the grey Stop square button) that currently says “reveal_loader” and change it to “iOSConsole”:
    Screenshot 2015-07-08 13.49.47
    Screenshot 2015-07-08 13.49.41
  3. Click on “iOSConsole” in the left sidebar:
    Screenshot 2015-07-08 13.50.00
  4. Go to the Product menu -> Scheme -> Edit Scheme:
    Screenshot 2015-07-08 13.50.10Screenshot 2015-07-08 13.50.16
  5. Change Build Configuration to “Release” (it defaults to Debug):
    Screenshot 2015-07-08 13.50.18
    Screenshot 2015-07-08 13.50.20
  6. With “iOSConsole” highlighted in left sidebar, you should see Build Settings menu:
  7. Scroll down to Code Signing section and make sure “Don’t code sign” is selected:
    Screenshot 2015-07-08 13.50.42
  8. Go to the Product menu -> Build for -> Running:
    Screenshot 2015-07-08 13.50.56
    Screenshot 2015-07-08 13.50.59
  9. The build should succeed after compiling.
  10. Click the disclosure triangle underneath iOSConsole in left sidebar.
  11. Click the disclosure triangle underneath “Products” under “iOSConsole”:
    Screenshot 2015-07-08 13.51.41
  12. Right click “iOSConsole” under “Products” and choose “Show in Finder”:
    Screenshot 2015-07-08 13.51.44
  13. This will open up the “Release” folder:
    Screenshot 2015-07-08 13.51.47

Using iOSConsole

Navigate to this in Terminal (cd :drag folder into Terminal:):

./iOSConsole --help to see available commands:

-h [service|query] : list available services or queries
-l,--list : list attached devices
-d,--device [UDID] : specify a device
-s,--attach [service] : attach to [service]
-q,--query <domain>=<key> : query value for <key> in <domain>, specify 'null' for global domain
-a,--apps : display installed apps
-i,--info : display info of a device
-r,--run [bundle id] : run an application with specified [bundle id]
-p,--diag [sleep|reboot|shutdown] : perform diag power operations on a device
-x,--develop : setup device for development
-t,--install [.app path] : install specificed .app to a device
-c,--profile [.mobileconfig path] : install specificed .mobileconfig to a device

./iOSConsole -h query to see available information domains and keys to query

./iOSConsole --list (or ./iOSConsole -l) to list all attached iOS devices and get device identifiers for each device. You’ll need these device identifiers to specify which devices you want to query information from, using the -d argument.

./iOSConsole -l
Currently connected devices: (1)
1) d194149c6d840dcbabdd638d5aa5cee3e4eeb0c7 : iPad (iPad Air) (USB)

To get Ethernet Address (aka WiFi MAC address):
./iOSConsole -d d194149c6d840dcbabdd638d5aa5cee3e4eeb0c7 -q null=EthernetAddress
Bluetooth address:
./iOSConsole -d d194149c6d840dcbabdd638d5aa5cee3e4eeb0c7 -q null=BluetoothAddress

Note that most values are unavailable until after activation takes place.

If you want to get all possible information from the device (this is quite a lot):
./iOSConsole -d d194149c6d840dcbabdd638d5aa5cee3e4eeb0c7 -q NULL=NULL

This tool allows you to get some hardware information from devices without having to complete the setup process first.

Suppressing Adobe CC 2015 Splash Screens

Adobe has released new version of the CC apps, now called the “2015” versions. With the new Adobe CC apps comes new behavior.

Many of thew new CC 2015 apps have new splash screens. Some of them use a new welcome screen called “Hello” which is actually an interactive web page that requires network connection to function. This has resulted in some problems) for some users or bad network conditions.

Even if it works fine, it’s an extra step for users who just want to get started. In some cases, I’d prefer to suppress this welcome screen if possible. Here’s how you can do it for the new CC 2015 products:

Centrally managed

Photoshop, Illustrator, InDesign:

These apps are helpfully documented by Adobe. It involves downloading a file called “prevent_project_hello_launching.jsx” and placing it into the startup scripts folders for the Adobe software.

Here’s a script to build a package to do this, once you download the file and extract it from the .zip:


After Effects uses the same “Startup Scripts CC” folder as the ones above, but requires a slightly different script.
Place this content into /Library/Application Support/Adobe/Startup Scripts CC/Adobe After Effects/suppress_welcome.jsx:

app.preferences.savePrefAsBool("General Section", "Show Welcome Screen", false) ;

Alternatively, you can also just run this script to build a package to do this (you do not need to save the above file):

Lightroom 6

Lightroom 6, thankfully, uses the built in preference system. All the settings are stored in ~/Library/Preferences/com.adobe.Lightroom6.plist, and can be changed using defaults.

Lightroom launches a barrage of messages, notices, and prompts at the user when it first launches, but they all correspond to preference keys that can be changed or managed. Here are the preference keys:

firstLaunchHasRun30 = 1
noAutomaticallyCheckUpdates = 1
noSplashScreenOnStartup = 1
"com.adobe.ag.library_Showed_Walkthroughs" = 1
"Showed_Sync_Walkthrough" = 1
HighBeamParticipationNoticeHasShowed = 1

Here’s a profile that will configure those settings as well.

Per-user Preferences


Unfortunately, Dreamweaver doesn’t seem to have any nice mechanism for centrally managing the preferences via startup scripts or anything. The only way I’ve discovered to manage this is by copying in a pre-fabbed “Adobe Dreamweaver CC 2015 Prefs” file into the preferences folder, which is located at:
~/Library/Preferences/Adobe Dreamweaver CC 2015 Prefs.

To apply this setting to all users, we need to deploy it to a central location (such as /Library/Preferences/Adobe/Adobe Dreamweaver CC 2015 Prefs, which is simply the non-user-specific equivalent of the preferences path), and then copy it into each user’s Library at login. We can use Outset to accomplish this easily.

These are the settings you need to provide in the Prefs file that will turn off the hello page on startup (including first run). Save this file as “Adobe Dreamweaver CC 2015 Prefs”:

show hello page=FALSE

This script will be run by Outset to copy the preferences file from a centralized Library location into the user account’s correct Preferences location:

Download and save this script as “AdobeDWCCPrefs.sh”. The ending destination for this script is going to be /usr/local/outset/login-once/, which will trigger the first time a user logs in.

This script will build the package that will deposit all the necessary files in the right places, once you’ve downloaded and saved the two files above (the script, and Prefs file):


Muse has the same problem as Dreamweaver. You will need to drop a pre-configured “helloPrefStore.xml” into the Muse preferences folder, which is located at:
~/Library/Preferences/Adobe/Adobe Muse CC/2015.0/helloPrefStore.xml

As with Dreamweaver, we’ll deploy this file into a central location (such as /Library/Preferences/Adobe/Adobe Muse CC/2015.0/helloPrefStore.xml), and then copy it into each user’s Library at login with Outset.

Save this file as helloPrefStore.xml in a local directory:


This script will be run by Outset to copy the preferences file from a centralized Library location into the user account’s correct Preferences location:

Download and save this script as “AdobeMuseCCHelloPrefStore.sh”. The ending destination for this script is going to be /usr/local/outset/login-once/, which will trigger the first time a user logs in.

This script will build the package that will deposit all the necessary files in the right places, once you’ve downloaded and saved the two files above (the script, and XML file):

Required welcome screens

Edge Animate

Unlike some of the other CC applications, Edge Animate CC 2015’s welcome screen is required to load the rest of the app content. The welcome screen serves as the entrypoint into creating or loading up a project (similar to iMovie or GarageBand’s introductory windows). The welcome screen will appear regardless of whether or not you have a project available or previously opened, and closing the welcome screen will quit the application.

Flash CC

Flash CC uses the welcome screen as part of its window templates, so it can’t be suppressed.


Similar to Edge Animate, the welcome screen is a required way to access projects. However, by default, it will show this window every startup regardless of whether or not you are loading a project already.

You can uncheck that box by default by pre-providing a stripped down copy of Adobe Prelude’s preferences file, located at ~/Library/Application Support/Adobe/Prelude/4.0/Adobe Prelude Prefs:

You can use the same mechanism as described above in Muse to do so. Save the above gist as “Adobe Prelude Prefs”.

Save this Outset script as “AdobePreludeCCPrefs.sh”:

Use this script to build a package for it:

Just remember that you cannot completely suppress the Prelude welcome screen, but you can prevent it from coming up by default in the future.

Premiere Pro

Premiere Pro displays a similar “Hello” welcome screen that Photoshop, InDesign, and Illustrator do. With Premiere Pro, like Adobe Prelude, the splash screen will always display on startup if no default project has been selected / created. Otherwise, it will open the last project. It does not seem possible to isolate a specific key to disable the welcome screen – if anyone finds one, please let me know in the comments!

Premiere Pro’s preferences are stored in ~/Documents/Adobe/Premiere Pro/9.0/Profile-/Adobe Premiere Pro Prefs.

Although I’m not sure what happens if you make changes here, it also lists a “SystemPrefPath” as /Library/Application Support/Adobe/Adobe Premiere Pro Cc 2015. You may be able to centralize preferences there.

CC applications with no welcome screens:

  • Audition
  • Character Animator (Preview)
  • InCopy
  • Media Encoder
  • SpeedGrade

Adobe CC 2015: Another Circle Around The Drain

Well, Adobe has updated the CC products to 2015 versions. That means another day spent dedicated to downloading and building packages via CCP.

In my previous blog post about Adobe CC, I covered how to mass-import them into Munki while still addressing the nasty uninstaller bug.

The Uninstaller Bug

As described in the previous post (linked above), the problem with device-based licensing for Adobe is that the uninstallers are very aggressive. Uninstalling a single device-based package will nuke the licensing for all other Adobe software that is licensed with that same serial number. In other words, if you install Photoshop CC and Dreamweaver CC, and uninstall Dreamweaver CC, Photoshop CC will complain that it is not licensed and needs to be fixed (and thus won’t run).

That’s irritating.

To address this, one solution is to use the Serial number installer package with the Named License uninstaller package. The Named License uninstaller will not nuke the entire license database on that machine. This allows us to successfully install and uninstall without affecting other Adobe products on the machine.

Note: There are other issues with this approach if you do not have unlimited licensing agreements (ETLA), please see the previous blog post for details.

The simplest way to handle this is to create two folders – “CC Packages” and “CC NoSerial Packages”. Use CCP to create Serial Number licensing packages in the “CC Packages” folder for all new CC 2015 products. Then create a Named license package for the same product in the “CC NoSerial Packages.”

IMPORTANT NOTE about Munki: The import script will use filenames as item names. You may wish to either create your CCP packages with “2015” as a suffix to differentiate it from the previous versions, or adjust the names in the pkginfo files manually, or adjust your manifests to include the appropriate new item names. Also, you may need to adjust icon names. You probably don’t want to reuse the same item name for CC 2015 products as CC 2014 products, otherwise Munki may try to install Adobe updates imported via aamporter on versions that are too high.

Importing The Packages Into Munki

Now that you have two copies of each product in separate folders, we can combine the right parts to allow easy importing into Munki.

Copy the Uninstaller packages from the “CC NoSerial Packages” folder for each product into the equivalent “CC Packages” product folder.

End result is that the “CC Packages” folder will now contain each of the separate CCP products, each of which will contain a “Build” folder with the Serial Number license installer, and a Named license uninstaller.

Now we can run Tim Sutton’s mass Adobe CC import script. Before executing, however, you may wish to open it up and change Line 42 to “2015”:

    "--subdirectory", "apps/Adobe/CC/2014",
    "--developer", "Adobe",
    "--category", "Creativity",


    "--subdirectory", "apps/Adobe/CC/2015",
    "--developer", "Adobe",
    "--category", "Creativity",

Now you can run the script on your “CC Packages” folder:
./munkiimport_cc_installers.py CC\ Packages

The script will create the appropriate install and uninstall DMGs, and pkginfos for all of these products. Don’t forget to run makecatalogs afterwards.

In my initial testing, none of the CC 2015 apps produced any errors or problems installing or uninstalling via Munki.

We Are Imagr (And So Can You)

For a long time, Mac Admins have been relying on the wonderfully useful tool DeployStudio. This free tool has been the mainstay of many deployment strategies, and has been my old faithful for years.

Despite its utility and functionality, it has some issues. Some of its functionality is almost black-box opaque, and we have to make some guesses and assumptions, and do some wrestling to make it work the way we want. The biggest issue, of course, is that it’s OS X-only, meaning that Mac deployment relies on having a Mac in a data-center environment. This makes many network administrators unhappy, as Apple does not offer an enterprise-level Mac anymore, not since the 2009 XServe was discontinued. Putting Mac Minis in the data center has raised eyebrows among the server purists and network neckbeards, but no one could deny how useful DeployStudio is. But since it’s not open-source, there’s not much the Mac admin community can really do about it.

Finally, that’s about to change – thanks to Graham Gilbert and his incredible work on his tool called Imagr. Imagr is an open-source deployment tool that runs in a NetInstall environment and makes running scripts, restoring images, and installing packages easy.

Imagr now leverages the functionality of Pepijn Bruienne’s AutoNBI to make the creation of all-inclusive NetBoot Images (hereafter referred to as “NBIs”) easy and straightforward.

Imagr is in its early stages of development, so there’s obviously lots of rough edges and work to be done. Improvements are being made constantly, and by many contributors, so you can get an idea of the excitement of the community around an open-source tool to do deployments with.

Despite Graham’s excellent work at the wiki, getting a chance to play with it may be a bit foreboding to admins who aren’t sure where to start.

First, I’d suggest reading Greg Neagle’s excellent starting point blog posts: Part 1 and Part 2.

I’d like to get into a bit more detail so that future admins who want to test can investigate this thoroughly.

The Setup

You need four things to test out Imagr:

  1. A NetBoot server.
    The easiest way to accomplish this is with an OS X Server running the NetBoot service. If you have an existing DeployStudio server running on OS X Server, that would be a perfect choice. See Greg Neagle’s post above for how you can use Imagr with existing DeployStudio NBIs. I’ll be referring to this as the “Netboot server.”
  2. A target device or VM to test with.
    I highly recommend using VMWare Fusion’s NetBoot compatibility to make testing faster, but if you’ve got a physical machine you’re willing to blow away, that works just fine too. I’ll be referring to this as the “client.”
  3. A web server.
    OS X Server running a Web service will work just fine. I’m using my existing Munki repo. I’ll refer to this as the “web server.” If you want to use OS X Server 4’s default web server, just place all relevant files in /Library/Server/Web/Data/Sites/Default/.
  4. A machine to generate your NBIs on, preferably running 10.10.3 build 14D136 (to be compatible with all current Apple hardware).
    This could also be the OS X Server if you’re using one, as long as it’s fully updated to the latest version of OS X. I’ll be referring to this as the “admin computer.”

In this post, I’m going to use my existing 10.9.5 OS X Server 3 to serve out NetBoot (since it’s already used for DeployStudio). I’m using my own workstation as the admin computer, running 10.10.3 build 14D136 to generate the NBIs. I’m testing with a VMWare Fusion 7 Pro VM as client that will be NetBooting.

Preparing Your NBI:

Note: pre-step zero: download the latest Yosemite installer from the Mac App Store. I’ll be using the default path: /Applications/Install OS X Yosemite.app in this example.

  1. Create a file called imagr_config.plist that is hosted on your Web server.
    I’m hosting it on my munki repo in a folder called ‘imagr’, so it can accessed at http://munki/repo/imagr/imagr_config.plist. We’ll fill the contents of this file shortly. For now, just make sure it’s accessible.
  2. On the admin computer, download or clone Imagr from the GitHub page:
    git clone https://github.com/grahamgilbert/imagr.
  3. Create a file called config.mk in the same directory as the “Makefile” that is included with the Imagr clone (~/Applications/imagr/config.mk would be the path in my example).
  4. This config.mk file is going to be used to override the variables. Change the URL, which should be the accessible URL of your “imagr_config.plist” file. Additionally, if you have existing NBIs (from DeployStudio, for example), make sure you choose an index that does not collide with an existing one. The default is 5000:
    APP="/Applications/Install OS X Yosemite.app"
    ARGS="-e -p" # Enable Netboot set, include python

  5. From inside the imagr directory, run this command:
    make nbi

  6. At the end of the lengthy command run, you should have a file located in your OUTPUT folder (in this example, on the Desktop) called “Imagr-Testing.nbi”.
  7. This NBI needs to be copied to your NetBoot server. If you’re using OS X Server as your NetBoot server, you need to place this file in /Library/NetBoot/NetBootSP0/.
  8. Launch Server.app and go the NetInstall section. You should see it show up:
    Screen Shot 2015-05-12 at 8.55.00 AM
  9. Select your “Imagr-Testing” boot image and click on the gear icon to get its properties and details:
    Screen Shot 2015-05-12 at 8.55.12 AM
  10. First, make sure that the index it chooses (by default, 5000) does not collide with other NBI indexes. If it does, change the index number here (and then change your config.mk to build NBIs using a different index).
    Screen Shot 2015-05-12 at 8.54.48 AM
  11. By default, the NBI only makes itself available to compatible Mac models. This is great for actual deployment, but if you want to do testing with a VM, you might need to turn this off. Change the availability so that it’s open for “all Mac models”:
    Screen Shot 2015-05-12 at 8.55.12 AM
  12. If you’re doing testing with a VMWare Fusion VM, you’ll also need to make sure that the “Imagr-Testing” NBI is the Default netboot image, as Fusion only works with the default. Select the image, click the gear icon, choose “Use as Default Boot Image.”
    Screen Shot 2015-05-12 at 8.55.19 AM
  13. Verify on your clients. You should be able to see the “Imagr-Testing” netboot image in the Startup Disk pane of the System Preferences.

Creating a Workflow:

Now that the NBI is ready, we need an actual Imagr deployment configuration so that it can be tested. This workflow information is stored in the imagr-config.plist file we created on the Web server earlier. The workflow structure is documented on the wiki.

You can use any text editor to create this plist. For visual completeness, I’ll also include screenshots of Xcode’s visual plist editor.

Here’s the starting empty plist:

First, we need a password. On any computer with Python (such as the Admin computer):
python -c 'import hashlib; print hashlib.sha512("YOURPASSWORDHERE").hexdigest()'
Copy the long hash string and set that as the value of the Password key at the root of the plist:

Screenshot 2015-05-12 09.09.44

Now it’s time to fill out a workflow. This basic workflow is going to install a live package (i.e. not at first boot). This package is a CreateOSXInstallPkg package for 10.10.3 build 14D136. It needs to be accessible via HTTP somewhere, so it’s a prime candidate for going into the “imagr” directory on the Web server I already created.

The package installation can be described like this:

Screenshot 2015-05-12 09.24.06

In this workflow, we’re specifying a restart_action value of restart, which forces Imagr to reboot when the workflow completes (successfully).

The components key lists the actual process for things we want to do. Here, we’re just using a component of type package, which installs a package. We specify the url to the package, and we’re installing this package live, not at first boot, thanks to the value of false for first_boot.

Note that the url key points to a disk image, not a package itself. CreateOSXInstallPkg produces a bundle package, not a flat package, and thus it must be wrapped in a disk image. If you aren’t sure how to do that, follow these steps:

  1. mkdir InstallYosemite-10.10.3
  2. mv InstallYosemite-10.10.3.pkg InstallYosemite-10.10.3/
  3. /usr/bin/hdiutil create -srcfolder InstallYosemite-10.10.3 InstallYosemite-10.10.3.dmg
  4. Copy the resulting disk image to the location where it can be served via Web.

All this workflow accomplishes is installing the CreateOSXInstallPkg package, and then restarts.

Read the documentation to see how you can also leverage Scripts and Image cloning in addition to package installs. The workflow is fairly extensible.

Testing Out Imagr:

We have a NBI image we created, serving out NetBoot from the NetBoot server. We have a workflow in our imagr_config.plist file to install a package. Now it’s time for an actual test.

  1. Boot up a VM or physical device (the client device) to the NetBoot image called “Imagr-Testing” (or whatever you named it).
  2. Log in using the password you specified (in my example, it was “YOURPASSWORDHERE”).
  3. Run your workflow!

Testing it out again, and then again

If you want to make any changes to your NBI (or are testing out new forks/updates to Imagr), you need to rebuild the NBI each time.

  1. make update to rebuild the NBI without needing to build from scratch. This will save time (hat tip to Erik Gomez and Clayton Burlison).
  2. Copy it to your NetBoot server into /Library/NetBoot/NetBootSP0.
  3. Change index number if necessary (which you should do by specifying a unique index number in config.mk.
  4. Change the models to “All Mac models” if booting into a VM.
  5. Set the boot image as default.
  6. Reboot the client machine.
  7. Rinse, wash, repeat!

Fixing Adobe CCP’s Broken Uninstallers

I wrote previously about Adobe Creative Cloud products and Munki.

It’s an ongoing struggle.

This week, I discovered a rather unfriendly issue with the process. If you use Serial Number Licensing (i.e. serialized per-device installs) in Creative Cloud Packager (CCP), you get an Installer.pkg and Uninstaller.pkg that contains all the CC products you checked the boxes for. In most cases, with some exceptions, the Uninstaller packages do the right thing with Munki and uninstall the product.
Screen Shot 2015-04-23 at 9.48.40 AM

However, because these packages are all serialized, if you uninstall any serialized package, it removes the serialization from the device completely. This will break any existing CC apps still remaining on the machine.

More specific example:

  1. Create a Serial Number CCP package for Photoshop CC.
  2. Create a Serial Number CCP package for Dreamweaver CC.
  3. Install them both on the same machine / VM.
  4. Launch both Photoshop CC and Dreamweaver CC.
  5. Use the CCP Uninstaller package for Dreamweaver CC on that machine.
  6. Try to launch Photoshop.

Instead of Photoshop launching as you’d expect, you’re instead greeted by the Adobe Application Manager asking you to sign in and serialize your product – because there are no longer any serialization data on the device.

Uninstalling any CCP-generated product like this will completely remove all serialization.

I spoke to Adobe about this, and this was the response:
Screenshot 2015-04-22 12.20.43

Not great news – this is “semi-expected” behavior, and is potentially a huge problem.

Who Does This Affect?

Anyone who generates CCP packages for individual CC products using Serial Number Licensing can be affected by this. Admins already using Named Licensing will not encounter this issue.

The Solution

Patrick Fergus brought to my attention a clever idea. Since CCP allows the creation of both serialized (Serial Number Licensing) and non-serialized (Named Licensing) packages, we might already have a solution in place. The non-serialized packages don’t uninstall the serialization – because they never install it in the first place.

Thus, it’s possible to combine a serialized installer with a non-serialized uninstaller.

Here’s the general workflow:

  1. Create a Serial Number-licensed CCP package for Photoshop CC.
  2. Create a Serial Number-licensed CCP package for Dreamweaver CC.
  3. Install them both on the same machine / VM.
  4. Launch both Photoshop CC and Dreamweaver CC.
  5. Create a Name-licensed (non-serialized) CCP package for Dreamweaver CC.
  6. Use the Name-licensed (non-serialized) Uninstaller package for Dreamweaver CC to uninstall Dreamweaver on that machine.
  7. Try to launch Photoshop.
  8. It works! Photoshop launches as expected.

Using This Solution With Munki

Incorporating this into Munki is a bit more work.

If you’re starting fresh and haven’t already imported the Adobe CC products yet, you’re in luck, because this is relatively simple. Otherwise, we have to fix the pkginfos for each of the products in the repo.

Haven’t Yet Imported Adobe CC Products Into Munki:

The first step is to read the Munki wiki page about Adobe CC products.

Before you run Tim Sutton’s munkiimport script for CC installers, there’s some setup to be done.

You’ll need to run CCP twice for each package – once to create the Serial Number-licensed installer, and once to create the Name-license installer.

Copy/overwrite the Uninstaller packages from the Name-license versions into the Build folders for each Serial Number-licensed CCP package you created. The end goal here is that each product should be using the Serial Number Installer package and Named Uninstaller package.

Now go ahead and run Tim Sutton’s munkiimport script, and it will do the right thing.

Test thoroughly!

Fixing existing Adobe CC products in a Munki repo:

If you’ve already used Tim Sutton’s munkiimport script for CC installers to import your existing CC packages, it’s now time to fix the broken uninstallers.

You’ll need to run the CCP packages again for each product you want to fix – this time using Named licensing instead of Serial Number licensing, to create non-serialized packages. You can safely delete the Installer.pkg files to save space, as you don’t need them – you only need to keep the ~4mb Uninstaller.pkg files.

Next, you need to wrap each of the Uninstaller.pkg files in a DMG to replace the existing uninstaller DMGs in your Munki repo. You can do this using the same method munkiimport does:
hdiutil create -srcFolder /path/to/Named/Build /path/to/nonserialized/uninstaller.dmg

If I’m creating an uninstaller DMG for Adobe After Effects CC, for example:
hdiutil create -srcFolder Adobe/CC_Packages-NoSerial/AfterEffectsCCNoSerial/Build AfterEffectsCC_Uninstall-13.0.0.dmg
It’s important to make sure that the name of the DMG you are creating is identical to the one you are replacing.

The pkginfo files also need to be fixed for each product. Since the uninstaller items are being replaced, the hash sums for these DMGs must also be replaced with the new one – or Munki will complain that the hashes don’t much and won’t uninstall.

To calculate the SHA256 sum of the DMG, use this command:
shasum -a 256 /path/to/uninstaller.dmg
Then copy the resulting hash (the long string of letters and numbers) into the value of the uninstaller_item_hash key for each pkginfo you are replacing the uninstaller DMG for.

Copy the uninstaller DMGs to the Munki repo in exactly the same place as the previous ones, overwriting the previous DMGs.

Finally, run makecatalogs.

Test thoroughly!

Obvious Downsides

There’s one major issue here – uninstalling an Adobe CC product that was installed with serialization in this manner will not remove the serialization for that product on the device. In other words, if you are trying to count the number of licenses for Photoshop you have, uninstalling Photoshop CC via the Named-license uninstaller will not give you your license back (in the eyes of Adobe).

More than that, even if you uninstall all Adobe CC products from a machine using these non-serialized uninstallers, it won’t actually remove the serialization at the end. According to Adobe licensing, the device will still be using up a seat at the table.

With an ETLA agreement, where we have unlimited licenses and pay annual cost based on the number of Full-Time Employees, this isn’t an issue in our environment. But for anyone who has limited licenses of any of the products, this is an issue that has to be accounted for and worked around.

Possible solutions

If the goal is to remove all serialization from a machine after removing all Adobe CC products, you can use CCP to create a “License File package” – which isn’t actually a package, but a collection of files that includes a binary to serialize, and one to remove all serialization. This “RemoveVolumeSerial” binary (which is not an editable script!) could be run on the machines to remove all serialization.

If you need to remove a specific product license but leave the others untouched, you may have to look into the Adobe Provisioning Toolkit to accomplish what you need.