My cfengine policies explained – part 3

One good thing in cfengine 3 is the enhanced ability to create reusable snippets of code containing promises (bundles) or parameters (bodies); plus, you can make them parametric for added flexibility. The cfengine standard library, also known as Community Open Promise Body Library (COPBL), is a comprehensive collection of reusable bodies and bundles that can simplify the task of writing policies. However, although comprehensive, it can't fit all possible needs. But there is good news: you can write your own libraries, too, with either completely new bodies/bundles, or by modifying those already available in the standard library!

As promised, we are about to see a library called that complements the standard library, addressing needs specific to our installation. It's quite small however, which is a a further demonstration that the standard library covers almost everything.

Rather than listing the whole file, we'll either examine each the library's body/bundle separately, or in small groups. …The first one is useful when, in a files promise, you want to edit a portion of a text file, starting from the beginning of the file and ending at a chosen delimiter; the delimiter itself will not be included in the selection:

# Select a region of text, from the first line (select_start matches anything)
# up to the requested delimiter. delimiter may be preceded and followed by any
# number of spaces, and must be matched as a whole word (not part of a string)
# thanks to the b pattern.

body select_region until(delimiter)
        select_start          => "^.*$" ;
        select_end            => "^s*b$(delimiter)b.*$" ;
        include_end_delimiter => "no" ;

E.g., if you want to edit an sshd_config file from the beginning up to and excluded a Match directive, you'll say

select_region => until("Match")

in your files promise.

The second is a location body to be used in files promises, and will point the internal file "cursor" to the first line of the file:

# Move the internal file "cursor" to the beginning of the file
# I think I "stole" this from the examples, but I am not sure...

body location first_line
        before_after         => "before";
        first_last           => "first";
        select_line_matching => "^.*$";

Very little to say here, so we go directly to the third body in the library. This time it is a classes body, and already in the standard library, but with a name that doesn't really tell me what it is for. So I just rewrote the body with a mnemonic name:

# like if_else in COBL, but we are not interested in knowing if the promise
# was kept; this already exists actually and it's called cf2_if_else, but
# it will clearer for me in the future if I call it if_repaired_else

body classes if_repaired_else(yes,no)
	  promise_repaired => { "$(yes)" };
	  repair_failed    => { "$(no)" };
	  repair_denied    => { "$(no)" };
	  repair_timeout   => { "$(no)" };

I use this body when, in a promise, I am interested in knowing if it was repaired or not –if it was already kept, I am not interested– and set different classes to act accordingly. This is the case when, for example, we want to ensure a certain user exists in the system: if it does already, we don't want to take action; but if it isn't we want to know if cfengine was able to create it or not. A promise like

      "/usr/sbin/useradd -d $(nagios_home) -m -g nagios -s /bin/bash nagios"
          comment => "Add a nagios user, and put it in the nagios group",
          classes => if_repaired_else("nagios_user_created","nagios_user_failed") ;

will try to add a user called nagios if it doesn't exist already, and set two classes appropriately to tell the rest of the policy if it was created or not.

Remember that, in cfengine, a promise is considered kept if a certain resource is already in the desired state and doesn't need to be changed. A promise is repaired if cfengine was able to put it in the desired state when it previously wasn't.

For the cases where we want to know if a promise was kept, repaired, or an attempt to repair it failed, there is a slightly different body:

# Sometimes it's good to distinguish if a promise was kept,
# repaired, or just failed. This body helps.

body classes kept_rep_fail(kept_class,rep_class,fail_class)
      promise_kept     => { "$(kept_class)" } ;
      promise_repaired => { "$(rep_class)"  } ;
      repair_failed    => { "$(fail_class)" } ;
      repair_denied    => { "$(fail_class)" } ;
      repair_timeout   => { "$(fail_class)" } ;

It is used more or less the same way as the previous one, so I'd not spend more time with it.

The next one is another example of a body of the standard library slightly adapted for a specific need.

# Don't rely on aptitude and use apt-get instead
# Adapted from the standard library

body package_method aptget
	package_changes => "individual";
	package_list_command => "/usr/bin/dpkg -l";
	package_list_name_regex    => "iis+([^s]+).*";
	package_list_version_regex => "iis+[^s]+s+([^s]+).*";
	package_installed_regex => ".*"; # all reported are installed
	package_name_convention => "$(name)";

      # set it to "0" to avoid caching of list during upgrade
	package_list_update_ifelapsed => "240";

	package_add_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes install";
	package_list_update_command => "/usr/bin/apt-get update";
	package_delete_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes -q remove";
	package_update_command =>  "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes install";
	package_verify_command => "/usr/bin/dpkg -s";
	package_noverify_returncode => "1";

If you compare this body with the standard library's apt, you'll notice that:

  • we are using apt-get only here, aptitute was stripped;
  • the package_changes attribute is set to individual

Why? There is an entire blog post dedicated to that, so you may want to read it.

Finally, there are a bundle and a body that go together. They are used to terminate a process that is running for longer than we deem appropriate for it:

# This bundle helps you to kill a runaway process
# You call the bundle and pass a process pattern, and the number of minutes
# it is allowed to run. cfengine will signal the runaway processes with
# term first, and kill afterwards.

bundle agent signal_runaway_process(process,minutes)
	  process_select => runaway_by_minutes($(minutes)),
	  signals        => { "term", "kill" } ;

# This body selects processes that are running for more than a requested
# number of minutes

body process_select runaway_by_minutes(minutes)
	  ttime_range    => irange(0,accumulated(0,0,0,0,$(minutes),0)) ;
	  process_result => "!ttime" ;

Let's say that you have a process named "myprocess" that, in normal conditions, runs for no more than 6 minutes. It is reasonable to think that if you find one that ran for more than 30, something must be wrong with it, and you'll want to kill it. Cfengine can take care of it. Just use a methods promise, like this:

    "kill_runaway" usebundle => signal_runaway_process("myprocess",30) ;

and that will do the trick. But how?

At first, the processes promise will consider all the processes in the process table that contain the string "myprocess"; it will then select those among them that are returned to the process_select clause by the body runaway_by_minutes, and signal them. The runaway_by_minutes body works by checking which processes ran for a time in the range 0-$(minutes) minutes (0-30 in our case), and return those that are out of that range (process_result is set to "!ttime", not ttime, so those that were not in the range specified). Yes, it is that simple 😉

And that's all for today, I hope that I was able to make it clear enough. Next time we'll see a slightly complicated policy:

Take care, and I'll see you next time.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.