Push Settings with Munki’s new Profile support

Note: please consider supporting supporting this webpage by purchasing one of my books! Thank you!

The combination of AutoPkg and Munki (or another package distribution system) is very powerful. You can use AutoPkg to automatically download, package and import software from the vendor’s site and then use Munki manifests to choose where and how to deploy. With the Managed Software Center interface, you can give users an App Store like interface to choose from software your insitution provides.

This is all wonderful until the user gets to the “Please Enter Your License Number!” dialog. Installing the software is just the first part of software management. You also need to manage configuration.

Since we do not want to mess around with websites to publish the serial numbers, we need another means of pushing configuration settings with Munki.

Note: your institution’s license agreement with a software vendor may or may not allow distribution of the serial number in this way. Some software may not work this way at all. Please check with the license manager/lawyer at your institution and/or the software vendor before you do this.

Which settings to manage?

My example will be the application Fetch. FetchSoftworks is very gracious as it provides free licences to educational and charitable institutions and it also stores the licensing information in a standard preferences file, which makes it perfect for this example.

You can find a Fetch recipe here. To run it in autopkg do:

autopkg repo-add jleggat-recipes
autopkg run Fetch.munki

And add it to the managed installs or optional installs section for your test client(s).

On a test client, install Fetch and open it. Before you enter any licensing info. Open the ~/Library/Preferences folder in list view and sort the contents by date modified, so the newest changes come first. Quit Fetch again, without entering anything and wait for the Fetch preference file to appear at the top of the list. Note its name (com.fetchsoftworks.Fetch.plist). You can select the file and hit the space bar to see the contents in QuickView.

Note: It may take a few seconds after quitting the application for the plist file to appear. This is because preference management is handled thorugh a system daemon cfprefs which will hold values in memory and wait for an “opportune moment” to actually write new and changed values to disk. Keep this in mind when fiddling with preference files.
The safest way to get an up-to-date view of preferences is using the defaults command in Terminal:

defaults read com.fetchsoftworks.Fetch

Then start Fetch again and enter your licensing information. Once you have successfully licensed Fetch, quit it again. Wait for the list of files in the Preferences folder to update. We see that Fetch has created or updated three plist files. com.fetchsoftworks.Fetch.plist, com.fetchsoftworks.Fetch.License.plist and com.fetchsoftworks.Fetch.Shortcuts.plist. It is pretty obvious which one we are interested in and looking at the contents confirms that the Fetch.License file contains the licensing info. We can verify once more on the Terminal with:

defaults read com.fetchsoftworks.Fetch.License

Note: the com.fetchsoftworks.Fetch domain also contains redundant license information, you could manage it either way. Most applications do not split information this way.

Creating the Profile

There are a few approaches to pushing this plist file to a client machine. You could build an installer package that places this file in a specific user’s home directory, but that would fail if the user name varies from client to client. You could write a postinstall script that moves the file to the current user’s Preferences folder, but that would only work for the current user, not other users and especially not users that may log in the future. There are work arounds for all of these problems, and we may need them for other software. But if we want to control preferences in plist files in either ~/Library/Preferences or /Library/Preferences then the best solution are Configuration Profiles.

Configuration Profiles are also property list files, but use a different schema because they contain more metadata around the settings. Tim Sutton has written a wonderful tool to convert ‘normal’ preference plist files to conifguration profiles: mcxToProfile.

You run mcxToProfile like this:

./mcxToProfile.py --plist ~/Library/Preferences com.fetchsoftworks.Fetch.License.plist --identifier FetchLicense --manage Once

The --manage Once option sets up the profile, so that it sets the preference keys in it once per user and then stops enforcing them. This is usually the safest and least intrusive choice for management, since it allows the application or the user to override or change settings.

Another option for the --manage setting are Often, which will reset the value on every user login. If you do not specify the --manage switch, then the preference keys in the profile will be always enforced and cannot be changed by the user or the application. This might be very useful in a scrictly controlled environment. Some third party applications may only work with one of the possible --manage options. Experiment to find out which.

