Show notes: The agent is in - Episode 27 - CFEngine Q&A: Policy questions

Posted by Nick Anderson
July 27, 2023

Unlock the power of CFEngine with expert insights and get your burning policy questions.

Cody, Craig and Nick discuss and answer CFEngine policy questions submitted by users.

Video

The video recording is available on YouTube:

At the end of every webinar, we stop the recording for a nice and relaxed, off-the-record chat with attendees. Join the next webinar to not miss this discussion.

Questions

These are the questions and policy used during the episode but each question has a separate blog post that goes into more detail.

How can I get a list of specific key values from an array of objects in JSON

bundle agent __main__
{
  vars:
      "d" data => '[
        {"name": "Aurora", "value": "1"},
        {"name": "Orion", "value": "2"},
        {"name": "Luna", "value": "3"},
        {"name": "Phoenix", "value": "4"},
        {"name": "Atlas", "value": "5"}
        ]';

      "d_names"
        slist => sort( maparray( "$(d[$(this.k)][name])",
                           d ), lex );
}

Output:

Variable name                            Variable value                                               Meta tags                                Comment
default:main.d                           [{"name":"Aurora","value":"1"},{"name":"Orion","value":"2"},{"name":"Luna","value":"3"},{"name":"Phoenix","value":"4"},{"name":"Atlas","value":"5"}] source=promise
default:main.d_names                      {"Atlas","Aurora","Luna","Orion","Phoenix"}                 source=promise

Refactored to use an associative array.

bundle agent __main__
{
  vars:
      "d" data => '[
        {"name": "Aurora", "value": "1"},
        {"name": "Orion", "value": "2"},
        {"name": "Luna", "value": "3"},
        {"name": "Phoenix", "value": "4"},
        {"name": "Atlas", "value": "5"}
        ]';

      "di" slist => getindices( d );

      "d_names[$(di)]"
        string => "$(d[$(di)][name])";

      "names" slist => sort( getvalues( d_names ), lex);

}

Output:

Variable name                            Variable value                                               Meta tags                                Comment
default:main.d                           [{"name":"Aurora","value":"1"},{"name":"Orion","value":"2"},{"name":"Luna","value":"3"},{"name":"Phoenix","value":"4"},{"name":"Atlas","value":"5"}] source=promise
default:main.d_names[0]                  Aurora                                                       source=promise
default:main.d_names[1]                  Orion                                                        source=promise
default:main.d_names[2]                  Luna                                                         source=promise
default:main.d_names[3]                  Phoenix                                                      source=promise
default:main.d_names[4]                  Atlas                                                        source=promise
default:main.di                           {"0","1","2","3","4"}                                       source=promise
default:main.names                        {"Atlas","Aurora","Luna","Orion","Phoenix"}                 source=promise

Refactoring to show additional flexibility building a data structure key by key.

bundle agent __main__
{
  vars:
      "d" data => '[
        {"name": "Aurora", "value": "1"},
        {"name": "Orion", "value": "2"},
        {"name": "Luna", "value": "3"},
        {"name": "Phoenix", "value": "4"},
        {"name": "Atlas", "value": "5"}
        ]';

      "di" slist => getindices( d );

      "d_names[$(di)]"
        string => "$(d[$(di)][name])",
        if => isgreaterthan( "$(d[$(di)][value])", 3 );

      "names" slist => sort( getvalues( d_names ), lex);

}
Variable name                            Variable value                                               Meta tags                                Comment
default:main.d                           [{"name":"Aurora","value":"1"},{"name":"Orion","value":"2"},{"name":"Luna","value":"3"},{"name":"Phoenix","value":"4"},{"name":"Atlas","value":"5"}] source=promise
default:main.d_names[3]                  Phoenix                                                      source=promise
default:main.d_names[4]                  Atlas                                                        source=promise
default:main.di                           {"0","1","2","3","4"}                                       source=promise
default:main.names                        {"Atlas","Phoenix"}                                         source=promise

Read more in the related blog post.

How can I Test CFEngine Policy

bundle agent __main__
{
  methods:
      "init";
      "test";
      "check";

}
bundle agent init
{
      reports: "$(this.bundle)";
}
bundle agent test
{
      reports: "$(this.bundle)";
}
bundle agent check
{
      reports: "$(this.bundle)";
}

Output:

R: init
R: test
R: check

Refactored to use lib/testing.cf for emitting JUnit.

