Extending autorun

Posted by Nick Anderson
August 11, 2022

What’s autorun?

Autorun is a feature of the Masterfiles Policy Framework (MPF)1 that simplifies the process of adding and executing new policy.

We have talked about Modular policies with autorun and the Augments before. This time, we dig into autorun a bit deeper to explore some of its current features and look at how to implement your own as we did during The agent is in, Episode 15 - Extending autorun

Note: All paths unless otherwise noted are relative to the root of your policy set (typically /var/cfengine/masterfiles is the distribution point). cf-agent and other commands are run as the root user.

When enabled (by defining the default:services_autorun class) .cf suffixed files located in services/autorun are automatically included in inputs2 and bundles tagged with autorun are actuated in lexical order (a bundle gets tagged by having a meta promise, "tags", defined as a list with "autorun" as a value).

Let’s try it out.

Taking autorun for a spin

First, add the following policy in services/autorun/example.cf.

bundle agent __main__
# @brief Drive default bundlesequence if called as policy entry
{
  methods:
    "illustrating autorun"
      usebundle => example();
}
bundle agent example()
# @brief example illustrating autorun
{
  meta:
    "tags" slist => { "autorun" };

  reports:
    "Hello World! from '$(this.namespace):$(this.bundle)' in '$(this.promise_filename)'";
}

Now, let’s run the policy with cf-agent --no-lock --file update.cf; cf-agent --no-lock.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock

Nothing happened, as expected since we neglected to define the services_autorun class. Let’s try it again, this time with the appropriate class defined.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock --define services_autorun
R: Hello World! from 'default:example' in '/var/cfengine/inputs/services/autorun/example.cf'

Great! It works.

But, what if we want to load and run policy files that live in some other directory?

Additional autorun directories

When autorun is enabled the MPF will also look for .cf suffixed files in directories defined in default:def.mpf_extra_autorun_inputs in addition to services/autorun.

Let’s move our example policy file to a different directory. Create a MyAutorun directory and move the example policy there.

$ mkdir -p /var/cfengine/masterfiles/MyAutorun
$ mv /var/cfengine/masterfiles/services/autorun/example.cf /var/cfengine/masterfiles/MyAutorun/

Next, let’s use Augments3 to enable autorun and define the path to our additional directory. Populate def.json with the following content.

{
  "classes": {
    "default:services_autorun": {
      "class_expressions": ["any::"],
      "comment": "This simplifies the process of integrating new policy. Simply drop it in place."
    }
  },
  "variables": {
    "default:def.mpf_extra_autorun_inputs": {
      "value": ["MyAutorun"],
      "comment": "Unqualified directories relative to policy entry dirname and fully qualified directories that we should load policy files from"
    }
  }
}

Note: In the above JSON we use an Augments format that is valid for CFEngine agents version 3.18.0 and greater which allow us to be explicit in targeting the namespace a class should be defined in, and the namespace and bundle variables should be defined in, as well as applying other meta-data like tags and comments.

This is an equivalent version that works for older versions as well.

{
  "classes": {
    "services_autorun": ["any::"]
  },
  "vars": {
    "mpf_extra_autorun_inputs": ["MyAutorun"]
  }
}

Now that autorun is configured, run the policy again, this time without having to explicitly define services_autorun since we defined it in Augments.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock
R: Hello World! from 'default:example' in '/var/cfengine/inputs/MyAutorun/./example.cf'

Note, you are not limited to including policy that is within your main policy set. Policy can be included from anywhere. Create /etc/cfengine/inputs-annex/another-example.cf with the following content.

bundle agent another_example()
{
  meta:
    "tags" slist => { "autorun" };

  reports:
    "Hello World! from '$(this.namespace):$(this.bundle)' in '$(this.promise_filename)'";
}

Now, this policy file is outside of our policy set so be sure to set appropriate permissions on the file.

