Charmed-Kubernetes/nrpe/hooks/charmhelpers/contrib/ansible/__init__.py

307 lines
10 KiB
Python

# Copyright 2014-2015 Canonical Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Copyright 2013 Canonical Ltd.
#
# Authors:
# Charm Helpers Developers <juju@lists.ubuntu.com>
"""
The ansible package enables you to easily use the configuration management
tool `Ansible`_ to setup and configure your charm. All of your charm
configuration options and relation-data are available as regular Ansible
variables which can be used in your playbooks and templates.
.. _Ansible: https://www.ansible.com/
Usage
=====
Here is an example directory structure for a charm to get you started::
charm-ansible-example/
|-- ansible
| |-- playbook.yaml
| `-- templates
| `-- example.j2
|-- config.yaml
|-- copyright
|-- icon.svg
|-- layer.yaml
|-- metadata.yaml
|-- reactive
| `-- example.py
|-- README.md
Running a playbook called ``playbook.yaml`` when the ``install`` hook is run
can be as simple as::
from charmhelpers.contrib import ansible
from charms.reactive import hook
@hook('install')
def install():
ansible.install_ansible_support()
ansible.apply_playbook('ansible/playbook.yaml')
Here is an example playbook that uses the ``template`` module to template the
file ``example.j2`` to the charm host and then uses the ``debug`` module to
print out all the host and Juju variables that you can use in your playbooks.
Note that you must target ``localhost`` as the playbook is run locally on the
charm host::
---
- hosts: localhost
tasks:
- name: Template a file
template:
src: templates/example.j2
dest: /tmp/example.j2
- name: Print all variables available to Ansible
debug:
var: vars
Read more online about `playbooks`_ and standard Ansible `modules`_.
.. _playbooks: https://docs.ansible.com/ansible/latest/user_guide/playbooks.html
.. _modules: https://docs.ansible.com/ansible/latest/user_guide/modules.html
A further feature of the Ansible hooks is to provide a light weight "action"
scripting tool. This is a decorator that you apply to a function, and that
function can now receive cli args, and can pass extra args to the playbook::
@hooks.action()
def some_action(amount, force="False"):
"Usage: some-action AMOUNT [force=True]" # <-- shown on error
# process the arguments
# do some calls
# return extra-vars to be passed to ansible-playbook
return {
'amount': int(amount),
'type': force,
}
You can now create a symlink to hooks.py that can be invoked like a hook, but
with cli params::
# link actions/some-action to hooks/hooks.py
actions/some-action amount=10 force=true
Install Ansible via pip
=======================
If you want to install a specific version of Ansible via pip instead of
``install_ansible_support`` which uses APT, consider using the layer options
of `layer-basic`_ to install Ansible in a virtualenv::
options:
basic:
python_packages: ['ansible==2.9.0']
include_system_packages: true
use_venv: true
.. _layer-basic: https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#layer-configuration
"""
import os
import json
import stat
import subprocess
import functools
import charmhelpers.contrib.templating.contexts
import charmhelpers.core.host
import charmhelpers.core.hookenv
import charmhelpers.fetch
charm_dir = os.environ.get('CHARM_DIR', '')
ansible_hosts_path = '/etc/ansible/hosts'
# Ansible will automatically include any vars in the following
# file in its inventory when run locally.
ansible_vars_path = '/etc/ansible/host_vars/localhost'
def install_ansible_support(from_ppa=True, ppa_location='ppa:ansible/ansible'):
"""Installs Ansible via APT.
By default this installs Ansible from the `PPA`_ linked from
the Ansible `website`_ or from a PPA set in ``ppa_location``.
.. _PPA: https://launchpad.net/~ansible/+archive/ubuntu/ansible
.. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
If ``from_ppa`` is ``False``, then Ansible will be installed from
Ubuntu's Universe repositories.
"""
if from_ppa:
charmhelpers.fetch.add_source(ppa_location)
charmhelpers.fetch.apt_update(fatal=True)
charmhelpers.fetch.apt_install('ansible')
with open(ansible_hosts_path, 'w+') as hosts_file:
hosts_file.write('localhost ansible_connection=local ansible_remote_tmp=/root/.ansible/tmp')
def apply_playbook(playbook, tags=None, extra_vars=None):
"""Run a playbook.
This helper runs a playbook with juju state variables as context,
therefore variables set in application config can be used directly.
List of tags (--tags) and dictionary with extra_vars (--extra-vars)
can be passed as additional parameters.
Read more about playbook `_variables`_ online.
.. _variables: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html
Example::
# Run ansible/playbook.yaml with tag install and pass extra
# variables var_a and var_b
apply_playbook(
playbook='ansible/playbook.yaml',
tags=['install'],
extra_vars={'var_a': 'val_a', 'var_b': 'val_b'}
)
# Run ansible/playbook.yaml with tag config and extra variable nested,
# which is passed as json and can be used as dictionary in playbook
apply_playbook(
playbook='ansible/playbook.yaml',
tags=['config'],
extra_vars={'nested': {'a': 'value1', 'b': 'value2'}}
)
# Custom config file can be passed within extra_vars
apply_playbook(
playbook='ansible/playbook.yaml',
extra_vars="@some_file.json"
)
"""
tags = tags or []
tags = ",".join(tags)
charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
ansible_vars_path, namespace_separator='__',
allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
# we want ansible's log output to be unbuffered
env = os.environ.copy()
proxy_settings = charmhelpers.core.hookenv.env_proxy_settings()
if proxy_settings:
env.update(proxy_settings)
env['PYTHONUNBUFFERED'] = "1"
call = [
'ansible-playbook',
'-c',
'local',
playbook,
]
if tags:
call.extend(['--tags', '{}'.format(tags)])
if extra_vars:
call.extend(['--extra-vars', json.dumps(extra_vars)])
subprocess.check_call(call, env=env)
class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
"""Run a playbook with the hook-name as the tag.
This helper builds on the standard hookenv.Hooks helper,
but additionally runs the playbook with the hook-name specified
using --tags (ie. running all the tasks tagged with the hook-name).
Example::
hooks = AnsibleHooks(playbook_path='ansible/my_machine_state.yaml')
# All the tasks within my_machine_state.yaml tagged with 'install'
# will be run automatically after do_custom_work()
@hooks.hook()
def install():
do_custom_work()
# For most of your hooks, you won't need to do anything other
# than run the tagged tasks for the hook:
@hooks.hook('config-changed', 'start', 'stop')
def just_use_playbook():
pass
# As a convenience, you can avoid the above noop function by specifying
# the hooks which are handled by ansible-only and they'll be registered
# for you:
# hooks = AnsibleHooks(
# 'ansible/my_machine_state.yaml',
# default_hooks=['config-changed', 'start', 'stop'])
if __name__ == "__main__":
# execute a hook based on the name the program is called by
hooks.execute(sys.argv)
"""
def __init__(self, playbook_path, default_hooks=None):
"""Register any hooks handled by ansible."""
super(AnsibleHooks, self).__init__()
self._actions = {}
self.playbook_path = playbook_path
default_hooks = default_hooks or []
def noop(*args, **kwargs):
pass
for hook in default_hooks:
self.register(hook, noop)
def register_action(self, name, function):
"""Register a hook"""
self._actions[name] = function
def execute(self, args):
"""Execute the hook followed by the playbook using the hook as tag."""
hook_name = os.path.basename(args[0])
extra_vars = None
if hook_name in self._actions:
extra_vars = self._actions[hook_name](args[1:])
else:
super(AnsibleHooks, self).execute(args)
charmhelpers.contrib.ansible.apply_playbook(
self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
def action(self, *action_names):
"""Decorator, registering them as actions"""
def action_wrapper(decorated):
@functools.wraps(decorated)
def wrapper(argv):
kwargs = dict(arg.split('=') for arg in argv)
try:
return decorated(**kwargs)
except TypeError as e:
if decorated.__doc__:
e.args += (decorated.__doc__,)
raise
self.register_action(decorated.__name__, wrapper)
if '_' in decorated.__name__:
self.register_action(
decorated.__name__.replace('_', '-'), wrapper)
return wrapper
return action_wrapper