Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions cfbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,56 @@
}
]
},
"run-ansible-playbooks": {
"description": "Run Ansible playbooks",
"subdirectory": "management/run-ansible-playbooks",
"dependencies": ["promise-type-ansible"],
"steps": [
"copy main.cf services/cfbs/modules/run-ansible-playbooks/main.cf",
"policy_files services/cfbs/modules/run-ansible-playbooks/main.cf",
"bundles run_ansible_playbooks:main",
"input ./input.json def.json"
],
"input": [
{
"type": "list",
"variable": "playbooks",
"namespace": "run_ansible_playbooks",
"bundle": "main",
"label": "Playbooks",
"subtype": [
{
"key": "path",
"type": "string",
"label": "Path",
"question": "Playbooks to run"
},
{
"key": "condition",
"type": "string",
"label": "Condition",
"question": "Condition for when to run",
"default": "any"
},
{
"key": "ifelapsed",
"type": "string",
"label": "ifelapsed",
"question": "Number of minutes between playbook assessments",
"default": "5"
}
],
"while": "Do you want to specify more playbooks to be run?"
}
]
},
"promise-type-ansible": {
"description": "Promise type to manage systemd services.",
"subdirectory": "promise-types/ansible",
"dependencies": ["library-for-promise-types-in-python"],
"steps": [
"copy ansible_promise.py modules/promises/",
"copy ansible_promise.sh modules/promises/",
"append enable.cf services/init.cf"
]
},
Expand Down
40 changes: 40 additions & 0 deletions management/run-ansible-playbooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
This module enables the running of Ansible playbooks in masterfiles.
Playbook paths are specified based on input from Build in Mission Portal or `cfbs input`.

**Note:** Each playbook must be stored in masterfiles.
You can achieve this by copying them into a subdirectory in your cfbs project (e.g. `playbooks/`), followed by:

```
$ cfbs add ./playbooks/
WARNING: Did not find any bundles to add to bundlesequence
Added module: ./playbooks/
```

Don't mind the warning.
After building and deploying your project the playbooks will end up in `$(sys.inputdir)/services/cfbs/playbooks/`.

**Note:** The playbooks will be distributed and run locally with root-privilege (uid=0).

