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
Xen DetectionFor 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=rootOnce 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 )
Detecting Debian VersionA 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"
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
many thanks for it!
Thorsten
[ Parent | Reply to this comment ]
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 ]
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 ]
- 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)
[ Parent | Reply to this comment ]