NOTE: This post does NOT include any information about setting up a Chef server. There is quite a bit of documentation on Chef’s own site as well as blog posts (including my own older ones around the internet for setting up a Chef server and getting that infrastructure started. This article can be done entirely in Chef Local Mode (which obviously does not require a Chef server), or with an existing Chef infrastructure.
Introduction
Facebook has recently open-sourced a number of its Mac-specific Chef cookbooks. These are the actual tools we use to manage certain features, using Chef’s config management model. In this blog post, I’m going to discuss how to use them, how to benefit from them, and what features they offer.
Target Audience
The target for this blog post is a Mac admin with a budding interest in config management. I will endeavor to explain things in a way that does not require a deep understanding of Chef, so please don’t run away screaming if you aren’t already a user of some config management system (like Chef, Puppet, etc.). The goal here is to show what kind of benefits we get from using a system like this that aren’t really offered by other tools.
I’m new to Chef, what do I need to know?
Unsurprisingly, there are lots of results for a Google search of “Getting started with Chef”. I’ll generally point people to the official “basic idea” documentation on Chef’s website.
For this article, let me give you a brief rundown of Chef (which I may eventually spin into a new blog post).
Chef is a config management system that is structured as a set of operations that need to happen, which then may or may not trigger based on certain other conditions you’ve specified. Ultimately, each cookbook contains a (sometimes series of) recipe(s) – which tells Chef what operations to do – that is bolstered by helper code (libraries, resources, etc.).
The API Model
At Facebook, we try to design our cookbooks using an “API model.” That model is based on the idea that you have a set of variables (in Chef, they’re called attributes) that have basic sane defaults or values, and those variables can be overridden.
Each “API” cookbook will generally not do much on its own (or at least shouldn’t do anything harmful) unless the default values in the attributes are set to something useful.
Thus, the overall idea behind Facebook Chef is that you have a series of cookbooks that each do basic management operations – such as install profiles, launch daemons, manage a specific setting, etc. – based on what other cookbooks have put into those attributes.
The basic Chef model
The basic understanding of Chef you’ll need for this blog post is about Chef’s phases. Chef has, essentially, two primary phases, compile time and run time:
- Compile time – first, Chef goes through all the cookbooks and loads up all the attributes it will use (think of these as “variables” that exist throughout the Chef run).
- Compile time part two – Chef builds a list of all the resources (think of them as “actions” that use these attributes for data) it will need to execute, in order.
- Run time (a.k.a. convergence) – Chef goes through the list of resources and executes all of them in order.
Facebook’s API model, as described above, is based on the idea that most interaction with these cookbooks will be entirely based on overriding the attributes with values you want. These values are gathered at compile time, and then consumed at run time. By using this philosophy, we can make some cool implementations of dynamic management routines.
I recommend reading through the Quick Start guide on Facebook’s Github repo to get a basic idea of how to use it.
Getting Your Feet Wet
The basic structure of CPE Chef
The first place we start, using Facebook CPE Chef, is in the cpe_init
cookbook. This will be the jump-off point for everything else that happens. As documented in the Quick Start guide, we’ll be using cpe_init
as the cookbook that triggers all other cookbooks (which is provided by the quickstart.json
file).
If you take a peek in cpe_init::mac_os_x_init.rb
, you’ll see the overall cookbook run list that will actually happen – these are all the cookbooks that will run. On lines 18-22, the first item in the run list is cpe_init::company_init.rb
.
company_init
is where all the natural “overrides” are going to take place, where you can customize what you want to have happen on your client machines. As described in the “API model” section above, we’re going to use this recipe to set the values of the attributes to useful data, which will then be consumed by the API cookbooks during run time.
For this blog post, this will generally be the only file you’ll need or want to edit to see results.
Start with a clean slate
Let’s start with something simple. For now, take the default company_init
and remove everything after line 21. You’ll need to keep lines 18-20 in order for the cpe_launchd
and cpe_profiles
cookbooks to function, though, and we’re going to be using them. Go ahead and replace the three occurrences of “MYCOMPANY” with whatever you want:
node.default['organization'] = 'pretendco' node.default['cpe_launchd']['prefix'] = 'com.pretendco.chef' node.default['cpe_profiles']['prefix'] = 'com.pretendco.chef'
QUICK CHEF TIP: In Chef parlance, node
refers to the machine itself during a Chef run. node
is a dictionary / hash of key/value pairs containing data about the node that will last throughout the entire Chef run. Attributes from cookbooks are stored as keys in this node object, and can be accessed the way any dictionary/hash value is normally accessed – node[key]
. Attributes are normally set in the attributes::default.rb part of a cookbook. To change the value of an attribute during a recipe, you’ll need to use node.default[key]
. Trying to change a value without using node.default
will result in a Chef compile error.
Let’s start with a simple example – setting a profile that controls that the screensaver behavior.
Using cpe_screensaver to dynamically create a ScreenSaver profile
Controlling the ScreenSaver is relatively easy for Mac Admins – most of the relevant settings we’d want to manage can be done with a configuration profile that manages the com.apple.screensaver
preference domain. Profiles are easy to install with most Mac management tools (MDM, Munki, etc.), so this is a simple win for Mac admins.
With Chef, we have a nice little toy called cpe_profiles
, which allows us to dynamically specify what profiles we want installed, which are also dynamically created each time Chef runs. But we’ll get to the value of dynamic configuration soon.
The cpe_screensaver
cookbook essentially does one thing – it generates a profile (in Ruby hash form) to manage the settings specified in its attributes, which is then fed to the cpe_profiles
cookbook. cpe_profiles
creates and installs all the profiles it was given at the end of the run.
In a bit more detail, cpe_screensaver
sets up the namespace for the attributes we can override. You can see these in the cpe_screensaver::attributes
file. It contains these three attributes:
default['cpe_screensaver']['idleTime'] = 600 default['cpe_screensaver']['askForPassword'] = 1 default['cpe_screensaver']['askForPasswordDelay'] = 0
QUICK CHEF TIP: The attributes file declares its attributes (and appropriate namespace) using the default[key]
syntax. This both declares the existence of, and sets the default value for a node attribute, which can then be accessed during recipes with node[key]
, and modified during recipes with node.default[key]
.
For the screensaver, these three attributes correspond to keys we see in com.apple.screensaver
. The idleTime
attribute determines how much idle time (in seconds) must pass before the screensaver activates; the askForPassword
attribute is a boolean determining whether or not unlocking the screensaver requires a password; and the askForPasswordDelay
is how much time must pass (in seconds) after the screensaver locks before prompting for a password.
By default, we are mandating a value of 10 minute idle time lock, which requires a password immediately after locking.
Let’s alter these values and then do our first Chef-zero run. In your company_init.rb
file, we can override these attributes:
node.default['cpe_screensaver']['idleTime'] = 60 node.default['cpe_screensaver']['askForPassword'] = 0 node.default['cpe_screensaver']['askForPasswordDelay'] = 0
Save the changes, and run Chef-zero:
cd /Users/Shared/IT-CPE/chef sudo chef-client -z -j quickstart.json
This will initiate a “local-only” Chef run (also known as a “Chef zero” run, where it creates its own local Chef server on demand and runs Chef against it).
Some relevant snippets of Chef output:
Recipe: cpe_screensaver::default * ruby_block[screensaver_profile] action run - execute the ruby block screensaver_profile
<snip>
Recipe: cpe_profiles::default * cpe_profiles[Managing all of Configuration Profiles] action run Recipe: <Dynamically Defined Resource> * osx_profile[com.pretendco.chef.screensaver] action install - install profile com.pretendco.chef.screensaver
In the (admittedly verbose) Chef output, you’ll see the section where cpe_profiles
applies the “com.pretendco.chef.screensaver”. You can also verify this in System Preferences -> Profiles and see the Screen Saver settings being managed.
Success!
How does it work?
The interaction between your company_init
changes, cpe_screensaver
, and cpe_profiles
is the core concept behind our API model.
To understand how we got to the point of a profile being installed, let’s go through the route that the Chef took:
Compile Time
- Assemble recipes –
cpe_init
was called (thanks to thequickstart.json
), which gave Chef a list of recipes to run. Among these recipes,company_init
is going to be run first (as it is first it the runlist).cpe_screensaver
is added to the list, and finallycpe_profiles
comes last. (This order is very important). - Attributes – since Chef has a list of recipes it wants to run, it now goes through all the attributes files and creates the namespaces for each of the attributes. This is where
cpe_screensaver
‘s attributes are created and set to default values (which are specified in thecpe_screensaver::attributes
file). At the same time,cpe_profiles
also creates its namespace and attribute fornode['cpe_profiles']
. - Assemble resources – now that all the attributes have been created with their default values, Chef identifies all the resources that are going to be run. This is also where all non-resource code gets processed, including attribute overrides (anything with
node.default
for example). This is the point where the node attributes forcpe_screensaver
are changed bycpe_init::company_init
.
The first resource (relevant to our example) that is going to be run is that ofcpe_screensaver
, whosedefault
recipe contains aruby_block
on line 16.
cpe_profiles
is last in the runlist, but it contains two resources that are going to be executed: thecpe_profiles:run
default action and thecpe_profiles:clean_up
action. (These are custom resources with custom actions, defined in the “cpe_profiles/resources” folder).
At the end of compile time, the resource run list will look like this:
cpe_screensaver::ruby_block
cpe_profiles::run
cpe_profiles::clean_up
Run Time
- Run the
cpe_screensaver
ruby_block – the resource run list is executed in order, and first in the list is this block.
This ruby_block essentially does one thing – it creates a Ruby hash that will be used to create a mobileconfig plist file, and then assigns this mobileconfig plist to thecpe_profiles
node attribute. In the profile payload, it sets the preference keys for the screensaver to the value of whatever is currently in the equivalent node attributes. Since those were just assigned in thecompany_init
recipe, this profile will be created with the values we want. - Run the
cpe_profiles::run
action – this action iterates through each object (mobileconfig plist) in thecpe_profiles
node attribute(node['cpe_profiles']['com.pretendco.screensaver']
), and then writes that plist to disk as a .mobileconfig file, and then installs that profile (using/usr/bin/profiles
). This part of the run is where the profile is actually installed. - Run the
cpe_profiles::cleanup
action – in this example, it won’t do anything, but this will remove any profiles matching the prefix that are currently installed but not listed in the node attribute.
This is what makes the API model powerful – the interaction of multiple cookbooks together creates the desired state on the machine. By itself, cpe_profiles
doesn’t do anything to the node. By itself, cpe_screensaver
doesn’t do anything to the node. Similarly, by itself, cpe_init::company_init
doesn’t do anything either.
Yet, similar in concept to a “model-view-controller” design model (used throughout Apple development), it’s a chain reaction of inputs and outputs. The model is set up by the attributes of all the cookbooks, whose data is then filled in by the company_init
recipe. The cpe_screensaver
takes on the role of a controller in this analogy, in that it takes data from the company_init
and makes useful data that it feeds to cpe_profiles
. Then, the cpe_profiles
recipe actually interacts with the node and installs the profiles (which would be similar to the “view”, which is where the user sees interaction happen).
Awesome! Where do we go from here?
Hopefully this covered the basic underlying concept behind the API model used by CPE Chef. What we did here is dynamically generate a ScreenSaver profile simply by overriding three attribute variables. With this kind of framework in place, we can do a lot of really cool things.
Part two coming soon!