Skip to content

Instantly share code, notes, and snippets.

@nickanderson
Last active September 14, 2024 19:18
Show Gist options
  • Save nickanderson/1f3fce4fa46bd8ee5249b99a49b67378 to your computer and use it in GitHub Desktop.
Save nickanderson/1f3fce4fa46bd8ee5249b99a49b67378 to your computer and use it in GitHub Desktop.
An example of doing something as the result of a promise outcome just for James Just James
@nickanderson The title piqued my interest so I had a read. Bearing in mind that I don't use cfengine,
so it's likely my fault for not understanding, but I would have liked to see how you do different
things _as_a_result_ of the different repaired vs kept vs reached scenarios.

Is it possible to do that? Say create a new file with contents that vary based on which of those
scenarios occured?

Thanks!
(⭐) (🔖) James Just James (@purpleidea@mastodon.social) 2024-09-13 20:45:34

To give you the full context, I have embedded the body classes results from the standard library. It’s in the MPF (Masterfiles Policy Framework) documentation here and source on github along with body delete tidy and body printfile cat() so that the example is fully standalone. Generally, “how you do different things” in CFEngine is with the use of classes. You use them to express the context under which a promise should be allowed. There is some documentation for classes and decisions. Classes can be used to restrict many promises at once and individually.

To create a new file with contents that vary based on the promise outcomes here is an example that tries to illustrate a few different things concurrently. This example is intentionally obtuse trying to show various ways that you can leverage classes to make decisions for one or more promises at a time and how trying to apply too many constraints can become problematic.

bundle agent __main__
{
  vars:
      "file" string => "/tmp/a-new-file.txt";

  classes:

      # This class will be automatically canonified We will use this general
      # knoledge of having seen classes related to $(file) being defined a
      # couple times later

      "have_classes_related_to_$(file)"
        expression => isgreaterthan( length( classesmatching( concat( canonify( "$(file)" ), ".*" ) ) ), 0 );

  files:

    _bin_false_reached._bin_true_reached.announce_working_reached::
      # Until the next promise type (<promise>:) or class expression aka class
      # guard (<expression>::) for a promise to run both commands have to
      # already have been executed for the class defined by the `results`
      # classes body.

      # Here we simply promise the full content of the file. Once a promise is
      # kept or repaired withing a single agent run it won't be executed again.
      "$(file)"
        content => "The commands /bin/false and /bin/true have both been run$(const.n)",
        classes => results( "bundle", "$(file)" );

      # Here we create a file if the command execution for /bin/true was seen to
      # have been repaired. This is an additional constraint on top of the
      # classes indicating the command has been run/reached specified above. As
      # a result of this promise we will get classes prefixed with
      # $(file)_a_promise_repaired, so, for example when the content of the file
      # is modified we would see a class like
      # _tmp_a_new_file_txt_a_promise_repaired_repaired and if the file already
      # contained the correct content we would see a class like
      # _tmp_a_new_file_txt_a_promise_repaired_kept

      "$(file).a_promise_repaired"
        content => concat(
                           "The commands /bin/false and /bin/true have both been run$(const.n)",
                           "The promise with handle a_promise_repaired was seen to be repaired$(const.n)",
                           "But wait, there's more ...$(const.n)"),
        if => "_bin_true_repaired",
        classes => results( "bundle", "$(file)_a_promise_repaired" );

    final_report_reached::
      # We have put so many dependant conditions forcing order into this bundle
      # that this context wont be in scope within the three passes of
      # convergence. You can see this in verbose logging.

      "$(file).*"
        handle => "tidy",
        delete => tidy;

  commands:
      # This will run and define various bundle scoped classes prefixed with
      # _bin_false. By default we interpret a command that retuns non-zero as a
      # promise not-kept, so we expect to see the class _bin_false_not_kept
      # defined
      "/bin/false"
        handle => "a_promise_not_kept",
        classes => results( "bundle", "_bin_false" );

      # This will run and define various bundle scoped classes prefixed with
      # _bin_true. By default we interpret a command that retuns zero as a
      # promise repaired, so we expect to see the class _bin_true_repaired
      # defined
      "/bin/true"
        handle => "a_promise_repaired",
        classes => results( "bundle", "_bin_true" );

  reports:
      # Announce the file we are woking with and use this to prevent working
      # with the file prior to the announcement
      "Working with $(file)"
        classes => results( "bundle", "announce_working" );

      # Print the content of a file if there is a class defined indicating the
      # file was repaired. Note, if the file isn't repaired the content won't be
      # printed.
      "I see we repaired $(file).a_promise_repaired. It contains:"
        printfile => cat( "$(file).a_promise_repaired" ),
        if => canonify( "$(file).a_promise_repaired_repaired" );


      # Emit the classes defined related to $(file) if we see some
      "I see classes defined realted to $(file):$(const.n)$(with)"
        with => join( "$(const.n)",
                      classesmatching( concat( canonify( "$(file)" ), ".*" ) ) ),
        if => canonify( "have_classes_related_to_$(file)" );

      "We should clean up after ourselves and delete the file"
        classes => results( "bundle", "final_report" ),
        if => canonify( "have_classes_related_to_$(file)" );
}

