Do you have existing ansible playbooks you want to use in Juju Charms? This
library works in tandem with layer:ansible
to deliver a consistent Ansible with
juju experience. Just bring your playbooks and operational knowledge.
This library offers two methods: method 1 is based on calling
ansible-playbook
CLI, where method 2 is using Ansible Python API.
This module handles installing ansible from ppa:ansible/ansible
by default.
This is tuneable when calling install_ansible to point to a different PPA if
required
from charms.ansible import install_ansible_support
@when_not('ansible.available')
def bootstrap_with_ansible():
install_ansible_support('ppa:rquillo/ansible')
By default, ansible is setup to apply against localhost
. By this convention
we also have a cache of all the unit data that charms.ansible
is aware of in
/etc/ansible/host_vars/localhost
. Additionally, playbooks are executed
filtered by tags when you need to group actions to a similar event.
This code would reside in your charms reactive module: reactive/somefile.py
from charms.ansible import apply_playbook
@when('ansible.available')
def run_playbook():
apply_playbook('files/playbook.yaml')
Invoking Ansible against this yaml
- hosts: localhost
vars:
- service_name: "{{ local_unit.split('/')[0] }}"
tasks:
- include: tasks/install-widgets.yml
tags:
- ansible.available
Notice the filter of tags
- Tags are executed in or
fashion by default. Meaning
if any tags match, the associated play is executed.
Caveat - Due to this pattern, if you attempt to apply_playbook() on a playbook that is tagged, and there is no matching tag in the environment, it will raise an error. Great care should be taken to not encounter codepaths in this nature.
Charm configuration is passed implicitly into rendering contexts when called from an Ansible playbook, and can be referenced directly.
Example config.yaml
options:
version_number:
type: string
default: v2.0.15
description: Version number of widget to fetch
profile:
type: string
default: fast
description: Deployment profile to render
Example tasks/install_widgets.yaml
for inline config variable expansion
tasks:
- include: tasks/install_widget_{{version_number}}.yaml
tags:
- widget.notinstalled
- name: Update configuration
template: src={{ charm_dir }}/templates/widget_config.toml
dest=/etc/widget/config.yml
mode=0644
backup=yes
notify:
- Restart Widget
Example templates/widget_config.toml
- Jinja2 template can make direct reference
to config in the template being rendered, when invoked from an ansible
template
play.
{% if profile %}
tuning_profile={{ profile }}
{% endif %}
This library is heavilly based (right now its a direct fork) of mnelson's work on charmhelpers.contrib.ansible module. Charms have evolved a long way since that was a 'gold standard'. This is an attempt at resurrecting that work and continuing the learning of using Ansible in Juju.
Major thanks to the early contributors, bug filers, and adopters of this great library. This wouldn't be possible without it!
Design objectives:
- Design as reusable layer(s)
- Be compatible with Ubuntu and CentOS
- Simple pattern to execute a playbook
Assumption:
- Playbooks will be local (in charm) so to maintain the atomic nature of a HW charm — the model contains both the declaration of attributes and actions to handle runtime state transitions.
This method is inspired by this article. This is to take advantage of the Ansible Python API.
-
Install prerequisites. Installing
Ansible
will fail on a vanilla Ubuntu because it misses a few dependencies. Using layer-basic by listing them out inlayer.yaml
:includes: - 'layer:basic' options: basic: packages: - libffi-dev - libssl-dev - python - python3-dev
- Install Ansible. Use Python wheel supported
by layer-basic. In
wheelhouse.txt
:
```
ansible==2.2.0
```
-
ansible.cfg
. Instead of using a global config, this is local so each charm can have its own variation if desired.[defaults] inventory = ./hosts log_path = /var/log/ansible/ansible.log remote_tmp = $HOME/.ansible/tmp local_tmp = $HOME/.ansible/tmp [ssh_connection] ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s control_path = ~/.ansible/cp/ansible-ssh-%%h-%%p-%%r
-
Options. Constructed a class to be the abstraction of Ansible options:
class Options(object): """ Options class to replace Ansible OptParser """ .... verbosity=None, inventory=None, listhosts=None, subset=None, module_paths=None, extra_vars=None, forks=None, ask_vault_pass=None, vault_password_files=None, new_vault_password_file=None, output_file=None, tags=None, skip_tags=[], one_line=None, tree=None, ask_sudo_pass=None, ask_su_pass=None, sudo=None, sudo_user=None, become=None, become_method=None, become_user=None, become_ask_pass=None, ask_pass=None, private_key_file=None, remote_user=None, connection=None, timeout=None, ssh_common_args=None, sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, poll_interval=None, seconds=None, check=None, syntax=None, diff=None, force_handlers=None, flush_cache=None, listtasks=None, listtags=[], module_path=None
-
Playbook execution. Running it is to use Ansible's API call
PlaybookExecutor
.self.pbex = playbook_executor.PlaybookExecutor( playbooks=pbs, inventory=self.inventory, variable_manager=self.variable_manager, loader=self.loader, options=self.options, passwords=passwords) .... self.pbex.run()
Integrating with charm takes the followings:
-
Include layer. In
layer.yaml
:includes: - 'layer:basic' - 'layer:ansible'
-
Create a
playbooks
folder and place playbooks here:. ├── config.yaml ├── icon.svg ├── layer.yaml ├── metadata.yaml ├── playbooks │ └── test.yaml └── reactive └── solution.py
-
Using
config.yaml
to pass in playbook for each action that is defined in the charm states. For example, definetest.yaml
for an action instate-0
:options: state-0-playbook: type: string default: "test.yaml" description: "Playbook for..."
-
Define the playbook. For example, a hello world that will create a file `/tmp/testfile.txt'.
- name: This is a hello-world example hosts: 127.0.0.1 tasks: - name: Create a file called '/tmp/testfile.txt' with the content 'hello world'. copy: content="hello world\n" dest=/tmp/testfile.txt tags: - sth
Note that
tags
valuesth
must match playbook run call (see below). -
In charm
.py
file,from charms.layer.task import Runner
, then instate-0
to call given playbook:playbook = config['state-0-playbook'] runner = Runner( tags = 'sth', # <-- must match the tag in the playbook connection = 'local', # <-- must be "local" hostnames = '127.0.0.1', # <-- assuming execution in localhost playbooks = [playbook], private_key_file = '', run_data = {}, become_pass = '', verbosity = 0 ) stats = runner.run()