Structuring cfengine programs

Having learnt what cfengine is and how you can use it, we start to develop a cfengine program and learn a few things about how it works.

Starting to write the ''cfengine'' program.

First of all, some terminology: The cfengine documentation refers to the input that describes the goal state as a \"program\", which means when we talk about the cfengine program we don't mean the program called cfengine which reads your description but instead the description of the goal state that you write for cfengine...

Anyway, there are a few quirks about the cfengine description language that you should be aware of. The first thing to note is that cfengine separates each of the types of actions it can perform into sections: e.g. copying files, creating directories, setting permissions, editing files, restarting processes, and running commands. There is also a section named control in which you can define variables and describe to cfengine how it is to behave, and a section named classes in which you can group systems together.

The groups section is a very powerful part of the cfengine program. Obviously your servers do different things, so there is a limit to the amount of convergence you want to perform on your network. You don't need every machine to be a mail server, DNS server, web server, and all running your company's credit card payment gateway system at the same time, so you want to classify the machines by the roles they play. Creating groups of machines for each role simplifies the actions later on, instead of having to replicate long lists of machines for individual actions. And the whole point of the exercise is to offload your work onto the computer, right?

So, here's a very simple cfengine program:

control:
    Warnings = ( on )
    Inform = ( on )

    access = ( root )
    domain = ( anchor.net.au )
    timezone = ( EST EDT )
    smtpserver = ( localhost )
    sysadm = ( root@anchor.net.au )

    actionsequence = (
    )

This program sets the internal cfengine variables to turn on warnings and informative messages by default, sets the access user to root, the domain for the machine to anchor.net.au, our timezone, the server which will accept mail from cfengine, and who to mail any output to. Finally there's a list of actions, the actionsequence, which is currently empty. So this is just a suboptimal null program. Another thing to realise is that cfengine loads and parses any imported files after it's finished reading the current file. This seems like odd behaviour to people used to the C preprocessor where you can include a file and immediately rely on that file's contents. Instead, most of the cfengine community use the following idiom for the main program file:

import:
    main.cf
    ssh.cf
    ftp.cf
    http.cf

The file contains nothing but import statements. At the end of the file, each of the imported files are read, starting with main.cf, which contains the first control block (as above) and tells cfengine how to act.

Why do that?

Well, why would you want to use an import at all? It would definitely be possible to put the entire configuration into one file, and then you only need to worry about reading it. Splitting the program up into separate files lets you encapsulate each service or task into its own piece.

But what about some common definitions? System wide classes that you'd like to reference in each of the files. For example, if you had a group of machines who were subscribed to a support pack that meant you did service monitoring and you wanted cfengine to make sure the service was configured to run at boot, and restart it if necessary.

In that case you'd want to include the common definitons file in all the services files... but you can't because the definitions won't get imported until after the services file is done. Instead, you put your common file at the start of the import list in the master file, so these definitions are available to the rest of the program. (There's also the question of redefinition of classes and variables, not something that cfengine is very tolerant of.) Thus you'd have your master file cfagent.conf:

import:
   main.cf
   common.cf
   ssh.cf

and your common.cf might look like this:

classes:

   anchor_secure = ( bill ted )

I've now introduced the grouping syntax in cfengine. We've specified the classes section, and defined a class named anchor_secure which contains the machines named bill and ted. (There's a bit more to it than that, but for the time being that explanation will suffice). Later on, in ssh.cf, I may want to apply some configuration to the machines in the anchor_secure group, like allowing root logins from anywhere with an empty password. Rather than type in all the names, I can just use anchor_secure and see the change happen on the correct set of machines.

Now we need to flesh out the ssh.cf file. Ideally we'll check that sshd is running, and that the configuration is locked down to a point that we're happy with the security of the service. This is a key point about automated configuration: we can ensure that a service is configured correctly on all our machines, so there is little risk of a misconfiguration on a forgotten machine becoming exploitable.

Editing the configuration file, however, is an article by itself, so for now we'll just make sure that the service is running and some key files exist with the correct permissions.

classes:

    sshd_server = ( any )

files:

    sshd_server::

        /root/.ssh/authorized_keys2
            mode=0600
            owner=root
            group=root
            action=touch

processes:

    sshd_server::

        "sshd"
            restart "/etc/init.d/sshd restart"

We first define a class sshd_server, which is true if the class any is defined. The class any is always true, as it matches any machine. The notation here is a convention I use to keep the rest of the file clear; we could use any where sshd_server appears but the sshd_server name carries more meaning to the reader. We're also going to check that the file /root/.ssh/authorized_keys2 is owned by root:root, and is only read/writeable by root. The action=touch parameter instructs cfengine to alter the permissions and ownership if the file doesn't match the specification. Finally we ask that cfengine check for a process called sshd on the sshd_servers, and if it's not running restart it with the command /etc/init.d/sshd restart.

We're using the files and processes sections in this file, so we should go back to main.cf and add them to the actionsequence:

    actionsequence = (
        files
        processes
    )

So, the overall structure of the cfengine program is each service in its own file, for eay maintenance. Network-wide groups are stored in a file that is processed early, and the whole lot is tied together using the file that cfengine looks for: cfagent.conf.

Keywords: cfengine examples structuring cfengine programs Author : Jamie Wilkinson

Related links