My cfengine policies explained – part 2

The second policy we are going to explore is the one contained in the file. There are two use cases for it: stop puppet from changing the system, or remove it entirely. We'll see the whole code first, and then we'll see how cfengine handles it in each of the two cases. …

bundle agent puppet
      "puppetlock" string => "/var/lib/puppet/state/puppetdlock" ;
      "puppetcron" string => "/var/spool/cron/crontabs/root" ;
      "puppetpkgs" slist  => {
			     } ;

      "pkgclass[$(puppetpkgs)]" string => canonify("$(puppetpkgs)") ;

	  create  => "yes",
	  comment => "Disable puppet, so that it doesn't step on cfengine's feet" ;

          edit_line  => disable_puppetd_cronjob,
	  comment    => "prevent cron from trying to keep puppet alive" ;

	  edit_line => remove_puppet_cronjobs,
	  comment   => "cleanup puppet stuff from root's crontab" ;

	  package_method => aptget,
	  package_policy => "verify",
	  classes        => if_else(
				   ) ;

	  package_method => apt,
	  package_policy => "delete",
	  ifvarclass     => "$(pkgclass[$(puppetpkgs)])_installed",
	  classes        => if_else(
				   ) ;

      "$(puppetpkgs) is installed"
	  ifvarclass => "$(pkgclass[$(puppetpkgs)])_installed" ;

      "$(puppetpkgs) is NOT installed"
	  ifvarclass => "$(pkgclass[$(puppetpkgs)])_not_installed" ;

      "$(puppetpkgs) killed"
	  ifvarclass => "$(pkgclass[$(puppetpkgs)])_killed" ;

      "$(puppetpkgs) NOT killed"
	  ifvarclass => "$(pkgclass[$(puppetpkgs)])_not_killed" ;

bundle edit_line disable_puppetd_cronjob
      "# Puppet Name: cron_puppetds*" ;
      ".+s+/etc/init.d/puppets+starts+.+" ;

bundle edit_line remove_puppet_cronjobs
      "# HEADER: .*" ;
      "# Puppet Name: .*" ;
      ".+s+/etc/init.d/puppet.*" ;

The promises in this policy are sorted according to cfengine's normal ordering, so that they are listed in the same order the agent will read them — this makes the policy much easier to understand and I recommend you do the same for your policies.

Let's start with the case where we just want to stop puppet from doing stuff without actually removing it. It's the case where the class kill_puppet is NOT set.

The agent will start its first pass through the puppet bundle and read the vars promises first; it will set two strings, a list, and an array.

The most interesting part is the array: for each element in the puppetpkgs list, it will create an array element containing the canonified version of the list element. E.g.: the value for $(pkgclass[puppet-common]) is puppet_common. Here we are using cfengine's implicit looping, so be sure to take a peek at the reference guide if you're not sure what it is.

It's time for the files promises. Remember that the class kill_puppet is not set, and as of now puppet_installed isn't as well, so the agent will skip all these promises altogether.

And we get to the packages promises. Here we use the implicit looping again, iterating over the package names and checking if they are installed (the package_policy is set to "verify"). The package method aptget is not a standard one, I had to create my own due to a problem related to the standard apt method and package verification. Check an earlier blog post for the details. We also use an if_else body from the standard library: if the package promise is kept or repaired, the promise will set the class used as the first argument, otherwise it will set the class used as the second argument. The promises depending on kill_puppet will be skipped instead.

Confused? OK, let's make a practical example. Suppose we are running this policy on a typical puppet client, where the puppetmaster package is not installed and the other two are. The puppetmasterd will not be verified, and the class puppetmaster_not_installed will be set; the other two packages will be verified, and the classes puppet_installed and puppet_common_installed will be set. So far, so good: let's see what happens next.

The last promises to be verified are the reports. For starters, they all depend on the class report_normal (what's that? read my previous post). Then, puppet will iterate again over the values in puppetpkgs, and a string will be printed depending (via ifvarclass) on which accessory class is also set. In our example before, cfengine will print something like:

puppet is installed
puppet-common is installed
puppetmaster is NOT installed

This ends the first pass, and our tireless agent will start immediately with the second. It will find that nothing has changed regarding the vars promises, and go ahead with the files promises. Compared to the first pass, the situation has changed: now the puppet_installed class is defined, so the first two promises will be applied: the first one will ensure that the file /var/lib/puppet/state/puppetdlock (the value of $(puppetlock)) is present. This will disable the puppet agent.

Besides, it will edit the file /var/spool/cron/crontabs/root to ensure that it doesn't contain neither lines starting with "# Puppet Name: cron_puppetd" and followed by any number of spaces, or lines containing "/etc/init.d/puppet" and "start" separated by one or more spaces. The magic of all this is in the edit_line bundle called disable_puppetd_cronjob, that is referenced in the promise by the edit_line clause, and is located right below the agent bundle.

Why I do all this? Because it happened to me that the puppet daemon died, and I had asked puppet to set a cron job to ensure it would be restarted if it died for any reason.

But let's go further! The agent will go through the packages and reports promises, find that nothing has changed from the previous pass, and take no action. In the third pass, it will find that nothing has changed since the second pass, and will take no action at all. Done!

Now, let's take a second stroll through the code, this time with the kill_puppet set. Everything stays very much the same for the vars promises, but for the others it's a bit different. In the files promises, the promise on the crontab is run immediately, and it will remove all lines in the crontab that are somehow related to puppet (see the edit_line bundle called remove_puppet_cronjobs: all the lines beginning with either "# HEADER: " or "# Puppet_name: " or containing "/etc/init.d/puppet" are wiped off). Then we still verify which packages are installed, but this time we also run a package promise with the "delete" policy, and only for those that are installed (via ifvarclass again). If we use the same example as before, the agent will try to uninstall the puppet and puppet-common packages; if it succeeds, it will define the classes puppet_killed and puppet_common_killed.

With this set of classes now defined, we get to the reports promises. In our example, they'll produce the same output as before, plus two more lines:

puppet killed
puppet-common killed

The other two passes of the agent will find that every promise is already kept, and take no further action. Done, again!

And that's it! For next time I am a bit undecided, but I think we'll go through the library. Until then, take care!


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 )

Google+ photo

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

Connecting to %s