Cfengine Style Guide
Christian Pearce
Author
Match 4th, 2005
1. The Cfengine Style Guide
Why a Style Guide?
Why do we need a Cfengine Style Guide? Very simple. Ugly code sucks to read.
I have been working on Cfengine code for the better part of three years.
During this time I have developed several best practices, conventions,
and, of course, styles. In the coming sections of this article I will explain
all the different styles I have developed over the years.
2. Whitespace
Indentation and Line Length
Typically you indent 4 spaces, and never use tabs. Never let the length of a
line exceed 80 characters. Unless there are cases where breaking the line
would break the parsing of the code. (i.e. long shellcommands statements)
Code listing 2.1: Indentation Example |
control:
variable = ( "foobar" )
|
Sections
All sections should have a newline after them. This helps increase
readability, and makes it easy to pick out the different sections when
editing.
Sections should always start at the beginning of a line. This also helps with
readability. Sections stick out immediately.
Line up the Equals
Make the equals for variable definitions line up together. Likewise
start groups definitions longer than 80 characters on the
next line at the place of the previous line. See the following example.
Code listing 2.2: Lined up Equals |
control:
logwatch_rev = ( "5.1-1" )
logwatch_package = ( "logwatch-$(logwatch_rev).noarch.rpm" )
groups:
group1 = ( host1 host2 host3 )
groupxyz = ( host1 host2 host3 host4 host5 host6
host7 host8 host9 host10 host11 host12 )
|
Often it makes sense to comment a few assignments at once. When doing this
it looks better to line up the equals according to commented blocks of code.
Also when you comment blocks of code have a new line between the comment and the
previous block of code.
Code listing 2.3: Comments break up assignments |
groups:
# Solaris packages
orca_installed = ( IsLink(${prefix}/sbin/start_orcallator) )
orca_upgraded = ( FileExists(${prefix}/sbin/start_orcallator-${orca_rev}) )
# TODO: Add the orcallator.se so we get the freshest.
se_installed = ( ReturnsZero(${pkginfo} -q RICHPse) )
se_upgraded = ( FileExists(/opt/RICHPse/se_${se_rev}) )
# Linux packages
proca_rc_installed = ( FileExists(/etc/rc.d/init.d/procallator) )
proca_installed = ( IsLink(${prefix}/bin/procallator.pl) )
proca_upgraded = ( FileExists(${prefix}/bin/procallator-${proca_rev}.pl) )
|
3. Class Usage
New Lines Between Class and Statement Blocks
When using classes in statement blocks it is important to keep proper whitespace and
indentation.
In the folllowing example you will notice indentations and whitespace between the statements.
Code listing 3.1 |
shellcommands:
# All proxy nodes rsync the binaries, scripts and configs down to the
# child proxy nodes
_proxynode.hasChildPN::
"$(rsync) -aq -e $(ssh) --delete $(sysnav_bin_path)/* $(pn_ips):$(sysnav_var_path)/bin/"
owner=$(sysnav_user)
"$(rsync) -aq -e $(ssh) --delete $(sysnav_conf_path)/* $(pn_ips):$(sysnav_var_path)/conf/"
owner=$(sysnav_user)
"$(rsync) -aq -e $(ssh) --delete $(sysnav_pack_path)/* $(pn_ips):$(sysnav_var_path)/packaging/"
owner=$(sysnav_user)
|
It is also acceptable practice to put the class and variable definitions on
the same line. Only as long as it is a single variable definition and it is
short.
Code listing 3.2: Variable Oneliner |
control:
linux:: variable = ( "foo" )
solaris:: variable = ( "bar" )
|
Explicit Definition of the any Class
The any class should be used in every section that allows the use of classes.
This is important for two reasons. One it helps with readability.
Indenting for a class then for a statement makes the code look uniformed.
If you don't use the any class, the statements where the any class is implied
start after the first indent, where statements after defined classes start
after the second indent.
The other reason is it helps with development. When you see the any class you
think about how statements should occur based on classes. The following examples
describe a typical problem by not explicitly using the any class.
Code listing 3.3: Implicit use of the any class |
control:
foo = ( "bar" )
local = ( "place" )
# Comment
yoink = ( "insight" )
|
Imagine, if you will, the previous example is a larger set of code, where there
are several variable definitions. In the following example a mistake is made
by defining the local variable by a class.
Code listing 3.4: Implicit any class stomped |
control:
foo = ( "bar" )
linux::
local = ( "place" )
solaris::
local = ( "place3" )
# Comment
yoink = ( "insight" )
|
Without realizing it the yoink variable is now getting defined when the
solaris class is defined. When before you wanted it defined for any machine.
I call this class stomping. You can also
see how the readability is reduced because the statements are starting
at different indentations. Look at how the use of the any class improves
readability and we moved the yoink variable underneath the any class.
Code listing 3.5: Explicit use of the any class |
control:
any::
foo = ( "bar" )
# Comment
yoink = ( "insight" )
linux::
local = ( "place" )
solaris::
local = ( "place3" )
|
4. Variable Definition
Maybe this goes without saying, but don't be afraid to use variables. If
something gets used more than three times you should make it a variable.
Quote All Strings
When defining a variable use double quotes for all strings. Even though you
only need double quotes for strings that have spaces it helps to stay
consistent, especially if you use some basic syntax highlighting. It will
keep it the same color. This also makes it easier to read.
Variable Naming
Extensive use of variables can be useful in reducing the amount of redundancy
of statements in non control sections. We do this by using classes to
give the variables a different assignment based on a class in the control
section. Since we use a lot of variables it helps to split the variable
by name and some form of type.
Defining the name is a method of giving it a local assignment, followed
by a type helps you stay consistant across multiple Cfengine files. If the
variable is universal or global just give it a type for the variable name.
Code listing 4.1 |
$(logwatch_package) - A variable specific to logwatch that contains the package filename.
$(sysnav_pack_path) - A variable that is a path that is the packaging directory for SysNav.
$(platform) - A global variable that defines the platform of the machine
|
Cfengine and User Variable Definitions
When defining variables in a control section define all the Cfengine
variables first. Then define your variables in like group definitions.
Code listing 4.2 |
control:
# Cfengine definitions
any::
actionsequence = ( directories copy processes shellcommands tidy links )
domain = ( sysnav )
AddInstallable = ( new_cfenvd new_cfservd )
Repository = ( /var/cfengine/repo/ )
linux::
grep = ( /bin/grep )
echo = ( "/bin/echo -e" )
crontab = ( /usr/bin/crontab )
sh = ( /bin/sh )
# Path that the cfengine binaries install from
cfengine_install_path = ( /usr/sbin )
solaris::
grep = ( /bin/grep )
echo = ( /bin/echo )
crontab = ( /usr/bin/crontab )
sh = ( /bin/sh )
# Path that the cfengine binaries install from
cfengine_install_path = ( /opt/csw/sbin )
|
5. Miscellaneous
Statements with Attributes
There are several different types of statements depending on the section you
are coding. Most statements will take some form of attribute. It is
important to use the full attribute name. The idea is to stay consistent, and
it makes it more intuitive to read. Each attribute is on a new line
indented beyond the statement itself. It also helps to keep the same order
for all the different statements. The example below shows indenting, a
new line for each attribute, fullname of each attribute and the ordering
of the copy attribute.
Code listing 5.1: Statement using Fullname Attributes |
copy:
linux::
$(sysnav_pack_path)/$(platform)/$(logwatch_package)
dest=$(sysnav_rep_path)/$(logwatch_package)
server=$(parent_ip)
mode=700
owner=root
group=root
type=checksum
encrypt=true
trustkey=true
verify=true
|
Statements without Attributes
Sometimes statements do not have attributes. In this case each line
come one right after the next. Similar to assignments in the control
section. You also follow normal rules of commenting and whitespace.
Code listing 5.2: Statements with no Attributes |
shellcommands:
integrit_just_installed::
# Create a new database
"$(integrit) -C $(integrit_conf) -u"
# Copy the current db to the known db
"$(cp) -f $(integrit_current_db) $(integrit_known_db)"
# Create an MD5 hash of the known database, and copy it to the portal
'$(openssl) md5 $(integrit_known_db) | $(cut) -d " " -f2 > $(integrit_md5_file)'
"$(chown) $(sysnav_user) $(integrit_md5_file)"
"$(chmod) 640 $(integrit_md5_file)"
|
Statements and Ownership Attributes
It has been my experience, and this is subject to change, that if you
start setting ownership for shellcommands or copy statements to something other
than root, it may carry over that ownership to the next statement. As a measure
of safety it is good practice to define this ownership explicitly. This is similar to the
explicit any class. Since the ordering of events is broken down by the sections in
the actionsequence, you could have unpredictable results, if the ownership carried
over to a statement when the classes defined changed during each run of cfengine.
Ordering Sections
Typically control comes first, then groups, then each section
in order of the actionsequnce. This of course isn't always possible to maintain
but if you can it is a good idea, since it gives you the proper mind set
of what order things will occur when editing code.
Dynamically Generated Code
You should make the same attempt at adding the correct indentation and
styles if possible when generating cfengine code. Of course you should follow the
style guide for the language you are using to dynamically generate Cfengine.
This will blend well with the cfengine style guide. Here documents work
especially well for repeating stanzas of code.
Code listing 5.3: Embedded PHP |
groups:
# Define what patches are installed
<?PHP
foreach ($clusters as $cluster) {
$clstrID = $cluster->getObjID();
echo <<<END
PatchClstr_${clstrID}_installed = ( IsPlain(\${sysnav_var_path}/.clusters/PatchClstr-$clstrID) )
PatchClstr_${clstrID}_exists = ( IsPlain(\${sysnav_var_path}/tar/PatchClstr-$clstrID.zip) )
END;
}
?>
|
Notice the use of indenting to keep with the cfengine style. Plus the code
is more natural to Cfengine, rather having a bunch echos our a bunch of
for cfengine code.
6. Final Remarks
It was my hopes that people find this article useful and decide to follow
the style guidelines I set forth. I am not trying to pretend to have
invented anything here. I just want to document what I felt looked good
based on my experience and what I have seen in other examples.
Some people believe the style is purely a personal thing. But if you need
a place to start this is as good as anything out there.
If there are comments, questions or additions please feel free to email or
the cfengine list. I hope to incorporate all valid feedback I receive.
7. Resources
Revision History
- 0.8 - Grammar fixes.
- 0.7 - Added NON-Commercial to the license (October 20th, 2004)
- 0.6 - Minor edits (October 18th, 2004)
- 0.5 - More spelling and grammar fixes from Ralph Angenendt (October 14th, 2004)
- 0.4 - Spelling and grammar fixes. (October 12th, 2004)
- 0.3 - Keep the same ordering of sections as what is in your sequence. Lining up equals only across like sections. Statements without parameters. Comments and lining up the equals. Explicitly owner and group in copy and shellcommands. (October 11th, 2004)
- 0.2 - Added Oneliner classes, and changed when variables should be used to follow the XP standard. (September 23rd, 2004)
- 0.1 - Initial release

This work is licensed under a Creative Commons License.
|