Configuration Management on FreeBSD with CFEngine - Part IV

If you've followed the handbook by this step, that means that now you have a basic CFEngine 3 policy server and clients already installed and configured.

But let's do something useful with CFEngine. In this and the next chapters you will find various CFEngine example configurations, which purpose is only to show you how and what you can do with CFEngine.

The presented here examples do basic things, but after reading the next chapters you will get the idea, of how you can extend them to more complex and useful ones.

Domain configuration with CFEngine

In this example we will see how we can use CFEngine for configuring various settings of our domains.

In the example shown here, we will add one additional CFEngine configuration file, which will take care of configuring various settings for our domains.

For the purposes of the example we will have a fictional network, that is spreaded accross multiple sites, each site having it's own domain.

First we start by updating our main configuration file - promises.cf.

#####################################################
#                                                   #
# promises.cf - Main CFEngine configuration file    #
#                                                   #
#####################################################

body common control {

    any::

        bundlesequence => { @(g.bundlesequence) };

    site1::

        domain => "site1.example.org";

    site2::

        domain => "site2.example.org";

    any:: 

        inputs => {
            "update.cf",
            "library.cf",
            "classes.cf",
            "cf-execd.cf",
            "cf-serverd.cf",
            "cf-report.cf",
            "domain.cf",
            "cleanup.cf"
        };

    output_prefix => "cf3>";
}

# global vars
bundle common g {

vars:

    "workdir"           string => "/var/cfengine";
    "masterfiles"       string => "$(workdir)/masterfiles";
    "inputfiles"        string => "$(workdir)/inputs";
    "policyhost"        string => "cfengine.example.org";
    "bundlesequence"    slist  => { "update", "executor", "server", "domain", "cleanup" };
}

body runagent control {
    hosts => { "127.0.0.1", "10.1.16.0/20" };
}

In promises.cf we have added a few things.

First we have defined the domain names of the two fictional sites we have - site1 and site2.

site1 and site2 here are just user defined classes, which we will define in our classes.cf file.

We have also added one new configuration file - domain.cf

And finally we've updated the global bundlesequence, so that now it contains one new bundle - domain.

Now let's define the site classes. The site classes will contain all the systems from that site.

For example if we have a system called www.site1.example.org, that means that the site1 class will be defined for that system.

Classes are just a context in CFEngine, remember?

Now let's update our classes.cf to include our sites.

#################################################
#                                               #
# classes.cf - CFEngine user-defined classes    #
#                                               #
#################################################

bundle common myclasses {

vars:

    "sysctl_jailed" string => execresult("/sbin/sysctl -n security.jail.jailed", "noshell");

classes:

    "freebsd_jail" expression => strcmp("$(sysctl_jailed)", "1");
    "freebsd_host" expression => strcmp("$(sysctl_jailed)", "0");

    "site1" or => {
        classify("www.site1.example.org"),
        classify("www-proxy.site1.example.org"),
        classify("svn.site1.example.org"),
        classify("pkgmaster.site1.example.org"),
        classify("mysql.site1.example.org"),
        classify("monitor.site1.example.org"),
        classify("jail-test.site1.example.org")
    };

    "site2" or => {
        classify("webserver.site2.example.org"),
        classify("mail.site2.example.org"),
        classify("squid.site2.example.org")
    };

    "policy_servers" or => {
        classify("$(g.policyhost)")
    };
}

In classes.cf we now have two new classes defined, each containing the systems of the two fictional sites we have.

Now it is time to actually do something with CFEngine and that will be done by the bundle domain, which we define in the domain.cf configuration file.

#############################################
#                                           #
# domain.cf - Domain specific configuration #
#                                           #
#############################################

bundle agent domain { 

files:

    "/etc/rc.conf"

        comment     => "Copy rc.conf file for $(sys.fqhost)",
        create      => "true",
        perms       => mog("0644", "root", "wheel"),
        copy_from   => remote_copy("$(g.masterfiles)/rc.conf/rc.conf-$(sys.fqhost)", "$(g.policyhost)");

    "/etc/resolv.conf"

        comment     => "Copy resolv.conf file for domain $(sys.domain)",
        create      => "true",
        perms       => mog("0644", "root", "wheel"),
        copy_from   => remote_copy("$(g.masterfiles)/domain/resolv.conf-$(sys.domain)", "$(g.policyhost)");

    freebsd_host::

        "/etc/ethers"

            comment     => "Copy ethernet address database for domain $(sys.domain)",
            create      => "true",
            perms       => mog("0644", "root", "wheel"),
            copy_from   => remote_copy("$(g.masterfiles)/domain/ethers-$(sys.domain)", "$(g.policyhost)");
}

