Whether you are migrating from Ansible to CFEngine to gain some of the benefits of scale or autonomy or just need some functionality in an Ansible module, the ansible promise type can be a great tool to utilize.
It also provides a compelling alternative to ansible-pull and works around some of the caveats included with that strategy. CFEngine has battle-tested features needed for the pull architecture:
- cf-execd handles scheduling periodic runs as ansible-pull suggests using cron
- cf-agent handles locking to avoid concurrent runs of the same playbooks
A tiny Ansible project example
Taking some first-step tips from 5 ways to harden a new system with Ansible let’s make a sample playbook project which patches Linux systems.
Ansible has some good documentation about best practices and roles that make it easy to develop a somewhat realistic mini ansible playbook project.
For reference, I have shared a cfbs project in the promise-type-ansible branch of our repository of example projects at play-cfbs.
The crux of our tiny Ansible project is to patch our hosts.
This will be implemented as a patch role in roles/patch/tasks/main.yml
:
- name: Perform full patching
package:
name: '*'
state: latest
This role is included in our main site.yml
file as follows:
---
- hosts: managed
tasks:
- ansible.builtin.debug:
msg: first play
roles:
- patch
In this tiny project the managed
group includes all hosts.
CFEngine policy to ensure the playbooks are run
The next step is to make an inventory file for Ansible which will enable running the playbook on each managed host but using a local connection so that no SSH credentials need be configured.
For a typical set of playbooks you would likely have many groups in your inventory. We can map classes in CFEngine to groups in this localhost inventory with some CFEngine policy:
classes:
"managed"
meta => { "ansible_group" };
"webserver"
meta => { "ansible_group" };
vars:
"localhost_inventory"
string => "$(sys.statedir)/localhost_inventory.ini";
"ansible_groups"
slist => classesmatching(".*", "ansible_group");
files:
"$(localhost_inventory)"
create => "true",
content => string_mustach("[all:vars]
ansible_connection=local
{{#-top-}}[{{.}}]
localhost
{{/-top-}}",
"ansible_groups"),
classes => if_ok("localhost_inventory_created");
This creates a file we will use later in our policy:
[all:vars]
ansible_connection=local
[managed]
localhost
[webserver]
localhost
We use the built-in group all
to set ansible_connection=local
so we can run the playbooks manually using the generated inventory.
The ansible
promise type defaults to this setting.
Of course, we need Ansible to be installed on this host, so we use a package promise and set a class to wait until it is installed before trying to run the playbook.
packages:
"ansible"
classes => if_ok("ansible_package_installed");
Finally our policy handles copying the ansible_project
directory to a nice place in /var/cfengine/state
and runs site.yml
the top-level site playbook.
vars:
"remote_ansible_project"
string => string_replace("$(this.promise_dirname)", "inputs", "masterfiles");
"ansible_project"
string => "$(sys.statedir)/ansible_project";
files:
"$(ansible_project)"
copy_from => remote_dcp("$(remote_ansible_project)", @(def.policy_servers)),
depth_search => recurse(inf);
ansible:
ansible_package_installed.localhost_inventory_created::
"site.yml"
playbook => "$(ansible_project)/site.yml",
inventory => "$(localhost_inventory)";
The complete policy
bundle agent ansible_main
{
vars:
"ansible_project"
string => "$(sys.statedir)/ansible_project";
"localhost_inventory"
string => "$(sys.statedir)/localhost_inventory.ini";
"remote_ansible_project"
string => string_replace("$(this.promise_dirname)", "inputs", "masterfiles");
classes:
# initial thought about how to translate CFEngine classification into Ansible groups classification for dynamic inventory
"managed"
meta => { "ansible_group" };
"webserver"
meta => { "ansible_group" };
vars:
"ansible_groups" slist => classesmatching(".*", "ansible_group");
reports:
"ansible_groups: ${ansible_groups}";
files:
"$(localhost_inventory)"
create => "true",
content => string_mustache("[all:vars]
ansible_connection=local
{{#-top-}}[{{.}}]
localhost
{{/-top-}}",
"ansible_groups"
),
classes => if_ok("localhost_inventory_created");
"$(ansible_project)"
copy_from => remote_dcp($(remote_ansible_project), @(def.policy_servers)),
depth_search => recurse(inf);
packages:
"ansible"
classes => if_ok("ansible_package_installed");
ansible:
ansible_package_installed.localhost_inventory_created::
"site.yml"
playbook => "$(ansible_project)/site.yml",
inventory => "$(localhost_inventory)";
}
Debugging CFEngine and Ansible
Debugging problems is a little tricky. You will have to use both your CFEngine and Ansible debugging skills. To get started you can run the policy with debug logging enabled and focus on the bundle in question with
cf-agent -Kdb ansible_main | tee ~/log
And to debug the Ansible side run the playbook something like this:
ansible-playbook -i /var/cfengine/state/localhost_inventory.ini /var/cfengine/state/ansible_project/site.yml
In the cf-agent debug output will look something like the following:
debug: DeRefCopyPromise(): promiser:'site.yml'
debug: DeRefCopyPromise(): copying constraint: 'playbook'
debug: DeRefCopyPromise(): copying constraint: 'inventory'
verbose: Starting custom promise module '/var/cfengine/modules/promises/ansible_promise.py' with command '/usr/bin/python3 /var/cfengine/modules/promises/ansible_promise.py'
debug: Received header from promise module: 'ansible_promise_module 0.2.2 v1 json_based'
debug: Received line from module: '{"operation": "validate_promise", "promiser": "site.yml", "attributes": {"inventory": "/var/cfengine/state/localhost_inventory.ini", "playbook": "/var/cfengine/state/ansible_project/site.yml"}, "result": "valid"}'
debug: Received line from module: 'log_verbose=Task 'Gathering Facts' started on 'localhost''
verbose: Task 'Gathering Facts' started on 'localhost'
debug: Received line from module: 'log_verbose=Task 'Gathering Facts' didn't change'
verbose: Task 'Gathering Facts' didn't change
debug: Received line from module: 'log_verbose=Task 'Perform full patching' started on 'localhost''
verbose: Task 'Perform full patching' started on 'localhost'
debug: Received line from module: 'log_error=Task 'Perform full patching' failed'
error: Task 'Perform full patching' failed
debug: Received line from module: 'log_verbose=Summary of the tasks for 'localhost' is: ok=1 failures=1'
verbose: Summary of the tasks for 'localhost' is: ok=1 failures=1
debug: Received line from module: '{"operation": "evaluate_promise", "promiser": "site.yml", "attributes": {"inventory": "/var/cfengine/state/localhost_inventory.ini", "playbook": "/var/cfengine/state/ansible_project/site.yml"}, "result": "not_kept", "result_classes": ["site.yml_failed"]}'
debug: Setting class: default:site_yml_failed
verbose: Additional promise info: version 'CFEngine Promises.cf 3.21.4' source path '/var/cfengine/inputs/services/cfbs/ansible_project/main.cf' at line 43
verbose: Promise with promiser 'site.yml' was not kept by promise module '/var/cfengine/modules/promises/ansible_promise.py'
verbose: A: Promise NOT KEPT!
verbose: P: END ansible promise (site.yml)
Let us know how it goes if you try it out!
Questions?
If you have questions or need help, reach out on the mailing list or GitHub discussions. If you have a support contract, feel free to open a ticket in our support system.