Thanks to Nick Anderson and Aleksey Tsalolikhin for feedback and valuable insight.
Purpose
In this document I will show you how autorun and meta tags will simplify your daily work with CFEngine. There will be no more hard coding of bundles in bundlesequence and you may still run bundles in order by name.
Prerequisite
This document assumes that you have installed a binary package from
CFEngine’s official site cfengine.com. The code in this document is
tested with CFEngine community version 3.6.5. All paths are relative to
/var/cfengine/inputs
unless stated otherwise. For an introduction
to CFEngine please see
here. All
files created in this post shall be put in services/autorun
.
Autorun
Autorun enables you to create a dynamic bundlesequence without hard coding the names of bundles. The meta promise type let you tag bundles for easy look up. Autorun is disabled by default. In def.cf, change the line:
"services_autorun" expression => "!any";
to:
"services_autorun" expression => "any";
This will run all bundles in services/autorun with tag autorun:
meta:
"tags" slist => { "autorun" };
For this blog post we only want one bundle with this tag. Create my_autorun.cf with the following contents:
body file control
{
inputs => { @(my_autorun.inputs) };
}
bundle agent my_autorun
{
meta:
"tags" slist => { "autorun" };
vars:
any::
"role" string => "none";
"osrole" string => ifelse("sunos_5_10.db", "sunos_5.10_db",
"redhat_6.app", "redhat_6_app",
"aix_6_1.web", "aix_6.1_web",
"none");
web::
"role" string => "web";
app::
"role" string => "app";
db::
"role" string => "db";
any::
"bundle_input" slist =>
bundlesmatching(".*",
"^(any|$(sys.class)|$(sys.flavour)|$(sys.uqhost)|$(role)|$(osrole))$");
"inputs" slist => findfiles("$(this.promise_dirname)/*/*\.cf");
list_populated::
"bundle" slist => sort("bundle_input","lex");
classes:
# Stop spam from sort function.
"list_populated" expression => isvariable("bundle_input");
# Create role class.
"web" or => { classmatch("www.*") };
"app" or => { classmatch("app.*") };
"db" or => { classmatch("db.*") };
methods:
"$(bundle)" usebundle => "$(bundle)",
comment => "Run all discovered bundles";
reports:
inform_mode::
"autorun is executing";
"$(this.bundle): found bundle $(bundle)";
}
The function findfiles() will look for all files with the .cf suffix in
all sub directories under services/autorun. The bundlesmatching() will
search for tags in bundles in files found. any
- Run on all hosts.
Even though this is a class, here it is just a string. $(sys.class)
- linux, solaris, aix etc.
$(sys.flavour)
- redhat_6, oracle_6, sunos_5.10, aix_6.1$(sys.uqhost)
- Unqualified hostname, ex: db01. Dashes allowed. No need to canonify.$(role)
- The role of a host. Can be anything. Ex web, app, db etc.$(osrole)
- OS and role combined like an and statement. It is important that all variables are defined otherwise the “bundles_input” list will be empty and no bundles will be found. It will match the whole string with (^….$) so on a Solaris 10 node the tag “sunos_5” will not match $(sys.flavour). Role is just an example of how you may classify your nodes. We check to see if list “bundle_input” is populated so the sort function has something to consume. The sorted list is looped over under methods. Bundles that need to be run in a specific order may be prefixed with a string that does not exist anywhere else. For example a three letter string followed by a digit. Names with lowercase a are run first. aaa1_first aaa2_second bundles beginning with zzz will run last: zzz1_second_to_last zzz2_last For global variables etc. accessible everywhere we create a file called global/global.cf:
bundle common G
{
meta:
"tags" slist => { "any" };
vars:
"cache_base" string => "/var/cache";
"cache" string => "$(G.cache_base)/cfengine";
reports:
"$(this.bundle)";
}
To make sure globals are run first we use an uppercase G. All other bundles must begin with lowercase letters and not a digit. AAA1 and 1aaa will run before G. We keep the paths as short as possible and G still tells us what it is all about. Bundles that are to be run everywhere regardless of OS or role goes into the file os/any.cf with tag any:
bundle agent aaa1_create_agent_cache
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
bundle agent aaa2_download_files
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
Bundles for Solaris goes into os/solaris.cf:
bundle agent my_solaris
{
meta:
"tags" slist => { "solaris" };
reports:
"$(this.bundle)";
}
Files for specific roles role/web.cf:
bundle agent my_web
{
meta:
"tags" slist => { "web" };
reports:
"$(this.bundle)";
}
role/app.cf:
bundle agent my_app
{
meta:
"tags" slist => { "app" };
reports:
"$(this.bundle)";
}
role/db.cf:
bundle agent my_db
{
meta:
"tags" slist => { "db" };
reports:
"$(this.bundle)";
}
Bundles that shall run last goes into last/last.cf:
bundle agent zzz1_second_to_last
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
bundle agent zzz2_last
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
Usually order between bundles does not matter but if it does you can use
a prefix to group them together. We append the following to os/any.cf
:
bundle agent nnn1_do_something_in_order
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
bundle agent nnn2_do_something_in_order
{
meta:
"tags" slist => { "any" };
reports:
"$(this.bundle)";
}
The structure in services/autorun will look like this:
my_autorun.cf
global/global.cf
os/any.cf
os/solaris.cf
role/app.cf
role/db.cf
role/web.cf
last/last.cf
With this extra sub directory layer bundle services_autorun will only find and parse my_autorun.cf. On a Solaris node called db01 we get:
/var/cfengine/bin/cf-agent -K
R: G
R: aaa1_create_agent_cache
R: aaa2_download_files
R: my_db
R: my_solaris
R: nnn1_do_something_in_order
R: nnn2_do_something_in_order
R: zzz1_second_to_last
R: zzz2_last
If you specify more than one tag you get an or:
meta:
"tags" slist => { "sunos_5.10","oracle_6" };
Creating an and statement can be achieved by using the ifelse() function. Here we have the variable osrole which combines OS and role.
"osrole" string => ifelse("sunos_5_10.db", "sunos_5.10_db",
"redhat_6.app", "redhat_6_app",
"aix_6_1.web", "aix_6.1_web",
"none");
You can of course create an and statement using classmatch(), it is up to you. You have to make sure that contents of services/autorun match your repository. Extra or old files can easily get you into duplicate definition of bundles. Purging contents periodically will minimize the risk. To enable purge, in update.cf and def.cf change:
"cfengine_internal_purge_policies" expression => "!any";
to
"cfengine_internal_purge_policies" expression => "any";
Known problem
I discovered that cf-promises is unable to catch syntax errors in bundles defined in sub directories under services/autorun. It finds the .cf files and seem to parse them but syntax is not checked. It does not help running:
cf-promises -c --eval-functions
As a workaround you can use:
cf-agent -nK
For more information please look here: https://dev.cfengine.com/issues/7231
Conclusion
You can have several bundles per “type” of tag and you don’t have to canonify hostnames. Autorun and meta tags will help you add policies faster than a traditional bundlesequence.
Next step
This blog post started out as a thread in the CFEngine help forum: https://groups.google.com/forum/#!topic/help-cfengine/tj1vbpula7o Please feel free to join or give feedback.