Dynamic definition of classes with CFEngine modules

Posted by Steve on Thu 28 Dec 2006 at 09:14

There are times when you'd like to conduct complex conditional actions within a CFEngine setup. Whilst it is possible to use built in classes, or dynamic tests for the existence of files, directories, or other things using an external plugin module gives you a lot of additional flexability.

CFEngine allows you to conduct different actions by "groups" and by "classses". Groups are generally defined in a static fashion such as this:

groups:
   internal = ( mine yours cfmaster )
   dhcp     = ( mine )

Here we define a group named "internal" to match the hosts "mine", "yours", and "cfmaster". We also define the hostname "mine" as being the value of the group "dhcp".

Classes can be a little bit more dynamic, since they can be set based upon tests such as these following examples:

classes:
    has_dupload  = ( FileExists(/usr/bin/dupload) )
    has_monit    = ( FileExists(/usr/sbin/monit) )
    has_php4     = ( FileExists(/etc/apache2/mods-available/php4.load) )

Here we have defined several classes based upon the existence of files. You can use many testing operations to define classes in this manner, although it can get a little tricky with complex tests.

(You can also define classes after a successful file copy, and at other points mid-execution.)

When your level of complexity starts to rise it is time to look at modules. Modules are essentially plugins which are executed on each node. When a module is executed CFEngine will process the output.

Module output which begin with a + sign is treated as a class to be defined, whilst lines which begin with a - sign are treated as classes to be undefined.

As an example the following module, when executed, will always define the class "Steve", and always unset the class "debian" (if present):

#!/bin/sh

echo "+Steve"
echo "-debian"
Installing Modules

For CFEngine to execute a module it must be located in the module-directory which is /var/lib/cfengine2/modules/ by default. It must also be named with a module: prefix.

For example the previous sample could have been named /var/lib/cfengine2/modules/module:sample.

As the modules must be copied to your managed clients you will probably need to add them to your cfagent.conf, or update.conf files, to make sure they are always up to date. For my setup I can copy all modules with a stanza such as this:

copy:
  $(host_base)/modules/ dest=/var/lib/cfengine2/modules/ server=$(server)
     recurse=inf mode=755 owner=root group=root

Once the modules are in place you need to cause them to be executed. Simply add their names to your actionsequence line:

  actionsequence  = ( module:sample copy ... )

The final thing you must do is tell CFEngine which classes the module will be defining/undefining:

  AddInstallable  = ( Steve debian )
Xen Detection

A simple example of a useful module would be the following script which attempts to detect whether the current host is either a Xen host, a Xen guest, or a Xen-free system:

#!/bin/sh
#  module:xen
#
#  Define one of "dom0", "domU", or "noxen".
#
test -d /proc/xen && test -x /usr/sbin/xm && echo "+dom0" && exit
test -d /proc/xen && echo "+domU" && exit
echo "+noxen"

Save this script as the file "module:xen" and copy it to all your CFEngine-managed nodes, into the modules directory. Now you can use it as follows:

control:
  # Allow these values to be dynamically set/updated
  AddInstallable  = ( dom0 domU noxen )

  Actionsequence = ( module:xen shellcommands .. )


#
#  Now we can run different commands based on the system type
#
shellcommands:
  domU::
    "/bin/echo This is domU"
  dom0::
   "/bin/echo This is dom0"
  noxen::
   "/bin/echo This system is not xen"
Detecting Debian Version

As another simple example the module module:release will detect the Debian release upon the current host, defining one of :

  • debian_sarge: For nodes running Sarge.
  • debian_etch: For nodes running Etch.
  • debian_sid: For nodes running Sid.

Arrange for the file to be copied to your CFEngine-managed nodes, in the file /var/lib/cfengine2/modules/module:release then add the following to cfagent.conf to ensure it will run:

control:
  AddInstallable  = ( debian_etch debian_sarge debian_sid )
  ActionSequence  = ( module:release ... )

Now you can use the result to control file copying, shell command execution, etc. For example we could copy a different sources.list file with a stanza such as:

#
#  Copy files from the master to the host.
#
copy:

 debian_etch::
  $(host_base)/etc/apt/sources.list.etch dest=/etc/apt/sources.list 
     server=$(server) mode=755 owner=root group=root

 debian_sarge::
  $(host_base)/etc/apt/sources.list.sarge dest=/etc/apt/sources.list
     server=$(server) mode=755 owner=root group=root

 debian_sid::
  $(host_base)/etc/apt/sources.list.sid dest=/etc/apt/sources.list 
     server=$(server) mode=755 owner=root group=root
Share/Save/Bookmark


