CFEngine contains a powerful language for controlling all aspects of a system. CFEngine runs primarily on UNIX and UNIX-like operating systems, but can also run on Windows.
Goal for this presentation: Learn 20% of CFEngine enabling 80% of your work.
This presentation was made with love using org-mode 9.2.4, cfengine 3.12.2, and reveal.js 3.8.0. You can get a copy of this presentation any time on Github.
These are the major components of CFEngine that you will encounter on a day to day basis.
cf-agentcf-execdcf-serverdcf-monitordcf-agent
cf-agent is the command you will use most often. It is used to run
policy(code) and ensure your system is in the desired state. If you are running
any CFEngine command from the command line, there's a greater than 99% chance
that this is it.
cf-execd
cf-execd is a periodic task scheduler. You can think of it like cron with an
understanding of CFEngine classes.
By default CFEngine runs and enforces policies every five minutes. cf-execd
is responsible for making that happen.
cf-serverd
cf-serverd runs on the CFEngine server, as well as all clients.
cf-runagent requestscf-hub
cf-runagent allows you to request ad-hoc policy runs. I rarely use it.
cf-monitord
cf-monitord monitors various statistics about the running system. This
information is made available in the form of classes and variables.
You'll almost never use cf-monitord directly. However the data provided by
cf-monitord is available to cf-agent.
It is very likely that you have only ever used imperative languages. Examples of imperative languages include C, Perl, Ruby, Python, shell scripting, etc. Name a language. It's probably imperative.
CFEngine is a declarative language. The CFEngine language is merely a description of the final state. CFEngine uses convergence to arrive at the described state.
bundle agent main | #!/bin/env/bash
{ |
packages: | rpm -q openssh-server || yum install openssh-server
| yum check-update openssh-server
"openssh-server" | if [ $? -eq 100 ]; then # update available
policy => "present", | yum upgrade openssh-server
version => "latest"; | fi
}
Imperative languages execute step by step in sequence.
For Example:
Imperative starts at known state A and transforms to known state B.
It is not a list of steps to achieve an outcome but a description of the desired state. Because of this any deviation from the desired state can be detected and corrected.
In other words, a declarative system can begin in any state, not simply a known state, and transform into the desired state.
Declarative states a list of things which must be true. It does not state how to make them true.
When a system has reached the desired state it is said to have reached convergence.
Promise theory is the fundamental underlying philosophy that drives CFEngine.
It is a model of voluntary cooperation between individual, autonomous actors or agents who publish their intentions to one another in the form of promises.
A file (e.g., /etc/apache2/httpd.conf) can make promises about its own
contents, attributes, etc. But it does not make any promises about a process.
A process (e.g., httpd) can make a promise that it will be running. But it
does not make any promises about its configuration.
The configuration file and the process are autonomous. Each makes promises about itself which cooperate toward an end.
bundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
bundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
bundle_typecf-agent, cf-serverd, cf-monitord).bundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
bundle_namebundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
type:files, commands, packages, etc …).bundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
context::any:: (no restriction, run on any
host). The context restriction applies until the next
context/class expression or until it's reset to default at the
start of the next promise type.bundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
promiserbundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
promisee/stakeholderbundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
promise bodybundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
valuebundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
bodybundle bundle_type bundle_name
{
type:
context::
"promiser" -> { "promisee", "stakeholder" }
----------|
attribute1 => "value", |
attribute2 => body, |-- Promise Body
attributeN => bundle; |
} ----------|
bundleedit_line take bundles. Note, promise attributes that take
bundles must not be quoted;bundle agent main { files: linux:: "/tmp/example" -> { "Instructor", "Students" } create => "true", touch => "true", action => warn_only; }
# cf-agent --no-lock --file ./examples/example_promise.cf warning: Warning promised, need to create file '/tmp/example'
bundle type name { type: context:: "promiser" -> { "promisee" } attribute1 => "value", attribute2 => value; type: context:: "promiser" -> { "promisee" } attribute1 => "value", attribute2 => value; }
Bundles apply to the binary that executes them. E.g., agent bundles apply to
cf-agent while server bundles apply to cf-serverd.
Bundles of type common apply to any CFEngine binary.
apache2 package is installedhttpd process is runninghttpd process is restarted when the configuration changesThis matters when you call the same bundle more than one time within a given execution.
if => isvariable().bundle common globals { vars: "tool_path" string => "/srv/tools" }
bundle server my_access_rules { access: "$(globals.tool_path)" admit_ips => { "192.168.0.0/24" }; }
bundle agent my_policy { vars: "config[PermitRootLogin]" string => "no"; "config[Port]" string => "22"; files: "/etc/ssh/sshd_config" edit_line => set_line_based( "my_policy.config", " ", "\s+", ".*", "\s*#\s*"); }
bundle monitor measure_cf_serverd { vars: "pid[cf-serverd]" string => readfile( "$(sys.piddir)/cf-serverd.pid", 4k ); "reg_stat[rss]" string =>"(?:[^\s+]*\s+){23}([^\s]+)(?:.*)"; measurements: "/proc/$(pid[cf-serverd])/stat" handle => "cf_serverd_vsize", stream_type => "file", data_type => "int", history_type => "weekly", units => "pages in memory", match_value => line_match_value(".*", $(reg_stat[rss]) ); }
I stated before that the attributes of a promise, collectively, are called the body. Depending on the specific attribute the value of an attribute can be an external body.
A body is a collection of attributes. These are attributes that supplement the promise.
body TYPE NAME(OPTIONAL, PARAMS) { ATTRIBUTE => "value"; ATTRIBUTEn => { "more", "values" }; }
The difference between a bundle and a body is that a bundle contains promises while a body contains only attributes.
The distinction is subtle, especially at first and many people are tripped up by this.
In a body, each attribute ends with a semicolon.
(Note that in a bundle each promise ends with a semicolon, while attributes of each promise are separated by commas)
bundle agent main { files: "/tmp/file" perms => m(600); }
PRO TIP: The cf-locate script in core/contrib can help you find and view
body and bundle definitions within your policy set.
cf-locate --plain --full "perms m\(.*"
body perms m(mode)
# @brief Set the file mode
# @param mode The new mode
{
mode => "$(mode)";
}
Bundles and bodies can be paramaterized for abstraction and re-usability. In other words you can define one and call it even passing in parameters which will implicitly become variables.
body type name (my_param) { attribute1 => "$(my_param)"; }
The parameter my_param is accessed as a variable by $(my_param).
The Masterfiles Policy Framework is the default policy that ships with CFEngine. The standard library is included.
The CFEngine Standard Library comes bundled with CFEngine in the
masterfiles/lib/ directory.
The standard library contains ready to use bundles and bodies that you can include in your promises and is growing with every version of CFEngine. Get to know the standard library well, it will save you much time.
$ cf-agent --no-locks --log-level=info --file ./test.cf --bundle bundlename
Note: Make sure you use the correct file and bundle name! For any examples including a bundle named main or main you can skip specifying the bundle.
By default promises (excluding defaults, vars, classes, and methods) are locked for 1 minute once they are evaluated.
for i in $(seq 3); do cf-agent -f ./examples/reports_show_locking.cf sleep 30 done
R: I started running CFEngine 3.14.0a.4e12fcf75 at 'Fri Jul 19 11:23:01 2019' R: Hello World! R: I started running CFEngine 3.14.0a.4e12fcf75 at 'Fri Jul 19 11:23:31 2019' R: I started running CFEngine 3.14.0a.4e12fcf75 at 'Fri Jul 19 11:24:01 2019' R: Hello World!
bundle agent main { reports: "I started running CFEngine $(sys.cf_version) at '$(sys.date)'" action => immediate; "Hello World!"; } body action immediate # @brief Evaluate the promise at every `cf-agent` execution. { ifelapsed => "0"; }
bundle agent main { commands: "/bin/echo Hello World!"; }
# cf-agent --no-lock --file ./examples/commands_echo_hello_world.cf notice: Q: ".../bin/echo Hello": Hello World!
bundle agent main { files: "/etc/shadow" perms => perms_for_shadow_files; "/etc/gshadow" perms => perms_for_shadow_files; reports: "Please run this policy as root" if => not( strcmp( "$(sys.user_data[gid])", "0" ) ); } body perms perms_for_shadow_files { owners => { "root" }; groups => { "root" }; mode => "0640"; }
cf-agent)./etc/shadow and /etc/gshadow.perms_for_shadow_files.
Note: The values for owners and groups is enclosed in curly braces. This is
because these attributes take a list of strings (aka, an slist).
bundle agent example { files: "/etc/motd" copy_from => cp("/repo/motd"); } body copy_from cp (from) { servers => { "$(sys.policy_hub)" }; source => "$(from)"; compare => "digest"; } bundle server my_access_rules { access: policy_server|am_policy_hub:: "/repo" admit_ips => { "192.168.0.1/24" }, admit_keys => { "SHA=12345" }; }
bundle agent example { files: "/etc/motd" copy_from => cp("/repo/motd"); }
/etc/motd should be a copy of a file described by the cp
copy_from body.copy_from bodybody copy_from cp (from) { servers => { "$(sys.policy_hub)" }; source => "$(from)"; compare => "digest"; }
sourceserverscompareserver bundlebundle server my_access_rules { access: policy_server|am_policy_hub:: "/repo" admit_ips => { "192.168.0.1/24" }, admit_keys => { "SHA=12345" }; }
admit_ips/repo.admi_keys/repo.bundle agent main { files: "/etc/ssh/sshd_config" edit_line => deny_root_ssh; } bundle edit_line deny_root_ssh { delete_lines: "^PermitRootLogin.*"; insert_lines: "PermitRootLogin no"; }
Can be one of several types:
Reference: Special Variables, Language Concepts -> Variables, Promise Types and Attributes -> vars
CFEngine doesn't have for loops, but it implicitly iterates over lists and data structure values.
bundle agent main { vars: "l" slist => { "two", "one", "three" }; "d" data => '[ "three", "one", "two"]'; "d2" data => '{ "one":"1", "two":"2", "three":"3"}'; reports: "l contains $(l)"; "d contains $(d)"; "d2 contains $(d2)"; }
# cf-agent --no-lock --file ./examples/list-iteration.cf R: l contains two R: l contains one R: l contains three R: d contains three R: d contains one R: d contains two R: d2 contains 1 R: d2 contains 2 R: d2 contains 3
bundle agent main { vars: "d" data => '{ "key": { "subkey": "value" } }'; "a[key][subkey]" string => "value"; reports: "$(const.dollar)(d[key][subkey]) == $(d[key][subkey])"; "$(const.dollar)(a[key][subkey]) == $(a[key][subkey])"; "d contains$(const.n)$(with)" with => string_mustache( "{{%-top-}}", d ); "a contains$(const.n)$(with)" with => string_mustache( "{{%-top-}}", a ); }
# cf-agent --no-lock --file ./examples/data-and-arrays.cf
R: $(d[key][subkey]) == value
R: $(a[key][subkey]) == value
R: d contains
{
"key": {
"subkey": "value"
}
}
R: a contains
{
"key": {
"subkey": "value"
}
}
A class is like a tag (like tagging a photo). Classes are used to give a promise context. Valid characters in classes are [A-Za-z0-9_] (alphanumeric and underscores). There are two types of classes.
linux.
Use cf-promsies --show-classes to see the first order of resolved classes.
cf-promises --show-classes | head
Class name Meta tags 127_0_0_1 inventory,attribute_name=none,source=agent,hardclass 172_17_0_1 inventory,attribute_name=none,source=agent,hardclass 172_27_224_133 inventory,attribute_name=none,source=agent,hardclass 192_168_122_1 inventory,attribute_name=none,source=agent,hardclass 192_168_42_189 inventory,attribute_name=none,source=agent,hardclass 4_cpus source=agent,derived-from=sys.cpus,hardclass 64_bit source=agent,hardclass Afternoon time_based,cfengine_internal_time_based_autoremove,source=agent,hardclass Day19 time_based,cfengine_internal_time_based_autoremove,source=agent,hardclass
bundle agent apache_config { files: debian:: "/etc/apache2/apache2.conf" copy_from => remote_cp("/cfengine/repo/debian/apache2.conf","$(sys.policy_hub)"); redhat:: "/etc/httpd/conf/httpd.conf" copy_from => remote_cp("/cfengine/repo/redhat/httpd.conf","$(sys.policy_hub)"); solaris:: "/etc/apache2/2.2/httpd.conf" copy_from => remote_cp("/cfengine/repo/solaris/httpd.conf","$(sys.policy_hub)"); }
bundle agent example { files: solaris:: "/tmp/hello/world" create => "true"; "/tmp/foo/bar" create => "true"; linux:: "/dev/shm/hello_world" create => "true"; commands: "/bin/echo Hello World"; }
any (implicit default)bundle agent main { reports: redhat:: # <- This context has no promises. 64_bit:: # <- This context has one promise. (not additive) "I am $(sys.flavor) running on $(sys.arch)"; }
redhat context64_bit context# cf-agent --no-lock --file ./examples/example_no_nested_classes.cf R: I am ubuntu_19 running on x86_64
bundle agent apache_config { commands: apache_config_repaired:: "/usr/sbin/apache2ctl graceful"; files: "/etc/apache2/apache2.conf" copy_from => remote_cp("/cfengine/repo/debian/apache2.conf", $(sys.policy_hub)), classes => results("bundle", "apache_config"); }
apache_config_repaired.apache_config_repaired is defined execute the command to restart
the service.commands: apache_config_repaired.debian:: "/usr/sbin/apache2ctl graceful"; apache_config_reparied.redhat:: "/usr/sbin/apachectl graceful";
| Operator | Meaning | Example |
|---|---|---|
. and & |
boolean and | debian.Tuesday:: |
ǀ and ǀǀ |
boolean or | TuesdayǀWednesday:: |
! |
boolean not | !Monday:: |
( ) |
Explicit grouping | (debianǀredhat).!ubuntu.!centos:: |
Since 3.7.0 CFEngine is able to dereference variables directly within class
expressions. Note that quotes surrounding the entire expression ending before
the :: are required.
bundle agent main { vars: "variable_containing_class" string => "cfengine"; reports: "$(variable_containing_class)":: "'$(variable_containing_class)' is defined"; "!$(variable_containing_class)":: "'$(variable_containing_class)' is NOT defined"; }
# cf-agent --no-lock --file ./examples/example_variable_class_expressions.cf R: 'cfengine' is defined
I said that only Debian systems will run debian:: and only Red Hat will run
redhat::. This isn't exactly true.
ubuntu and debian defined
as hard classes.centos and
redhat defined as hard classes.redat_pure and debian_pure.def.json)def.json is found next to policy entrydef bundle scopesupported_platform if the class ubuntu_14, ubuntu_16, or
ubuntu_17 is defined.by_hostname if the class nickanderson_thinkpad_w550s is defined.{ "classes": { "supported_platform": [ "ubuntu_\\d+" ], "by_hostname": [ "nickanderson_thinkpad_w550s" ] }, "vars": { "myvar1": "defined from augments", "myvar2": "defined from augments" } }
bundle agent main { reports: "I defined '$(const.dollar)(def.myvar1)' as '$(def.myvar1)'"; supported_platform:: "This is a supported platform"; by_hostname:: "You can define classes from augments based on defined hostname"; }
cf-agent --no-lock --file ./examples/augments/augments.cf
R: I defined '$(def.myvar1)' as 'defined from augments' R: This is a supported platform R: You can define classes from augments based on defined hostname
bundle common def { vars: "myvar1" string => "Defined in policy"; "myvar2" string => "Defined in policy", if => not( isvariable( myvar2 ) ); } bundle agent main { reports: "I defined '$(const.dollar)(def.myvar1)' as '$(def.myvar1)'"; "I defined '$(const.dollar)(def.myvar2)' as '$(def.myvar2)'"; supported_platform:: "This is a supported platform"; by_hostname:: "You can define classes from augments based on defined hostname"; }
cf-agent --no-lock --file ./examples/augments/augments-policy-wins.cf
R: I defined '$(def.myvar1)' as 'Defined in policy' R: I defined '$(def.myvar2)' as 'defined from augments' R: This is a supported platform R: You can define classes from augments based on defined hostname
Merge more specific augments (based on sys vars) on top.
{ "vars": { "myvar1": "defined from augments for all", "myvar2": "defined from augments for all" }, "augments": [ "$(sys.policy_entry_dirname)/$(sys.os).json" ] }
{ "vars": { "myvar2": "override for linux hosts" } }
bundle agent main { reports: "'$(const.dollar)(def.myvar1)' is '$(def.myvar1)'"; "'$(const.dollar)(def.myvar2)' is '$(def.myvar2)'"; }
cf-agent --no-lock --file ./examples/augments-multiple/promises.cf
R: '$(def.myvar1)' is 'defined from augments for all' R: '$(def.myvar2)' is 'override for linux hosts'
bundle agent apache { processes: ".*apache2.*" restart_class => "apache2_not_running"; commands: apache2_not_running:: "/etc/init.d/apache2 start"; }
bundle agent stop_bluetooth { processes: "bluetoothd" process_stop => "/etc/init.d/bluetooth stop"; }
This policy uses a processes promise to check the process table (with ps)
for the regular expression .*bluetoothd.*. If it is found the process_stop
command is executed.
bundle agent stop_bluetooth { processes: ".*bluetoothd.*" signals => { "term", "kill" }; }
This policy uses a processes promise to check the process table (with ps)
for the regular expression .*bluetoothd.*. Any matching process is sent the
term signal, then sent the kill signal.
bundle agent apache { services: "www" service_policy => "start"; }
This uses the services promise type to ensure that Apache is always running.
The standard_services bundle implementation currently covers systemd,
chkconfig, the service command, svcadm and systemV init scripts. Proper
functionality relies on each installed service correctly implementing a service
check as appropriate for the init system in use.
bundle agent stop_bluetoothd { services: "bluetoothd" service_policy => "stop"; }
This policy uses a services promise type to ensure that Bluetooth services are
not running. Again, this only works for services that are defined under
standard_services in the standard library and requires cfengine 3.4.0 or
higher.
The same restrictions about distros apply to stopping services promises.
Services promises are really an abstraction on bundles.
apt_get, pkgsrc, freebsd_ports, slackpkg, msiexec, yum, nimclient,
zypper, pkg
bundle agent install { packages: "zsh" policy => "present", package_module => yum, version => "latest"; }
policy of present will make sure the package is installed on the
system, while a policy of absent will ensure a package is not installed.package_module of yum is included in the Masterfiles Policy Framework.version of latest means the installed version should be the latest
available. Alternatively you can provide an explicit version.
alpinelinux, freebsd, opencsw, solaris_install, apt,
freebsd_portmaster, pacman, windows_feature, apt_get, generic, pip,
yum, apt_get_permissive, ips, rpm_filebased, yum_group,
apt_get_release, msi_explicit, rpm_version, yum_rpm, brew,
msi_implicit, smartos, yum_rpm_enable_repo, dpkg_version, npm,
smartos_pkg_add(repo), yum_rpm_permissive, emerge, npm_g, solaris,
zypper,
bundle agent install { packages: "zsh" package_policy => "addupdate", package_method => apt, package_select => ">=, package_version => "4.3.10-14"; }
package_policy of addupdate will install or upgrade. Using add
will only install, never upgrade, upgrade will upgrade only and delete
will uninstall.package_method of apt is in the standard library, look there for other
package methods (e.g., rpm, ips, etc.).package_select of >= means the installed version must be equal to or
newer than the specified version or it will be replaced. Using <= would
downgrade, if the package_method supports downgrading and == will
require the exact version.Hello from {{{vars.sys.fqhost}}}!
{{#classes.linux}}I am a Linux Box!{{/classes.linux}}
{{^classes.windows}}I am NOT a Windows Box{{/classes.windows}}
bundle agent main{ files: "/tmp/example" create => "true", edit_template => "$(this.promise_dirname)/template.mustache", template_method => "mustache"; }
-top-@%$packagesmatching()
packagesmatching() returns data. Render the multiline JSON representation of the data.
bundle agent main { vars: "p" data => packagesmatching( "emacs.*", ".*", ".*", ".*"); "r" string => string_mustache( "{{%-top-}}", p ), if => not(isvariable( r ) ); reports: "$(r)"; }
packagesmatching()
R: [
{
"arch": "default",
"method": "dpkg",
"name": "emacsen-common",
"version": "2.0.8"
},
{
"arch": "default",
"method": "dpkg",
"name": "emacs24-common-non-dfsg",
"version": "24.4+1-2"
},
{
"arch": "default",
"method": "dpkg",
"name": "emacs24-common",
"version": "24.5+1-1ubuntu2"
},
{
"arch": "default",
"method": "dpkg",
"name": "emacs24-bin-common",
"version": "24.5+1-1ubuntu2"
},
{
"arch": "default",
"method": "dpkg",
"name": "emacs24",
"version": "24.5+1-1ubuntu2"
},
{
"arch": "default",
"method": "dpkg",
"name": "emacs",
"version": "46.1"
}
]
{{{VAR}}} or {{& VAR}}. Mustache html escapes by
default.bundle agent tidy { files: "/var/log/.*" file_select => days_old("7"), delete => tidy; }
This policy will delete any files in /var/log/ older than 7 days. The
days_old() and tidy bodies are included in the standard library,
To delete a file indiscriminately, omit the file_select.
Look up file_select and tidy in the reference-manual to find more ways to
use this.
cat /var/cfengine/policy_server.datcf-promises --show-vars | grep sys.policy_hubps -ef | grep [c]f-
You should expect to find cf-execd, cf-serverd, and cf-monitord on all
hosts. Additional processes will be seen on Enterprise Hubs
ls -lh /var/cfengine/promise_summary.logls /var/cfengine/outputscat /var/cfengine/outputs/previouscf-hub --hail <IP|HOSTNAME> --verbose --query rebasecf-hub -H <IP|HOSTNAME> -v -q delta[root@hub ~]# cf-hub -H 10.10.10.11 -q rebase error: Abort transmission: got " Unspecified server refusal (see verbose server output)" from 10.10.10.11
--verbose and --no-fork to see why it's
refusing5308cf-serverd not running on remote hostBefore starting you need to have cfengine installed on the server and the client and the server FQDN must be set properly in DNS (or use the IP addresses). This is ideally handled by your provisioning process. Along with automating server function you should also be automating your provisioning process.
Some ways of automating provisioning are kickstart, preseed, fai, cobbler, disk imaging, instance cloning, etc, etc. This, of course, is not a complete list.
Edit /var/cfengine/masterfiles/def.cf to set the acl list for the IP
addresses of your network, then run:
cf-agent --bootstrap $(hostname --fqdn) cf-agent -KI
Simply run:
cf-agent --bootstrap server.fqdn.example.com
You can use the server's IP address instead of the DNS name.
Policy is distributed from /var/cfengine/masterfiles on the server (also known as
the policy_hub) and are copied to /var/cfengine/inputs. All clients then
copy /var/cfengine/inputs from the server.
CFEngine logs to /var/cfengine/promise_summary.log. Here's an example log message:
1463018982,1463018990: Outcome of version CFEngine Promises.cf 3.7.0 (agent-0):\ Promises observed - Total promise compliance: 93% kept, 3% repaired,\ 4% not kept (out of 148 events).\ User promise compliance: 93% kept, 2% repaired, 5% not kept (out of 130 events). CFEngine system compliance: 94% kept, 6% repaired, 0% not kept (out of 18 events).
Note: The timestamp is a Unix epoch.
CFEngine will also send an email to the configured address in body executor
control= any time there is output from an agent run that differed from the
previous run.
And finally you can use the -I flag to have CFEngine inform you of repairs.
(Shown here along with the -K flag which ignores any lock timers).
cf-agent -KI
Running the agent in verbose mode ( cf-agent --verbose | cf-agent -v )
provides all of the details about each promise and its result
bundle agent main { files: "/tmp/example" handle => "example_file_exists_and_contains_date", create => "true", edit_line => lines_present( $(sys.date) ); } bundle edit_line lines_present(lines) # @brief Ensure `lines` are present in the file. Lines that do not exist are appended to the file # @param List or string that should be present in the file # # **Example:** # # ```cf3 # bundle agent example # { # vars: # "nameservers" slist => { "8.8.8.8", "8.8.4.4" }; # # files: # "/etc/resolv.conf" edit_line => lines_present( @(nameservers) ); # "/etc/ssh/sshd_config" edit_line => lines_present( "PermitRootLogin no" ); # } # ``` { insert_lines: "$(lines)" comment => "Append lines if they don't exist"; }
In the verbose output as each promise is actuated a BEGIN promsie is emitted
with the promise handle or filename and line number position if it does not have
a handle. In the example output we can see that the promise for /tmp/example
was REPAIRED.
verbose: B: ***************************************************************** verbose: B: BEGIN bundle main verbose: B: ***************************************************************** verbose: P: ......................................................... verbose: P: BEGIN promise 'example_file_exists_and_contains_date' of type "files" (pass 1) verbose: P: Promiser/affected object: '/tmp/example' verbose: P: Part of bundle: main verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'[1] verbose: Using literal pathtype for '/tmp/example' verbose: No mode was set, choose plain file default 0600 info: Created file '/tmp/example', mode 0600 verbose: Handling file edits in edit_line bundle 'lines_present' verbose: V: + Private parameter: 'lines' in scope 'lines_present' (type: s) in pass 1 verbose: P: ......................................................... verbose: P: BEGIN promise 'promise_example_cf_32' of type "insert_lines" (pass 1) verbose: P: Promiser/affected object: 'Mon Dec 4 21:08:38 2017' verbose: P: Part of bundle: lines_present verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'/default/lines_present/insert_lines/'Mon Dec 4 21:08:38 2017'[1] verbose: P: verbose: P: Comment: Append lines if they don't exist verbose: Additional promise info: source path './example.cf' at line 32 comment 'Append lines if they don't exist' verbose: Inserting the promised line 'Mon Dec 4 21:08:38 2017' into '/tmp/example' after locator verbose: P: ......................................................... verbose: P: BEGIN promise 'promise_example_cf_32' of type "insert_lines" (pass 1) verbose: P: Promiser/affected object: 'Mon Dec 4 21:08:38 2017' verbose: P: Part of bundle: lines_present verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'/default/lines_present/insert_lines/'Mon Dec 4 21:08:38 2017'[1] verbose: P: verbose: P: Comment: Append lines if they don't exist verbose: P: ......................................................... verbose: P: BEGIN promise 'promise_example_cf_32' of type "insert_lines" (pass 1) verbose: P: Promiser/affected object: 'Mon Dec 4 21:08:38 2017' verbose: P: Part of bundle: lines_present verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'/default/lines_present/insert_lines/'Mon Dec 4 21:08:38 2017'[1] verbose: P: verbose: P: Comment: Append lines if they don't exist info: Edit file '/tmp/example' verbose: Handling file existence constraints on '/tmp/example' verbose: A: Promise REPAIRED verbose: P: END files promise (/tmp/example) verbose: P: ......................................................... verbose: P: BEGIN promise 'example_file_exists_and_contains_date' of type "files" (pass 2) verbose: P: Promiser/affected object: '/tmp/example' verbose: P: Part of bundle: main verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'[1] verbose: Using literal pathtype for '/tmp/example' verbose: P: ......................................................... verbose: P: BEGIN promise 'example_file_exists_and_contains_date' of type "files" (pass 3) verbose: P: Promiser/affected object: '/tmp/example' verbose: P: Part of bundle: main verbose: P: Base context class: any verbose: P: Stack path: /default/main/files/'/tmp/example'[1] verbose: Using literal pathtype for '/tmp/example' verbose: A: ................................................... verbose: A: Bundle Accounting Summary for 'main' in namespace default verbose: A: Promises kept in 'main' = 0 verbose: A: Promises not kept in 'main' = 0 verbose: A: Promises repaired in 'main' = 2 verbose: A: Aggregate compliance (promises kept/repaired) for bundle 'main' = 100.0% verbose: A: ................................................... verbose: B: ***************************************************************** verbose: B: END bundle main verbose: B: ***************************************************************** verbose: Generate diff state reports for policy './example.cf' SKIPPED verbose: No lock purging scheduled verbose: Outcome of version (not specified) (agent-0): Promises observed - Total promise compliance: 0% kept, 100% repaired, 0% not kept (out of 2 events). User promise compliance: 0% kept, 100% repaired, 0% not kept (out of 2 events). CFEngine system compliance: 0% kept, 0% repaired, 0% not kept (out of 0 events).
Promises can be configured to log their outcomes to a file with log_kept,
log_repaired, and log_failed attributes in an action body.
bundle agent main { commands: "/bin/true" action => log_my_repairs( '/tmp/repaired.log' ); reports: "/tmp/repaired.log" printfile => cat( $(this.promiser) ); } body action log_my_repairs( file ) { log_repaired => "$(file)"; log_string => "$(sys.date) REPAIRED $(this.promiser)"; }
R: /tmp/repaired.log R: Mon Dec 4 21:21:38 2017 REPAIRED /bin/true
CFEngine enterprise provides details logging without special configuration.
The changes reporting interface is the easiest way to what repairs the agent is making to your infrastructure.
Changes can also be queried from the changes rest api. Here we query for repairs made
by files type promises.
[root@hub ~]# curl https://hub/api/v2/changes/policy?promisetype=files
{
"data": [
{
"bundlename": "cfe_internal_update_policy",
"changetime": 1512427971,
"hostkey": "SHA=01fe75e93ca88bbd381eb720e9b43d0840ea8727aae8fc84391c297c42798f5c",
"hostname": "hub",
"logmessages": [
"Copying from 'localhost:/var/cfengine/masterfiles/cf_promises_release_id'"
],
"policyfile": "/var/cfengine/inputs/cfe_internal/update/update_policy.cf",
"promisees": [],
"promisehandle": "cfe_internal_update_policy_files_inputs_dir",
"promiser": "/var/cfengine/inputs",
"promisetype": "files",
"stackpath": "/default/cfe_internal_update_policy/files/'/var/cfengine/inputs'[1]"
},
{
"bundlename": "cfe_internal_setup_knowledge",
"changetime": 1512428912,
"hostkey": "SHA=01fe75e93ca88bbd381eb720e9b43d0840ea8727aae8fc84391c297c42798f5c",
"hostname": "hub",
"logmessages": [
"Owner of '/var/cfengine/httpd/htdocs/application/logs/./log-2017-12-04.log' was 0, setting to 497",
"Group of '/var/cfengine/httpd/htdocs/application/logs/./log-2017-12-04.log' was 0, setting to 497",
"Object '/var/cfengine/httpd/htdocs/application/logs/./log-2017-12-04.log' had permission 0644, changed it to 0640"
],
"policyfile": "/var/cfengine/inputs/cfe_internal/enterprise/CFE_knowledge.cf",
"promisees": [],
"promisehandle": "cfe_internal_setup_knowledge_files_doc_root_application_logs",
"promiser": "/var/cfengine/httpd/htdocs/application/logs/.",
"promisetype": "files",
"stackpath": "/default/cfe_internal_management/methods/'CFEngine_Internals'/default/cfe_internal_enterprise_main/methods/'hub'/default/cfe_internal_setup_knowledge/files/'/var/cfengine/httpd/htdocs/application/logs/.'[1]"
}
],
"total": 2,
"next": null,
"previous": null
}
See Also: query rest api
The custom reports interface and associated query rest api allow more flexible reports to be run.
Queries can be made against the promiselog table. This query finds the
promises that are repaired the most excluding internal cfengine related promises
and promises from the stdlib.
-- Find most frequently repaired promises excluding lib and cfe_internal directories SELECT namespace,bundlename,promisetype,promisehandle, promiser, count(promiseoutcome) AS count FROM promiselog WHERE promiseoutcome = 'REPAIRED' AND policyfile NOT ilike '%/lib/%' AND policyfile NOT ilike '%cfe_internal%' GROUP BY namespace, bundlename, promisetype,promisehandle,promiser ORDER BY count DESC
Reference: query api examples
WARNING: These logs are purged upon collection by the hub.
In Enterprise 3.7 each agent run logs to a CSV file named for the time the agent
started in $(sys.workdir)/state/promise_log/.
The fields are promise hash, policy file, release id, unknown (waiting on
developer feedback), namespace, bundle, promise type, stack path (call
tree), promise handle, promisees, log messages
719b756d3dc8fd7bdd20284c1fd894ae40bac55d8790855b074159db8fe187ae,/var/cfengine/inputs/cfe_internal/enterprise/CFE_hub_specific.cf,<unknown-release-id>,114,default,cfe_internal_update_folders,files,/var/cfengine/master_software_updates/windows_i686,/default/cfe_internal_management/methods/'CFEngine_Internals'/default/cfe_internal_enterprise_main/methods/'hub'/default/cfe_internal_update_folders/files/'/var/cfengine/master_software_updates/windows_i686'[40],cfe_internal_update_folders_files_create_dirs,"[""goal_updated""]","[""Created directory '/var/cfengine/master_software_updates/windows_i686/.'""]"
WARNING: These logs are purged upon collection by the hub.
Beginning with Enterprise 3.9 we began logging promise outcomes to a JSON format
in $(sys.statedir)/promise_log.jsonl.
Each promise outcome is logged along with the bundle name, promise handle, log messages near the promise actuation, the promise namespace, policy filename, promise hash, promise type, promisees, promiser, release id, stack path (call path), and the timestamp of the agent ran.
Here is an example of the output:
{ "execution": { "bundle":"file_make_mustache", "handle":"", "log_messages":[ "Created file '/var/cfengine/httpd/conf/httpd.conf.staged', mode 0600", "Updated rendering of '/var/cfengine/httpd/conf/httpd.conf.staged' from mustache template '/var/cfengine/inputs/cfe_internal/enterprise/templates/httpd.conf.mustache'" ], "namespace":"default", "policy_filename":"/var/cfengine/inputs/lib/files.cf", "promise_hash":"ebc3dce615bcdb724e53a9761a24f2e7ed4f2e01aed1ce85dc217a9d3429fed7", "promise_outcome":"REPAIRED", "promise_type":"files", "promisees":[ "CFEngine Enterprise", "Mission Portal"], "promiser":"/var/cfengine/httpd/conf/httpd.conf.staged", "release_id":"<unknown-release-id>", "stack_path":"/default/cfe_internal_management/methods/'CFEngine_Internals'/default/cfe_internal_enterprise_mission_portal/methods/'Apache Configuration'/default/cfe_internal_enterprise_mission_portal_apache/methods/'Stage Apache Config'/default/file_make_mustache/files/'/var/cfengine/httpd/conf/httpd.conf.staged'[0]" }, "timestamp":1470326639 }, { "execution":{ "bundle":"mission_portal_apache_from_stage", "handle":"", "log_messages":[ "Updated '/var/cfengine/httpd/conf/httpd.conf' from source '/var/cfengine/httpd/conf/httpd.conf.staged' on 'localhost'" ], "namespace":"default", "policy_filename":"/var/cfengine/inputs/cfe_internal/enterprise/mission_portal.cf", "promise_hash":"d730f2911834395411e4f3168847fc6cc522955f97652de41e02c8bc15f3f761", "promise_outcome":"REPAIRED", "promise_type":"files", "promisees":[ "CFEngine Enterprise", "Mission Portal" ], "promiser":"/var/cfengine/httpd/conf/httpd.conf", "release_id":"<unknown-release-id>", "stack_path":"/default/cfe_internal_management/methods/'CFEngine_Internals'/default/cfe_internal_enterprise_mission_portal/methods/'Apache Configuration'/default/cfe_internal_enterprise_mission_portal_apache/methods/'Manage Final Apache Config'/default/mission_portal_apache_from_stage/files/'/var/cfengine/httpd/conf/httpd.conf'[0]" }, "timestamp":1470326639 }
Inevitably, something will go wrong, and you will need to dig deep to figure something out. Lucky for you, I have some tips for debugging.
Again, using -K to disable locks is useful.
CFEngine's verbose output can be fantastic for debugging. Use the -v flag to
turn it on.
cf-agent -Kv | grep -A 5 "BEGIN bundle"
When viewing verbose output, look for BUNDLE <name> for the bundle that you
suspect is having trouble.
verbose: B: BEGIN bundle main verbose: B: ***************************************************************** verbose: P: ......................................................... verbose: P: BEGIN promise 'promise_promises_cf_4' of type "reports" (pass 1) verbose: P: Promiser/affected object: 'Hello World!' verbose: P: Part of bundle: main
CFEngine will tell you exactly what is going on with each promise, in excruciating detail.
verbose: Using literal pathtype for '/tmp/touch' verbose: No mode was set, choose plain file default 0600 info: Created file '/tmp/touch', mode 0600 verbose: Handling file existence constraints on '/tmp/touch' verbose: A: Promise REPAIRED verbose: P: END files promise (/tmp/touch...)
CFEngine supports comments as part of its data structure. Every promise can
have a comment attribute whose value is a quoted text string.
bundle agent example { files: "/etc/bind/named.cache" copy_from => scp("$(def.files)/bind/named.cache"), comment => "More recent copy of named.cache than shipped with bind"; }
Comments show up in the verbose output.
verbose: P: Container path : '/default/main/files/'/etc/bind/named.cache'[0]' verbose: P: verbose: P: Comment: More recent copy of named.cache than shipped with bind. verbose: P: .........................................................
The comment should always be why the promise is being made. Up until now none of the examples have used comments to save space on the slide. When writing your policies for real every promise should have a meaningful comment.
You'll thank me when this saves the day.
When debugging, promise handles are also useful. Again, every promise can have
a handle attribute whose value is a quoted canonical string.
bundle agent example{ files: "/etc/bind/named.cache" copy_from => scp("$(def.files)/bind/named.cache"), handle => "update_etc_bind_named_cache", comment => "More recent copy of named.cache than shipped with bind"; }
CFEngine will tell you the handle of each promise in the verbose output.
verbose: P: BEGIN promise 'update_etc_bind_named_cache' of type "files" (pass 1) verbose: P: Promiser/affected object: '/etc/bind/named.cache' verbose: P: Part of bundle: example verbose: P: Base context class: any
By giving each promise a unique handle you can swiftly jump back and forth between your debug output and your policy file. When writing your policies for real every promise should have a unique handle.
You'll thank me when this saves the day.
When debugging, promise stakeholders aka promisees are useful for understanding who cares about a given promise.
bundle agent example { files: "/etc/bind/named.cache" -> { "Operations", "Nick Anderson" } copy_from => scp("$(def.files)/bind/named.cache"), handle => "update_etc_bind_named_cache", comment => "More recent copy of named.cache than shipped with bind"; }
CFEngine will tell you additional info about each promise.
verbose: Additional promise info: handle 'update_etc_bind_named_cache'\
source path './t.cf' at line 4 promisee {'Operations','Nick Anderson'}\
comment 'More recent copy of named.cache than shipped with bind.'
When debugging variables and classes promise meta data is useful to help identify variables and classes with specific attributes.
bundle agent main{ classes: "my_class" expression => "any", meta => { "mytag" }; vars: "my_var" string => "value", meta => { "mytag" }; "my_vars" slist => variablesmatching(".*", "mytag" ); "my_classes" slist => classesmatching(".*", "mytag" ); reports: "My var: $(my_vars)"; "My class: $(my_classes)"; }
Note: Promise meta data is not currently displayed in the CFEngine's verbose output.
Here's a list of topics that I didn't cover. This is to give you a taste of the rest of the power that is behind CFEngine. Dig deeper by checking them out in the reference manual.
vars: promises — Varables, strings, integers and reals (and lists of each).methods: promises — Create a self-contained bundle that can be called like a
function.storage: promises — For local or remote (NFS) filesystems.edit_xml: promises - Promise by path, CFEngine does the XML for you.cf-monitord.site_lib.cf and add your custom
library bundles and bodies there. This helps with upgrading because you won't
have to patch your changes into the new version of the library. When you feel
a bundle or body is ready for public use you can submit it to CFEngine by
opening a pull request on Github.all_lower_case_separated_by_underscores.
Whenever I define classes myself I use CamelCase.masterfiles?
git to revision control masterfiles.For example:
bundle agent main { classes: "my-illegal-class"; reports: "$(with)" with => join( " ", classesmatching( "my.illegal.class" ) ); }
R: my_illegal_class
The agent assumes you intended to canonify the string in the spirit of auto correction it canonifies it for you.
This courtesy is not extended when checking classes. You must explicitly canonify your string when using it in a class expression.
For example:
bundle agent main { vars: "hostname" string => "$(sys.uqhost)"; reports: any:: "$(hostname) contains invalid class characters"; "The class expression containing a nonvalid character is not a valid class expression"; "The agent silently skips the section"; "$(hostname)":: "hello"; any:: "See that explicit canonification works"; "Hi" if => canonify( $(hostname) ); }
R: nickanderson-thinkpad-w550s contains invalid class characters R: The class expression containing a nonvalid character is not a valid class expression R: The agent silently skips the section R: See that explicit canonification works R: Hi
For example:
bundle agent main { files: "/tmp/dir/." create => "true", perms => m(600); vars: "mode" string => filestat( "/tmp/dir", permoct ); reports: "/tmp/dir mode is $(with)" with => filestat( "/tmp/dir", permoct ); }
R: /tmp/dir mode is 700
This is configurable behavior but by default if you promise a directory should be readable (list the files within the directory) the agent assumes that you also meant for it to be executable so that it can be entered and access the file and directories inside.
To disable the feature set rxdirs to false in the perms body you are
using.
For example:
bundle agent main { files: "/tmp/dir/." create => "true", perms => my_m_norxdir(600); vars: "mode" string => filestat( "/tmp/dir", permoct ); reports: "/tmp/dir mode is $(with)" with => filestat( "/tmp/dir", permoct ); } body perms my_m_norxdir(mode) { rxdirs => "false"; inherit_from => m( $(mode) ); # body inheritance available since 3.8.0 }
R: /tmp/dir mode is 600
Created by Nick Anderson.