Ever wanted to make sure a promise only runs if some other promise has succeeded?
Consider this contrived example with two reports type promises, It's Friday! and I love CFEngine Feature Friday. Per normal ordering1, these two promises will be emitted in the written order.
/tmp/feature-friday-7.cf bundle agent feature_friday { reports: "It's Friday, Friday"; "Gotta get down on Friday"; } command cf-agent --no-lock --bundlesequence feature_friday --file /tmp/feature-friday-7.cf output R: It's Friday! R: I love CFEngine Feature Friday. If we want them in the opposite order, we could either change the order or define classes based on the results of the promises. But today’s feature, depends_on, lets us influence the ordering using a more lightweight method via handle.
Will your policy work? cf-promises can check the CFEngine policy for syntax errors and give you an overview of the host’s context.
It’s always a good idea to check your policy for syntax errors.
Consider this policy file:
/tmp/feature-friday-6.cf bundle agent feature_friday { reports: "$(this.promise_filename)" printfile => cat( "$(this.promise_filename)" ) } Can you spot the error? Let’s see if cf-promises can help:
command cf-promises -f /tmp/feature-friday-6.cf output /tmp/feature-friday-6.cf:6:2: error: syntax error } ^ /tmp/feature-friday-6.cf:6:2: error: Check previous line, Expected ';', got '}' } ^ error: There are syntax errors in policy files The output tells us that there is a syntax error near line 6, column 2. A semicolon (;) was expected but instead, a closing curly brace (}) was found. We are missing the semicolon that terminates the promise for bundle feature_friday_6 which is called as a methods promise.
When you want to inspect both the return code and output from a command execresult_as_data() might be the function you are searching for.
Most CFEngine policy writers have used execresult() and returnszero(). They are useful when you want to do something based on the output of a command or based on its successful execution (returning zero). For example:
/tmp/feature-friday-3.cf bundle agent __main__ { vars: "hostname" string => execresult( "$(paths.hostname)", "useshell" ); classes: "my_command_returned_zero" expression => returnszero( "$(paths.hostname)", "noshell" ); reports: "$(hostname)"; my_command_returned_zero:: "$(paths.hostname) returned 0"; } command cf-agent --no-lock --log-level info --file /tmp/feature-friday-3.cf output precision-5570 R: precision-5570 R: /bin/hostname returned 0 But, sometimes, you care about specific return codes. However, you might also want to consider the output from a command. execresult_as_data() facilitates inspecting both output and return code from a command’s execution, by returning a data structure. Let’s take a look.
Have you ever wanted some hosts to organize themselves into different groups, but without spending time to worry about assigning a specific group?
Cody, Craig, and Nick talk about using the select_class feature in CFEngine. Nick implements automatic assignment of a maintainer for each host:
You want to assign a maintainer to each host that should be the primary point of contact. Some hosts should may have a specific maintainer, but generally, you don’t care about the relationship between the person and the actual function of the machine. It’s okay if the groups are not perfectly balanced. First we provide a list of maintainers to select from.
Ever wanted to manipulate a string - temporarily - for an individual promise? Check out the with attribute and its special, $(with) variable.
Sometimes you need some variation on a string for a specific case. Traditionally, to achieve this you’d simply define another variable.
Here is a contrived example:
I have a string, nginx and I want to emit a report that contains both the string itself and the upper case version of the string.
Looking for a way to concisely set a variable conditionally? Have you heard of ifelse()?
In CFEngine, traditionally class expressions are used to constrain promises to different contexts. Setting a variable to different values based on context might look like this:
/tmp/feature-friday-1.cf bundle agent __main__ { vars: "MyVariable" string => "My Default value"; redhat_8|centos_8|rocky_8:: "MyVariable" string => "My value for EL 8"; ubuntu_22:: "MyVariable" string => "My value for Ubuntu 22"; any:: "MyVariable" string => "My value on Friday", if => "Friday"; reports: "It's $(sys.date) and I am running on $(sys.os_release[PRETTY_NAME])"; "MyVariable is '$(MyVariable)'"; } command cf-agent --no-lock --log-level info --file /tmp/feature-friday-1.cf output R: It's Mon Mar 11 12:36:41 2024 and I am running on Ubuntu 22.04.4 LTS R: MyVariable is 'My value for Ubuntu 22' That’s great, lots of flexibility, but with an increasing number of options the policy can get quite long and it’s easier for a human interpreter to lose track of the context. The same can be achieved in a single statement using ifelse().
Imagine having the power to identify the exact lines of your CFEngine policy that are slowing down your executions. In this episode, we’ll guide you through the art of profiling CFEngine policy for improved performance.
In Episode 30 of “The agent is in,” Nick and team dives into the topic of profiling CFEngine policy. We explore tools and techniques to identify performance bottlenecks and optimize CFEngine deployments. The episode covers the following main points:
Have you been interested in automating the testing of your CFEngine policy?
Cody, Craig and Nick follow up on the Policy Examples episode and dive a bit deeper into testing. Nick walks through some policy and related tests that leverage lib/testing.cf from the Masterfiles Policy Framework and Craig walks through implementing a GitHub Workflow to run the tests in a Docker container for each Pull Request.
Video The video recording is available on YouTube:
This question was covered in The agent is in, Episode 27 - CFEngine Q&A: Policy questions.
Given the following JSON, how can I get a list containing just the values of name?
[ { "name": "Aurora", "description": "Illuminating" }, { "name": "Orion", "description": "Stellar" }, { "name": "Luna", "description": "Serene" }, { "name": "Phoenix", "description": "Resilient" }, { "name": "Atlas", "description": "Strong" } ] Using maparray() The most concise and direct way to achieve something like this is to use the maparray() function. It iterates over a list or data container applying a pattern based on $(this.k) and $(this.v) of the currently iterated element to produce a list.
This question was covered in The agent is in, Episode 27 - CFEngine Q&A: Policy questions.
Testing is an important part of the software life-cycle. Writing tests for your CFEngine policy can help to bring improved assurance that your policy behaves as expected. Follow along and write your first test policy.
Test stages When writing tests there are three or four basic stages that typically need to be handled.
Initialization - Set up the necessary conditions for the test, e.g. create some files to be edited. Testing - Running the policy whose behavior you wish to test. Checking - Inspecting the results of the test policy to see if they conform with expectations. Cleanup - You might need to cleanup artifacts produced by the test if your testing system does not handle it for you. These stages map well to a sequence of bundles. So, a simple test template could look like this: