Managing the network with Cfengine

Maintaining a Service

Suppose I want to use Cfengine to install and configure Apache httpd. In fact, I will even build httpd from source so the solution will be portable across many distributions and platforms. In a production environment, I would hesitate to have servers compile their own software. If I truly needed to build from source, I would most likely build a custom package and then distribute that. However, the use of cf-agent to build from source directly offers a nice (cross-platform) way to display some of the available features.

First, download the Apache source into the Cfengine repository [3].

Rather than configuring the client to download the source from the Internet, it is better to cache the source code locally, so you are not dependent on external resources. Just put the tarball in /srv/cf-serverd/inputs on PolicyServer (in a subdirectory for good organization), then let the update bundle take care of distributing it.

Create a new file to store all the httpd-related code – say, web_server.cf. This file needs to be added to inputs in promises.cf, and any bundles contained within it to bundlesequence. The first step is to create a bundle with some variables that can be re-used by other bundles. A bundle of type common can be consumed by any Cfengine component and need not be listed in bundlesequence. Each bundle has its own scope, and variables from a foreign bundle can be accessed with the interpolation form ${bundlename.variable}. So, the code in Listing 5 allows other bundles to make use of, for example, ${httpd.conf}, which will evaluate to the full path.

Listing 5

The common Bundle

01     bundle common httpd {
02     vars:
03         any::
04             "version" string => "httpd-2.2.10";
05             "prefix" string => "/opt/httpd/${version}";
06             "server" string => "${prefix}/bin/httpd";
07             "apachectl" string => "${prefix}/bin/apachectl";
08             "conf" string => "${prefix}/conf/httpd.conf";
09     }

Particular promises, such as commands, files, or reports, often have parameters that determine the nature of the promise. The appropriate key/value pairs follow the promise. For example, consider the following promise:

processes:
   any::
      "cf-execd"
         restart_class => "start_cfexecd";

This promise has a parameter called restart_class that takes a string value for its right-hand side. (In this case, that string will become a defined class if no cf-execd processes are running.) Some parameters take external bodies for their right-hand side. The use of an external body allows multiple key/value pairs and further parameterization, which allows reuse. To make the concept concrete, consider the example that I will soon use to compile Apache. The following body, which takes one argument, allows me to run commands in a particular directory and without a shell:

body contain cd(dir) {
   useshell => "false";
   chdir => "${dir}";
 }

Such bodies can be stored in any Cfengine input file, but because they are often general and can be reused by many promises, it makes sense to keep them in their own file, which I will call library.cf. If you have not already done so, put this cd body in library.cf and add it to the bundlesequence in promises.cf. Remember, when changing such an external body later, you might be affecting numerous active promises, so it makes sense to treat them with the care afforded to any shared resource.

In Cfengine, a class is a boolean condition meant to represent some aspect of the system state, be that state an operating system or the time of day. Many classes are defined automatically by cf-agent, and you can define others from the return values of programs and by other means. Any promises following a class expression (strings ending with ::) are only enforced when the class is true. For example, read

bundle agent a { reports: linux:: "asdf"; }

as "print asdf if the class linux is defined." As it happens, cf-agent automatically defines the class linux on Linux nodes. The special class any has been used several times already; this class is always true. It is often used, even when not strictly necessary, to maintain correct indentation. By running cf-agent -pv (this will not execute policy code, so it is always safe), you can see all the automatically defined classes. On one of my test nodes, some of the automatically defined classes are: 64_bit, Friday, debian_4, and xen.

Apache Example

Listing 6 shows the bundle that will unpack, compile, and install Apache. On most systems, the special predefined variable sys.workdir will resolve to /var/cfengine, which essentially says: Test to see whether the software is installed by checking for a particular file (more precise heuristics could be devised); if not, build the program with the standard untar, configure, make, and make install procedure as usual.

Listing 6

Setting Up Apache

01     bundle agent install_web_server {
02     vars:
03       any::
04         "source" string =>
05           "${sys.workdir}/inputs/support_files/             ${httpd.version}.tar.gz";
06
07         # Will get automatically cleaned up by the            update purge
08         "compiledir" string => "${sys.workdir}/                         inputs/${httpd.version}";
09     classes:
10       "web_server_installed" expression =>       fileexists("${httpd.server}");
11       commands:
12        !web_server_installed::
13         "/bin/tar xzf ${source} -C            ${sys.workdir}/inputs";
14         "/bin/sh configure --prefix=            ${httpd.prefix} --enable-modules=all"
15         contain => cd("${compiledir}");
16         "/usr/bin/make"
17         contain => cd("${compiledir}");
18         "/usr/bin/make install"
19         contain => cd("${compiledir}");
20     }

Many server applications come with configuration files that must be in place before a complete service is deployed. In this case, I will configure Apache to allow server-info and server-status requests. This requires editing two different configuration files. Cfengine 3 includes four types of promises that reside in special external edit_lines bundles – delete_lines, replace_patterns, field_edits, and insert_lines – and support additional parameters.

With these promises, you can set configuration variables, comment out key lines, and maintain configuration files. Before you can use edit_lines in a "files" promise, you need to create some edit_lines bundles. Think of these edit_lines bundles as custom-made editfiles functions; they are usually general enough to re-use over many components. Two that I will make use of are DeleteLinesContaining and ReplaceAll. If you are following the file organization I have been using so far, it makes sense to put these in the library.cf file with other shared bodies (Listing 7). As you can see, they have pretty much the same structure as other bundles, and they can be parameterized as well.

