graham gilbert

Mac administration and assorted nerdity

Updating Boxen

| Comments

As you might know, I’m a bit of a fan of Munki and Puppet for managing the Macs I look after. Around a year ago, I really wanted to be able to automate my own setup across my own Macs the same way. I was forever finding that the particular git repository or app wasn’t on the Mac I was working on. Then there came the time when I wanted to do a clean install – that was easily a day down the drain there!

Automate all of the things

Then Boxen was released – based on Puppet, but targeted at setting up individual’s machines. I got on board just over a year ago, and haven’t really looked back – manually installing an app on my Mac seems very strange now. I’m not going to cover how to get started with Boxen, as there are many getting started guides out there (however, Gary Larizza’s is rather good).

There will come a time when you need to update the core part of Boxen. This happened to me when I clean installed 10.9 on my work laptop – all kinds of shit broke (somehow it managed to survive the upgrade process – go figure). I looked around, but couldn’t really find a definitive guide, so here it is (it’s shorter than this piece of rambling).

Ok, stop talking

As Boxen is made by GitHub, updating it is much like updating any other project on there that you’ve made a fork of. First we’re going to add it as a remote repository:

1
2
$ cd ~/src/our-boxen
$ git remote add upstream https://github.com/boxen/our-boxen.git

Then we’re going to fetch the stuff from the upstream repository:

1
$ git fetch upstream

Now we’re going to merge the updated repository with our own:

1
2
$ git checkout master
$ git merge upstream/master

If you haven’t modified any of the core Boxen files (Puppetfile, Gemfile or manifests/site.pp in my case), you might get away without having to fix any conflicts (you can ignore any in Puppetfile.lock and Gemfile.lock, we’ll deal with those next). I had conflicts as I had previously:

  • Been stupid and tried to update Boxen by just changing the Puppet Module and Gem versions
  • Edited site.pp as I didn’t want Nginx or node.js installed
  • Been dumb and put my custom Puppet modules in the wrong place in my Puppetfile

None of these were particularly arduous to fix, but annoying none the less. If you find you have loads, you might want to run:

1
$ git mergetool

The next step is to update your Puppet modules and RubyGems. First delete Puppetfile.lock and Gemfile.lock. Now go back to your trusty Terminal and:

1
2
$ bundle install --without development
$ bundle exec librarian-puppet install --clean

At this point, you might want to go through the custom modules you’ve added to your Puppetfile and update those, although this is by no means required – some apps I’ve installed through Boxen don’t have a built in updater, so Boxen is more convenient than hunting for installers on various vendor’s websites. Once your modules are up to date in your Puppetfile, you’re done! You can now get your Mac back to how you like it by issuing the usual:

1
$ boxen

Binding to Active Directory With Munki

| Comments

Many organisations need to bind their Macs to AD. There are quite a few options however, that need to be changed. It’s quite a straightforward process to automate this with Munki, although you do have a few options to consider.

First off, how are you going to deliver the actual bind script? You have the option of a no-pkg pkginfo file, with the script directly in the pkginfo plist. Whilst the script is now easily editable in the pkginfo, it does pose a security issue in that the catalog is kept in /Library/Managed Installs/catalogs, which will contain your script. Along with your AD bind account’s details. Whoops!

Prepare the Bind!

My preferred way of deploying the bind script is with a payload-free package made with The Luggage. My bind script is nothing special, it was originally borrowed from DeployStudio. You can find the script and the Makefile on my macscripts repo. If you need a primer on The Luggage, I wrote about it in August 2013. You just need to edit the variables at the top of the script to suit your environment and build the package.

So you’ve got the machine bound to AD. Great. What happens if the binding doesn’t go to plan? Or a well meaning tech manages to unbind the machine, but can’t manage to re-bind it? Or even worse, the user manages to unbind it themselves? We need to make Munki check that the Mac is still bound to AD.

Writing Plugins for Sal: Part 3

| Comments

