Feature Friday #40: What would CFEngine do?

Posted by Nick Anderson
December 13, 2024

CFEngine works by defining a desired state for a given context and converging towards that goal. Given there is no fixed starting point and that the current context might change wildly it can be challenging to succinctly answer the question “What would CFEngine do?”.

In Feature Friday #22: Don’t fix, just warn we saw how an individual promise could be made to warn instead of trying to automatically converge towards the desired state, a granular --dry-run mode. This time, let’s take a look at the --simulate option of cf-agent.

The --simulate option added back in November 2020 as part of CFEngine 3.17.0 expands on the notion of simply warning that a change would be desired as is the case with --dry-run or -n (which set action_policy to warn for the entire policy run). As with the --dry-run option most promises ( commands, packages, users etc … ) are not actuated. --simulate differs in the case of vars and classes promises and the use of functions that are considered unsafe ( execresult(), execresult_as_data(), returnszero(), and usemodule() ). With --dry-run those promises would be evaluated (as there is a general expectation that they are not used to modify system state ) while with the --simulate option vars and classes promises leveraging those functions would only actuate if they are tagged with simulate_safe.

Let’s take a look at a small example:

In this policy we have two classes class_defined_by_returnszero_not_marked_safe and class_defined_by_returnszero_marked_safe with the latter tagged as safe for simulation and two reports to be emitted based on the definition of the classes.

/tmp/feature-friday-40-0.cf
bundle agent __main__
{
  classes:
      "class_defined_by_returnszero_not_marked_safe"
        expression => returnszero( "/bin/true", noshell );

      "class_defined_by_returnszero_marked_safe"
        expression => returnszero( "/bin/true", noshell ),
        meta => { "simulate_safe" };


  reports:

      "class_defined_by_returnszero_marked_safe is defined"
        if => "class_defined_by_returnszero_not_marked_safe";

      "class_defined_by_returnszero_marked_safe is defined"
        if => "class_defined_by_returnszero_marked_safe";
}

Running the example with --dry-run we see warnings that indicate a desire to emit the reports “class_defined_by_returnszero_marked_safe is defined” and “class_defined_by_returnszero_marked_safe is defined”.

command
cf-agent --no-lock --dry-run --file /tmp/feature-friday-40-0.cf
output
 warning: Need to repair reports promise: class_defined_by_returnszero_marked_safe is defined
 warning: Need to repair reports promise: class_defined_by_returnszero_marked_safe is defined

The reports were not emitted because the policy was executed with --dry-run, but we can infer that both classes were defined as part of the policy evaluation.

Next, let’s run the same policy with --simulate=diff:

command
cf-agent --no-lock --simulate=diff --file /tmp/feature-friday-40-0.cf
output
 warning: All changes in files will be made in the '/home/nickanderson/.cfagent/state/99979.changes' chroot
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Need to repair reports promise: class_defined_by_returnszero_marked_safe is defined
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Not calling unsafe function 'returnszero' in simulate mode
 warning: Not calling unsafe function 'returnszero' in simulate mode

As you might have noticed from the above output, --simulate also differs from --dry-run in that files promises operate within a change-root so that we can safely observe the effects.

This time we see that only a single warning about a desire to report is emitted, the report which we expect to see if the class class_defined_by_returnszero_marked_safe defined by the promise tagged simulate_safe. We see many cases where returnszero() would be executed but was avoided because it was not tagged simulate_safe and we can infer that the other class was not defined by the lack of a warning about a desire to report.

Let’s try a policy that makes a promise about file content.

/tmp/feature-friday-40-1.cf
bundle agent __main__
{
  files:
      "/tmp/feature-friday-40.txt"
        content => "Happy Feature Friday #40!";
}

First, let’s see what --dry-run will do:

command
cf-agent --no-lock --dry-run --file /tmp/feature-friday-40-0.cf

We can see from the output that the file would be created:

output
 warning: Should create file '/tmp/feature-friday-40.txt', mode '0600'
 warning: Warnings encountered when actuating files promise '/tmp/feature-friday-40.txt'

Next, let’s try --simulate:

command
cf-agent --no-lock --simulate=diff --file /tmp/feature-friday-40-0.cf