$ mkdir -p /etc/cfengine/inputs-annex
$ vi /etc/cfengine/inputs-annex/another-example.cf
$ chmod 600 /etc/cfengine/inputs-annex/another-example.cf

Edit def.json in the root of your policy set and add the new directory.

{
  "classes": {
    "default:services_autorun": {
      "class_expressions": ["any::"],
      "comment": "This simplifies the process of integrating new policy. Simply drop it in place."
    }
  },
  "variables": {
    "default:def.mpf_extra_autorun_inputs": {
      "value": ["MyAutorun", "/etc/cfengine/inputs-annex"],
      "comment": "Unqualified directories relative to policy entry dirname and fully qualified directories that we should load policy files from"
    }
  }
}

Run the policy again to prove that the new policy file outside the policy set was picked up as expected.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock
R: Hello World! from 'default:another_example' in '/etc/cfengine/inputs-annex/./another-example.cf'
R: Hello World! from 'default:example' in '/var/cfengine/inputs/MyAutorun/./example.cf'

Great, it works! But, what if you only want part of that behavior, for example to only find and load policy files but not automatically actuate any bundles?

Auto inputs (without autorun)

Note: This feature was first introduced to the MPF in CFEngine 3.18.1. Unfortunately, a bug prevented it from working until 3.18.3 (which has yet to be released, so please use nightlies if you would like to enjoy this feature before then).

Let’s amend def.json and switch from default:services_autorun to default:services_autorun_inputs.

After editing it should look similar to the following.

{
  "classes": {
    "default:services_autorun_inputs": {
      "class_expressions": ["any::"],
      "comment": "This simplifies the process of integrating new policy inputs. Simply drop them in services/autorun or any of the directories listed in default:def.mpf_extra_autorun_inputs."
    }
  },
  "variables": {
    "default:def.mpf_extra_autorun_inputs": {
      "value": ["MyAutorun", "/etc/cfengine/inputs-annex"],
      "comment": "Unqualified directories relative to policy entry dirname and fully qualified directories that we should load policy files from"
    }
  }
}

With that in place, we can see that running cf-agent --no-lock --file update.cf; cf-agent --no-lock does not result in the same reports being emitted as we saw previously.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock

Now, let’s explicitly run default:another_example to run by editing services/main.cf as shown below.

###############################################################################
#
# bundle agent main
#  - User/Site policy entry
#
###############################################################################

bundle agent main
# User Defined Service Catalogue
{
  methods:
      # Activate your custom policies here

      "Actuating a bundle in a file that was autoloaded"
        usebundle => default:another_example;
}

Now, when we run the policy, we see that the bundle from an automatically included file was found and emitted the expected report.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock
R: Hello World! from 'default:another_example' in '/etc/cfengine/inputs-annex/./another-example.cf'

Some people (including yours truly) are uncomfortable with dynamically loading policy files and prefer to explicitly enumerate all policy inputs, let’s take a look at automatically running bundles without automatically loading inputs.

Auto run bundle (without inputs)

The MPF supports this methodology via the default:services_autorun_bundles class. When defined (assuming that neither services_autorun or services_autorun_inputs is defined) no policy files will be automatically added to inputs. However, any bundle tagged with autorun will be actuated in lexical order.

Edit def.json to explicitly include MyAutorun/example.cf and switch from defining the class default:services_autorun_inputs to default:services_autorun_bundles as shown below.

{
  "inputs": ["MyAutorun/example.cf"],
  "classes": {
    "default:services_autorun_bundles": {
      "class_expressions": ["any::"],
      "comment": "This simplifies the process of running bundles."
    }
  },
  "variables": {
    "default:def.mpf_extra_autorun_inputs": {
      "value": ["/etc/cfengine/inputs-annex"],
      "comment": "Unqualified directories relative to policy entry dirname and fully qualified directories that we should load policy files from"
    }
  }
}