The command above will generate a file called FetchLicense.mobileconfig open it in a text or property list editor. At the top there is a PayloadContent area where you can find the preference keys from the Fetch.License file nested inside. You can delete any keys and values you do not want to set on the clients.

Further down there are a few top level keys that you will want to modify:

  • PayloadDisplayName: this is the ‘Name’ of the config that will be displayed to the User in System Preferences. You will want to change this from the default supplied by mcxToProfile. You can also use the --displayname option to provide that during profile generation.
  • PayloadOrganization: Your institution’s name. You can also provide this during profile generation with the --organization key.
  • PayloadDescription: a description of what the profile does.

When you later import the profile into Munki it will use these values to fill the pkginfo file.

Testing the Profile

Now quit Fetch and delete all the preferences it may have set. Use the defaults command rather than just deleting the files to make sure cfprefsd is not still caching values:

defaults delete com.fetchsoftworks.Fetch
defaults delete com.fetchsoftworks.Fetch.License
defaults delete com.fetchsoftworks.Fetch.Shortcuts

Then double-click the configuration profile we just created in the Finder. System Preferences should open and prompt to install the profile. Click on “Show Profile” to view the contents, note where and how the data we entered earlier appears. You can also see the keys and settings.

Click “Continue” and confirm again to actually install the profile. You can still inspect the profile in the “Profiles” pane of System Preferences. Remember this for future debugging.

Now launch Fetch again and it should not prompt you for a license. If you can try again with a different user and/or on a different computer/virtual machine.

Importing the Profile into Munki

Once you have confirmed that the profile works as expected. You can import it into Munki. With version 2.2 and higher Munki recognizes files with the .mobileconfig extension as profiles and know how to install, update and un-install them.

To add a configuration profile, you use munkiimport

munkiimport Profile.mobileconfig

As usual munkiimport will interactively confirm extra metadata. It will parse most of the metadata from the profile and use it where appropriate. Then you can use the profile like any other item in munki as a managed install, managed un-install, optional install or update.

In our case we want to deploy the FetchLicense profile whenever the Fetch software is installed. In Munki terminology it is an update-for.

munkiimport FetchLicense.mobileconfig --update-for Fetch

Answer the prompts for the remaining metadata inputs. As an update, this profile does not have to be added to a manifest. As soon as the Fetch package itself is installed on a client, Munki will install the FetchLicense.mobileconfig.

To test, use Managed Software Center to add Fetch or run managedsoftwareupdate if Fetch is a managed install. You should also see the FetchLicense.mobileprofile appear as a required install. Once the install is done, check the Profiles pane in System Preferences to see if the profile is installed. Then open Terminal and run:

defaults read com.fetchsoftworks.Fetch.License

Finally launch Fetch and it should not ask for the license.

Obviously you can use this method to control other settings in Fetch and other applications as well.

Control ssh access with munki nopkg scripts

Often you want to control settings on client machine with scripts rather than packages. One such example is controlling ssh acces, aka ‘Remote Login’ in the OS X UI.

Note: please consider supporting the author of this site by purchasing one of my books! Thank you!

Having ssh access to the clients allows for remote access for trouble shooting and analysis. You can also remotely start or stop processes, like initiating managedsoftwareupdate right now instead of waiting.

However, since there is potential to abuse ssh, we should restrict ssh access to a subset of users. The OS X UI allows you restrict ssh access to a list of users or groups. In our example we will enable Remote Login and give access to any user with administrative privileges, i.e. the admin group.

You could write the scripts to enable these settings and put them in a so called “payload free package.” However, Munki has a simpler and more flexible way of handling this. Ironically, you create a pkginfo file with the nopkg setting.

Building the Install_Check script

Usually when you create the pkginfo for a package or dmg installation, Munki will analyze the files you give it and create conditions on which to install. If the dmg contains an application Firefox, it will look in /Applications for an application named Firefox and compare versions. If the application on the client is not present or of an older version, Munki will perform the installation.

For configurations other than files or applications, we can provide a script that runs and can tell Munki wether to perform the installation first. It might seem a little odd, but it makes a lot of sense to develop this install_check script first.

We need to check wether

  • Remote Login/ssh is enabled
  • access is not set to ‘All Users’
  • the admin user group is allowed access

