Managing the network with Cfengine

Big Engine

© Kit-Wai-Chan, Fotolia

© Kit-Wai-Chan, Fotolia

Article from Issue 101/2009
Author(s):

Automate admin tasks with the powerful Cfengine framework.

Cfengine [1] is a flexible framework for automating system administration tasks. With Cfengine, you can manage one machine or a heterogeneous network. The first version of Cfengine was released more than 15 years ago by Mark Burgess, a professor at Oslo University. According to usage estimates, Cfengine has managed more than 1 million computers over the years. Version 3 of the Cfengine framework rolls out some new capabilities and does away with all the old historical layers. The developers have even retooled the language so that all elements are handled in a uniform way.

To show what is possible with Cfengine 3, I introduce various Cfengine components in a running example. To follow along, you need two networked Linux machines that I call PolicyServer and Client. The end goal is to have the client machine running a fully configured and managed Apache web server, with no manual configuration required, other than installing Cfengine.

The basic model I use will store and distribute all of the policy code centrally from a single server. Cfengine can be used many ways because it is very flexible, but this is a common design, and it serves many sys admins well. PolicyServer will hold and make available the central repository of Cfengine code, and the Client machine will receive the Apache configuration.

Design Principles

Cfengine is built around a number of design principles. In general, the language is descriptive rather than iterative: As much as is possible, you are attempting to describe the "what" of the system rather than the "how." In practice, this approach usually means that Cfengine actions are idempotent; that is, applying the same function twice will result in the same result. This characteristic is important because Cfengine continually monitors the state of your nodes and, depending on how you write your policy, corrects any divergences.

Another principle that Cfengine adheres to is the "pull" architecture, which means that clients request new policy code from a server. This behavior is in contrast to the "push" system, which requires a central node to connect periodically to all clients to configure them. The use of a pull architecture allows you to configure a machine that is down or not yet built because any changes will be picked up automatically when the machine comes onto the network. Cfengine has the facilities to do a push if you really need it, but even these features are built around an underlying pull mechanism. The pull principle also has important implications for the autonomy of the configured node: If the Cfengine server crashes, or if it is unavailable to the client for some reason, the client can continue to use its cached policy until the next time it can connect successfully.

Downloading and Building

First, install Cfengine on both the client and server. Packages are available for many popular Linux distributions, or you can build the tool from source code.

CFengine 3 has very few build-time dependencies and even fewer run-time dependencies (only OpenSSL libcrypto and Berkeley DB libdb). Although not strictly necessary, the Perl-compatible regular expression library (libpcre) also contributes significantly to Cfengine.

If you are building Cfengine from source, first obtain the latest Cfengine 2 and 3 tarballs from the project website [2]. Also, you need Flex, Bison, and Make to compile Cfengine, as well as the static library libcfengine from Cfengine 2. Once you have the dependencies in place, first build Cfengine 2, then Cfengine 3 (you can use the same procedure for both):

./configure
make
sudo make install # Or use su

By default, files are installed to /usr/local, but you can change this by adding the --prefix=/some/other/path argument to configure, although you need to make sure that the Cfengine build process can find libcfengine. All the binaries are installed in sbin under the relevant prefix (by default, /usr/local/sbin). The next version of Cfengine, 3.0.1, due for release later this year, will not require libcfengine, so this is just an intermediate measure.

Hello, World

