~charmers/charms/trusty/neutron-api-plumgrid/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/ansible/__init__.py

  • Committer: bbaqar at plumgrid
  • Date: 2015-07-29 18:35:16 UTC
  • Revision ID: bbaqar@plumgrid.com-20150729183516-pip6xlo91rx9h2yy
neutron-plumgrid-plugin renamed to neutron-api-plumgrid

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
 
17
# Copyright 2013 Canonical Ltd.
 
18
#
 
19
# Authors:
 
20
#  Charm Helpers Developers <juju@lists.ubuntu.com>
 
21
"""Charm Helpers ansible - declare the state of your machines.
 
22
 
 
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::
 
26
 
 
27
    {{{
 
28
    import charmhelpers.contrib.ansible
 
29
 
 
30
 
 
31
    def install():
 
32
        charmhelpers.contrib.ansible.install_ansible_support()
 
33
        charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
 
34
    }}}
 
35
 
 
36
and won't need to change (nor will its tests) when you change the machine
 
37
state.
 
38
 
 
39
All of your juju config and relation-data are available as template
 
40
variables within your playbooks and templates. An install playbook looks
 
41
something like::
 
42
 
 
43
    {{{
 
44
    ---
 
45
    - hosts: localhost
 
46
      user: root
 
47
 
 
48
      tasks:
 
49
        - name: Add private repositories.
 
50
          template:
 
51
            src: ../templates/private-repositories.list.jinja2
 
52
            dest: /etc/apt/sources.list.d/private.list
 
53
 
 
54
        - name: Update the cache.
 
55
          apt: update_cache=yes
 
56
 
 
57
        - name: Install dependencies.
 
58
          apt: pkg={{ item }}
 
59
          with_items:
 
60
            - python-mimeparse
 
61
            - python-webob
 
62
            - sunburnt
 
63
 
 
64
        - name: Setup groups.
 
65
          group: name={{ item.name }} gid={{ item.gid }}
 
66
          with_items:
 
67
            - { name: 'deploy_user', gid: 1800 }
 
68
            - { name: 'service_user', gid: 1500 }
 
69
 
 
70
      ...
 
71
    }}}
 
72
 
 
73
Read more online about `playbooks`_ and standard ansible `modules`_.
 
74
 
 
75
.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
 
76
.. _modules: http://www.ansibleworks.com/docs/modules.html
 
77
 
 
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.
 
81
 
 
82
e.g.
 
83
 
 
84
 
 
85
@hooks.action()
 
86
def some_action(amount, force="False"):
 
87
    "Usage: some-action AMOUNT [force=True]"  # <-- shown on error
 
88
    # process the arguments
 
89
    # do some calls
 
90
    # return extra-vars to be passed to ansible-playbook
 
91
    return {
 
92
        'amount': int(amount),
 
93
        'type': force,
 
94
    }
 
95
 
 
96
You can now create a symlink to hooks.py that can be invoked like a hook, but
 
97
with cli params:
 
98
 
 
99
# link actions/some-action to hooks/hooks.py
 
100
 
 
101
actions/some-action amount=10 force=true
 
102
 
 
103
"""
 
104
import os
 
105
import stat
 
106
import subprocess
 
107
import functools
 
108
 
 
109
import charmhelpers.contrib.templating.contexts
 
110
import charmhelpers.core.host
 
111
import charmhelpers.core.hookenv
 
112
import charmhelpers.fetch
 
113
 
 
114
 
 
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'
 
120
 
 
121
 
 
122
def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
 
123
    """Installs the ansible package.
 
124
 
 
125
    By default it is installed from the `PPA`_ linked from
 
126
    the ansible `website`_ or from a ppa specified by a charm config..
 
127
 
 
128
    .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
 
129
    .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
 
130
 
 
131
    If from_ppa is empty, you must ensure that the package is available
 
132
    from a configured repository.
 
133
    """
 
134
    if from_ppa:
 
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')
 
140
 
 
141
 
 
142
def apply_playbook(playbook, tags=None, extra_vars=None):
 
143
    tags = tags or []
 
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))
 
148
 
 
149
    # we want ansible's log output to be unbuffered
 
150
    env = os.environ.copy()
 
151
    env['PYTHONUNBUFFERED'] = "1"
 
152
    call = [
 
153
        'ansible-playbook',
 
154
        '-c',
 
155
        'local',
 
156
        playbook,
 
157
    ]
 
158
    if tags:
 
159
        call.extend(['--tags', '{}'.format(tags)])
 
160
    if extra_vars:
 
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)
 
164
 
 
165
 
 
166
class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
 
167
    """Run a playbook with the hook-name as the tag.
 
168
 
 
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).
 
172
 
 
173
    Example::
 
174
 
 
175
        hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
 
176
 
 
177
        # All the tasks within my_machine_state.yaml tagged with 'install'
 
178
        # will be run automatically after do_custom_work()
 
179
        @hooks.hook()
 
180
        def install():
 
181
            do_custom_work()
 
182
 
 
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():
 
187
            pass
 
188
 
 
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
 
191
        # for you:
 
192
        # hooks = AnsibleHooks(
 
193
        #     'playbooks/my_machine_state.yaml',
 
194
        #     default_hooks=['config-changed', 'start', 'stop'])
 
195
 
 
196
        if __name__ == "__main__":
 
197
            # execute a hook based on the name the program is called by
 
198
            hooks.execute(sys.argv)
 
199
 
 
200
    """
 
201
 
 
202
    def __init__(self, playbook_path, default_hooks=None):
 
203
        """Register any hooks handled by ansible."""
 
204
        super(AnsibleHooks, self).__init__()
 
205
 
 
206
        self._actions = {}
 
207
        self.playbook_path = playbook_path
 
208
 
 
209
        default_hooks = default_hooks or []
 
210
 
 
211
        def noop(*args, **kwargs):
 
212
            pass
 
213
 
 
214
        for hook in default_hooks:
 
215
            self.register(hook, noop)
 
216
 
 
217
    def register_action(self, name, function):
 
218
        """Register a hook"""
 
219
        self._actions[name] = function
 
220
 
 
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])
 
224
        extra_vars = None
 
225
        if hook_name in self._actions:
 
226
            extra_vars = self._actions[hook_name](args[1:])
 
227
        else:
 
228
            super(AnsibleHooks, self).execute(args)
 
229
 
 
230
        charmhelpers.contrib.ansible.apply_playbook(
 
231
            self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
 
232
 
 
233
    def action(self, *action_names):
 
234
        """Decorator, registering them as actions"""
 
235
        def action_wrapper(decorated):
 
236
 
 
237
            @functools.wraps(decorated)
 
238
            def wrapper(argv):
 
239
                kwargs = dict(arg.split('=') for arg in argv)
 
240
                try:
 
241
                    return decorated(**kwargs)
 
242
                except TypeError as e:
 
243
                    if decorated.__doc__:
 
244
                        e.args += (decorated.__doc__,)
 
245
                    raise
 
246
 
 
247
            self.register_action(decorated.__name__, wrapper)
 
248
            if '_' in decorated.__name__:
 
249
                self.register_action(
 
250
                    decorated.__name__.replace('_', '-'), wrapper)
 
251
 
 
252
            return wrapper
 
253
 
 
254
        return action_wrapper