Open your favorite text editor and create a new script called ssh_install_check.sh and start typing:

#!/bin/bash

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki export PATH

# this will run as a munki install_check script
# exit status of 0 means install needs to run
# exit status not 0 means no installation necessary

So if the script runs and returns 0 as the exit code, Munki will perform the installation. If the script returns any non-zero code, then Munki will assume everything is alright and not perform the installation.

First thing we want to test for is wether Remote Login/ssh is enabled. The CLI tool systemsetup has a command for that:

systemsetup -getremotelogin

Will return Remote Login: On/Off depending on the status. Testing for this is easy enough. Add these lines to the script:

# Is SSH enabled
if [[ $(systemsetup -getremotelogin) = 'Remote Login: Off' ]]; then
    echo 'Remote login is off!'
    exit 0
fi

You can save and test the script on your Mac right now. Use the UI in System Preferences > Sharing to toggle Remote Login and see what the script returns.

Next we want to test wether the access is set to ‘All Users’. There is a group called com.apple.access_ssh on your Mac that contains the users which are allowed ssh access. However, if the access is set to ‘All Users’ the OS renames this group to com.apple.access_ssh-disabled. Add these lines to the script:

ssh_group="com.apple.access_ssh"

# Does a group named "com.apple.access_ssh" exist?
if [[ $(dscl /Local/Default list /Groups | grep "${ssh_group}-disabled" | wc -l) -eq 1 ]]; then
    echo "access set to 'All Users'"
    exit 0
elif [[ $(dscl /Local/Default list /Groups | grep "$ssh_group" | wc -l) -eq 0 ]]; then
    echo "no group '$ssh_group'"
    exit 0
fi

First we defined a variable with the name ofthe group test for, this saves a lot of typing, reduces errors and makes the code more re-usable incase you want to use this for other control groups as well.

Then we run dscl /Local/Default list Groups and grep for the names to see if these groups exist. Then we do the same again in case the group does not exist at all (it really should, but it cannot hurt to test).

You can save and run the script to test again. Switch from ‘All Users’ to ‘Only these users’ in the UI. and see the results of the script.

Finally we want to test wether the admin group is allowed for ssh. We need to see if the group ‘admin’ is contained in the access_ssh group. This is a bit harder thatn it sounds. We can list nested groups using dscl /Local/Default read Groups/com.apple.access_ssh NestedGroups, but this will list long UUID strings, not the names of the groups.

To get the UUID of the admin we can use dsmemberutil getuuid -G admin. and then rest is easy. Add these lines to your script:

# does the group contain the admin group?
admin_uuid=$(dsmemberutil getuuid -G admin)
if [[ $(dscl /Local/Default read Groups/com.apple.access_ssh NestedGroups | grep "$admin_uuid" | wc -l) -eq 0 ]]; then
    echo 'admin group not nested in $ssh_group!'
    exit 0
fi

Sidenote 1: this will only test wether the admin group is directly nested in the access_ssh group. You could have another group in the access_ssh group, which itself contains the admin group. So the admin group would have the privilege to use ssh, but this script would still fail. You could also have all members of the admin group listed directly in the access_ssh group, and the script would not check for that. Either way the script will fail and cause Munki to add the admin group (we will learn how later) and it wouldn’t change access, so we are ok with this somewhat superficial test.
Sidenote 2: this script will also ignore other groups or users in the access_ssh group. If you wanted to enforce stricter access policies, you might want to test wether admin is the only group nested in access_ssh.

Finally the script should return a non-zero value when all the tests are passed. Our final script will look like this (cleaning it up a bit):

#!/bin/bash

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki export PATH

# this will run as a munki install_check script
# exit status of 0 means install needs to run
# exit status not 0 means no installation necessary

ssh_group="com.apple.access_ssh"

# Is SSH enabled
if [[ $(systemsetup -getremotelogin) = 'Remote Login: Off' ]]; then
    echo 'Remote login is off!'
    exit 0
fi