In the domain bundle we have defined the following:

First we deploy /etc/rc.conf configuration files for the systems under CFEngine control.

This promise simply copies the /etc/rc.conf file on the systems, which are under CFEngine control.

An /etc/rc.conf on the CFEngine policy server is in the form of rc.conf-<hostname>.

The $(sys.fqhost) CFEngine variable contains the fully-qualified domain name of the system that cf-agent(8) runs on.

Then we deploy the /etc/resolv.conf file on the systems in the domain.

This promise copies the /etc/resolv.conf file on the systems under CFEngine control for their domain. The $(sys.domain) CFEngine variable contains the domain name of the system.

After that we deploy /etc/ethers file, which contains hostname / MAC addresses of the systems in the domain.

If you often need to wake a system over the lan like I do, keeping a record of the systems hostname and MAC address in that file allows you to use wake(8) only by specifying the system hostname.

Finally we have also defined one new promise body - remote_copy.

remote_copy is a promise body, which we will add to our CFEngine Community Open Promise Body Library file - library.cf

body copy_from remote_copy(source, server) {
    source      => "$(source)";
    servers     => { "$(server)" };
    compare     => "digest";
    purge       => "true";
    copy_backup => "false";
}

Now add the files you've created and updated to Git, commit them, and then push them to the correct branch of the remote Git repository.

The last thing we need to do is to add the rc.conf, resolv.conf and ethers files of our client machines and domain to CFEngine.

These files will be in the masterfiles directory in the CFEngine Git repository.

Adding /etc/rc.conf files of the systems to CFEngine

The /etc/rc.conf file for each system under CFEngine control will be in the masterfiles/rc.conf directory in our Git repository.

$ cd ~/PROJECTS/cfengine/masterfiles
$ mkdir rc.conf && cd rc.conf

Now, get the /etc/rc.conf files for each system in your domains and copy them to the masterfiles/rc.conf directory of your Git repository in the form of rc.conf-<hostname>, e.g. for the system www.site1.example.org it's rc.conf file will be rc.conf-www.site1.example.org.

Do this for all systems under CFEngine control, then add the files to Git, commit them, and push them to the correct branch of the remote Git repository.

Adding /etc/resolv.conf files to CFEngine

Similarly to the previous step, where we have added the /etc/rc.conf files of our systems to CFEngine, in this step we will add the /etc/resolv.conf files for our domains.

The /etc/resolv.conf files will be in the masterfiles/domain directory in our Git repository.

$ cd ~/PROJECTS/cfengine/masterfiles
$ mkdir domain && cd domain

Now, get the /etc/resolv.conf files for your domains and add them to the masterfiles/domain directory of your Git repository in the form of resolv.conf-<domain-name>, e.g. if the domain name for site1 is site1.example.org, then your resolv.conf filename for this domain will be resolv.conf-site1.example.org.

Do this for all domains you have, then add the files to Git, commit them, and push them to the correct branch of the remote Git repository.

Adding /etc/ethers files to CFEngine

The /etc/ethers file simply contains pairs of hostname and MAC addresses of the systems in your domain.

I often have to wake(8) a system up over the network, and remembering it's MAC address is not always possible.

For all your systems of the domain (or those you only need) add their hostnames and MAC addresses in the masterfiles/domain/ethers-<domain-name> file, in a similar way you did it in the previous step for resolv.conf.

When ready, add the files to Git, commit them, and then push them to the correct branch of the remote Git repository.

Updating the CFEngine policy server

When you are done setting up your configuration files for systems and domains, it is time to update the configuration on the CFEngine policy server.

To do so, login to the CFEngine policy server and execute:

cd /var/cfengine && git pull

Verify that the files you've added/updated are present and validate the new promises.

$ sudo cf-promises -v

If everything is OK, then just login to a system in your domain and execute cf-agent(8) - now your /etc/rc.conf, /etc/resolv and /etc/ethers files will be under CFEngine control.

To verify that this works as expected, modify manually /etc/rc.conf and then again execute cf-agent(8).

When you do a change to a file that is under CFEngine control, and CFEngine sees this change it will try to conform to the promises it made, and will restore the file to it's original state.

Having these basic promises for managing domain settings, you should get an idea how to further automate your systems with CFEngine.

Written on April 16, 2011