Singlecopy Nirvana

From Cfwiki

Jump to: navigation, search

The Problem

The problem is a pretty common one: you have a configuration file that is the same across a class of machines, different between classes, and maybe has a couple of special-case exceptions for one-off hosts. You want to manage the configfile in revision control and distribute it via cfengine, but it's unwieldy to use "copy: myclass.!specialhost::" to pick the right file out of the repository. What you'd really like is a way to do "hierarchical" copies: to provide a list of alternatives and have cfengine pick the most applicable one for a given host. Here's how.

The Solution

There are three parts to the solution. First, set 'control: singlecopy = (on)'. This will cause cfengine to treat multiple 'copy:' statements with the same destination file as a hierarchical copy instead of an error.

control:
   singlecopy = ( on )
   DefaultCopyType = ( checksum )

Second, create a variable and define it to a different value for each class of machine that'll have variant files. This is a key step, because it allows you to use your 'groups:' section to pick the "best" variant, without requiring 'copy: myclass::' style decision trees. I use the generic-sounding $(role), but as long as it's not a reserved word, it doesn't matter. Note that this step is not needed if you use a builtin like ${ostype} as in the documentation But I have not found my config files to vary strictly with ostype, so it's nice to be able to use arbitrary 'groups:' definitions to expand a variable.

groups:
   secure = ( robotron sinistar joust )

control:
  AllowRedefinitionOf = ( role )
  any::    role       = ( nevermatch )
  secure:: role       = ( secure )

That odd definition for 'any:: role = (nevermatch)' is because if a variable is undefined, cfengine passes it through as a literal string, rather than expand it out to "" (the null string), so without a definition for the variable (even if it should never be used), you'll see file requests for a file named 'myfile.conf.$(role)'. (Note: this behavior is under discussion)

Lastly, set up your copy section (and matching files in the repository) such that there are copy statments for each level of specificity that you need to look for. Remember, cfengine will stop looking for files to copy after the *first* successful stat() against the server's repository. So the most-specific file (which might only exist for a few machines) goes first, then it'll fall through to second-most-specific, which also may not exist. Only if both of those fail should it hit the default, least-specific file.

control:
   dr = ( /path/to/repository )
   fs = ( cfmaster )

copy:
     ${dr}/etc/ldap.conf.${host} server=${fs} dest=/etc/ldap.conf
     ${dr}/etc/ldap.conf.${role} server=${fs} dest=/etc/ldap.conf
     ${dr}/etc/ldap.conf server=${fs} dest=/etc/ldap.conf

So, using the example 'groups:', if our filesystem looked like

[eric@cfmaster /path/to/repository/etc]$ ls ldap.conf*
ldap.conf  ldap.conf.joust  ldap.conf.secure

then robotron and sinistar would get 'ldap.conf.secure', joust would get 'ldap.conf.joust', and everybody else would get 'ldap.conf'. This lets you keep a nice clean sparse filesystem in CVS, and you can very easily tell which file a particular host will pick.

The only downside to this that I found was that cfservd generates a bunch of error messages every time a client tries to stat a file that doesn't exist. This happens a lot, because everyone will try to look for a file with their hostname extension and almost everyone will look for a .$(role) extension for their role, most of which will fail. I made the attached patch to cfservd, which reduced the log volume tremendously (we were over 700 megabytes a week!) but still makes the error messages available if you run cfservd in the foreground with '-v'.

cfwiki.org has uploads disabled, so here's the patch inline; note code may have drifted a bit since this is against 2.1.11 - If you apply cleanly to a newer release please update this section!

   diff -ru cfengine-2.1.11/src/cfservd.c cfengine-2.1.11-eric/src/cfservd.c
--- cfengine-2.1.11/src/cfservd.c       2004-09-20 06:47:29.000000000 -0700
+++ cfengine-2.1.11-eric/src/cfservd.c  2004-12-07 15:33:53.000000000 -0800
@@ -1986,7 +1986,6 @@
    {
    snprintf(conn->output,CF_BUFSIZE*2,"Couldn't stat filename %s from host %s\n",filename,conn->hostname);
    CfLog(cfverbose,conn->output,"lstat");
-   CfLog(cflogonly,conn->output,"lstat");
    return false;
    }

@@ -2956,15 +2955,15 @@
    }

 snprintf(sendbuffer,CF_BUFSIZE,"%s",CF_FAILEDSTR);
-CfLog(cfinform,"Host authorization/authentication failed or access denied\n","");
+CfLog(cfverbose,"Host authorization/authentication failed or access denied\n","");
 SendTransaction(conn->sd_reply,sendbuffer,size,CF_DONE);
 snprintf(sendbuffer,CF_BUFSIZE,"From (host=%s,user=%s,ip=%s)",hostname,username,ipaddr);
-CfLog(cfinform,sendbuffer,"");
+CfLog(cfverbose,sendbuffer,"");

 if (strlen(errmesg) > 0)
    {
    snprintf(OUTPUT,CF_BUFSIZE,"ID from connecting host: (%s)",errmesg);
-   CfLog(cflogonly,OUTPUT,"");
+   CfLog(cfverbose,OUTPUT,"");
    }
 }
Personal tools