Friday, December 23, 2005

cfengine Best Practices

I believe that a cfengine site configuration is easiest to maintain if you only use a subset of the available functionality. Over the course of its ten year history, cfengine has grown organically. Many special cases have been introduced that complicate the syntax. Because of this, learning the tool can be daunting for a newcomer.

The cfengine language is composed of sections. It is useful to categorize the sections into two types, implicit and explicit (these terms are mine). Implicit sections are evaluated no matter what and tend to either probe the state of the system and define classes (groups, strategies) or affect the way that cfengine functions (control, filters, ignore). Explicit sections must be included in the actionsequence to be evaluated and are mostly used to change the state of the system and bring it into line with site policy (editfiles, shellcommands, etc).

You should limit yourself to these implicit sections:
alerts
control
filters
groups
import
strategies
And these explicit sections:
copy
disable
disks
editfiles
files
links
packages
processes
shellcommands
tidy
You should avoid implicit sections like ignore, because they can interact with your policy in hard to predict ways, particularly if you have multiple administrators all working on the same site and modifying cfengine code. You should not expect all of them to understand the entire cfengine setup. In the case of ignore, individual sections often have an ignore= parameter which has a similar function (but with locality).

For the sections with more than one name (classes and groups, files and directories, etc) pick one and never use the other. This will make it easier for people who are less familiar with cfengine to follow your code. It's probably best to use the older notation in all cases, since you get the benefit of backwards compatibility.

Avoid all the sections that have to do with NFS (binservers, homeservers, mailserver, miscmounts, mountables, unmount). These services can be configured in the same way as other services, by editing configuration files and restarting daemons. I think that these sections predate the existence of automounters. If you must use network filesystems, automounters are the way to go, though avoiding network filesystems in general (and NFS in particular) will make your systems more robust (but that is another topic).

Try to avoid solving more than one problem per input file. Instead, break your policy files into one file per task, and make heavy use of import. Try to keep your input files short. Declarative code (the cfengine language is mostly declarative) is not dependent on order, which can make for some very nonlinear narratives. It is hard to follow nonlinear narratives, especially if you are not the author.

When using import, many people end up being confused about the ordering of their code. This is because cfagent first reads an entire file and then performs any necessary imports. In other words, imports are not processed when they are reached. This means that any variables that are defined in an imported file are not available further on in the importing file. This can be quite confusing to people who are used to other programming languages. Therefore, I suggest bypassing the issue entirely: a cfengine input file should either be entirely imports or contain no imports at all. This gets rid of any ambiguity and promotes code modularity.

Ordering is tough in cfengine. If you are doing something that requires complicated ordering you are probably doing something wrong. This is annoying, but still true. cfagent uses a two-pass strategy to try to resolve class dependencies, which is not good enough to guarantee order for arbitrary dependency relationships. If you want to understand the details, I explained them on the help-cfengine list a while back. In any case, it's probably not worth thinking about that. To avoid the problem completely, stick to one-level dependencies. (An example of a one-level dependency is something like "if file A is edited, restart daemon B".) And if you want to be even more transparent, stick to your global actionsequence as much as possible.

Speaking of which, the first file you import should be your global actionsequence. Mine looks like this:
control:
actionsequence = (
disks
disable
packages
copy
files
editfiles
links
tidy
shellcommands
processes
)
This example is somewhat arbitrary, but it is pretty good. I often need to restart services using shellcommands after a file is updated with copy or editfiles, so clearly shellcommands should go near the end. Another reason that the above example is not completely arbitrary is that the packages section is often used like an implicit section. That is, it is used to gather info about the state of the system and define classes. Though it functions in this way, it still needs to be included in the actionsequence. And if you are using a new enough version of cfengine packages can cause changes to the state of the system (with the action=install parameter). Because packages is often used to define classes, it should go near the beginning of the actionsequence. A global actionsequence will not be good enough for all of the tasks you might want cfengine to accomplish, but it should be good enough for the common cases, and it's always best to make the common cases easy.

If you must have more complicated ordering, you can use actionsequence classes. By example, if you have a simple global actionsequence of:
actionsequence = ( copy shellcommands )
and later import a file that specifies:
actionsequence = ( copy.asdf )
this is what happens: first all copy actions are executed, then all shellcommands actions are executed. Then the asdf class is defined and all of the copy actions are executed again (locks will keep anything that was recently executed from being executed again). Generalizing from this example, you can construct arbitrary ordering schemes, though I admit to not trying anything too bogus. Avoid this technique if possible because it is too confusing. If you must use it, try to confine it to individual files that are included with import.

editfiles is wonderful, but requires self control to use effectively. Several other people have already discussed this. Here are some constructs that I use often and consider safe:
EmptyEntireFilePlease
# Other editfiles directives follow to build up a file

# Set a variable, being unconcerned about order
DeleteLinesStarting "somevar"
AppendIfNoSuchLine "somevar=somevalue"
If I ever find myself using directives like IncrementPointer, I know the code will be fragile. In other words, try to stick to the declarative editfiles directives. Also, end all editfiles stanzas with CatchAbort. cfagent aggregates all edits of the same file into a single action. Thus, if an early edit of a file aborts after a failed LocateLineMatching, future edits of the file during that same run will not happen without a CatchAbort at the bottom of that first stanza.

In the planning document for cfengine 3, there are also some very good suggestions about what editfiles directives to use and what directives to avoid.

One should avoid complicated quotation. The cfengine parser does not behave as you might expect it to, and is under continuous revision. This does not cause problems for simple code, but if you are embedding complicated shell commands in your cfengine code with ReturnsZero or ExecResult, expect problems. As recently as 2.1.17, the handling of function arguments has been changed. It is probably safer to rely on modules to set classes and define variables, since the language rules for common interpreters are less likely to change (and if they do, at least you don't have to worry about it when upgrading cfengine). It may seem more complicated to break your logic into two files, but that is the price you pay for component isolation. Also, it is hard to read embedded code with lots of escaping, so in the long run your code will probably be more maintainable.

You should never use the string /var/cfengine in your code directly. Not all cfengine package maintainers conform to the /var/cfengine convention (Debian, for example, uses /var/lib/cfengine2). Instead, define a variable (I use ${workdir}). Remember to add this variable to AllowRedefinitionOf, since multiple values contingent upon different classes is the whole point. And make sure that the file that defines this variable is imported early, so all your policy code can make use of it.

There is also a Style Guide that contains lots of good points regarding readable cfengine code.

I don't claim that this is an exhaustive list of cfengine best practices, or even that it is appropriate for all sites, but I think it might help some people avoid headaches later on.