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.
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”.
cf-agent --no-lock --dry-run --file /tmp/feature-friday-40-0.cf
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
:
cf-agent --no-lock --simulate=diff --file /tmp/feature-friday-40-0.cf
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.
bundle agent __main__
{
files:
"/tmp/feature-friday-40.txt"
content => "Happy Feature Friday #40!";
}
First, let’s see what --dry-run
will do:
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:
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
:
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:
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.
echo "Hello World!" > /tmp/feature-friday-40.txt;\
cf-agent --no-lock --simulate=diff --file /tmp/feature-friday-40-0.cf
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.
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
:
cf-agent --no-lock --simulate=manifest --file /tmp/feature-friday-40-2.cf
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
.
cf-agent --no-lock --simulate=manifest-full --file /tmp/feature-friday-40-2.cf
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! 🎉