Feature Friday Feature Friday #20: Macros

Posted by Nick Anderson
July 26, 2024

Did you know CFEngine has Macros?

They were first introduced in CFEngine 3.7 (back in 2015), and more have been introduced since then. Macros are convenient for preventing the parsing of a section of the policy. It is handy for protecting older binaries from getting tripped up on newer syntax the agent does not understand. Let’s take a look.

Currently there are 8 macros.

  • minimum_version - Prevent the section of policy from being parsed unless the agent meets a minimum version.
  • maximum_version - Prevent the section of policy from being parsed when the agent exceeds a maximum version.
  • at_version - Prevents the section of policy from being parsed unless the agent is of a specific version.
  • between_versions - Prevents a section of policy from being parsed unless the agent is between (inclusive) a minimum and maximum version.
  • before_version - Prevents a section of policy from being parsed unless the agent is below a specified version (not inclusive).
  • after_version - Prevents a section of policy from being parsed unless the agent is above a specified version (not inclusive).
  • else - Allows the agent to parse a section of policy only if the preceding macro is not applicable.
  • feature - Prevents a section of policy from being parsed based on feature availability.

You can find examples of use within the Masterfiles Policy Framework. For example, body action fresh_systemd_state uses the minimum_version macro to control the setting of ifelapsed => "0" to versions 3.18.1 and higher since versions below might produce a warning.

body action fresh_systemd_state
# @brief An 'action' body ensuring the state information for a systemd service is always fresh
#
# This 'action' body disables caching for functions, in particular the
# execresult*() family of functions.
#
# Although it's now the same as the 'immediate' action body, this may change in
# the future.
{
    cfengine::
      # ^Needed for versions 3.15.x and older that did not
      # support empty bodies. When 3.15.x is no longer
      # supported, this can be removed.

@if minimum_version(3.18.1)
      # Beginning with 3.18.1 ifelapsed being set to 0 results in bypassing of
      # function caching. In versions prior to 3.18, if an action body with
      # ifelapsed set to 0 was used in a vars type promise a warning was
      # emitted. This is guarded to suppress that warning in older versions
      # where this setting would not change the behavior.

    ifelapsed => "0";
@endif
}

I think that is a lot more manageable as opposed to using class expressions, which would get quite lengthy if we wanted to extend it to cover even older versions:

body action fresh_systemd_state
# @brief An 'action' body ensuring the state information for a systemd service is always fresh
#
# This 'action' body disables caching for functions, in particular the
# execresult*() family of functions.
#
# Although it's now the same as the 'immediate' action body, this may change in
# the future.
{
    cfengine::
      # ^Needed for versions 3.15.x and older that did not
      # support empty bodies. When 3.15.x is no longer
      # supported, this can be removed.

    !(cfengine_3_18_0|cfengine_3_17|cfengine_3_16|cfengine_3_15)::
      # Beginning with 3.18.1 ifelapsed being set to 0 results in bypassing of
      # function caching. In versions prior to 3.18, if an action body with
      # ifelapsed set to 0 was used in a vars type promise a warning was
      # emitted. This is guarded to suppress that warning in older versions
      # where this setting would not change the behavior.

    ifelapsed => "0";
}

The feature macro can conveniently be abused. For example, you could include content you don’t want CFEngine to parse, perhaps inline documentation or examples.

bundle agent __main__
{

@if feature(documentation_markdown)
# My embedded Markdown
* list item
```sh
    echo hello world!
```
@endif

  vars:

@if feature(example__etc_os_release)
PRETTY_NAME="postmarketOS v23.12"
NAME="postmarketOS"
VERSION_ID="v23.12"
VERSION="v23.12"
ID="postmarketos"
ID_LIKE="alpine"
HOME_URL="https://www.postmarketos.org/"
SUPPORT_URL="https://gitlab.com/postmarketOS"
BUG_REPORT_URL="https://gitlab.com/postmarketOS/pmaports/issues"
LOGO="postmarketos-logo"
@endif
    "os_release" data => readenvfile( "/etc/os-release" );
}

As long as the parameter to the feature macro is not a feature class provided by the agent, the content won’t be parsed by CFEngine. Hence, you don’t have to prefix lines with hash comments. Accordingly, the copying/pasting of inner content is more practical.

I hope you can find some good uses for macros.

Happy Friday! 🎉