**Note:** Ansible must be installed on the hosts.
You can use a module to install Ansible *_(See [install-ansible](https://build.cfengine.com/modules/install-ansible/) build module)_*:
```
cfbs add install-ansible
```

**Usage:**
- `path` - The playbook path.
Must be absolute, e.g. `$(sys.inputdir)/services/cfbs/playbooks/playbook.yaml`.
- `condition` - Condition for running the playbook.
Use a class expression (e.g., `linux|bsd`).
Defaults to `any`.
- `ifelapsed` - Minimum number of minutes between each run.
Defaults to 5 minutes.

## Contribute

Feel free to open pull requests to expand this documentation, add features or fix problems.
You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/projects/CFE).

## License

This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text.
61 changes: 61 additions & 0 deletions management/run-ansible-playbooks/main.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
body file control
{
namespace => "run_ansible_playbooks";
}

body action ifelapsed(minutes)
{
ifelapsed => "$(minutes)";
}

bundle agent playbook(playbook, inventory)
{
ansible:
"$(playbook)" inventory => "$(inventory)";
}

bundle agent main
{
classes:
"enable" if => isvariable("playbooks");

vars:
"i" slist => getindices("playbooks");
"inventory" string => "$(sys.statedir)/run-ansible-playbooks/inventory.ini";

files:
enable::
"$(with)/."
create => "true",
with => dirname("$(inventory)");

"$(inventory)"
content => "[local]$(const.n)localhost ansible_connection=local",
comment => concat(
"Prevents ansible from complaining about a missing inventory file ",
"and causes the playbook to be run on the local host."
);

methods:
enable::
"playbooks[$(i)]"
usebundle => playbook("$(playbooks[$(i)][path])", "$(inventory)"),
if => "$(playbooks[$(i)][condition])",
action => ifelapsed("$(playbooks[$(i)][ifelapsed])"),
comment => concat(
"Run playbook '$(playbooks[$(i)][path])' ",
"if condition '$(playbooks[$(i)][condition])' ",
"every $(playbooks[$(i)][ifelapsed]) minutes"
);
}

body file control
{
namespace => "default";
}

bundle agent __main__
{
methods:
"run_ansible_playbooks:main";
}
4 changes: 4 additions & 0 deletions promise-types/ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ bundle agent main

* Ansible >= 2.8.0

The promise type runs under the Python interpreter of the pipx virtualenv that
the `install-ansible` module creates, falling back to `/usr/bin/python3` when
Ansible was installed some other way.

## Attributes

| Name | Type | Description | Mandatory | Default |
Expand Down
5 changes: 5 additions & 0 deletions promise-types/ansible/ansible_promise.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ def must_be_absolute(v):
self.add_attribute("private_key_file", str, validator=must_be_absolute)
self.add_attribute("remote_user", str, default="root")

# Standard CFEngine promise attribute, forwarded to the promise module
# by the agent instead of being consumed by it. Declared so that it is
# accepted rather than rejected as an unknown attribute.
self.add_attribute("comment", str)

def prepare_promiser_and_attributes(self, promiser, attributes):
safe_promiser = promiser.replace(",", "_")
return (safe_promiser, attributes)
Expand Down
38 changes: 38 additions & 0 deletions promise-types/ansible/ansible_promise.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh
# Wrapper choosing the Python interpreter that runs ansible_promise.py.
#
# The [install-ansible](https://build.cfengine.com/modules/install-ansible/)
# build module installs Ansible with `pipx install --global`, which places it in
# an isolated virtualenv under /opt/pipx/venvs. The system interpreter cannot
# import Ansible from there, so prefer the virtualenv's interpreter and fall
# back to /usr/bin/python3 for hosts where Ansible was installed some other way.

module="$(dirname "$0")/ansible_promise.py"

can_import_ansible() {
if [ ! -x "$1" ]; then
return 1
fi
"$1" -c "import ansible" >/dev/null 2>&1
}

# The ansible command is a Python console script, so its shebang names the
# interpreter it was installed for. Reading it finds the right virtualenv no
# matter where pipx put it, and whether ansible or ansible-core was installed.
ansible_bin="$(command -v ansible)"
if [ -n "$ansible_bin" ]; then
python="$(sed -n '1s|^#! *\([^ ]*\).*|\1|p' "$ansible_bin")"
if can_import_ansible "$python"; then
exec "$python" "$module" "$@"
fi
fi

# Default locations, in case the command above is missing from the PATH that
# cf-agent inherited.
for python in /opt/pipx/venvs/ansible/bin/python /opt/pipx/venvs/ansible-core/bin/python; do
if can_import_ansible "$python"; then
exec "$python" "$module" "$@"
fi
done

exec /usr/bin/python3 "$module" "$@"
4 changes: 2 additions & 2 deletions promise-types/ansible/enable.cf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
promise agent ansible
# @brief Define ansible promise type
{
path => "$(sys.workdir)/modules/promises/ansible_promise.py";
interpreter => "/usr/bin/python3";
path => "$(sys.workdir)/modules/promises/ansible_promise.sh";
interpreter => "/bin/sh";
}
4 changes: 2 additions & 2 deletions promise-types/ansible/example.cf
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
promise agent ansible
# @brief Define ansible promise type
{
path => "$(sys.workdir)/modules/promises/ansible_promise.py";
interpreter => "/usr/bin/python3";
path => "$(sys.workdir)/modules/promises/ansible_promise.sh";
interpreter => "/bin/sh";
}

bundle agent main
Expand Down
50 changes: 50 additions & 0 deletions tests/deploy/05-run-ansible-playbooks-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# assumes deploy.sh has already run adjacent to this file

set -ex
thisdir="$(cd "$(dirname "$0")" && pwd)"
cd "$thisdir"

sudo cf-agent -Kd -Ddata:install_ansible --bundle install_ansible
ansible --version

markerdir=$(mktemp -d)
trap 'sudo rm -rf "$markerdir"' EXIT
marker="$markerdir/marker"

# The playbook touches a marker file so we can tell that it actually ran
mkdir -p playbooks
cat >playbooks/playbook.yaml <<EOF
- hosts: all
gather_facts: false
tasks:
- name: Create marker file
ansible.builtin.file:
path: $marker
state: touch
EOF
cfbs --non-interactive add ./playbooks/

# Answers are path, condition, ifelapsed and whether to add more playbooks.
# The delimiter is quoted so that $(sys.inputdir) is left for cf-agent to expand.
rm -rf run-ansible-playbooks
cfbs input run-ansible-playbooks <<'EOF'
$(sys.inputdir)/services/cfbs/playbooks/playbook.yaml
any
5
no
EOF

cfbs build
sudo cfbs install
sudo cf-agent -Kf update.cf
sudo cf-agent -KI | tee log
if grep 'error:' log; then
grep 'error:' log
exit 1
fi

if [ ! -f "$marker" ]; then
echo "expected the playbook to create '$marker'"
exit 1
fi
Loading