# Does a group named "com.apple.access_ssh" exist?
if [[ $(dscl /Local/Default list /Groups | grep "${ssh_group}-disabled" | wc -l) -eq 1 ]]; then
    echo "access set to 'All Users'"
    exit 0
elif [[ $(dscl /Local/Default list /Groups | grep "$ssh_group" | wc -l) -eq 0 ]]; then
    echo "no group '$ssh_group'"
    exit 0
fi

# does the group contain the admin group?
admin_uuid=$(dsmemberutil getuuid -G admin)
if [[ $(dscl /Local/Default read Groups/com.apple.access_ssh NestedGroups | grep "$admin_uuid" | wc -l) -eq 0 ]]; then
    echo 'admin group not nested in $ssh_group!'
    exit 0
fi

echo "everything seems as it should be, no install needed"
exit 1

Building the postinstall_script

Now that we have a script that tests wether we need to change settings, we can start building the actual script to do the changes. The good news is that while writing the install_check script we already built the outline for the actual install script. We use the same same tests, but instead of merely reporting and exiting, we now perform the necessary change:

#!/bin/bash

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki export PATH

ssh_group="com.apple.access_ssh"

# enable ssh
if [[ $(systemsetup -getremotelogin) = 'Remote Login: Off' ]]; then
    echo "turning on Remote Login/SSH"
    systemsetup -setremotelogin On
fi

# Does a group named "com.apple.access_ssh" exist?
if [[ $(dscl /Local/Default list /Groups | grep "${ssh_group}-disabled" | wc -l) -eq 1 ]]; then
    #rename this group
    echo "renaming group '${ssh_group}-disabled'"
    dscl localhost change /Local/Default/Groups/${ssh_group}-disabled RecordName ${ssh_group}-disabled $ssh_group
elif [[ $(dscl /Local/Default list /Groups | grep "$ssh_group" | wc -l) -eq 0 ]]; then
    # create group
    echo "creating group $ssh_group"
    dseditgroup -o create -n "/Local/Default" -r "Remote Login Group" -T group $ssh_group
fi

# does the group contain the admin group?
admin_uuid=$(dsmemberutil getuuid -G admin)
if [[ $(dscl /Local/Default read Groups/$ssh_group NestedGroups | grep "$admin_uuid" | wc -l) -eq 0 ]]; then
    echo "adding admin group to $ssh_group"
    dseditgroup -o edit -n "/Local/Default" -a admin -t group $ssh_group
fi

exit 0

Note that we use dseditgroup instead of manipulating the NestedGroup property with dscl. This is the official and safe way to manipulate groups in OS X.

Save this as ssh_postinstall.sh. Open the Remote Login Sharing UI in System Preferences > Sharing, change the settings and then run this script. To see the changes from the script reflected in the Preference you have to quit and restart System Preferences.

Building the pkginfo file

Now we that we have a script to test and another to change the settings we have to build a pkginfo file that will explain all of this to Munki. Let’s use makepkginfo to get us started:

makepkginfo --name EnableSSH --nopkg --pkgvers=1.0 --installcheck_script=ssh_install_check.sh --postinstall_script=ssh_postinstall.sh --unattended_install > enableSSH.pkginfo

--nopkg sets the install_type and tells Munki that this item has no pkg or dmg file associated with it. The two scripts we built are included in the plist. Munki will read this pkginfo and execute the code when necessary.

You can change or add some more keys (such as description or displayname) and then copy the file to your Munki repository and run makecatalogs. Then add EnableSSH to a manifest on your test machine(s) and run Managed Software Update on it. EnableSSH should appear and run the postinstall script. Check wether SSH works now.

Further, you can go in the UI and change some setting for Remote login, then run Managed Software Update again (or logout to make it run). Notice how our scripts detect that the settings changed from what they are supposed to be and re-ran our postinstall script to re-set them.

more options

You could add a third script to the pkginfo to ‘uninstall’ the settings. That way you could revert your settings to the default. There is even an option for a uninstallcheck_script that will check wether Munki should uninstall your settings.

Summary

The nopkg option allows us to run scripts with Munki, without need to bundle a pkg or dmg payload. Using an installcheck_script allows us to give Munki very precise instructions on when to run the script and repeatedly enforce configurations.

