Feature Friday #39: The power of lists and implicit iteration

Posted by Nick Anderson
December 6, 2024

Implicit list iteration in CFEngine is quite a unique and novel feature. Today we look at a practice example showing how lists can improve the readability and maintainability of your policy.

A novel feature in CFEngine is how a list variable is iterated when referenced as a scalar ($(variable)).

Let’s take a look at a contrived example. Here we see a list of strings (slist) defined as toys and we have a single reports promise to emit toys we want to play with.

/tmp/feature-friday-39-toys.cf
bundle agent __main__
{
  vars:

    "toys" slist => { "Legos",
                      "Dolls",
                      "Action figures",
                      "Play-Doh" };

  reports:
    "Let's play with $(toys)";
}

Running the example we see that the report is emitted for each toy in our list as a separate line. This is effectively making 4 individual promises, expressed in a single statement.

command
# cf-agent --no-lock --log-level info \
  --file /tmp/feature-friday-39-toys.cf
output
R: Let's play with Legos
R: Let's play with Dolls
R: Let's play with Action figures
R: Let's play with Play-Doh

Now, let’s move on to a more practice example. Let’s say that you want to inventory the amount of free disk space available for various paths. Here we have several variables defining the amount of free disk space for each and then we have several more variables that do some math to make the data more user friendly which is actually tagged for inventory.

/tmp/feature-friday-39-no-iteration.cf
bundle agent __main__
{
  vars:
      "boot_disk_free" int => diskfree("/boot");
      "root_disk_free" int => diskfree("/");
      "usr_disk_free"  int => diskfree("/usr");
      "var_disk_free"  int => diskfree("/var");

      "boot_disk_free_mb"
        string => eval("$(boot_disk_free) / 1024)","math", "infix"),
        meta => { "inventory", "attribute_name=Disk free boot (MB)" };

      "root_disk_free_mb"
        string => eval("$(root_disk_free) / 1024)", "math", "infix"),
        meta => { "inventory", "attribute_name=Disk free root (MB)" };

      "usr_disk_free_mb"
        string => eval("$(usr_disk_free) / 1024)", "math", "infix"),
        meta => { "inventory", "attribute_name=Disk free usr (MB)" };

      "var_disk_free_mb"
        string => eval("$(var_disk_free) / 1024)", "math", "infix"),
        meta => { "inventory", "attribute_name=Disk free var (MB)" };

  reports:
      "/boot has $(boot_disk_free_mb) MB of free disk space.";
      "/ has $(root_disk_free_mb) MB of free disk space.";
      "/usr has $(usr_disk_free_mb) MB of free disk space.";
      "/var has $(var_disk_free_mb) MB of free disk space.";

}

Running the policy we see it works without issue:

command
# cf-agent --no-lock --log-level info \
  --file /tmp/feature-friday-39-no-iteration.cf
output
R: /boot has 3454.355469 MB of free disk space.
R: / has 1486371.406250 MB of free disk space.
R: /usr has 1486371.406250 MB of free disk space.
R: /var has 1486371.406250 MB of free disk space.

But, using a list can significantly improve this policy. Let’s refactor it so that we define a list of disks. Then we can iterate over that list of disks to get the free disk space for each while calculating the more human friendly value. You can see the policy required is significantly less.

/tmp/feature-friday-39-iteration.cf
bundle agent __main__
{
    vars:
      "disks" slist => { "/boot", "/", "/usr", "/var" };

      "disk_free_mb[$(disks)]"
        string => eval("$(with) / 1024", "math", "infix"),
        meta => { "inventory", "attribute_name=Disk free $(disks) (MB)" },
        with => diskfree("$(disks)");

    reports:
    "$(disks) has $(disk_free_mb[$(disks)]) MB of free disk space.";
}

Executing the policy we can see that the results are the same.

command
# cf-agent --no-lock --log-level info \
  --file /tmp/feature-friday-39-iteration.cf
output
R: /boot has 3454.355469 MB of free disk space.
R: / has 1486371.210938 MB of free disk space.
R: /usr has 1486371.210938 MB of free disk space.
R: /var has 1486371.210938 MB of free disk space.

We can even take this a step further. Maybe 3454.355469 MB isn’t so friendly after all. Instead we would prefer to see just a whole number. We can achieve this with a very minor refactor by wrapping the eval() with format("%d"), or as shown here, switching the type to int and wrapping it with int() instead.

/tmp/feature-friday-39-iteration-formatted.cf
bundle agent __main__
{
    vars:
      "disks" slist => { "/boot", "/", "/usr", "/var" };

      "disk_free_mb[$(disks)]"
        int => int( eval("$(with) / 1024", "math", "infix") ),
        meta => { "inventory", "attribute_name=Disk free $(disks) (MB)" },
        with => diskfree("$(disks)");

    reports:
    "$(disks) has $(disk_free_mb[$(disks)]) MB of free disk space.";
}

Executing the policy we see a much better final result.

command
# cf-agent --no-lock --log-level info \
  --file /tmp/feature-friday-39-iteration-formatted.cf
output
R: /boot has 3454 MB of free disk space.
R: / has 1486369 MB of free disk space.
R: /usr has 1486369 MB of free disk space.
R: /var has 1486369 MB of free disk space.

I hope this real-world example helps you see the great power that comes with CFEngine’s implicit list iteration.

Happy Friday! 🎉