Please note: this will be 100% overkill when cfengine 3.3.0 will be out: the new awesome templating engine will make this kind of file editing trivial. Nevertheless, this was buzzing in my head for too long after I read the early release of Learning cfengine, so I wanted to give it a go in any case.
Objective: write a parametrised method to create an ntp.conf file, with all directives in a chosen order, and in a "human friendly" form.
As always, when you want to impose your own order to cfEngine (right or wrong that may be), you have to clearly understand what the normal ordering is, so that you'll have a clear picture of what is defined where. …Let's look at the test bundle, which I put in a file called tt3.cf
(don't ask why):
body common control { bundlesequence => { "test" } ; inputs => { "cfengine_stdlib.cf","t3.cf" } ; version => "Testing ntpconf"; } bundle agent test { vars: test_ucast:: "config[cast]" string => "ucast" ; test_mcast:: "config[cast]" string => "mcast" ; test_client:: "config[role]" string => "client" ; test_server:: "config[role]" string => "server" ; any:: "config[keyfile]" string => "/etc/ntp/ntp.keys" ; "config[keys]" string => "2 4 8 16" ; "config[mykey]" string => "2" ; "config[xen]" string => "workaround" ; "config[servers]" slist => { "time1", "time2", "time3", "time4" } ; "config[peers]" slist => { "peer1.oslo.osa", "peer2.oslo.osa" } ; methods: "ntp" usebundle => ntpconf("test.config") ; }
In the control body, we are just saying that we want to run the agent bundle named test, and that we are importing the standard library, plus a file called t3.cf (which will contain our method bundle).
The test bundle is super-simple: depending on how we define classes on the command line (e.g.: test_ucasts and test_client), it defines an array called config, and then passes it to the method ntpconf (note that we are passing the fully qualified name of the array). Done.
Now let's see the ntpconf bundle:
bundle agent ntpconf(config) { files: "/tmp/ntp.conf" edit_defaults => empty, create => "true", edit_line => write_ntp_conf("$(config)") ; } bundle edit_line write_ntp_conf(c) { vars: any:: "index" slist => getindices("$(c)") ; "cast" string => "$($(c)[cast])" ; "role" string => "$($(c)[role])" ; "defopts" string => "default kod notrap nomodify nopeer noquery" ; "servers" slist => { "@($(c)[servers])" } ; "peers" slist => { "@($(c)[peers])" } ; # The variable "options" will be set at the beginning of the # second pass. The ACLs will find them set at the right moment # as they depend on the "latepass" class that is still undefined # in "vars:" at the second pass, but it will be after the second # pass in "classes:". ntpconf_header_set.mcastclient:: "options" string => "$(defopts) notrust" ; ntpconf_header_set.!mcastclient:: "options" string => "$(defopts)" ; classes: # has_* classes will be true only at second pass on "classes:" # note that "vars:" promises are evaluated before "classes:", so they # will still be false at the second "vars:" pass, but true after # the second "classes:" pass "has_$(index)" expression => "ntpconf_header_set" ; # the same holds for this latepass class, that we'll use later to # delay the compilation of the ACLs at the very end "latepass" expression => "ntpconf_header_set" ; # These will be defined right at the first pass "is_$(cast)" expression => "any" ; "is_$(role)" expression => "any" ; "ucastclient" and => { "is_client", "is_ucast" } ; "ucastserver" and => { "is_server", "is_ucast" } ; "mcastclient" and => { "is_client", "is_mcast" } ; "mcastserver" and => { "is_server", "is_mcast" } ; insert_lines: any:: "# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help" ; "$(const.n)driftfile /var/lib/ntp/ntp.drift" insert_type => "preserve_block", classes => if_ok("ntpconf_header_set") ; is_mcast:: "keys $($(c)[keyfile])" ; "trustedkey $($(c)[keys])" ; (is_server|is_ucast).has_servers:: "$(const.n)# Upstream servers" insert_type => "preserve_block" ; "server $(servers)" ; (is_server|is_ucast).has_peers:: "$(const.n)# Peer servers" insert_type => "preserve_block" ; "peer $(peers)" ; mcastserver.has_mykey:: "$(const.n)broadcast 224.0.1.1 key $($(c)[mykey]) ttl 7" insert_type => "preserve_block" ; mcastclient:: "$(const.n)multicastclient 224.0.1.1" insert_type => "preserve_block" ; has_xen:: "$(const.n)# Apply xen workaround$(const.n)disable kernel" insert_type => "preserve_block" ; latepass:: "$(const.n)# Access control restrictions" insert_type => "preserve_block" ; "restrict -4 $(options)" ; "restrict -6 $(options)" ; "restrict 127.0.0.1" ; "restrict ::1" ; }
The ntpconf bundle is, again, as simple as it can be: one "files:" promise. It scratches the file, and delegates all the editing to the write_ntp_conf edit_line bundle. Of course, that's where the meat is. Note, however, that ntpconf passes on the configuration array (config) again.
Let's get to write_ntp_conf then. Here the config array is called just c for brevity because, as you will see, we'll have to dereference it many times. The promises in this bundle appear in the same order as they are in the normal ordering for clarity, so you'll read them in the same order they will run.
First we have a number of "vars:" promises: index will take the keys of the array pointed by $(c); cast and role will take the values associated to the same key in the array, defopts is a fixed string that we'll use later, and servers and list get the lists associated to the keys with the same name in the array pointed by $(c). These variables will be set right there and then.
A bit below there is a variable called options, whose value is conditioned by the setting of two classes; both of them are currently false at this stage, so the variable won't be set yet.
And then come "classes:" promises. Here we define a set of "has_*" classes, one for each value in $(index); these classes will have the same "definedness" of the class ntpconf_header_set, hence they are currently false. Same holds for the latepass class. Then comes a set of classes that, being aliases of the class any, will be defined at this pass and keep the same state throughout the whole execution. For example, if the variable $(cast) holds the value "ucast", we'll be defining a new class called is_ucast; and we'll be doing the same thing with the $(role) variable. Based on those two, one out of four classes will be defined: ucastclient, ucastserver, mcastclient, or mcastserver.
And then come "insert_lines:" promises, it was about time π
The first two lines will be inserted, and the ntpconf_header_set will be set immediately. After that, two more lines will be inserted but only if the is_mcast class is defined.
Then there are a few set of promises that all depend on an has_* class to be defined, so they'll all be skipped at the first pass. The first one that could be applied at this run, if conditions are met, is the one that depends on the class mcastclient only.
And then comes the second pass. This time all conditions are met to set the options variable; its value will depend on being a multicast client or not. As for classes, all has_* classes and latepass will be now defined. Let's see what happens when we go through the insert_lines promises.
The first two promises, which set the header, are already kept, so they are skipped. And that's good.
If is_mcast is defined, this promise is already kept as well, and skipped at this stage. Good again.
Then comes the set of promises which couldn't be worked on before due to the has_* classes being undefined. But this time is different, so "server" and "peer" lines will be output if needed, and a multicast server will be configured if so desired.
The promise depending on mcastclient only was already managed before and won't need any further treatment, so it is skipped.
The last set of promises depends on has_xen and latepass, which are now defined (has_xen is defined because $(c) actually has a "xen" key in our example). Note also that the promises depending on latepass ("Access control restrictions") use the variable $(options) that we set just in time, since it depended on classes that were defined at the beginning of pass 2. If we used, e.g., latepass.mcastclient instead, these promises would have been skipped because latepass was not yet defined at that stage, so $(options) won't be set, and we'd have the wrong lines inserted at the second pass (with a literal "$(options)" string appearing in the configuration. Ugh :yuck:)
And that's basically it. Cfengine will run one more pass through this bundle, but it will find all promises are already kept and won't do anything more. Same thing when it will check the other bundles up in the call stack: we are all set. But how do we check if these promises are working? That's easy.
To check if we can generate a correct configuration for a multicast server, we'll run something like:
cf-agent -K -D test_mcast,test_server -f /var/cfengine/inputs/tt3.cf
and we'll get this file:
# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help driftfile /var/lib/ntp/ntp.drift keys /etc/ntp/ntp.keys trustedkey 2 4 8 16 # Upstream servers server time1 server time2 server time3 server time4 # Peer servers peer peer1.oslo.osa peer peer2.oslo.osa broadcast 224.0.1.1 key 2 ttl 7 # Apply xen workaround disable kernel # Access control restrictions restrict -4 default kod notrap nomodify nopeer noquery restrict -6 default kod notrap nomodify nopeer noquery restrict 127.0.0.1 restrict ::1
Nicely formatted and human readable, isn't it.
OK, so I proved I can make it. It's time to try the new templates out now, but that will come in another post. Maybe π