bundle edit_line
Line based editing is a simple model for editing files. Before XML, and later JSON, most configuration files were line based. The line-based editing offers a powerful environment for model-based editing and templating.
File editing in CFEngine 3
File editing is not just a single kind of promise but a whole range of 'promises within files'. It is therefore not merely a body to a single kind of promise, but a bundle of sub-promises. After all, inside each file are new objects that can make promises, quite separate from files' external attributes.
A typical file editing stanza has the elements in the following example:
######################################################################
#
# File editing
#
######################################################################
body common control
{
version => "1.2.3";
bundlesequence => { "outerbundle" };
}
########################################################
bundle agent outerbundle
{
files:
"/home/mark/tmp/cf3_test"
create => "true", # Like autocreate in cf2
edit_line => inner_bundle;
}
########################################################
bundle edit_line inner_bundle
{
vars:
"who" string => "SysAdmin John"; # private variable in bundle
insert_lines:
"/* This file is maintained by CFEngine (see $(who) for details) */",
location => first_line;
replace_patterns:
# replace shell comments with C comments
"#(.*)"
replace_with => C_comment,
select_region => MySection("New section");
reports:
"This is file $(edit.filename)";
}
########################################
# Bodies for the library ...
########################################
body replace_with C_comment
{
replace_value => "/* $(match.1) */"; # backreference
occurrences => "all"; # first, last all
}
########################################################
body select_region MySection(x)
{
select_start => "\[$(x)\]";
select_end => "\[.*\]";
}
########################################################
body location first_line
{
before_after => "before";
first_last => "first";
select_line_matching => ".*";
}
There are several things to notice:
- The line-editing promises are all convergent promises about patterns within the file. They have bodies, just like other attributes do and these allow us to make simple templates about file editing while extending the power of the basic primitives.
All file edits specified in a single
edit_line
bundle are handled "atomically". CFEngine edits files like this:- CFEngine makes a copy of the file you you want to edit.
- CFEngine makes all the edits in the copy of the file. The filename is the same as your original file with the extension .cf-after-edit appended.
- After all edits are complete (the
delete_lines
,field_edits
,insert_lines
, and finallyreplace_patterns
promises), CFEngine checks to see if the new file is the same as the original one. If there are no differences, the promises have converged, so it deletes the copy, and the original is left completely unmodified. - If there are any differences, CFEngine makes a copy of your original file with the extension .cf-before-edit (so you always have the most recent backup available), and then renames the edited version to your original filename.
Because file rename is an atomic operation (guaranteed by the operating system), any application program will either see the old version of the file or the new one. There is no "window of opportunity" where a partially edited file can be seen (unless an application intentionally looks for the .cf-after-edit file). Problems during editing (such as disk-full or permission errors) are likewise detected, and CFEngine will not rename a partial file over your original.
All pattern matching is through Perl Compatible Regular Expressions
Editing takes place within a marked region (which defaults to the whole file if not otherwise specified).
Search/replace functions now allow back-references.
The line edit model now contains a field or column model for dealing with tabular files such as Unix passwd and group files. We can now apply powerful convergent editing operations to single fields inside a table, to append, order and delete items from lists inside fields.
The special variable
$(edit.filename)
contains the name of the file being edited within an edit bundle.
In the example above, back references are used to allow conversion of comments from shell-style to C-style.
Another example of files promises is to look for changes in files. The
following example reports on all recent changes to files in a directory
by maintaining the most recent version of the md5
hash of the file
contents. Similar checks can be used to examine metadata or both the
contents and metadata, as well as using different difference checks. The
Community Edition only reports that changes were found, but Enterprise
versions of CFEngine can also report on what exactly the significant
changes were.
bundle agent example
{
files:
"/home/mark/tmp" -> "Security team"
changes => lay_a_tripwire,
depth_search => recurse("inf"),
action => background;
}
#########################################################
body changes lay_a_tripwire
{
hash => "md5";
report_changes => "content";
update => "yes";
}
Attributes
select_region
Type: body select_region
Description: Restrict edit_line promise to specific section
Restrict edits to a specific region of a file based on [select_start]bundle edit_line#select_start and select_end regular expressions. If the beginning and ending regular expressions match more than one region only the first region will be selected for editing.
include_start_delimiter
Description: Whether to include the section delimiter
In a sectioned file, the line that marks the opening of a section is not normally included in the defined region (that is, it is recognized as a delimiter, but it is not included as one of the lines available for editing). Setting this option to true makes it included.
Type: boolean
Default value: false
Example:
body select_region MySection(x)
{
select_start => "\[$(x)\]";
select_end => "\[.*\]";
include_start_delimiter => "true";
}
Input file:
[My section]
one
two
three
In this example, the section does not normally include the line [My
section]. By setting include_start_delimiter
to true
it would be
possible for example, to delete the entire section, including the
section header. If however include_start_delimiter
is false, the
contents of the section could be deleted, but the header would be
unaffected by any delete_lines
promises. See the next section on
include_start_delimiter
for further details.
History: This attribute was introduced in CFEngine version 3.0.5 (2010)
include_end_delimiter
Description: Whether to include the section delimiter
In a sectioned file, the line that marks the end of a section is not normally included in the defined region; that is, it is recognized as a delimiter, but it is not included as one of the lines available for editing. Setting this option to true makes it included.
Type: boolean
Default value: false
Example:
body select_region BracketSection(x)
{
select_start => "$(x) \{";
select_end => "}";
include_end_delimiter => "true";
}
Input file:
/var/log/mail.log {
monthly
missingok
notifempty
rotate 7
}
The section does not normally include the line containing }. By setting
include_end_delimiter
to true
it would be possible for example, to
delete the entire section, including the section trailer. If however
include_end_delimiter
is false, the contents of the section could be
deleted, but the header would be unaffected by any delete_lines
promises.
The use of include_start_delimiter
and include_end_delimiter
depend
on the type of sections you are dealing with, and what you want to do
with them. Note that sections can be bounded at both the start and end
(as in the example above) or just at the start (as in the sample shown
in include_start_delimiter
).
History: This attribute was introduced in CFEngine version 3.0.5 (2010)
select_start
Description: Regular expression matching start of edit region
See also select_end
. These delimiters mark
out the region of a file to be edited.
Type: string
Allowed input range: .*
Example:
body select_region example(x)
{
select_start => "\[$(x)\]";
select_end => "\[.*\]";
}
select_end
Description: Regular expression matches end of edit region from start
See also select_start
. These delimiters
mark out the region of a file to be edited.
Type: string
Allowed input range: .*
Example:
body select_region example(x)
{
select_start => "\[$(x)\]";
select_end => "\[.*\]";
}
If you want to match from a starting location to the end of the file
(even if there are other lines matching select_start
intervening),
then just omit the select_end
promise and the selected region will run
to the end of the file.