With the software safely installed on the server and client, you are ready for a first look at Cfengine in action. A simple "Hello, World" example will demonstrate a working Cfengine 3 program First, run cf-key with no arguments. This command creates some dot files in your home directory and generates a keypair (which you won't need right away, but it is necessary for remote copies).

This "Hello, World" program will be written for cf-agent, which is the program that does the bulk of the configuration work in Cfengine. cf-agent monitors system state and applies corrective action when necessary. Much as perl and sh binaries are interpreters for Perl and the Bourne shell, respectively, you can think of cf-agent as a command interpreter for the Cfengine language. By default, as an unprivileged user, cf-agent reads and executes code in ~/.cfagent/input/promises.cf. So, with your favorite editor, create that file and enter:

body common control {
 bundlesequence => { "hello" };
}
bundle agent hello {
reports:
   linux::
      # This is a comment
      "Hello, world.";
}

White space does not matter in the Cfengine language, so you can indent this code as you see fit. With no arguments at the shell, go ahead and run it with cf-agent. It doesn't matter which of the two machines you run this on because you have installed the software on both.

As you can see, the two main entities present in the code are a body named control and a bundle named hello. Bundles are the primary statement aggregation construct in Cfengine (in the same way that a function is the primary construct of C, although bundles are not functions in a mathematical sense). Bodies are groupings of parameters. Both the body and the bundle specify which component of Cfengine they are to be consumed by; in the case of the control body, the consumer is common, a special keyword meaning the Cfengine suite as a whole, and in the case, of the hello bundle, the consumer is agent, which refers to the Cfengine binary cf-agent.

The name of the bundle, hello, is referenced in bundlesequence, which is a special directive that tells cf-agent what code to execute, and in what order. The special token reports is a promise type – one of many kinds of statements that you can make about how you want your system to function.

Bundles are made up of promises. In this case, as you can probably guess, reports is a way to generate output. The next token, linux, followed by a double colon, is a class. Later, I will explain classes in more detail, but for now, just know that code following this class will only execute on a Linux node.

Config Files

Now that cf-agent is up and running, the next step is to configure the cf-serverd daemon on PolicyServer so that the client can download an updated policy. cf-serverd functions as a secure file server that provides external access to the cf-agent running on a specific node.

On PolicyServer, designate a directory as the canonical policy repository. Here, I use /srv/cf-serverd, but you can select whatever location fits best in your environment. (This should not be /var/cfengine. PolicyServer will probably also be a client; that is, the server will update its policy and evaluate it with cf-agent.)

Within your central repository, create an inputs directory (I am mirroring the contents of the working directory /var/cfengine, but this is the only subdirectory that I care about for now). In /srv/cf-serverd, you need to create four files. The first step is to create cf-serverd.cf (Listing 1). This file will control which machines can connect to the server and which files they will have access to, and it also will have some cf-serverd--specific configuration variables.

Listing 1

cf-serverd.cf

01     body server control {
02         trustkeysfrom => { "192.168.1.62", "192.168.1.61" };
03         allowconnects => { "192.168.1.62", "192.168.1.61" };
04         maxconnections => "10";
05         logallconnections => "true";
06     }
07     bundle server access_rules {
08     access:
09         "/srv/cf-serverd"
10             admit => { "192\.168\..*" };
11     }

Now, create update.cf, which will contain code that the client runs to synchronize its local policy to the central policy in the repository (Listing 2).

Listing 2

update.cf

01     body copy_from       remote(server, path) {
02         servers => {           "${server}" };
03         encrypt => "true";
04         trustkey => "true";
05         source => "${path}";
06         compare => "digest";
07         preserve => "true";          # Preserve permissions
08         verify => "true";
09         purge => "true";
10     }
11     body depth_search recurse     {
12       depth => "inf";
13     }
14     bundle agent update {
15     vars:
16       any::
17       "cfserverd" string =>          "192.168.1.61";
18       "policyfiles" string =>          "/srv/cf-serverd";
19      "server_inputs" string =>        "${policyfiles}/inputs";
20      "client_inputs" string =>        "${sys.workdir}/inputs";
21      files:
22        any::
23        "${client_inputs}"
24        copy_from =>          remote("${cfserverd}",          "${server_inputs}"),
25        depth_search =>          recurse;
26     }

Listing 2 introduces some Cfengine variables. Unlike past versions, variables are now all dynamic types (before, they were all just strings). Other variable types include slist (a list of strings), real (a number with decimal precision), and int (an integer). Variables are substituted (as in the shell) with ${variablename}.

Listing 3 shows the file promises.cf. The final configuration file is failsafe.cf, which simply contains the following:

body common control {
   bundlesequence => { "update" };
   inputs => { "update.cf" };
}

Listing 3

promises.cf

01     body common control {
02       bundlesequence => {        "update" };
03       inputs => { "update.cf",        "cf-serverd.cf" };
04     }
05     # Some arbitrary harmless actions that will generate some output
06     bundle agent hello {
07     commands:
08       any::
09        "/bin/date";
10        reports:
11         linux::
12          "Hello, world.";
13     }

The special promises.cf and failsafe.cf files are basically just dispatches specifying what other code cf-agent should execute. The names for cf-serverd.cf and update.cf I made up myself (you can call them whatever you want, but I suggest names that are similarly suggestive). The hard-coded entry point for cf-agent is promises.cf. All of the code you want to run needs to be either in this file or referenced by this file. Strictly speaking, failsafe.cf is not required, but if promises.cf does not parse, cf-agent will fall back to failsafe.cf, so it is a good idea to make sure that a very simple, known, good failsafe.cf is available.

failsafe.cf merely attempts to update the policy files from the server. Because I designed failsafe.cf to get only the most recent policy, it also functions as a bootstrap procedure for the Cfengine client. Thus, to configure the client initially, you only need to copy failsafe.cf (and any files it references) onto the client.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus

Direct Download

Read full article as PDF:

Cfengine.pdf (463.98 kB)

News