# https://github.com/cfengine/masterfiles/blob/master/lib/common.cf#L225-L319
body classes results(scope, class_prefix)
# @brief Define classes prefixed with `class_prefix` and suffixed with
# appropriate outcomes: _kept, _repaired, _not_kept, _error, _failed,
# _denied, _timeout, _reached
#
# @param scope The scope in which the class should be defined (`bundle` or `namespace`)
# @param class_prefix The prefix for the classes defined
#
# This body can be applied to any promise and sets global
# (`namespace`) or local (`bundle`) classes based on its outcome. For
# instance, with `class_prefix` set to `abc`:
#
# * if the promise is to change a file's owner to `nick` and the file
# was already owned by `nick`, the classes `abc_reached` and
# `abc_kept` will be set.
#
# * if the promise is to change a file's owner to `nick` and the file
# was owned by `adam` and the change succeeded, the classes
# `abc_reached` and `abc_repaired` will be set.
#
# This body is a simpler, more consistent version of the body
# `scoped_classes_generic`, which see. The key difference is that
# fewer classes are defined, and only for outcomes that we can know.
# For example this body does not define "OK/not OK" outcome classes,
# since a promise can be both kept and failed at the same time.
#
# It's important to understand that promises may do multiple things,
# so a promise is not simply "OK" or "not OK." The best way to
# understand what will happen when your specific promises get this
# body is to test it in all the possible combinations.
#
# **Suffix Notes:**
#
# * `_reached` indicates the promise was tried. Any outcome will result
#   in a class with this suffix being defined.
#
# * `_kept` indicates some aspect of the promise was kept
#
# * `_repaired` indicates some aspect of the promise was repaired
#
# * `_not_kept` indicates some aspect of the promise was not kept.
#   error, failed, denied and timeout outcomes will result in a class
#   with this suffix being defined
#
# * `_error` indicates the promise repair encountered an error
#
# * `_failed` indicates the promise failed
#
# * `_denied` indicates the promise repair was denied
#
# * `_timeout` indicates the promise timed out
#
# **Example:**
#
# ```cf3
# bundle agent example
# {
#   commands:
#     "/bin/true"
#       classes => results("bundle", "my_class_prefix");
#
#   reports:
#     my_class_prefix_kept::
#       "My promise was kept";
#
#     my_class_prefix_repaired::
#       "My promise was repaired";
# }
# ```
#
# **See also:** `scope`, `scoped_classes_generic`, `classes_generic`
{
        scope => "$(scope)";

        promise_kept => { "$(class_prefix)_reached",
                          "$(class_prefix)_kept" };

        promise_repaired => { "$(class_prefix)_reached",
                              "$(class_prefix)_repaired" };

        repair_failed => { "$(class_prefix)_reached",
                           "$(class_prefix)_error",
                           "$(class_prefix)_not_kept",
                           "$(class_prefix)_failed" };

        repair_denied => { "$(class_prefix)_reached",
                           "$(class_prefix)_error",
                           "$(class_prefix)_not_kept",
                           "$(class_prefix)_denied" };

        repair_timeout => { "$(class_prefix)_reached",
                            "$(class_prefix)_error",
                            "$(class_prefix)_not_kept",
                            "$(class_prefix)_timeout" };
}

# https://github.com/cfengine/masterfiles/blob/master/lib/files.cf#L1931-L1937
body delete tidy
# @brief Delete the file and remove empty directories
# and links to directories
{
        dirlinks => "delete";
        rmdirs   => "true";
}

# https://github.com/cfengine/masterfiles/blob/master/lib/reports.cf#L1-L7
body printfile cat(file)
# @brief Report the contents of a file
# @param file The full path of the file to report
{
        file_to_print => "$(file)";
        number_of_lines => "inf";
}

Running the policy the first time with inform logging (most common for manual executions)

# cf-agent --no-lock --log-level info --file /tmp/example.cf
    info: Executing 'no timeout' ... '/bin/false'
   error: Finished command related to promiser '/bin/false' -- an error occurred, returned 1
    info: Completed execution of '/bin/false'
    info: Executing 'no timeout' ... '/bin/true'
    info: Completed execution of '/bin/true'
R: Working with /tmp/a-new-file.txt
    info: Created file '/tmp/a-new-file.txt', mode 0600
    info: Updated file '/tmp/a-new-file.txt' with content 'The commands /bin/false and /bin/true have both been run
'
    info: Created file '/tmp/a-new-file.txt.a_promise_repaired', mode 0600
    info: Updated file '/tmp/a-new-file.txt.a_promise_repaired' with content 'The commands /bin/false and /bin/true have both been run
The promise with handle a_promise_repaired was seen to be repaired
But wait, there's more ...
'
R: I see we repaired /tmp/a-new-file.txt.a_promise_repaired. It contains:
R: The commands /bin/false and /bin/true have both been run
R: The promise with handle a_promise_repaired was seen to be repaired
R: But wait, there's more ...
R: I see classes defined realted to /tmp/a-new-file.txt:
_tmp_a_new_file_txt_a_promise_repaired_repaired
_tmp_a_new_file_txt_a_promise_repaired_reached
_tmp_a_new_file_txt_repaired
_tmp_a_new_file_txt_reached
R: We should clean up after ourselves and delete the file

And running the policy for a second time with inform logging we see slightly different output given the different context that now exists.

The verbose output is quite detailed. After deleting the files and running again, with verbose on the first execution we see:

And this is what a second run outputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment