graham gilbert

Mac administration and assorted nerdity

Sal: The Munki Puppet

| Comments

At pebble.it, we always wanted to have an easy dashboard to look at to visualise the information we could collect from Puppet and Munki. We tried a few options, but didn’t like any of them, so we made our own.

Say hi to Sal – the Munki Puppet. It’s a multi-tenanted reporting solution for Munki and optionally, Facter. You can find all of the details over on GitHub, including installation instructions and a package to send out to your clients.

There is a plugin system built in to Sal, and over the next few days I will have a couple of posts covering how to make your own.

Managing the Authorization Database With Munki

| Comments

Have you ever wished you didn’t have to take calls from your users to unlock various parts of System Preferences? That standard users could unlock Energy Saver or Date and Time preferences? Well dear reader, this is the article for you.

If, for some strange reason you can’t be bothered to read this overly long article (I do love to procrastinate), you can head over to my macscripts repo on GitHub for the scripts and resulting pkginfo files I’ve made for this.

Before we start, let’s get one thing out of the way – Munki isn’t at heart a configuration management system. I’ve traditionally preferred Puppet for these tasks, but as there is at the time of writing a bug open on modifying this with Puppet, I took it upon myself to make this work in my environment. I spent a couple of days trying to get my sub-par Ruby skills to match my aspirations, so I moved onto a much more comfortable technology for me: Python and Munki.

To tackle this issue, I’m going to be using the same Philosophy as Puppet:

  • Check if the resource exists and what it’s current value is
  • If required, change the value
  • And be able to revert back to how things were

These translate quite nicely into installcheck_script, postinstall_script and uninstall_script rolled into a nopkg pkginfo (for a good intro into how nopkg pkginfos work, see how to manage printers with them over on the Munki wiki). We could do this with a payload free package and an installcheck_script just as easily, but as we’re already putting code into our pkginfo, we might as well keep it all in one place.

This isn’t intended to be a tutorial on the theory of OS X’s authorization database – there are already excellent resources available.

installcheck_script

Our installcheck_script is going to be very basic. To first open up the root system.preferences right, we just need to make sure that the group is set to everyone rather than admin. If you want to use another group, just substitute it in the group variable in the installcheck_script and the postinstall_script.

installcheck.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()

formatted = plistlib.readPlistFromString(out)
# if group matches, exit 1 as we don't need to install
if formatted['group'] == group:
    sys.exit(1)
else:
    # if it doesn't we're exiting with 0 as we need to perform the install
    sys.exit(0)

postinstall_script

The postinstall_script is just an extension of the installcheck_script – but we’re going to make use of Python’s built-in plistlib to modify the plist and feed it back into security authorizationdb to set our desired settings.

postinstall.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    #input_plist = {}
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)

uninstall_script

We should be good admins and clean up after ourselves, so we’ll include an uninstall script.

uninstall.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Set the group back to admin
group = 'admin'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)

Getting it into Munki

Now we’ve got our three scripts, we need to get them together into a pkginfo file. Assuming the scripts you’ve just made live in ~/src/macscripts/Munki/Auth:

1
2
$ cd ~/src/macscripts/Munki/Auth
$ /usr/local/munki/makepkginfo --installcheck_script=installcheck.py --postinstall_script=postinstall.py --uninstall_script=uninstall.py > OpenSysPrefs-1.0.plist

Which will produce the bare bones of a pkginfo file, but there are a few other things we need to add into it. Modify OpenSysPref-1.0.plist to look like the below. For further documentation on what we’re doing here, have a look at the Munki wiki. The important parts you’ll need to add / modify are:

  • autoremove
  • catalog
  • description
  • display_name
  • name
  • installer_type
  • minimum_os_version
  • version
  • unattended_install (if you want it to apply in the background)
  • uninstall_method
  • uninstallable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>autoremove</key>
    <false/>
    <key>catalogs</key>
    <array>
        <string>production</string>
    </array>
    <key>description</key>
    <string>Opens System Preferences to Everyone</string>
    <key>display_name</key>
    <string>Open System Preferences</string>
    <key>name</key>
    <string>OpenSysPrefs</string>
    <key>installer_type</key>
    <string>nopkg</string>
    <key>minimum_os_version</key>
    <string>10.8.0</string>
    <key>unattended_install</key>
    <true/>
    <key>version</key>
    <string>1.0</string>
  <key>installcheck_script</key>
  <string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()

formatted = plistlib.readPlistFromString(out)

# if group matches, exit 1 as we don't need to install
if formatted['group'] == group:
    sys.exit(1)
else:
    # if it doesn't we're exiting with 0 as we need to perform the install
    sys.exit(0)</string>
  <key>postinstall_script</key>
  <string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    #input_plist = {}
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)</string>

  <key>uninstall_method</key>
  <string>uninstall_script</string>
  <key>uninstallable</key>
  <true/>
  <key>uninstall_script</key>
  <string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Set the group back to admin
group = 'admin'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)</string>
</dict>
</plist>

At this point, you should be able to add this pkginfo to your Munki repository, include it in a manifest and – well, nothing will happen, as this only unlocks the top level of System Preferences. If you want to do more, you’ll need to unlock additional parts as well – the scripts to do this can be found in my macscripts repository. I’ve specified that OpenSysPrefs is required in all of these – this means I can include only the needed modifications in the manifest and not worry about the top level being unlocked.

Also remember that Munki has conditional items built right in – you might only want to unlock the Network pane on laptops so they can install VPN profiles etc using something like this:

1
2
3
4
5
6
7
8
9
10
11
<key>conditional_items</key>
<array>
  <dict>
    <key>condition</key>
    <string>machine_type == "laptop"</string>
    <key>managed_installs</key>
    <array>
      <string>UnlockNetwork</string>
    </array>
  </dict>
</array>

Crypt 0.5 Released

| Comments

I just pushed up version 0.5 of Crypt – the release details are over at GitHub. This is the last version that will be compatibile with the current version of Crypt-Server – which has also been updated to be compatible with Django 1.5.

This is fully tested (in my environment!) with Mavericks, so go forth and escrow FileVault keys.

Setting a Desktop Picture in Mavericks

| Comments

Sometimes we are asked by clients to set a default desktop picture for new users – sometimes we are deleting home directories on logout, so need to warn the users, other times the client just wants their corporate wallpaper to be the default.

If you are lazy and don’t want to read this post then the script that changes the desktop picture is on GitHub.

Whatever, here’s what we used to do:

1
2
/usr/bin/defaults write com.apple.desktop Background '{default = {ImageFilePath = "/Library/Desktop Pictures/Black & White/Lightning.jpg"; };}'
/usr/bin/killall Dock

Nothing earth shattering there if you’ve managed Macs for any length of time.

But then 10.9 changed things – this stopped working.

I ran fs_usage to see what was happening whilst I changed the desktop picture on my machine:

1
$ sudo fs_usage -w | grep desktop

Obviously there was a metic buttload of information, but this line caught my eye.

1
15:25:06.884820    WrData[A]       D=0x0b2d1d90  B=0x1000   /dev/disk1  /Users/grahamgilbert/Library/Application Support/Dock/desktoppicture.db

Bingo! I opened up the database in the SQLite Manager Firefox extension (the only thing I use Firefox for these days) and had a peek. And then I got half a brain and googled the path of the desktoppicture.db file and found that there was a gist from Greg Neagle. Perfect!

Of course, he’d already improved upon this script and written a proof of concept to set a random desktop picture using PyObjC. This got me 90% of the way there, so this is my modified version of his script. The full code and usage instructions are over on GitHub.