PEARCEC.COM HOME
HOME
BIO
CONTACT
PORTFOLIO
RESUME
PERSONAL
AZ_TRIP
LINKS
WORK
BOOKS
ARTICLES

Archive
RSS Feed
Stats
Awstats
Webmail
WebCalendar

www.drbond.net
dejafoo.net
www.rbohn.com
luke.ehresman.org
www.openthought.org




Cfengine Style Guide

Christian Pearce
Author

Match 4th, 2005

Contents:

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

Creative Commons License
This work is licensed under a Creative Commons License.