body file control
{
  # We need to include the policy which we are leveraging
  inputs => {
              "$(sys.libdir)/lib/testing.cf", # The testing library
              "$(sys.libdir)/lib/files.cf",   # contains copy_from => default:local_dcp, edit_line => default:lines_present
              "$(sys.libdir)/lib/common.cf",  # Note: files.cf loads common.cf which contains printfile => default:cat, but we include it explicitly since we are using it here
            };
}
bundle agent __main__
{
  methods:
    "init";
    "test";
    "check";
    "cleanup";
}
bundle agent init
# @brief Setup the stage for the test
{
  files:

    # Let's initialize our test file from our pre-recorded starting state. This
    # is the file that we will make our promise against.

    "/tmp/test-file.txt"
      copy_from => default:local_dcp( "/tmp/starting-state.txt" );
}
bundle agent test
# @brief Excercise the policy we wish to test
{
  files:
    "/tmp/test-file.txt"
      edit_line => default:lines_present( "Big ones, small ones, some as big as your head!" );
}
bundle agent check
# @brief Verify that the state is as we expect
{
  vars:
    "test_check_required_files"
      slist => { "/tmp/test-file.txt", "/tmp/expected-end-state.txt" };

  classes:
    "edit_line_default_lines_present_ok"
      if => filesexist( @(test_check_required_files) ),
      scope => "namespace",
      expression => returnszero( concat( "/usr/bin/diff",
                                         " /tmp/test-file.txt",
                                         " /tmp/expected-end-state.txt" ),
                                "noshell" );
  methods:
    # Register each passing test
    "Register test for appending a missing line"
      usebundle => testing_ok_if("edit_line_default_lines_present_ok",
                                 "Test that bundle edit_line default:lines_present appends a line that is not present in the file",
                                 "error: bundle edit_line default:lines_present did not produce the expected result",
                                 "trace",
                                 "jUnit");

    # Write out the jUnit report
    "jUnit Report"
       usebundle => testing_junit_report( "/tmp/test-result.xml" );

  reports:
    inform_mode::
     "The full xml report:"
       printfile => default:cat( "/tmp/test-result.xml" );
}
bundle agent cleanup
# @brief Cleanup the files we used in our testing.
{
  files:
    "/tmp/test-file.txt.*"
      delete => default:tidy;
}
    info: Copied file '/tmp/starting-state.txt' to '/tmp/test-file.txt.cfnew' (mode '600')
    info: Moved '/tmp/test-file.txt.cfnew' to '/tmp/test-file.txt'
    info: Updated file '/tmp/test-file.txt' from 'localhost:/tmp/starting-state.txt'
    info: Inserted the promised line 'Big ones, small ones, some as big as your head!' into '/tmp/test-file.txt' after locator
    info: insert_lines promise 'Big ones, small ones, some as big as your head!' repaired
    info: Edited file '/tmp/test-file.txt'
R: testing_ok_if: adding testing report for class edit_line_default_lines_present_ok at position 1
    info: Updated rendering of '/tmp/test-result.xml' from mustache template '/home/nickanderson/.cfagent/inputs/lib/templates/junit.mustache'
R: testing_generic_report: report summary: counts = 1/1/0/0/0 failed = {  }, passed = { "testing_edit_line_default_lines_present_ok" }, skipped = {  }, todo = {  }, failed = {  }; tests = [{"tap_message":"ok Test that bundle edit_line default:lines_present appends a line that is not present in the file","test_message":"Test that bundle edit_line default:lines_present appends a line that is not present in the file","test_offset":1,"testcase":"edit_line_default_lines_present_ok"}]+[]+[]+[]
R: The full xml report:
R: <?xml version="1.0" encoding="UTF-8"?>
R: <testsuite tests="1" failures="0" timestamp="2023-07-27T15:40:29">
R:
R:   <testcase name="edit_line_default_lines_present_ok">Test that bundle edit_line default:lines_present appends a line that is not present in the file</testcase>
R:
R:
R:
R:
R: </testsuite>
R:
R: <!-- not implemented (yet):
R: 1) errors: <error message="my error message">my crash report</error>
R: 2) STDOUT: <system-out>my STDOUT dump</system-out>
R: 3) STDERR: <system-err>my STDERR dump</system-err>
R:  -->
    info: Deleted file '/tmp/test-file.txt'
    info: Deleted file '/tmp/test-file.txt.cf-before-edit'

Read more in the related blog post.