Here we can see that the proposed changes are made within a chroot:

output
 warning: All changes in files will be made in the '/home/nickanderson/.cfagent/state/105101.changes' chroot
===========================================================================
'/tmp/feature-friday-40.txt' is a regular file
Size: 25
Access: (0600/rw-------)  Uid: (1000/nickanderson)   Gid: (1000/nickanderson)
Access: 2024-10-21 16:04:55 -0500
Modify: 2024-10-21 16:04:55 -0500
Change: 2024-10-21 16:04:55 -0500

Contents of the file:
Happy Feature Friday #40!
\no newline at the end of file

And let’s also take a look at the output in the case where the promised file (/tmp/feature-friday-40.txt) exists on the host system.

command
echo "Hello World!" > /tmp/feature-friday-40.txt;\
cf-agent --no-lock --simulate=diff --file /tmp/feature-friday-40-0.cf
output
 warning: All changes in files will be made in the '/home/nickanderson/.cfagent/state/105943.changes' chroot
===========================================================================
--- original /tmp/feature-friday-40.txt
+++ changed  /tmp/feature-friday-40.txt
@@ -1 +1 @@
-Hello World!
+Happy Feature Friday #40!
\ No newline at end of file

Until now we have only looked at the output of the --simulate option when set to diff. Let’s briefly look at the output from the other two options manifest and manifest-full. The manifest modes do not output unified diff and instead are intended to help you see the full desired end state. Let’s adjust the policy a bit more and take a look.

/tmp/feature-friday-40-2.cf
bundle agent __main__
{
  files:
      "/tmp/feature-friday-40.txt"
        content => "Happy Feature Friday #40!";

      "/tmp/feature-friday-40-Hello-World.txt"
        content => "Hello World!";
}

Now we are making two files promises, let’s make the state of the host system match the second promise:

Now when we run the policy with --simulate=manifest we see the end state that would be achieved had the run not been executed with --simulate:

command
cf-agent --no-lock --simulate=manifest --file /tmp/feature-friday-40-2.cf
output
 warning: All changes in files will be made in the '/home/nickanderson/.cfagent/state/110986.changes' chroot
===========================================================================
'/tmp/feature-friday-40.txt' is a regular file
Size: 25
Access: (0664/rw-rw-r--)  Uid: (1000/nickanderson)   Gid: (1000/nickanderson)
Access: 2024-10-21 16:20:40 -0500
Modify: 2024-10-21 16:20:40 -0500
Change: 2024-10-21 16:20:40 -0500

Contents of the file:
Happy Feature Friday #40!
\no newline at the end of file

Notice that there was no mention of /tmp/feature-friday-40-Hello-World.txt since the execution of the agent was not going to result in a change to that file. To see a more full end state of what CFEngine manages we can run with --simulate=manifest-full.

command
cf-agent --no-lock --simulate=manifest-full --file /tmp/feature-friday-40-2.cf
output
 warning: All changes in files will be made in the '/home/nickanderson/.cfagent/state/111360.changes' chroot
===========================================================================
'/tmp/feature-friday-40.txt' is a regular file
Size: 25
Access: (0664/rw-rw-r--)  Uid: (1000/nickanderson)   Gid: (1000/nickanderson)
Access: 2024-10-21 16:22:40 -0500
Modify: 2024-10-21 16:22:40 -0500
Change: 2024-10-21 16:22:40 -0500

Contents of the file:
Happy Feature Friday #40!
\no newline at the end of file
===========================================================================
'/tmp/feature-friday-40-Hello-World.txt' is a regular file
Size: 12
Access: (0664/rw-rw-r--)  Uid: (1000/nickanderson)   Gid: (1000/nickanderson)
Access: 2024-10-21 16:22:40 -0500
Modify: 2024-10-21 16:22:40 -0500
Change: 2024-10-21 16:22:40 -0500

Contents of the file:
Hello World!
\no newline at the end of file

As we can see from the above output the state of /tmp/feature-friday-40-Hello-World.txt is included even though it already matches the desired end state.

I hope you found this quick tour of the --simulate option stimulating.

Happy Friday! 🎉