Create a NetInstall image to Bootstrap Munki

Munki’s wiki describes a process called Bootstrapping, where you install a Base OS, add the Munki tools and some settings and at first boot of the freshly installed Mac, Munki takes over and does the rest of the installations and configuration.

Here I will describe how to build a NetInstall Image (nbi) with System Image Utility to install a base OS with Munki. I will describe how to dip this for Mavericks 10.9.5 and Yosemite 10.10.1. There are some differences.

Either way, there is a restriction in System Image Utility that you can only build an NetInstall (or NetBoot or NetRestore) set of the same version of the OS you are running. So you if want both a Mavericks and Yosemite NetInstall set you will have to do this twice, once on a Mavericks machine and once on a Yosemite machine.

Download Mavericks or Yosemite

Open the Mac App Store and download the installer for Mavericks or Yosemite. The installed OS has to match the version of the installer and the NetInstall image you want to build.

Mac App Store will complain that the OS is already installed, just click ‘continue’ to start the download anyway. Once the download is complete the installer will actually open, just quit it, we do not need that now. You can also use a copy of the “Install OS X Mavericks|Yosemite.app” you may have archived somewhere.

Open System Image Utility

In Mavericks you can find it in /System/Library/CoreServices. On Yosemite they grouped several useful applications, including System Image Utility, into a subfolder /System/Library/CoreServices/Applications/. Easiest Way to get there is Finder’s “Go To Folder…” option (command-shift G).

In Yosemite, I find it useful to link this folder to the /Applications folder for easier access: ln -s /System/Library/CoreServices/Applications /Applications/System

If the Install application is in the default location (/Applications folder) SIU should pick it up automatically and show the version and build number below. Make sure they match your current OS.

NetInstall Image is the default selection. Choose “Customize” to proceed.

Customize the image workflow

The window will change to the customizable workflow view. You will have two steps: “Define Image Source” and “Create Image.”

“Define Image Source” only has one option and that should be already set. The default values in “Create Image” will need some adjusting though.

“Type” should be “NetInstall” which also grays out the “Installed Volume” field.

“Save to” defines the location where the nbi folder will be built. Ultimately you will have to upload this folder to your netboot server. Choose a local folder to build it. If you have a fast SSD drive, building it there will speed up the process significantly.

“Image Name” is the name of the folder set. Change the default to “Munki Yosemite” or “Munki Mavericks”.

“Network Disk” is the display that will be used in the Server application, Startup Disk and the EFI boot picker. Change the default to “Munki Yosemite” or “Munki Mavericks”.

“Image Index” should be an integer that is unique to each different NetBoot/NetInstall image in your network. If you are serving the image from a single NetBoot server, the number should be less than 4095, otherwise greater than. SIU will choose a random for you, but you can change it now or later in the Server app.

Finally the Description field. SIU puts a decent summary here, I would add the version of Munki you are installing.

Adding Custom Packages

There should be a second “Automator Library” window with additional workflow steps. Find the “Add Packages and Install Scripts” step and drag it between the two existing steps. You can also drag to change the order of the steps later.

Download the latest Munki release.