Posted by Thorsten (84.58.xx.xx) on Sat 30 Dec 2006 at 03:48
[ Send Message ]
Great article Steve - once more :)

many thanks for it!
Thorsten

[ Parent | Reply to this comment ]

Posted by yarikoptic (69.115.xx.xx) on Sat 30 Dec 2006 at 06:16
[ Send Message ]

Whenever I come in cfengine to handle the same file differently for different boxes, it seems to be better (in the long run) to compose it entirely from within cfengine, or otherwise you would be growing number of such files exponentially to number of free parameters -- distribution, architecture, flavor, etc.

So for sources.list I have (excerpts from cf.main):

control:
  RCSRev =     ( ExecResult("/usr/local/sbin/shh echo $Revision: 1.7 $     @P@ /usr/bin/cut -f2 -d@Q@ @Q@") )
  RCSFile =    ( ExecResult("/usr/local/sbin/shh echo $RCSfile: cf.main,v $ @P@ /usr/bin/cut -f2 -d@Q@ @Q@") )
  EditHeader = ( 'CFEngine $(host) $(RCSFile) $(RCSRev)' )

  debarch         = ( ExecResult(/usr/bin/dpkg  --print-architecture) )

 #
 # there is no official amd64 port of sarge so it resides elsewhere
 !sarge|!linux_x86_64::
  aptrepository    = ( file:/net/debmirror/share/debmirror/debian/ )

 sarge.linux_x86_64::
  aptrepository    = ( http://debian.csail.mit.edu/debian-amd64/debian/ )

 sarge::
  distribution     = ( sarge )
  distcodename     = ( stable )
  vimflavor        = ( vim )

 !sarge::
  vimflavor        = ( vim.basic )

 etch::
  distribution     = ( etch )
  distcodename     = ( testing )

 sid::
  distribution     = ( sid )
  distcodename     = ( unstable )

editfiles:
 allnodes::
     { /etc/apt/sources.list
        AutoCreate 

        BeginGroupIfNoLineMatching "##### BEGIN Main $(EditHeader) $(RCSRev)"
          # Recreate from scratch
          EmptyEntireFilePlease
  
          ############################################################
          # Begin block with tag
          #
          Append "##### BEGIN Main $(EditHeader) $(RCSRev)"
  
          ############################################################
          # Code inside block
          #
          Append "# Main repository"
          Append "deb     $(aptrepository) $(distribution) main contrib non-free"
          Append "deb-src $(aptrepository) $(distribution) main contrib non-free"
          Append ""
          Append "# Locally packaged"
          Append "deb     file:/net/debmirror/share/debmirror/rumba/ $(distribution) perspect backport local alianed"
          Append "deb-src file:/net/debmirror/share/debmirror/rumba/ $(distribution) perspect backport local alianed"
          Append ""
          Append "# Locally packaged cran"
          Append "deb     file:/net/debmirror/share/debmirror/rumba/ $(distribution) cran"
          Append "deb-src file:/net/debmirror/share/debmirror/rumba/ $(distribution) cran"
          Append ""
          Append "# FSL"
          Append "deb     file:/net/debmirror/share/debmirror/fsl/ $(distribution) main non-free"
          Append "deb-src file:/net/debmirror/share/debmirror/fsl/ $(distribution) main non-free"
  
          ############################################################
          # Terminate block with tag
          #
          Append "##### END Main $(EditHeader) $(RCSRev)"
          Append ""
  
          DefineInGroup "aptgetupdate"

        EndGroup
        }
  allnodes.!sid.!(sarge.linux_x86_64)::
    { /etc/apt/sources.list
        AutoCreate 
        BeginGroupIfNoLineMatching "##### BEGIN Updates $(EditHeader) $(RCSRev)"
          Append "##### BEGIN Updates $(EditHeader) $(RCSRev)"
          Append "# Updates repository"
          Append "deb     file:/net/debmirror/share/debmirror/debian/ $(distcodename)-proposed-updates main contrib non-free"
          Append "deb-src file:/net/debmirror/share/debmirror/debian/ $(distcodename)-proposed-updates main contrib non-free"
          Append "##### END Updates $(EditHeader) $(RCSRev)"
          Append ""
          DefineInGroup "aptgetupdate"
        EndGroup
    }

  allnodes.!(sarge.linux_x86_64)::
    { /etc/apt/sources.list
        AutoCreate 
        BeginGroupIfNoLineMatching "##### BEGIN Marillat $(EditHeader) $(RCSRev)"
          Append "##### BEGIN Marillat $(EditHeader) $(RCSRev)"
          Append "# Marillat repository for mplayer acroread etc"
          Append "deb     file:/net/debmirror/share/debmirror/marillat/ $(distribution) main"
          Append "deb-src file:/net/debmirror/share/debmirror/marillat/ $(distribution) main"
          Append "##### END Marillat $(EditHeader) $(RCSRev)"
          Append ""
          DefineInGroup "aptgetupdate"
        EndGroup
    }

  allnodes.(codestable|codeoldstable)::
    { /etc/apt/sources.list
        AutoCreate 
        BeginGroupIfNoLineMatching "##### BEGIN Backports $(EditHeader) $(RCSRev)"
          Append "##### BEGIN Backports $(EditHeader) $(RCSRev)"
          Append "# backports.org"
          Append "deb     http://www.backports.org/debian/ $(distribution)-backports main"
          Append "deb-src http://www.backports.org/debian/ $(distribution)-backports main"
          Append ""
          Append "# security updates"
          Append "deb     file:/net/debmirror/share/debmirror/debian-security $(distribution) updates/main updates/non-free updates/contr
ib"
          Append "deb-src file:/net/debmirror/share/debmirror/debian-security $(distribution) updates/main updates/non-free updates/contr
ib"
          Append "##### END Backports $(EditHeader) $(RCSRev)"
          Append ""
          DefineInGroup "aptgetupdate"
        EndGroup
    }
    { /etc/apt/preferences
        AutoCreate 
        BeginGroupIfNoLineMatching "Explanation: BEGIN Backports $(EditHeader) $(RCSRev)"
          Append "Explanation: BEGIN Backports $(EditHeader) $(RCSRev)"
          Append "Package: *"
          Append "Pin: release a=$(distribution)"
          Append "Pin-Priority: 500"
          Append ""
          Append "Package: *"
          Append "Pin: release a=$(distribution)-backports"
          Append "Pin-Priority: 101"
          Append "Explanation: END Backports $(EditHeader) $(RCSRev)"
          Append ""
          DefineInGroup "aptgetupdate"
        EndGroup
    }

shellcommands:
  aptgetupdate::
    "/usr/bin/apt-get update"
where shh is my script to get away from always changing and hard to manage escaping of commands in cfengine2 (or may be I just need a good howto or documentation):
#!/bin/bash
#     BUCK  QUOTE BACK  PIPE  SEMI
dict='@B@:$ @Q@:" @A@:\\ @P@:| @S@:;'

args="$@"

for pair in $dict;
do
  key=${pair%:*}
  value=${pair#*:}
  args=$(echo $args | sed -e "s/$key/$value/g")
done

bash -c "$args"

Nodes are assigned to sarge/etch/sid in groups of cfagent.conf

   # distro specific
   sarge = ( raider )
   etch  = ( opteronnodes ravana itanix )
   sid = ( none )
   #
   # codenames
   codeunstable = ( sid )
   codetesting = ( etch )
   codestable = ( sarge )
   codeoldstable = ( woody )

Now I just need to "cvs update" "cvs commit" if I've done any changes and run cfrun - that would update sources.list and run 'apt-get update' if necessary

[ Parent | Reply to this comment ]

Posted by sytoka (83.177.xx.xx) on Mon 1 Jan 2007 at 14:56
[ Send Message ]
I use something like you but not as sophisticated. To define debian class for sarge, etch... You can use theses simples lines
   debian_etch  = ( "/bin/grep -q 4.0 /etc/debian_version" )
   debian_sarge = ( "/bin/grep -q 3.1 /etc/debian_version" )
   debian_woody = ( "/bin/grep -q 3.0 /etc/debian_version" )
I do not like to hardcode computer names in class definition. Generally, it's never up to date. My main problem was I can not put pipe | in cfengine command. I am going to look at your shh meta script.

[ Parent | Reply to this comment ]

Posted by yarikoptic (71.241.xx.xx) on Mon 1 Jan 2007 at 18:33
[ Send Message ]
Indeed it is a good practice to don't rely on hardcoded names. In my case, for the purpose of distribution control, I am going after centralized control over boxes (instead of local definition of debian distribution given in /etc/debian_version) because
  • within those few lines I can see what box is running (or at least intended to run) what version
  • I can easily step into upgrading from one distribution into another (instead of adjusting debian_version file semi-manually on all relevant boxes)
  • I can easily define mixed distributions (having etch+sid+experimental + pinning)
What I am missing though - automated way to define netgroups from cfegnine classes so I could use them easily with dsh or similar beast. And that is kinda a piece of advice for any cfengine user - if there are host-set hardcoded classes, define them with the same names for the netgroups - so they can easily be used within shell as well.

[ Parent | Reply to this comment ]

User Login

Username:

Password:

[ Advanced Login ]

Register Account

Quick Site Search