We’ve already got a fairly decent plugin – it shows us how many machines we have that aren’t able to run 10.9. However, quite a few people won’t have any machines that fall into this category, and just want to know when one manages to sneak under the radar, so let’s hide the plugin if we don’t need to see it.

Previously on Lost

In the first part, you might remember that we had to tell Sal how much space our plugin needed. Well, we’re going to cover the eventuality of it not needing any space. First off, mavcompatibility.py.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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()

        if not_compatible:
            size = 3
        else:
            size = 0

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

    def filter_machines(self, machines, data):
        if data == 'notcompatible':
            machines = machines.filter(condition__condition_name='supported_major_os_upgrades').exclude(condition__condition_name='supported_major_os_upgrades', condition__condition_data__contains='10.9')
            title = 'Macs not compatible with OS X 10.9'
        else:
            machines = None
            title = None

        return machines, title

Take a look at lines 22 – 25. If we get any results from the query on line 20, we’re going to be showing the plugin. If there aren’t any applicable machines in our inventory, we don’t need to show the plugin. We are returning the size to Sal on line 33. Easy so far.

All that’s left to do now is make our templates not do anything if they don’t need to.

grahamgilbert/mavcompatibility/templates/front.html
1
2
3
4
5
6
7
8
9
{% if not_compatible > 0 %}
<div class="span3">
    <legend>{{ title }}</legend>
        <a href="{% url 'machine_list_front' 'MavCompatibility' 'notcompatible' %}" class="btn btn-danger">
            <span class="bigger"> {{ not_compatible }} </span><br />
            Not Compatible
        </a>
</div>
{% endif %}

Notice the if statement on line 1? If the number of machines is 0, we don’t need to show anything. You’ll need to make a similar change on grahamgilbert/mavcompatibility/templates/id.html.

That’s it – a simple plugin for Sal. You can find this completed plugin in my sal-plugins repository.

Writing Plugins for Sal: Part 2

| Comments

And now, time for the shocking second part of our series on how to write plugins for Sal.

In the previous part, we got our basic widget working. This time, we’re going to link it up so we can get lists of those pesky non-10.9 compatible Macs when we click on the button.

It’s a list, Jim

When displaying the list of machines, Sal will call the filter_machines function in your plugin. I’m sure you don’t want to disappoint, so here’s that function added on to the plugin we wrote last time.

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
29
30
31
32
33
34
35
36
37
38
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

    def filter_machines(self, machines, data):
        if data == 'notcompatible':
            machines = machines.filter(condition__condition_name='supported_major_os_upgrades').exclude(condition__condition_name='supported_major_os_upgrades', condition__condition_data__contains='10.9')
            title = 'Macs not compatible with OS X 10.9'
        else:
            machines = None
            title = None

        return machines, title

You’ll notice that our filter on the machines is pretty much identical to what we were looking for before – that’s because we’re looking for the same machines. We’re taking some input (a bunch of machines, and a string that we’ll come back to), and giving back the machine that fit our search and a title to show at the top of the page.

More templating

So, how did we pass that string? How do we even get to the page where a list of the machines is shown?

We need to edit the templates. First off, the template that is show on the front page of Sal:

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

The only difference here from last time is we’ve filled out the URL. The options for the first part are machine_list_front or machine_list_id – depending on whether you are coming from the front page (all of the Business Units) or from deeper in the application (the machines are limited), then we’re just passing the name of our plugin.

There isn’t a huge amount you need to change for the other template – just tell Sal what type of page you came from (group or business unit) and the ID of the page you came from.

grahamgilbert/mavcompatibility/templates/id.html
1
2
3
4
5
6
7
<div class="span3">
    <legend>{{ title }}</legend>
        <a href="{% url 'machine_list_id' 'MavCompatibility' 'notcompatible' page theid %}" class="btn btn-danger">
            <span class="bigger"> {{ not_compatible }} </span><br />
            Not Compatible
        </a>
</div>

There you go – a simple plugin for Sal. But don’t go away thinking we’re done. Whilst this is functional, it certainly leaves a fair bit to be desired. In the last part of this series, we’ll tidy everything up.

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!