1
# Copyright 2013 Canonical Ltd.
4
# Charm Helpers Developers <juju@lists.ubuntu.com>
5
"""Charm Helpers ansible - declare the state of your machines.
7
This helper enables you to declare your machine state, rather than
8
program it procedurally (and have to test each change to your procedures).
9
Your install hook can be as simple as:
12
import charmhelpers.contrib.ansible
14
hooks = charmhelpers.contrib.ansible.AnsibleHooks(
15
playbook_path="playbook.yaml")
17
@hooks.hook('install', 'upgrade-charm')
19
charmhelpers.contrib.ansible.install_ansible_support()
22
and won't need to change (nor will its tests) when you change the machine
25
All of your juju config and relation-data are available as template
26
variables within your playbooks and templates.
28
An install playbook looks
37
- name: Install dependencies.
50
group: name={{ item.name }} gid={{ item.gid }}
52
- { name: 'deploy_user', gid: 1800 }
53
- { name: 'service_user', gid: 1500 }
59
Alternatively, you can apply individual playbooks with:
61
charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
64
Read more online about playbooks[1] and standard ansible modules[2].
66
[1] http://www.ansibleworks.com/docs/playbooks.html
67
[2] http://www.ansibleworks.com/docs/modules.html
73
import charmhelpers.contrib.templating.contexts
74
import charmhelpers.core.host
75
import charmhelpers.core.hookenv
76
import charmhelpers.fetch
79
charm_dir = os.environ.get('CHARM_DIR', '')
80
ansible_hosts_path = '/etc/ansible/hosts'
81
# Ansible will automatically include any vars in the following
82
# file in its inventory when run locally.
83
ansible_vars_path = '/etc/ansible/host_vars/localhost'
84
available_tags = set([])
87
def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
88
"""Installs the ansible package.
90
By default it is installed from the PPA [1] linked from
91
the ansible website [2] or from a ppa specified by a charm config.
93
[1] https://launchpad.net/~rquillo/+archive/ansible
94
[2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian
96
If from_ppa is empty, you must ensure that the package is available
97
from a configured repository.
100
charmhelpers.fetch.add_source(ppa_location)
101
charmhelpers.fetch.apt_update(fatal=True)
102
charmhelpers.fetch.apt_install('ansible')
103
with open(ansible_hosts_path, 'w+') as hosts_file:
104
hosts_file.write('localhost ansible_connection=local')
107
def apply_playbook(playbook, tags=None):
109
tags = ",".join(tags)
110
charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
111
ansible_vars_path, namespace_separator='__',
112
allow_hyphens_in_keys=False)
120
call.extend(['--tags', '{}'.format(tags)])
121
subprocess.check_call(call)
124
def get_tags_for_playbook(playbook_path):
125
"""Return all tags within a playbook.
127
The charmhelpers lib should not depend on ansible, hence the
130
Discussion whether --list-tags should be a feature of ansible at
134
import ansible.callbacks
135
import ansible.playbook
136
stats = ansible.callbacks.AggregateStats()
137
callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats)
138
runner_callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats)
139
playbook = ansible.playbook.PlayBook(playbook=playbook_path,
141
runner_callbacks=runner_callbacks,
143
myplay = ansible.playbook.Play(playbook, ds=playbook.playbook[0],
144
basedir=os.path.dirname(playbook_path))
146
_, playbook_tags = myplay.compare_tags([])
147
playbook_tags.remove('all')
151
class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
152
"""Run a playbook with the hook-name as the tag.
154
This helper builds on the standard hookenv.Hooks helper,
155
but additionally runs the playbook with the hook-name specified
156
using --tags (ie. running all the tasks tagged with the hook-name).
159
hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
161
# All the tasks within my_machine_state.yaml tagged with 'install'
162
# will be run automatically after do_custom_work()
167
# For most of your hooks, you won't need to do anything other
168
# than run the tagged tasks for the hook:
169
@hooks.hook('config-changed', 'start', 'stop')
170
def just_use_playbook():
173
# As a convenience, you can avoid the above noop function by specifying
174
# the hooks which are handled by ansible-only and they'll be registered
176
# hooks = AnsibleHooks(
177
# 'playbooks/my_machine_state.yaml',
178
# default_hooks=['config-changed', 'start', 'stop'])
180
if __name__ == "__main__":
181
# execute a hook based on the name the program is called by
182
hooks.execute(sys.argv)
185
def __init__(self, playbook_path, default_hooks=None):
186
"""Register any hooks handled by ansible.
188
default_hooks is now deprecated, as we use ansible to
189
determine the supported hooks from the playbook.
191
super(AnsibleHooks, self).__init__()
193
self.playbook_path = playbook_path
195
# The hooks decorator is created at module load time, which on the
196
# first run, will be before ansible is itself installed.
198
available_tags.update(get_tags_for_playbook(playbook_path))
200
available_tags.add('install')
202
if default_hooks is not None:
204
"The use of default_hooks is deprecated. Ansible is now "
205
"used to query your playbook for available tags.",
207
noop = lambda *args, **kwargs: None
208
for hook in available_tags:
209
self.register(hook, noop)
211
def execute(self, args):
212
"""Execute the hook followed by the playbook using the hook as tag."""
213
super(AnsibleHooks, self).execute(args)
214
hook_name = os.path.basename(args[0])
216
if hook_name in available_tags:
217
charmhelpers.contrib.ansible.apply_playbook(
218
self.playbook_path, tags=[hook_name])