Editfiles Examples
From Cfwiki
Using editfiles, one can easily make quick one line changes, but this method tends to be discouraged for larger and more complex changes. Most of the advice on the mailing list and IRC seems to be that for anything more complex, just copy in a file from a central server. This is not always desirable.
In these examples, I'd like to demonstrate an easy and reliable way of editing configuration files.
Modifying an Entire File
The basic concept is to include the entire text of the target file in an editfiles stanza. This guarantees that the contents of the file will be correct, even when updating a large portion of the file.
A basic example of full file replacement:
editfiles:
any::
{ /etc/hosts
EmptyEntireFilePlease
AppendIfNoSuchLine "127.0.0.1 localhost"
AppendIfNoSuchLine "192.168.1.254 myrouter"
}
This has the unintended side effect of attempting to recreate the file each time the editfiles stanza executes.
So, the next logical step is to add a header, which would indicate whether or not the file is current and requires editing.
Example of using a file header to prevent unnecessary editing:
editfiles:
any::
{ /etc/hosts
# Check for header, edit if the header doesn't match
#
BeginGroupIfNoLineMatching "# /etc/hosts v1.00"
# Recreate from scratch
EmptyEntireFilePlease
# Add header
Append "# /etc/hosts v1.00"
# File contents
Append "127.0.0.1 localhost"
Append "192.168.1.254 myrouter"
EndGroup
}
The example successfully prevents repeated edits by matching a one line header at the beginning of the file.
To properly maintain this file, each time a change is made to the file contents specified in the editfiles stanza, the version number in the header line (both match and append) would have to be incremented.
If you're like me and insist on automation (funny, we're talking about CFEngine right?), there's another way to tie in a version number.
I use RCS version control on all of my CFEngine configuration files. RCS allows you to insert tags into your source files which are updated each time the file is checked out. Specifically, the RCSfile and Revision tags are very handy because they allow us to get the configuration file name and current version automatically.
Unfortunately, the syntax RCS uses for tags readily breaks the regexp engine, so we have to perform some initial parsing to extract the information.
The next example demonstrates using RCS tags inside a configuration file to pass a dynamic revision number to editfiles. The effect is that each time a change is made to the configuration file, the editfile header will use the current version number and rewrite the target file. This removes the need to manually increment version numbers in the header.
Editfiles with dynamic version numbers, using full file replacement and headers:
control:
actionsequence = ( editfiles )
############################################################
# Variables to support RCS data in editing.
#
# Because the $'s in RCS tags break regexps, cut out the
# data we want from a shell.
# Remember, RCS updates the tags each time we commit.
#
RCSRev = ( ExecResult(/bin/sh -c "echo '$Revision: 1.2 $' | /usr/bin/cut -f2 -d' ' ") )
RCSFile = ( ExecResult(/bin/sh -c "echo '$RCSfile: FullFileReplaceViaEdit.cf,v $' | /usr/bin/cut -f2 -d' '") )
# Save some typing by combining elements into a header variable.
#
EditHeader = ( 'CFEngine $(host) $(RCSFile)' )
editfiles:
any::
{ /etc/hosts
# Check for header, edit if the header doesn't match
# Use RCS data and header variables
#
BeginGroupIfNoLineMatching "# $(EditHeader) $(RCSRev)"
# Recreate from scratch
EmptyEntireFilePlease
# Add header
Append "# $(EditHeader) $(RCSRev)"
# File contents
Append "127.0.0.1 localhost"
Append "192.168.1.254 myrouter"
EndGroup
}
Now, each time a new version of the configuration file is commited, editfiles will automatically update the text files it contains.
The RCS tags in the shellcommands are automatically updated, and the header is stored in a convenient variable which lists indicates the file is owned by CFEngine, the local hostname, configuration file that made the edit, and the revision.
You might ask why I left the RCS Revision out of the header variable. This is intentional, allowing for a different kind of editing automation which I'll cover in my next article.
Please post feedback if you enjoyed this article and found it useful.
--RussellAdams 6-Dec-2004
Converting a File to Editfiles Statements
On a related note, it can be tedious to take a very long existing configuration file for an application (ie: exim, or apache) and convert it to editfiles statements for using the editfiles method of full file replacement discussed here.
I wrote a short perl script to simplify the process of moving a large file into editfiles syntax which I share below.
Usage is simple, feed the file to encode to the script via STDIN, and the editfiles statements goto STDOUT. I typically will either redirect STDOUT to a file for me to drop in a script later, or while I'm editing in emacs I'll use a shell buffer and just copy/paste the lines from STDOUT into the buffer I was editing.
config2cfengine.pl:
#!/usr/bin/perl -w
#
# $Id: config2cfengine.pl,v 1.6 2005/01/12 08:15:38 root Exp $
#
# Reads a config file on STDIN, outputs Append 'line' and escapes symbols.
#
# Written by Russell Adams <rladams@adamsinfoserv.com>
# Released under GPLv2
use strict;
while (<STDIN>) {
chomp;
# Escape ' and \ symbols
s/\\/\\\\/g;
s/'/\\'/g;
print "\t\t\tAppend '$_'\n";
}
--RussellAdams 12-Jan-2005
A suggestion: at least with bash, your
/bin/sh -c "echo '$Revision: 1.3 $' | /usr/bin/cut -f2 -d' ' "
can probably be replaced with something like
/bin/sh -c 'a=($Revision: 1.3 $); echo \${a[1]}'
That's untested within a cfengine file; some escaping might be needed. It's shorter and slightly more efficient, as it does not start another external process.
--RudiChiarito 21-Jan-2005
Adding a Line Conditionally
I was asked to copy my mailing list post here for reference. This is how I conditionally add support for the vnc.so extension module to the X server configuration file:
editfiles:
{ /etc/X11/XF86Config-4
Backup "single"
BeginGroupIfNoLineMatching ".*Load[ ]+\"vnc\".*"
LocateLineMatching "Section[ ]+\"Module\".*"
InsertLine " Load \"vnc\" # automatically added by cfengine"
EndGroup
BeginGroupIfNoLineMatching ".*Option[ ]+\"passwordFile\".*"
LocateLineMatching "Section[ ]+\"Screen\".*"
InsertLine " Option \"passwordFile\" \"/root/.vnc/passwd\" # automatically added by cfengine"
EndGroup
}
--SeanAtkinson 04-Mar-2005
