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.