CFEngine is a program meant to automate the job of system
administrators. Instead of writing ad hoc custom scripts, cfengine
gives you the easy way of expressing the solution with a simple and
straight forward declarative language. The declarative language allows
you to specify a policy for cfengine to implement. For example as a
policy, an administrator may want tmp directory to be emptied every
three days, may want the password file to always have the permission
644
, ensure that Apache is up all the time etc. Policies such as
these and many more can easily be written for cfengine to implement. A
simple code such as:
files: /etc/passwd owner=root mode=644 checksum=md5 action=fixall
will enable cfengine to implement a password file policy without any complex script. CFEngine makes the policy implementation easy and simple.
CFEngine has a number of components constituting the software. They
are cfagent
, cfexecd
, cfservd
, cfenvd
,
cfkey
, cfshow
, cfenvgraph
and cfrun
.
cfagent
is the most important component of cfengine software. It is
the robot that interprets the user policy and implements it in a
convergent manner.
cfexecd
is the component that can schedule the execution of cfengine
and inform the administrator about the results of the execution via
email.
cfservd
is the server daemon component. It is responsible only two
operations, remote copy and execution. This means you don’t need
cfservd to enable cfengine work. It is only becomes necessary when
remote copy and execution are concerned.
cfenvd
is used to collect statistical data about resource usage on
each for anomaly detection.
cfkey
is for public and private key generation.
cfshow
converts the cfengine database into ASCII format and dump it in a file.
cfenvgraph
formats the cfenvd
database in a form that can be plotted.
cfrun
allows you to request the execution of cfengine on remote hosts.
In this document we will first concentrate on how to use
cfagent
to implement simple policies.
To install cfengine you need the following software packages
# install Flex Bison Berkeley db CFEngine software # compile tar cfengine-2.2.8.tar cd cfengine-2.2.8 ./configure make make install
By default cfengine executables are placed in /usr/local/sbin.
The general structure of cfengine's declarative language is given below:
control: variable_1 = ( value1 value2 ... ) variable_2 = ( value1 value2 ... ) ... variable_n = ( value1 value2 ... ) type_stanza_1: [condition or class]:: target attribute1=value1 attribue2=value2 ... type_stanza_2: [condition or class]:: target attribute1=value1 attribue2=value2 ...
The control stanza is a special stanza where all the variables
that will be used in the policy file are defined. The control is also
used to specify the order of execution, access control
etc. variable1
, variable2
are variable names. Some variables are
reserved or predefined and some are user-defined variables. The values
of the variable are specified in the parentheses.
The stanzas defines the rule type, for example files
rule type,
disk
rule type etc. Each stanza has an optional condition for
which the policy should be implemented. The condition or class (in
cfengine parlance) must be followed by double colon.
Within every stanza or rule type we have attributes or options and values. Usually, such attributes are defined by cfengine. However, one can also define new class attributes using the cfengine "defined" keyword. For example the simple files stanza can have the following attributes or options: owner, mode, checksum, action etc
The values of the attributes are either predefined or user
defined. For example the value for the action attribute, fixall
, is
predefined but the value for the mode attribute 644
is user supplied.
We also have the target which specifies the object we want to apply the policy. A target is usually a file or a directory. The combination of target, options and value defines a policy.
In summary cfengine declarative language consist of
control
.
We are going to test cfengine with simple files rule type example. We will check if a file has mode 644 if not we change it to 644
#cf_ex1.conf control: actionsequence = ( files ) files: /home/linux/cfengineDoc/testfile mode=644 action=fixall #end
This policy specification asks cfengine to check if the target
(testfile) has permission 644, if not cfengine will change the
permission to 644
. This check can be done every minute, hour depending
on what you want. It will automate the administrator's job.
We have to run cfagent to test if this policy will be implemented. We
first have to create the file testfile and change the permission to
maybe 777
. i.e chmod 777 /home/linux/cfengineDoc/testfile
.
Now create a file called cf_ex1.conf and type in the file the policy in example 1. Enter the directory that contains cf_ex1.conf and execute the following command.
/usr/local/sbin/cfagent -vKf ./cf_ex1.conf
Thus after the policy is written the cfagent robot is used to implement or run the policy specifications.
/usr/local/sbin/cfagent is where the cfengine agent is located and -v
is for verbose output, -K
is a command to force the immediate implementation of
policy and -f
is used to specify the file which contains the policy
configuration. The policy configuration file is found in
cf_ex1.conf. By default cfagent runs every 1 minute and if that time
is not elapsed since the last run the current configuration will not
be executed so K option is used to override this default behavior.
In example 1 the actionsequence variable (directive or command) has
the value files
. The value of the actionsequenece variable is placed
in parentheses. There should a space before and after the variable
value like this: ( files )
.
The values of actionsequance represents the rule type and the sequence
of execution. The sequence of execution is simply first come first
serve. E.g actionsequenece = ( files shellcommands ) means the files
command must be executed before the shellcommnds. The files stanza or
rule type has the target testfile (the full path of the file
should be specified) and attributes or options of the files are mode
and action and the values are 644
and fixall
respectively.
This example adds a shellcommand to example 1 and executes it from cfengine
#cf_ex2.conf control: actionsequence = ( files shellcommands ) files: /home/linux/cfengineDoc/testfile mode=u+rxw action=fixall shellcommands: "/bin/echo ...second cfengine script ..." #end
The order in which you write the rule types in cfengine doesn't always affect the sequence of execution. For example, example 2 can be rewritten as
#cf_ex2a.conf control: actionsequence = ( files shellcommands ) shellcommands: "/bin/echo ...second cfengine script ..." files: /home/linux/cfengineDoc/testfile mode=644 action=fixall #end
The results will be the same. The files
rule type will be executed
first followed by shellcommands
since the actionsequence specifies
so. So the bulk sequence is specified by the actionsequence
but the order
of rules does not matter.
Variables allow us to store data in memory for processing. Variables are declared in cfengine as follows:
name = ( value1, value2 ... )
The name can be any name that follows the general programming standard. The name can also be a predefined name to set the value of a reserved control parameter. Example 3 shows how to work with cfengine variables.
#cf_ex3.conf control: actionsequence = ( shellcommands ) hello = ( "hello cfengine " ) #hello variable shellcommands: "/bin/echo ..$(hello).."
This code declares the hello
variable and assigns the value "hello
cfengine". We then use shellcommands stanza to display the value of
the variables. The code $(hello)
shows how cfengine reference
variable. There are two ways of referencing cfengine variables. We can
reference a variable with parenthesis or curly braces. For example
$(hello)
can rewritten as ${hello}
.
#cf_ex4.conf control: actionsequence = ( shellcommands ) hello = ( "hello cfengine " ) #hello variable shellcommands: "/bin/echo ..${hello}.."
Example 4 will produce the same result as example 3. The only
difference is that the parenthesis is changed to a curly brace i.e.
${hello}
.
The value of a variable can be specified in the cfengine configuration file or read from a file. Example 5 reads data from a file and stores it in a variable.
#cf_ex5.conf control: actionsequence = ( shellcommands ) data = ( ReadFile(/home/linux/cfengineDoc/data.txt,100) ) shellcommands: "/bin/echo ..$(data).."
This example reads data from the file data.txt
using the cfengine
ReadFile
method. The output is stored in the variable data and the
result displayed by shellcommands
. The ReadFile()
method has two
parameters, the file name and the (maximum) number of characters to be read.
Special keys such as tab, newline etc, can also be represented in cfengine to format output. The following are the some of the escape sequence.
cr
- Expands to the carriage-return character.
dblquote
- Expands to a double quote "
.
dollar
- Expands to $
.
lf
- Expands to a line-feed character.
n
- Expands to a newline character.
quote
- Expands to a single quote '
.
spc
- Expands simply to a single space. This can be used to place spaces in filenames etc.
tab
- Expands to a single tab character.
#cf_ex5a.conf control: actionsequence = ( shellcommands ) alerts: linux:: "The car cost $(n) $(dollar)20,000 "
The output will be: The car cost $20,000
Selection or classes allow a program to decide which code to
execute. In cfengine such commands are called classes. There are no if
statement in cfengine, only classes. By default cfengine classifies
the system environment and used the results to make decisions. The
classes are Boolean with true or false values. We can see the default
or defined classes by running cfagent -vp
command. You also see the
defined classes when you run cfagent -vf ./filename
.
Defined Classes = ( 192_168_189 192_168_189_128 32_bit Day16 Hr10 Hr10_Q2 July Min15_20 Min19 Q2 Wednesday Yr2008 any cfengine_2 cfengine_2_2 cfengine_2_2_7 compiled_on_linux_gnu debian fe80__20c_29ff_fec4_f41c i686 ipv4_192 ipv4_192_168 ipv4_192_168_189 ipv4_192_168_189_128 linux linux_2_6_24_19_generic linux_i686 linux_i686_2_6_24_19_generic linux_i686_2_6_24_19_generic__1_SMP_Wed_Jun_18_14_43_41_UTC_2008 linux_local lsb_compliant net_iface_eth0 ubuntu ubuntu_8 ubuntu_8_4 ubuntu_hardy undefined_domain )
Users can also define their own classes to use for decision making.
This example displays the message "...cfengine classes in action.." if
the operating system of my system is Linux. CFEngine will check if
linux
is part of the defined classes and displays the message
accordingly.
#cf_ex6.conf control: alerts: linux:: "..cfengine classes in action.."
We can have two or more classes as shown in example 7
#cf_ex7.conf control: alerts: linux:: "..cfengine classes in action..linux" SuSE:: "..cfengine classes in action..suse"
CFEngine will display only the linux message since there is no SuSE
class in the defined classes above.
Logical operators are use to combine conditional statements or classes. For example we can check if a distribution is either Ubuntu or SuSE, the distribution is SuSE and the time is quarter past two and so on. CFEngine has the following logical operators:
|
or ||
.
&
or .
(dot).
!
( )
#cf_ex8.conf control: alerts: redhat.SuSE:: "..cfengine classes in action.."
The output is nothing, as the condition can never be satisfied.
Example 8 shows the combination of two classes redhat
and
SuSE
. redhat.SuSE
means if the distribution is redhat and SuSE display
the "cfengine classes in action". Since a distribution cannot be both
redhat and SuSE the results is nothing.
#cf_ex9.conf control: alerts: redhat|SuSE:: "..cfengine classes in action.."
This means if the distribution is either redhat
or SuSE
display the message.
#cf_ex10.conf control: alerts: !redhat|Hr10:: "..cfengine classes in action.."
This means if distribution is not redhat
or the hour is 10am display the results.
It is possible to define your own class using the define keyword or classes rule type.
#cf_ex11.conf User define classes and sequence control: classes: Check = ( FileExists(/home/linux/cf) ) alerts: Check:: "yes"
This example defines the class Check
using the classes rule type. The
method FileExists()
is cfengine method which check whether or not a
file exists. The value of the class Check
is true if the file cf is
found in /home/linux directory. The output of the policy will be `yes'
if the file is found.
Sometimes you may want cfengine to be aware of your user defined class
before it is used. We do this by using the AddInstallable
method.
#cf_ex12.conf User define classes and sequence control: #add class that might become define at run time AddInstallable = ( Check ) classes: Check = ( FileExists(/home/linux/cf) ) alerts: Check:: "yes"
User defined classes can be define at runtime. Example 13 shows how this is done
#cf_ex13.conf #User define classes and sequence control: actionsequence = ( files ) #add class that might become define at run time #the order of usage is not important AddInstallable = ( RCheck ) files: RCheck:: #RCheck is true /home/linux/cfengineDoc/test mode=700 action=fixall files: #define stores the outcome of execution #if mode is not 777 then change to 777 and set RCheck to true /home/linux/cfengineDoc/test1 mode=777 action=fixall define=RCheck
This example uses the outcome of results in the second stanza (test1)
to fix the file permission for the test file. The entire policy
reads like this, if the file permission for test1 file is not 777
and the action is taken to change it to 777
then change the permission
of the test file to 700
. The order is not important; the right
instruction will be executed.
#User define classes control: #add class that might become define at run time actionsequence = ( files ) AddInstallable = ( WinXP Indigo ) files: Indigo:: /usr/etc/resolv.conf owner=root action=warn classes: #list of classes of pcs WinXP = ( pc121 pc122 linux ) #-box2 and -box4 not part Indigo = ( irix -box2 -box4 ) #if file exist RCheck = ( FileExists(/home/linux/cfengineDoc/sequence) ) files: RCheck.WinXP:: /home/linux/cfengineDoc/test mode=600 action=fixall
An example of a useful maintenance procedure which prevents system
failure is log rotation. We can do this task by using the disable
stanza. Disabling a file means renaming it so that it becomes
harmless. This feature is useful if you want to prevent certain
dangerous files from being around, but you don't want to delete them
— a deleted file cannot be examined later. The main syntax is
disable: class:: /filename dest=filename type=plain/file/link/links rotate=empty/truncate/numerical-value size=numerical-value define=classlist
If a destination filename is specified, cfagent renames the source file to the destination, where possible (renaming across filesystems is not allowed). If no destination is given, cfagent renames a given file by appending the name of the file with the suffix .cfdisabled. Note that directories are only renamed if they have a specific destination specified.
A typical example of a file you would probably want to disable would be the /etc/hosts.equiv file which is often found with the ‘+’ symbol written in it, opening the system concerned to the entire NIS universe without password protection! Here is an example:
disable: /etc/hosts.equiv /etc/nologin /usr/lib/sendmail.fc sun4:: /var/spool/cron/at.allow
Disabling a link deletes the link. If you wish you may use the optional syntax
disable: /directory/name type=file
in order to specify that a file object should only be disabled if it is a plain file. The optional element type= can take the values plain, file, link or links. If one of these is specified, cfengine checks the type and only disables the object if there is a match. This allows you to disable a file and replace it by a link to another file for instance. NOTE that if you regularly disable a file which then gets recreated by some process, the disabled file filename. cfdisabled will be overwritten each time cfengine disables the file and therefore the contents of the original are lost each time. The rotate facility was created for just this contingency. The disable feature can be used to control the size of system log files, such as /var/adm/messages using a further option rotate. If the value rotate is set to 4, say,
disable: filename rotate=4
then cfengine renames the file concerned by appending `.1' to it and a new, empty file is created in its place with the same owner and permissions. The next time disable is executed `.1' is renamed to `.2' and the file is renamed `.1' and a new empty file is created with the same permissions. CFEngine continues to rotate the files like this keeping a maximum of four files. This is similar to the behaviour of syslog. If you simply want to empty the contents of a log file, without retaining a copy then you can use rotate=empty or rotate=truncate. For instance, to keep control of your World Wide Web server logs:
disable: Sunday|Wednesday:: /usr/local/httpd/logs/access_log rotate=empty
This keeps a running log which is emptied each Sunday and Wednesday. The size= option in disable allows you to carry out a disable operation only if the size of the file is less than, equal to or greater than some specified size. Sizes are in bytes by default, but may also be quoted in kilobytes or megabytes using the notation:
numberbytes numberkbytes numbermbytes
Only the first characters of these strings are significant, so they may be written however is convenient: e.g. 14kB, 14k, 14kilobytes etc. Examples are:
size=<400 # disable if file size is < 400 bytes size=400 # disable if file size is equal to 400 bytes size=>400 # disable if file size > 400 bytes
This options works with rotate or normal disabling; it is just an extra condition which must be satisfied. If a disable command results in action being taken by cfengine, an optional list of classes becomes can be switched on with the aid of a statement define=classlist in order to trigger knock-on actions. The repository declaration allows a local override of the Repository variable, on an item by item basis. If set to “off” or “none” it cancels the value of a global repository and leaves the disabled file in the same directory.
CFEngine list allows you to specify a list of items which can be treated as a single variables. A list can be any set of names, file names etc. The example 22 shows how to create a list.
control: alist = ( "one:two:three:four:five" ) alerts: linux:: " $(alist)"
By default a list should be separated by colon. This can be change using the Split
option.
control: Split = ( , ) alist = ( "one,two,three,four,five" ) alerts: linux:: " $(alist)"
The Split
allows us to change the default colon to comma.
control: Split = ( " " ) mylist = ( "mark ricky bad-dude" ) tidy: /mnt/home1/$(mylist) pattern=*.cfsaved age=1
control: Split = ( , ) alist = ( "one,two,three,four,five" ) alerts: redhat:: " $(alist)"
Example 23b:
control: Split = ( " " ) mylist = ( "mark ricky bad-dude" ) tidy: /linux/home/$(mylist) pattern=*.cfsaved age=1
In this example cfengine will iterate and substitute mylist with each of the elements of in the list. It will then delete all .cfsaved file which is a day old in each of the directory.
The power of lists is made clear when supplied as a parameter to a method (see below).
Recursion is used to iterate over a directory of files and
subdirectories. The keyword for recursion is recurse
(from the
anonymous saying: to iterate is human, to recurse is divine). The
value of recurse is from 0
to inf
(infinity). Where 0
means only current directory container, 1 includes the files in the
current directory and infinity means everything in the directory
including subdirectories.
control: actionsequence = ( files ) files: /home/linux/cfengineDoc mode=644 action=fixall recurse=inf
This example means change the mode of everything in the directory cfengineDoc to 644
.
control: actionsequence = ( files ) files: /home/linux/cfengineDoc mode=644 action=fixall recurse=2
This example means change the mode of directory and subdirectory to 644
.
A method is a set of instructions that achieve a specific task. It is possible to encapsulate tasks using cfengine methods. Methods in cfengine are defined as ordinary cfengine configuration files with some extra control information:
control MethodName = ( name ) MethodParameters = ( p1 p2 ... ) method-body ...
The MethodName
directive is use to specify the method name and the
MethodParameters specifies the parameters. The number of parameters
can zero or more. The method body is the usual cfengine policy
code. It is not different from what we know so far. We begin with a
simple method that displays a message.
#cf_ex14.conf User define classes and sequence control: MethodName = ( UserClass ) classes: Check = ( FileExists(/home/linux/cf) ) alerts: Check:: "Yes the cf exist" !Check:: "No, cf does not exist"
This method checks if the file cf exist and display a message. The
method is called UserClass
and has no parameter. In cfengine it is
required that a method is placed in a special directory called
modules. The default location of this directory is /var/cfengine. So
if you install cfengine with default dicrectory then you must copy the
file cf_ex14.conf into it /var/cfengine/modules directory. The next
step is to create a method call configuration file to make use of the
method as in example 14.
#cf_ex15.conf control: actionsequence = ( methods ) methods: UserClass(void) action=cf_ex14.conf #include the method here returnvars=null #no return value server=localhost #run the method on local machine
Example 15 is the method call's configuration file. The method call has
the methods
rule type. It is used to call the method. Under methods
rule type we call the method by name. Since our method has no
parameters we supply void as parameter entry. The action
directive now
specifies which file contains the method. The returnvars
allows the
return value to be store and reuse it later. Our method has no return
type so the value of return type is null. The server attribute or
option allows you to specify which host will run the method. Here we
chose localhost.
The next example shows how to use parameter in methods.
#cf_ex16.conf User define classes and sequence control: MethodName = ( UserClass ) MethodParameters = ( filename ) #add class that might become define at run time AddInstallable = ( Check ) classes: Check = ( FileExists("/home/linux/$(filename)") ) alerts: Check:: "Yes the cf exists" !Check:: "No, cf does not exists"
This method performs the same task as example 15 however the file name will not be fix but vary. The name will now be supplied at method call. Example 17 is the method call for example 16. Note that the file path is in double quote.
control: actionsequence = ( methods ) methods: UserClass(cf) action=cf_ex16.conf returnvars=null server=localhost
This example calls the method UserClass
and supply the parameter
cf. The cf will replace the variable or parameter filename so that
the file path checked will be /home/linux/cf. As usual the action option
specifies the file which contains the method. The method file must be
located in the modules directory. Note that the method call
configuration file should not be in the modules directory, but in the usual
inputs directory.
A method can return a value which could be used for further operation. In this case the return value will not be null. Example 18 illustrates how this could be done.
#cf_ex18.conf User define classes and sequence control: MethodName = ( ReturnName ) MethodParameters = ( filename ) classes: every = ( any ) alerts: every:: ReturnVariables("$(filename)")
This method just accepts a file name and returns the same name. The
alerts
rule type is needed to return the results. The method call for
example 18 is shown below.
control: actionsequence = ( methods ) methods: ReturnName(cf) action=cf_ex18.conf returnvars=results server=localhost classes: every = ( any ) alerts: every:: "The file name is $(ReturnName.results)"
Here the returnvars
value is results. This means the file name being
returned by the method will be stored in the variable results. We then
use the alerts command to display the file name. The results variable
is accessed using the name of the method name.
This example shows how to use more than one parameters in cfengine method.
#cf_ex20.conf User define classes and sequence control: actionsequence = ( editfiles ) MethodName = ( EditMethod ) MethodParameters = ( filename data ) editfiles: { /home/linux/$(filename) AppendIfNoSuchLine "$(data)" } classes: every = ( any ) alerts: every:: ReturnVariables("The file name is $(filename) and was editted with $(data)")
Example 21 calls the method in example 20.
#Example 21 control: actionsequence = ( methods ) methods: EditMethod(cf,"insert data here") action=cf_ex20.conf returnvars=results server=localhost classes: every = ( any ) alerts: every:: "The file name is $(EditMethod.results)"
CFEngine provides a number of built-in functions for evaluating classes, based on file tests and perform other functions. Using these built-in functions is quicker than calling the shell test function.
IsDefined(var)
, and not IsDefined(${var})
)
control: actionsequence = ( files ) a = ( 2.12 ) b = ( 2.11 ) classes: lt = ( IsLessThan(${a},${b}) ) gt = ( IsGreaterThan(${a},${b}) ) alerts: lt:: "$(a) LESS THAN $(b)" gt:: "$(a) GREATER THAN $(b)"
$(n)
is always considered to be a
separator, no matter what the current separator is. (ii)
‘textkey’ If this format is specified, ReadArray
tries to
interpret the file as a list of lines of the form: key,value
key1,key2,value1 key3,key4,value2
This variable would then be references as $(table[key1][key2]).
hosts = ( ReadList(/var/cfengine/inputs/datafile,lines,#,1000) )
What makes cfengine method powerful is the iteration. It is possible iterate over a list using method. When a list is pass to a method cfengine will automatically iterate over the list.
#cf_ex24.conf control: MethodName = ( ReturnName ) MethodParameters = ( filename ) classes: every = ( any ) alerts: every:: ReturnVariables("$(filename)")
We call the above method with example 25
control: actionsequence = ( methods ) alist = ( one:two:three:four:five ) methods: ReturnName($(alist)) action=cf_ex24.conf returnvars=results server=localhost classes: every = ( any ) alerts: every:: "The file name is $(ReturnName.results)"
|
Table of Contents