graham gilbert

Mac administration and assorted nerdity

Writing Plugins for Sal: Part 1

| Comments

Writing a plugin for Sal isn’t hard. In fact, I’d go so far as to say it’s easy. We’re going to make a plugin that will flag up any machines that aren’t compatible with Mavericks, by using Tim Sutton’s script. To start off with, you’re going to need to get that script onto your Macs at /usr/local/munki/conditions. I’d personally use Puppet for that, but if you’re a purely Munki shop, you’ll be using a package. And handily, I’ve made one.

The convention I’d like everyone to follow is to drop your plugins into the plugins directory, in a subdirectory named after yourself – mine are going in plugins/grahamgilbert. The plugin we’re making today is going in plugins/grahamgilbert/mavcompatibility.

Metadata

The first piece you’ll need is a .yapsy-plugin file. This contains the metadata for your plugin. It’s all pretty self explanatory. This is plugins/grahamgilbert/mavcompatibility/mavcompatibility.yapsy-plugin.

1
2
3
4
5
6
7
8
9
[Core]
Name = MavCompatibility
Module = mavcompatibility

[Documentation]
Author = Graham Gilbert
Version = 0.1
Website = http://grahamgilbert.com
Description = Displays macs that aren't compatible with 10.9.

Now for the meat

Onto the actual plugin. Your plugin is going to be sent at least two pieces of information, possibly three.

  • page: This will be the page the plugin is going to be shown on. This will either be front, bu_dashboard or group_dashboard. You will need this information later on.
  • machines: This a collection of machines you are going to need to work on. Depending on the page, this might be all of them, or just a subset from a Business Unit or Machine Group.
  • theid: If you are displaying your plugin on either a Business Unit page or a Machine Group page, this is the unique ID of that Business Unit or Machine Group.

And in return, your plugin is expected to return two things:

  • Some HTML: You plugin needs to return it’s output.
  • The width of the output: Sal uses Bootstrap, and it uses a grid system. So Sal can wrap lines properly, you need to tell Sal how many columns your plugin needs. This should be an integer.

That’s the 50,000 ft view of a Sal plugin, let’s make one. The main thing to remember is that Sal is written in Django, so if you have any problems, looking at their documentation will help. You can also enable debug logging on your Sal install by uncommenting lines 24 and 25 in server/views.py (turn it off when you’re done though, it is VERY verbose).

First off, a little about how Sal stores the data you send it. Sal stores Munki’s conditions in the Condition table, and for each one, the name and it’s data is stored (this is the same for Facts). Munki’s conditions can consist of a variety of data types (strings, dates, arrays), so Sal will flatten any arrays it is given into a comma separated list. Each machine will have multiple Conditions and Facts associated with it.

When displaying the plugin, Sal will look for a function called show_widget, passing the information mentioned previously. Don’t worry too much about the templates, we’ll cover them later.

grahamgilbert/mavcompatibility/mavcompatibility.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
26
27
28
from yapsy.IPlugin import IPlugin
from yapsy.PluginManager import PluginManager
from django.template import loader, Context
from django.db.models import Count
from server.models import *

class MavCompatibility(IPlugin):
    def show_widget(self, page, machines=None, theid=None):

        if page == 'front':
            t = loader.get_template('grahamgilbert/mavcompatibility/templates/front.html')

        if page == 'bu_dashboard':
            t = loader.get_template('grahamgilbert/mavcompatibility/templates/id.html')

        if page == 'group_dashboard':
            t = loader.get_template('grahamgilbert/mavcompatibility/templates/id.html')


        not_compatible = machines.filter(condition__condition_name='supported_major_os_upgrades').exclude(condition__condition_name='supported_major_os_upgrades', condition__condition_data__contains='10.9').count()

        c = Context({
            'title': '10.9 Compatibility',
            'not_compatible': not_compatible,
            'page': page,
            'theid': theid
        })
        return t.render(c), 3

Skip to line 20 – this is where the real work starts. All we’re doing is taking the machines we were passed and first off finding the machines that have the condition we’re looking for. We then want to remove those that contain 10.9 in that data.

Templates

Then it’s just a case of passing those variables to our template. As we aren’t linking our buttons to anything for now, both of our templates will be the same, but we will still make two separate ones as we’re going to need them next time.

grahamgilbert/mavcompatibility/templates/front.html
1
2
3
4
5
6
7
<div class="span3">
    <legend>{{ title }}</legend>
        <a href="#" class="btn btn-danger">
            <span class="bigger"> {{ not_compatible }} </span><br />
            Not Compatible
        </a>
</div>

Make a file in templates called id.html with the same content for now – we’ll make them different in part two.

We return our plugin on line 28 of mavcompatibility.py. First we render the appropriate template, passing it our data, and we return how wide our plugin is – in this case it will take up three columns.

That’s it for a basic plugin – we’ve taken a bunch of machines, filtered them based on a Munki condition, and we’ve returned the data. But this obviously is lacking – the button doesn’t do anything and we still see a big fat zero when all of our machines are 10.9 capable. Anyway, you can get the code so far in my sal-plugins repository.

Tune in to part two for the thrilling conclusion!

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.