Comment out or remove the promise to run default:another_example from services/main.cf or you will get errors due to another-example.cf no longer being loaded. For example, if you neglect to comment out or remove the promise, expect to get an error similar to this:

error: Apparent bundle 'default:another_example' was undeclared, but used in a promise near line 14 of /var/cfengine/inputs/services/main.cf (possible unquoted literal value)
error: A method attempted to use a bundle 'default:another_example' that was apparently not defined
error: Fatal CFEngine error: Aborting due to missing bundle 'default:another_example'

Even though we left default:def.mpf_extra_autorun_inputs defined, since neither of the classes default:services_autorun or default:services_autorun_inputs are defined, the policy file was not parsed as part of inputs and thus the bundles from those files were not found. When executing the policy we find that only the bundle from the explicitly included file is actuated and emits a report.

$ cf-agent --no-lock --file update.cf; cf-agent --no-lock
R: Hello World! from 'default:example' in '/var/cfengine/inputs/MyAutorun/example.cf'

But, what if you want more? What if you want to descend into sub-directories to find policy files or use other logic to select which policy files should be loaded and which bundles should be actuated? For that, consider implementing your own autorun.

Implementing your own autorun

To implement your own autorun mechanism, you need a few things:

  • Some policy files to find and load as part of inputs
  • A bundle to find the policy files
  • A file control body to load the found files
  • A bundle to find and run bundles
  • Configuration to integrate your custom autorun policy

Policy files to load

So that we have something to work with, let’s generate some contrived policy and structure. Please do not consider any of this arrangement as an endorsement of any specific layout, it’s simply something to work with.

All of these policy files will share this common pattern:

# Generated contrived policy file ¯\_(ツ)_/¯
bundle agent __main__
{
  methods:
    "canonified_policy_filename";
}
bundle agent canonified_policy_filename
{
  meta:
    "tags"
      slist => { "my_autorun", "<odd|even>", "<unqualified_dirname_of_policy_file>"};

  reports:
    "From bundle '$(this.namespace):$(this.bundle)' tagged with '$(with)' in '$(this.promise_filename)'"
      with => join( ", ", "$(this.namespace):$(this.bundle)_meta.tags" );
}

This will allow us to easily see which bundles get automatically run and where they are from.

exec 2>&1
rm -rf /tmp/services
for each in /tmp/services/MyPolicies/onefish/twofish.cf \
                /tmp/services/MyPolicies/SomeWhere/OutThere.cf \
                /tmp/services/MyPolicies/beneath_the_pale/moonlight.cf \
                /tmp/services/MyPolicies/DOW/Monday/monday.cf \
                /tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf \
                /tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf \
                /tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf \
                /tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf \
                /tmp/services/MyPolicies/DOW/Thursday/thursday.cf \
                /tmp/services/MyPolicies/DOW/Friday/then.cf \
                /tmp/services/MyPolicies/DOW/Saturday/sat.cf \
                /tmp/services/MyPolicies/DOW/Sunday/what.cf \
                /tmp/services/MyPolicies/Platform/linux/three.cf \
                /tmp/services/MyPolicies/more/enabled/four.cf \
                /tmp/services/MyPolicies/more/disabled/five.cf; do
    chars=$(echo -n ${each} | wc -c )
    dirname=$(dirname ${each})
    dir_basename=$(basename ${dirname})
    bundlename=$(basename --suffix=".cf" ${each} | tr [[:punct:][:space:]] _  | sed 's/_$//')

    if [ $((chars%2)) -eq 0 ]; then
        odd_even="even"
    else
        odd_even="odd"
    fi
    mkdir -p $(dirname $each)
    printf "# Generated contrived policy file ¯\_(ツ)_/¯
