Cfengine 3 has a lot of things going for it. But its syntax is not
one of them.
Consider this situation: you have CentOS machines, SuSE machines and
Solaris machines. All of them should run, say, SSH, NTP and Apache
why not? The files are slightly different between them, and so is the
method of starting/stopping/enabling services, but mostly we're doing
the same thing.
I've got a bundle in Cfengine that looks like this:
bundle common services {
vars:
redhat|centos::
"cfg_file_prefix" string => "centos/5";
"cfg_file[httpd]" string => "/etc/httpd/conf/httpd.conf";
"daemon[httpd]" string => "httpd";
"start[httpd]" string => "/sbin/service httpd start";
"enable[httpd]" string => "/sbin/chkconfig httpd on";
"cfg_file[ssh]" string => "/etc/ssh/sshd_config";
"daemon[ssh]" string => "sshd";
"start[ssh]" string => "/sbin/service sshd restart";
"enable[ssh]" string => "/sbin/chkconfig sshd on";
...and so on. We're basically setting up four hashes -- daemon,
start, enable and cfg -- and populating them with the appropriate
entries for Red Hat/Centos ssh and Apache configs; you can imagine
slightly different entries for Solaris and SuSE. The
cfg_file_prefix allows me to put CentOS' config files in a separate
directory from other OS.
Then there's this bundle:
bundle agent fix_service(service) {
files:
"$(services.cfg_file[$(service)])"
copy_from => secure_cp("$(g.masterfiles)/$(services.cfg_file_prefix)/$(services.cfg_file[$(service)])", "$(g.masterserver)"),
classes => if_repaired("$(service)_restart"),
comment => "Copy a stock configuration file template from repository";
processes:
"$(services.daemon[$(service)])"
comment => "Check that the server process is running, and start if necessary",
restart_class => canonify("$(service)_restart"),
ifvarclass => canonify("$(services.daemon[$(service)])");
commands:
"$(services.start[$(service)])"
comment => "Method for starting this service",
ifvarclass => canonify("$(service)_restart");
"$(services.enable[$(service)])"
comment => "Method for enabling this service",
ifvarclass => canonify("$(service)_restart");
}
This bundle takes a service name as an argument, and assigns it to the
local variable "service". It copies the OS-and-service-appropriate
config file into place if it needs to, and enables/starts the service
if it needs to. How does it know if it needs to? By setting the
class "$(service)_restart" if the service isn't running, or if the
config file had to be copied.
So far, so good. Well, except for the mess of brackets. All those
hashes are in the services bundle, so you need to be explicit about
the scope. (There are provisions for global variables, but I've kept
my use of 'em to a minimum.) And so what in Perl would be, say:
$services->start{$service}
becomes
"$(services.start[$(service)])"
Square brackets for the hash, round brackets for the string (and to
indicate that you're using a variable -- IOW, it's "$(variable)", not
"$variable" like you're used to), and dots to indicate scope
("services.start" == the start variable in the services bundle).
It's...well, it's an ugly mess o' brackets. But I can deal with
that. And this arrangement/pattern, which came from the Cfengine
documentation itself, has been pretty helpful to me for dealing with
single config file services.
But what about the case where a service has more than one config file?
Like autofs: you gotta copy around a map file but in SuSE you also
need /etc/sysconfig/autofs to set the LDAP variables.
Again, in Perl this would be an anonymous array on top of a hash --
something like:
$services->cfg_file{"autofs"}[0] = "/etc/auto.master
$services->cfg_file{"autofs"}[1] = "/etc/sysconfig/aufofs"
and you'd walk it like so:
foreach my $i in ($services->cfg_file{"autofs"}) { # something with $i }
or even:
while ($services->cfg_file{"autofs"}) { # something with $_ }
(I think...I'm embarrassed sometimes at how rusty my Perl is.)
In Cfengine, you pile an anonymous array on top of a has like so:
"cfg_file[autofs]" slist => { "/etc/auto.master", "/etc/sysconfig/autofs" };
An slist is a list of strings. All right, fine; different layout,
same idea, stick it in the services bundle and away we go. But:
remote scalars can be referenced; remote lists cannot without gymnastics.
From the docs:
During list expansion, only local lists can be expanded, thus global
list references have to be mapped into a local context if you want to
use them for iteration. Instead of doing this in some arbitrary way,
with possibility of name collisions, cfengine asks you to make this
explicit. There are two possible approaches.
The first of those two approaches is, I think, passing the list as a
parameter, whereupon it just works? maybe? (It's a not-so-minor
nitpick that there are lots of examples in the Cf3 handbook that are
not explained and don't make much sense. They apparently work, but
how is not at all clear, or discernible.) I think it's meant to be
like Perl's let's-flatten-everything-into-a-list approach to passing
variables.
The second is to just go ahead and redeclare the remote slist (array)
as a local one that's set to the remote value. Again, from the
docs:
bundle common va {
vars:
"tmpdirs" slist => { "/tmp", "/var/tmp", "/usr/tmp" };
}
bundle agent hardening {
classes:
"ok" expression => "any";
vars:
"other" slist => { "/tmp", "/var/tmp" };
"x" slist => { @(va.tmpdirs) };
reports:
ok::
"Do $(x)";
"Other: $(other)";
}
which makes this prelude to all of that handwaving even more irritating:
Instead of doing this in some arbitrary way, with possibility of
name collisions...
...
...I mean...
...I mean, what is the point of requiring explicit paths to
variables in other scopes if you're just going to insert random
speedbumps to assauge needless worries about name collisions? What
the hell is with this let's-redeclare-it-AGAIN approach?
The rage, it fills me.
