1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
17
# Copyright 2013 Canonical Ltd.
20
# Charm Helpers Developers <juju@lists.ubuntu.com>
21
"""Charm Helpers ansible - declare the state of your machines.
23
This helper enables you to declare your machine state, rather than
24
program it procedurally (and have to test each change to your procedures).
25
Your install hook can be as simple as::
28
import charmhelpers.contrib.ansible
32
charmhelpers.contrib.ansible.install_ansible_support()
33
charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
36
and won't need to change (nor will its tests) when you change the machine
39
All of your juju config and relation-data are available as template
40
variables within your playbooks and templates. An install playbook looks
49
- name: Add private repositories.
51
src: ../templates/private-repositories.list.jinja2
52
dest: /etc/apt/sources.list.d/private.list
54
- name: Update the cache.
57
- name: Install dependencies.
65
group: name={{ item.name }} gid={{ item.gid }}
67
- { name: 'deploy_user', gid: 1800 }
68
- { name: 'service_user', gid: 1500 }
73
Read more online about `playbooks`_ and standard ansible `modules`_.
75
.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
76
.. _modules: http://www.ansibleworks.com/docs/modules.html
78
A further feature os the ansible hooks is to provide a light weight "action"
79
scripting tool. This is a decorator that you apply to a function, and that
80
function can now receive cli args, and can pass extra args to the playbook.
86
def some_action(amount, force="False"):
87
"Usage: some-action AMOUNT [force=True]" # <-- shown on error
88
# process the arguments
90
# return extra-vars to be passed to ansible-playbook
92
'amount': int(amount),
96
You can now create a symlink to hooks.py that can be invoked like a hook, but
99
# link actions/some-action to hooks/hooks.py
101
actions/some-action amount=10 force=true
109
import charmhelpers.contrib.templating.contexts
110
import charmhelpers.core.host
111
import charmhelpers.core.hookenv
112
import charmhelpers.fetch
115
charm_dir = os.environ.get('CHARM_DIR', '')
116
ansible_hosts_path = '/etc/ansible/hosts'
117
# Ansible will automatically include any vars in the following
118
# file in its inventory when run locally.
119
ansible_vars_path = '/etc/ansible/host_vars/localhost'
122
def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
123
"""Installs the ansible package.
125
By default it is installed from the `PPA`_ linked from
126
the ansible `website`_ or from a ppa specified by a charm config..
128
.. _PPA: https://launchpad.net/~rquillo/+archive/ansible
129
.. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
131
If from_ppa is empty, you must ensure that the package is available
132
from a configured repository.
135
charmhelpers.fetch.add_source(ppa_location)
136
charmhelpers.fetch.apt_update(fatal=True)
137
charmhelpers.fetch.apt_install('ansible')
138
with open(ansible_hosts_path, 'w+') as hosts_file:
139
hosts_file.write('localhost ansible_connection=local')
142
def apply_playbook(playbook, tags=None, extra_vars=None):
144
tags = ",".join(tags)
145
charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
146
ansible_vars_path, namespace_separator='__',
147
allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
149
# we want ansible's log output to be unbuffered
150
env = os.environ.copy()
151
env['PYTHONUNBUFFERED'] = "1"
159
call.extend(['--tags', '{}'.format(tags)])
161
extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
162
call.extend(['--extra-vars', " ".join(extra)])
163
subprocess.check_call(call, env=env)
166
class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
167
"""Run a playbook with the hook-name as the tag.
169
This helper builds on the standard hookenv.Hooks helper,
170
but additionally runs the playbook with the hook-name specified
171
using --tags (ie. running all the tasks tagged with the hook-name).
175
hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
177
# All the tasks within my_machine_state.yaml tagged with 'install'
178
# will be run automatically after do_custom_work()
183
# For most of your hooks, you won't need to do anything other
184
# than run the tagged tasks for the hook:
185
@hooks.hook('config-changed', 'start', 'stop')
186
def just_use_playbook():
189
# As a convenience, you can avoid the above noop function by specifying
190
# the hooks which are handled by ansible-only and they'll be registered
192
# hooks = AnsibleHooks(
193
# 'playbooks/my_machine_state.yaml',
194
# default_hooks=['config-changed', 'start', 'stop'])
196
if __name__ == "__main__":
197
# execute a hook based on the name the program is called by
198
hooks.execute(sys.argv)
202
def __init__(self, playbook_path, default_hooks=None):
203
"""Register any hooks handled by ansible."""
204
super(AnsibleHooks, self).__init__()
207
self.playbook_path = playbook_path
209
default_hooks = default_hooks or []
211
def noop(*args, **kwargs):
214
for hook in default_hooks:
215
self.register(hook, noop)
217
def register_action(self, name, function):
218
"""Register a hook"""
219
self._actions[name] = function
221
def execute(self, args):
222
"""Execute the hook followed by the playbook using the hook as tag."""
223
hook_name = os.path.basename(args[0])
225
if hook_name in self._actions:
226
extra_vars = self._actions[hook_name](args[1:])
228
super(AnsibleHooks, self).execute(args)
230
charmhelpers.contrib.ansible.apply_playbook(
231
self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
233
def action(self, *action_names):
234
"""Decorator, registering them as actions"""
235
def action_wrapper(decorated):
237
@functools.wraps(decorated)
239
kwargs = dict(arg.split('=') for arg in argv)
241
return decorated(**kwargs)
242
except TypeError as e:
243
if decorated.__doc__:
244
e.args += (decorated.__doc__,)
247
self.register_action(decorated.__name__, wrapper)
248
if '_' in decorated.__name__:
249
self.register_action(
250
decorated.__name__.replace('_', '-'), wrapper)
254
return action_wrapper