bundle agent __main__
{
    methods: \"%s\";
}
bundle agent %s
{
  meta:
    \"tags\" slist => { \"my_autorun\", \"%s\", \"%s\" };

  reports:
    \"From bundle '\$(this.namespace):\$(this.bundle)' tagged with '\$(with)' in '\$(this.promise_filename)'\"
      with => join( \", \", \"\$(this.namespace):\$(this.bundle)_meta.tags\" );
}
" ${bundlename} ${bundlename} ${odd_even} ${dir_basename} > ${each}
    chmod 600 ${each}
    lastfile=${each}
    printf "Created '${each}'\n"
done
printf "Preview '${each}':\n$(cat ${each})\n"
printf "Done generating contrived policy"
:

Now that we have some policy files to find let’s get to it.

A bundle to find policy files

All of the policy files generated previously are within services/MyPolicies. Let’s start by recursively searching for .cf suffixed files.

bundle agent find_my_policies_recursively
{
  vars:
    "found" slist => findfiles( "/tmp/services/MyPolicies/**/*.cf" );
  reports:
    "Found '$(found)'";
}

Running the policy, we see that all the various policy files are found as desired.

$ cf-agent --no-lock --log-level info --bundlesequence find_my_policies_recursively --file find_my_policies_recursively.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: Found '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: Found '/tmp/services/MyPolicies/more/disabled/five.cf'
R: Found '/tmp/services/MyPolicies/more/enabled/four.cf'
R: Found '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: Found '/tmp/services/MyPolicies/DOW/Sunday/what.cf'
R: Found '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: Found '/tmp/services/MyPolicies/onefish/twofish.cf'

Now, let’s take a look at some variations on the idea.

Find policy files that are relevant for this day of the week

Here we look for files in directories named for the current day of the week. This can be useful if you have policies that you only want to run during certain periods of time, the rest of the time don’t bother loading the policy files you won’t use.

bundle agent find_my_policies_for_this_dow
{
  vars:
    "today"
      string => strftime( "localtime", "%A", now() );
    "found"
      slist => findfiles( "/tmp/services/MyPolicies/DOW/$(today)/**.cf" );
  reports:
    "$(today) Found '$(found)'";
}

Running the policy, we see only the policy files under a directory named for the current day of the week are found.

$ cf-agent --bundlesequence find_my_policies_for_this_dow --no-lock --log-level info --file find_my_policies_for_this_dow.cf
    info: Using command line specified bundlesequence
R: Monday Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'

Find policy files that are relevant for the current platform

How about some policies that are platform specific, for example that only run on linux.

bundle agent find_my_policies_for_my_platform
{
  vars:
    "found"
      slist => findfiles( "/tmp/services/MyPolicies/**/$(sys.class)/*.cf" );

  reports:
    "Found '$(found)'";
}

We can see from running the policy that only policies under a directory named linux are found.

$ cf-agent --bundlesequence find_my_policies_for_my_platform --no-lock --log-level info --file find_my_policies_for_my_platform.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'

Tip: sys.os_release contains more specific info on most modern linux systems where /etc/os-release is present.

Find policy files who’s parent directory maps to a class suffixed with enabled

In this variation, we only want to use a file if we have a class that indicates we need it. This is a handy pattern for only parsing files which are needed by an individual host and reducing execution overhead.

bundle agent find_my_policies_in_enabled_dirs
{
  classes:
    # Classes defined here for examples sake. Consider using Augments
    # so that your classification information is ready from the very
    # start of cf-agent initialization
    "SomeWhere_enabled";
    "beneath_the_pale_enabled";

  vars:
    "candidates"
      slist => findfiles( "/tmp/services/MyPolicies/**/*.cf" );
    "picked[$(candidates)]"
      string => "$(candidates)",
      with => lastnode( dirname( $(candidates) ), "/" ),
      if => "$(with)_enabled";
    "found"
      slist => getindices( picked );

  reports:
    "Found '$(found)'";
}

Executing the policy we see that only policy files below the directories named for the classes with _enabled suffixes were found.

$ cf-agent --bundlesequence find_my_policies_in_enabled_dirs --no-lock --log-level info --file find_my_policies_in_enabled_dirs.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'

Find policy files that are not in a directory named disabled

How about omitting policies that are in directories named disabled?

bundle agent find_my_policies_for_my_platform
{
  vars:
    "candidates"
      slist => findfiles( "/tmp/services/MyPolicies/**/*.cf" );
    "found"
      slist => filter(".*/disabled/.*", candidates, true, true, inf );

  reports:
    "Found '$(found)'";
}

Again, we see that none of the found policy files live inside a directory named disabled.

$ cf-agent --bundlesequence find_my_policies_for_my_platform --no-lock --log-level info --file find_my_policies_for_my_platform.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: Found '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: Found '/tmp/services/MyPolicies/more/enabled/four.cf'
R: Found '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: Found '/tmp/services/MyPolicies/DOW/Sunday/what.cf'
R: Found '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: Found '/tmp/services/MyPolicies/onefish/twofish.cf'

Hopefully this gives you some ideas about the possibilities.

body file control to load the found files

Once the files we want to include in inputs have been found we need to get them parsed along with the rest of inputs. For that, we use body file control and simply reference the variable that holds the list of file paths we want to include. Here we use the first example to find policy files recursively.

bundle agent find_my_policies_recursively
{
  vars:
    "found"
      slist => findfiles( "/tmp/services/MyPolicies/**/*.cf" );
  reports:
    "Found '$(found)'";
}
body file control
{
  agent::
    inputs => { @(default:find_my_policies_recursively.found) };
}

Note: We guard the inclusion of the files with the class agent, this protects other components like cf-serverd, cf-monitord, and cf-execd from trying to parse the files.

A bundle to find and run bundles

Now that we have included the files in inputs we can proceed to make a bundle to run our bundles. Note: this methodology does not restrict itself to bundles that live within a specific directory. Bundles for running need to be found based on their name and any tags they have and the bundles must not take any parameters.

bundle agent find_and_run_bundles_tagged_my_autorun
{
  vars:
    "b"
      slist => sort( bundlesmatching( ".*", "my_autorun" ), lex );

  methods:
    "$(b)";
}

When we combine that with our bundle to find policy files and body file control to load the found policy files to inputs we get the following.

bundle agent find_my_policies_recursively
{
  vars:
    "found" slist => findfiles( "/tmp/services/MyPolicies/**/*.cf" );
  reports:
    "Found '$(found)'";
}
bundle agent find_and_run_bundles_tagged_my_autorun
{
  vars:
    "b" slist => sort( bundlesmatching( ".*", "my_autorun" ), lex );
  methods:
    "$(b)";
}
body file control
{
  agent::
    inputs => { @(default:find_my_policies_recursively.found) };
}

If we run it we can see that all the policy files are found as before and this time all the bundles tagged with my_autorun were actuated.

$ cf-agent --no-lock --log-level info --bundlesequence find_my_policies_recursively,find_and_run_bundles_tagged_my_autorun --file find_my_policies_recursively_with_find_and_run_bundles_tagged_my_autorun.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: Found '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: Found '/tmp/services/MyPolicies/more/disabled/five.cf'
R: Found '/tmp/services/MyPolicies/more/enabled/four.cf'
R: Found '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: Found '/tmp/services/MyPolicies/DOW/Sunday/what.cf'
R: Found '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: Found '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:OutThere' tagged with 'my_autorun, even, SomeWhere' in '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: From bundle 'default:five' tagged with 'my_autorun, even, disabled' in '/tmp/services/MyPolicies/more/disabled/five.cf'
R: From bundle 'default:four' tagged with 'my_autorun, odd, enabled' in '/tmp/services/MyPolicies/more/enabled/four.cf'
R: From bundle 'default:monday' tagged with 'my_autorun, odd, Monday' in '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: From bundle 'default:moonlight' tagged with 'my_autorun, even, beneath_the_pale' in '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: From bundle 'default:patch_tuesday' tagged with 'my_autorun, odd, Tuesday' in '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: From bundle 'default:sat' tagged with 'my_autorun, even, Saturday' in '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: From bundle 'default:then' tagged with 'my_autorun, odd, Friday' in '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: From bundle 'default:three' tagged with 'my_autorun, even, linux' in '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: From bundle 'default:thursday' tagged with 'my_autorun, odd, Thursday' in '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: From bundle 'default:twofish' tagged with 'my_autorun, odd, onefish' in '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:wed_afternoon' tagged with 'my_autorun, even, pm' in '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: From bundle 'default:wed_morn' tagged with 'my_autorun, odd, am' in '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: From bundle 'default:wednesda' tagged with 'my_autorun, even, Wednesday' in '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: From bundle 'default:what' tagged with 'my_autorun, odd, Sunday' in '/tmp/services/MyPolicies/DOW/Sunday/what.cf'

As with finding files, we can take it further. Let’s try a variation.

Find and run bundles tagged with my_autorun & odd

Let’s find and run the bundles that are tagged with both my_autorun and odd.

Since bundlesmatching() function takes n tags that are OR’d we need to do two searches and find the intersecting bundles.

bundle agent find_and_run_bundles_tagged_my_autorun_and_odd
{
  vars:
    "b_my_autorun"
      slist => sort( bundlesmatching( ".*", "my_autorun" ), lex );
    "b_odd"
      slist => sort( bundlesmatching( ".*", "odd" ), lex );
    "b"
      slist => sort( intersection( b_my_autorun, b_odd ) );

  methods:
    "$(b)";
}

Running the policy we can see that it finds all the same policy files but only the subset tagged with both my_autorun and odd emit the reports indicating they were actuated.

$ cf-agent --no-lock --log-level info --bundlesequence find_my_policies_recursively,find_and_run_bundles_tagged_my_autorun_and_odd --file find_my_policies_recursively_with_find_and_run_bundles_tagged_my_autorun_and_odd.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: Found '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: Found '/tmp/services/MyPolicies/more/disabled/five.cf'
R: Found '/tmp/services/MyPolicies/more/enabled/four.cf'
R: Found '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: Found '/tmp/services/MyPolicies/DOW/Sunday/what.cf'
R: Found '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: Found '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:four' tagged with 'my_autorun, odd, enabled' in '/tmp/services/MyPolicies/more/enabled/four.cf'
R: From bundle 'default:monday' tagged with 'my_autorun, odd, Monday' in '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: From bundle 'default:patch_tuesday' tagged with 'my_autorun, odd, Tuesday' in '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: From bundle 'default:then' tagged with 'my_autorun, odd, Friday' in '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: From bundle 'default:thursday' tagged with 'my_autorun, odd, Thursday' in '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: From bundle 'default:twofish' tagged with 'my_autorun, odd, onefish' in '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:wed_morn' tagged with 'my_autorun, odd, am' in '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: From bundle 'default:what' tagged with 'my_autorun, odd, Sunday' in '/tmp/services/MyPolicies/DOW/Sunday/what.cf'

We can even compress this down into a single statement.

bundle agent find_and_run_bundles_tagged_my_autorun_and_odd
{
  vars:
    "b"
      slist => sort( intersection(
          sort( bundlesmatching( ".*", "my_autorun" ), lex ),
          sort( bundlesmatching( ".*", "odd" ), lex ) )
      );

  methods:
    "$(b)";
}

Running the policy again, we get the same result.

$ cf-agent --no-lock --log-level info --bundlesequence find_my_policies_recursively,find_and_run_bundles_tagged_my_autorun_and_odd --file find_my_policies_recursively_with_find_and_run_bundles_tagged_my_autorun_and_odd.cf
    info: Using command line specified bundlesequence
R: Found '/tmp/services/MyPolicies/DOW/Saturday/sat.cf'
R: Found '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: Found '/tmp/services/MyPolicies/beneath_the_pale/moonlight.cf'
R: Found '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: Found '/tmp/services/MyPolicies/Platform/linux/three.cf'
R: Found '/tmp/services/MyPolicies/SomeWhere/OutThere.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/wednesda.cf'
R: Found '/tmp/services/MyPolicies/more/disabled/five.cf'
R: Found '/tmp/services/MyPolicies/more/enabled/four.cf'
R: Found '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/pm/wed-afternoon.cf'
R: Found '/tmp/services/MyPolicies/DOW/Sunday/what.cf'
R: Found '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: Found '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: Found '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:four' tagged with 'my_autorun, odd, enabled' in '/tmp/services/MyPolicies/more/enabled/four.cf'
R: From bundle 'default:monday' tagged with 'my_autorun, odd, Monday' in '/tmp/services/MyPolicies/DOW/Monday/monday.cf'
R: From bundle 'default:patch_tuesday' tagged with 'my_autorun, odd, Tuesday' in '/tmp/services/MyPolicies/DOW/Tuesday/patch-tuesday.cf'
R: From bundle 'default:then' tagged with 'my_autorun, odd, Friday' in '/tmp/services/MyPolicies/DOW/Friday/then.cf'
R: From bundle 'default:thursday' tagged with 'my_autorun, odd, Thursday' in '/tmp/services/MyPolicies/DOW/Thursday/thursday.cf'
R: From bundle 'default:twofish' tagged with 'my_autorun, odd, onefish' in '/tmp/services/MyPolicies/onefish/twofish.cf'
R: From bundle 'default:wed_morn' tagged with 'my_autorun, odd, am' in '/tmp/services/MyPolicies/DOW/Wednesday/am/wed-morn.cf'
R: From bundle 'default:what' tagged with 'my_autorun, odd, Sunday' in '/tmp/services/MyPolicies/DOW/Sunday/what.cf'

More variations are left to the reader, if you come up with some clever patterns, please share them on GitHub discussions , the mailing list and or chat via #cfengine on irc.libera.chat or gitter.im/cfengine.core (also available via Matrix).

Configuration to activate your custom autorun policy

Now the last step is to configure the policies so they are run as part of the normal scheduled executions. For that, we use Augments, def.json.

We place the finding of policy files early in the bundlesequence using default:def.control_common_bundlesequence_classification and then find and run bundles at the end of the main bundlesequence using default:def.control_common_bundlesequence_end. If you have autorun policies for classification, consider running those early as well so that the variables and classes are resolved by the time they get to any of your other policies that use them.

{
  "inputs": [
    "find_my_policies_recursively_with_find_and_run_bundles_tagged_my_autorun_and_odd_short.cf"
  ],
  "variables": {
    "default:def.control_common_bundlesequence_classification": {
      "value": ["find_my_policies_recursively"],
      "comment": "Unqualified directories relative to policy entry dirname and fully qualified directories that we should load policy files from"
    },
    "default:def.control_common_bundlesequence_end": {
      "value": ["find_and_run_bundles_tagged_my_autorun_and_odd_short"],
      "comment": "Find and run bundles tagged with both 'my_autorun' and 'odd'"
    }
  }
}

  1. The Masterfiles Policy framework is the default policy set, with documentation here: https://docs.cfengine.com/docs/master/reference-masterfiles-policy-framework.html↩︎

  2. inputs is the list of policy files which will be loaded by CFEngine. After a policy is loaded, you still need some way to specify which bundles inside it should be run. This can be achieved through methods promises, autorun (tags), or modifying the bundlesequence↩︎

  3. Augments, sometimes referred to as def.json, allows you to easily define classes, variables, input files, and more, through a JSON file without modifying .cf policy files; https://docs.cfengine.com/docs/master/reference-language-concepts-augments.html↩︎