Listing 7

library.cf

01     bundle edit_line DeleteLinesContaining(pattern) {
02     delete_lines:
03         ".*${pattern}.*";
04     }
05     body replace_with ReplaceValue(value) {
06         replace_value => "${value}";
07         occurrences => "all";
08     }
09     bundle edit_line ReplaceAll(from,to) {
10     replace_patterns:
11         "${from}"
12             replace_with => ReplaceValue(${to});
13     }

In addition, I need a way to define a class that, if I edit any files, lets me trigger a service restart later:

body classes DefineIfChanged(class) {
       promise_repaired => { "${class}" };
   }

Once I have all these components in place, I can tell cf-agent to use them to edit the config files; in this case, I need to uncomment the httpd-info line in httpd.conf and remove the access control from httpd-info.conf (Listing 8).

Listing 8

httpd.conf

01     bundle agent configure_web_server {
02     classes:
03         "web_server_installed" expression =>              fileexists("${httpd.server}");
04     vars:
05         "info" string => "Include conf/extra/httpd-info.conf";
06     files:
07         web_server_installed::
08            # Uncomment httpd-info.conf line
09            "/tmp/httpd.conf"
10               edit_line => ReplaceAll("^#${info}.*", "${info}"),
11               classes => DefineIfChanged("restart_httpd");
12            # Remove access control from httpd-info.conf
13            "/tmp/httpd-info.conf"
14               edit_line => DeleteLinesContaining("(Allow|Order|Deny)"),
15               classes => DefineIfChanged("restart_httpd");
16     commands:
17         restart_httpd::
18            "${httpd.apachectl} graceful";
19     }

Watchdog

If you need a way to keep an eye out for the web server process – to restart it if it is not running – create another bundle, or simply add the promises in Listing 9.

Listing 9

Watchdog

01   bundle agent monitor_web_server {
02   classes:
03       "web_server_installed" expression =>            fileexists("${httpd.server}");
04   processes:
05       web_server_installed::
06          # Define a class if httpd is not running so that we can start it
07          "httpd"
08               restart_class => "start_httpd";
09   commands:
10       start_httpd::
11          "${httpd.apachectl} start";
12   }

The code in Listing 9 wraps the process detection with a class so I am sure the web server is running on nodes that have a web server installed.

For even greater reliability, you might want to create a functional test – that is, a test that queries the service. In this case, you need to fetch some data from port 80 and make sure it is the data you expect.

Read full article as PDF:

Cfengine.pdf (463.98 kB)

Related content

Comments

  • reply to pghpete

    There is no package named flex-devel in RHEL/CentOS 5.4, although there should be. Instead, libfl.a is part of the flex package, so you cannot crosscompile. I found this attempting to compile the latest setkey (ipsec-tools) for i386 on an x86_64 host.
  • Can't believe the trouble...

    I can't believe the trouble I was having getting ver 3.0.3 of cfengine installed on either RHEL 5.4 or CentOS 5.4... as it turns out, it's still a personal problem. Argh. What an inept bum I am today... forgot cardinal rule number 1, read the INSTALL file and install all dependencies it asks for. Which, were surprisingly extensive...

    'yum install openssl openssl-devel db4 db4-deve flex flex-devel bison bison-devel pcre pcre-devel'

    Then your './configure && make && make install' should run without issues on either distro.
  • Many issues while trying to follow your article

    I read your article and enjoyed it. Thank you. I ran into a few problems so I figured I would comment for the benefit of others who may encounter the same issues. ** Long story short: compile and install from source if you want to follow this articles instructions... for details keep reading **

    I decided to use a package utility instead of compiling the source.

    RHEL 5.4:

    'yum install cfengine' worked without incident

    CentOS 5.4

    'yum install cfengine' reports package not found, nothing to do.

    I thought this was quite strange since CentOS, from my knowledge, is near identical to RHEL 5.4 ( including their repository content)

    Apparently, you have to install rpmforge just to get the package for CentOS 5.4. Here is what I did to accomplish that...

    'wget http://packages.sw.be/rpmfo...elease-0.5.1-1.e15.rf.i386.rpm'
    'rpm -Uvh rpmforge-release-0.5.1-1.e15.rf.i386.rpm'
    (as rpmrepo.net/RPMforge instructs)

    After that a 'yum install cfengine' worked without incident. At this point I figured my troubles where over,... nope!

    While trying to follow your "Hello, World" instructions, I couldn't figure out why there was no command cf-key, or cf-agent on my systems... a quick 'man cfengine' showed me why... ah... it's cfkey and cfagent. I figured it was just the authors typo(s). Then, the files and directories that I was directed to create/alter were not on my systems either. Hum... strange. I was about to give up but then I ran 'rpm -q cfengine' on both systems and had my "Ah ha" moment... both of my test distros are Enterprise OS systems and therefore, their package versions are way behind the most recent versions of anything. I totally missed the first sentence of paragraph two in which Mr. Strejcek states clearly, "To show what is possible with Cfengine 3,..."

    I can't believe I missed that! I had ton of problems, but they were all self-inflicted wounds. Had I just caught that line... aw well.
comments powered by Disqus

Direct Download

Read full article as PDF:

Cfengine.pdf (463.98 kB)

News