Drag the munkitools-VERSION.pkg file into the “Add Packages and Install Scripts” step. (As of this writing be sure to get the very latest package [2.0.1 or newer] as the older versions might not install completely during NetInstall. https://github.com/munki/munki/commit/53edd6a050b9e6612b2b827145b40a6cc9a50792)

This will install Munki tools along side OS X. Then at the first boot, Munki will take over the mac and install additional software and configurations according to Munki’s catalog and manifest.

However the munkitools installer does not know to point the client to your Munki repository. This is one step we need to configure.

Open your preferred text editor and paste

#!/bin/bash

MUNKI_REPO_URL="http://munki-server/munki_repo/"

/bin/echo "setting Munki Repo URL at $3/Library/Preferences/ManagedInstalls defaults to $MUNKI_REPO_URL"

/usr/bin/defaults write "$3/Library/Preferences/ManagedInstalls" SoftwareRepoURL $MUNKI_REPO_URL
/usr/bin/touch "$3/Users/Shared/.com.googlecode.munki.checkandinstallatstartup"

Save the script as ‘setupMunki.sh’ and drag the script into “Add Packages and Installation Scripts” step in your SIU workflow.

Modify the MUNKI_REPO_URL variable to point to your server.

The defaults command sets the SoftwareRepoURL in the proper preference file. The touch command creates a flag file so that Munki starts installing package right away on first boot as described here: https://github.com/munki/munki/wiki/Bootstrapping-With-Munki#details

The $3 parameter is passed into the script by the installer process and contains the target volume for the installation. In this case it will be something like /Volumes/Macintosh HD/. Note that this path likely contains spaces (like any OS X path), so you need to remember to quote this variable properly when you modify this script.

Build and Upload Netboot set

Save your Workflow and give it a meaningful name. Then run it. This may take several minutes. Once the workflow is done you can upload the nbi folder to your NetBoot Server. (OS X Server stores these folders by default in /Library/NetBoot/NetBootSP0/) Enable and try it on a client.

Further customization

(Note: every time you update or add a package or script, you need to rebuild and re-upload the netinstall folder)

The point of “bootstrapping” is to do as much configuration as possible with Munki later, so the modifications to the NetInstall process are minor. You can even update the Munki software on the client through Munki, so you will not have to update the NetInstall image on every Munki update.

There are a few things, however, that do make sense to do in the NetInstall process. Two common settings are to add a admin user and/or suppress the OS X setup process.

The setup dialogs will be suppressed if the file /var/db/.AppleSetupDone exist. So we can just add a line to our setupMunki.sh script:

/usr/bin/touch "$3/var/db/.AppleSetupDone"

Alternatively you can create a second script that does just this, so you separate OS X configuration from Munki configuration.

Create Default Users

You can use the application CreateUserPKG to create a pkg file that will create the desired user for you. If you need multiple default users you can create multiple different packages with CreateUserPKG.

Yosemite requires packages added to the NetInstall process to be of a specific format, called ‘distribution type packages.’ CreateUserPKG packages are not in this particular format, so we have to convert them.

productbuild --package createuser-1.0.pkg createuser-1.0dist.pkg

Applying Apple Software Updates

Munki allows you to include Software Updates from Apple during the Munki install/update process. If you want to do that at first boot, then add this line to your setupMunki.sh.

/usr/bin/defaults write "$3/Library/Preferences/ManagedInstalls" InstallAppleSoftwareUpdates -bool true

If you want to a local Software Update Server, then you can provide that, too:

/usr/bin/defaults write "$3/Library/Preferences/ManagedInstalls"  SoftwareUpdateServerURL http://sus.example.com/content/catalogs/index_production.sucatalog

Automate the installation

You may want to make the installation process completely without any user interaction. In this case a use will choose the NetInstall volume in System Preferences or at the EFI Boot Picker and the process will erase a partition with a pre-defined name and install a fresh OS with the Munki Tools and your settings.

This can be very useful, but also very dangerous.

To achieve this add the “Enabled Automated Installation” workflow step before the “Create Image” step. It only has three options:

  • the partition or volume name to install into
  • wether to erase the target volume
  • the language to use for the installation UI

If you enable automated installation, then there will be no UI provided, even if there is no volume with the expected name. (Then the installer process will just give up and boot back to the previous boot volume.) So you will have no access to other tools provided on the NetInstall netboot image.

Also if you choose a generic name for the automated volume (such as “Macintosh HD”) the potential that someone might accidentally delete a volume might be very high. In this case you really want to restrict access to the NetInstall by subnet or some other means.

Things Command Line Tool

So, between AutoPkg and some other tool I am working I have been spending a lot of time in Terminal recently. I have also (once again) tried to organize myself with the GTD app Things from CulturedCode. While I really like the simplicity and design of Things, I also wanted a fast way to add ideas I might have while on the command line and maybe also check off one or the other to do item from the Terminal. In short I wanted a command line tool for things.

Luckily, Things provides an AppleScript interface. So, I just wrote this tool for myself:

Things Command Line Tool

I have found it quite useful and fun. Also this is a nice example of writing a command line tool in AppleScript.