~niedbalski/charms/trusty/neutron-mellanox/fix-sed

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/services/base.py

  • Committer: Felipe Reyes
  • Date: 2015-09-24 01:56:44 UTC
  • Revision ID: felipe.reyes@canonical.com-20150924015644-xynh6h23y3d126m7
sync charm-helpers

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
import os
 
18
import json
 
19
from inspect import getargspec
 
20
from collections import Iterable, OrderedDict
 
21
 
 
22
from charmhelpers.core import host
 
23
from charmhelpers.core import hookenv
 
24
 
 
25
 
 
26
__all__ = ['ServiceManager', 'ManagerCallback',
 
27
           'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
 
28
           'service_restart', 'service_stop']
 
29
 
 
30
 
 
31
class ServiceManager(object):
 
32
    def __init__(self, services=None):
 
33
        """
 
34
        Register a list of services, given their definitions.
 
35
 
 
36
        Service definitions are dicts in the following formats (all keys except
 
37
        'service' are optional)::
 
38
 
 
39
            {
 
40
                "service": <service name>,
 
41
                "required_data": <list of required data contexts>,
 
42
                "provided_data": <list of provided data contexts>,
 
43
                "data_ready": <one or more callbacks>,
 
44
                "data_lost": <one or more callbacks>,
 
45
                "start": <one or more callbacks>,
 
46
                "stop": <one or more callbacks>,
 
47
                "ports": <list of ports to manage>,
 
48
            }
 
49
 
 
50
        The 'required_data' list should contain dicts of required data (or
 
51
        dependency managers that act like dicts and know how to collect the data).
 
52
        Only when all items in the 'required_data' list are populated are the list
 
53
        of 'data_ready' and 'start' callbacks executed.  See `is_ready()` for more
 
54
        information.
 
55
 
 
56
        The 'provided_data' list should contain relation data providers, most likely
 
57
        a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
 
58
        that will indicate a set of data to set on a given relation.
 
59
 
 
60
        The 'data_ready' value should be either a single callback, or a list of
 
61
        callbacks, to be called when all items in 'required_data' pass `is_ready()`.
 
62
        Each callback will be called with the service name as the only parameter.
 
63
        After all of the 'data_ready' callbacks are called, the 'start' callbacks
 
64
        are fired.
 
65
 
 
66
        The 'data_lost' value should be either a single callback, or a list of
 
67
        callbacks, to be called when a 'required_data' item no longer passes
 
68
        `is_ready()`.  Each callback will be called with the service name as the
 
69
        only parameter.  After all of the 'data_lost' callbacks are called,
 
70
        the 'stop' callbacks are fired.
 
71
 
 
72
        The 'start' value should be either a single callback, or a list of
 
73
        callbacks, to be called when starting the service, after the 'data_ready'
 
74
        callbacks are complete.  Each callback will be called with the service
 
75
        name as the only parameter.  This defaults to
 
76
        `[host.service_start, services.open_ports]`.
 
77
 
 
78
        The 'stop' value should be either a single callback, or a list of
 
79
        callbacks, to be called when stopping the service.  If the service is
 
80
        being stopped because it no longer has all of its 'required_data', this
 
81
        will be called after all of the 'data_lost' callbacks are complete.
 
82
        Each callback will be called with the service name as the only parameter.
 
83
        This defaults to `[services.close_ports, host.service_stop]`.
 
84
 
 
85
        The 'ports' value should be a list of ports to manage.  The default
 
86
        'start' handler will open the ports after the service is started,
 
87
        and the default 'stop' handler will close the ports prior to stopping
 
88
        the service.
 
89
 
 
90
 
 
91
        Examples:
 
92
 
 
93
        The following registers an Upstart service called bingod that depends on
 
94
        a mongodb relation and which runs a custom `db_migrate` function prior to
 
95
        restarting the service, and a Runit service called spadesd::
 
96
 
 
97
            manager = services.ServiceManager([
 
98
                {
 
99
                    'service': 'bingod',
 
100
                    'ports': [80, 443],
 
101
                    'required_data': [MongoRelation(), config(), {'my': 'data'}],
 
102
                    'data_ready': [
 
103
                        services.template(source='bingod.conf'),
 
104
                        services.template(source='bingod.ini',
 
105
                                          target='/etc/bingod.ini',
 
106
                                          owner='bingo', perms=0400),
 
107
                    ],
 
108
                },
 
109
                {
 
110
                    'service': 'spadesd',
 
111
                    'data_ready': services.template(source='spadesd_run.j2',
 
112
                                                    target='/etc/sv/spadesd/run',
 
113
                                                    perms=0555),
 
114
                    'start': runit_start,
 
115
                    'stop': runit_stop,
 
116
                },
 
117
            ])
 
118
            manager.manage()
 
119
        """
 
120
        self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
 
121
        self._ready = None
 
122
        self.services = OrderedDict()
 
123
        for service in services or []:
 
124
            service_name = service['service']
 
125
            self.services[service_name] = service
 
126
 
 
127
    def manage(self):
 
128
        """
 
129
        Handle the current hook by doing The Right Thing with the registered services.
 
130
        """
 
131
        hookenv._run_atstart()
 
132
        try:
 
133
            hook_name = hookenv.hook_name()
 
134
            if hook_name == 'stop':
 
135
                self.stop_services()
 
136
            else:
 
137
                self.reconfigure_services()
 
138
                self.provide_data()
 
139
        except SystemExit as x:
 
140
            if x.code is None or x.code == 0:
 
141
                hookenv._run_atexit()
 
142
        hookenv._run_atexit()
 
143
 
 
144
    def provide_data(self):
 
145
        """
 
146
        Set the relation data for each provider in the ``provided_data`` list.
 
147
 
 
148
        A provider must have a `name` attribute, which indicates which relation
 
149
        to set data on, and a `provide_data()` method, which returns a dict of
 
150
        data to set.
 
151
 
 
152
        The `provide_data()` method can optionally accept two parameters:
 
153
 
 
154
          * ``remote_service`` The name of the remote service that the data will
 
155
            be provided to.  The `provide_data()` method will be called once
 
156
            for each connected service (not unit).  This allows the method to
 
157
            tailor its data to the given service.
 
158
          * ``service_ready`` Whether or not the service definition had all of
 
159
            its requirements met, and thus the ``data_ready`` callbacks run.
 
160
 
 
161
        Note that the ``provided_data`` methods are now called **after** the
 
162
        ``data_ready`` callbacks are run.  This gives the ``data_ready`` callbacks
 
163
        a chance to generate any data necessary for the providing to the remote
 
164
        services.
 
165
        """
 
166
        for service_name, service in self.services.items():
 
167
            service_ready = self.is_ready(service_name)
 
168
            for provider in service.get('provided_data', []):
 
169
                for relid in hookenv.relation_ids(provider.name):
 
170
                    units = hookenv.related_units(relid)
 
171
                    if not units:
 
172
                        continue
 
173
                    remote_service = units[0].split('/')[0]
 
174
                    argspec = getargspec(provider.provide_data)
 
175
                    if len(argspec.args) > 1:
 
176
                        data = provider.provide_data(remote_service, service_ready)
 
177
                    else:
 
178
                        data = provider.provide_data()
 
179
                    if data:
 
180
                        hookenv.relation_set(relid, data)
 
181
 
 
182
    def reconfigure_services(self, *service_names):
 
183
        """
 
184
        Update all files for one or more registered services, and,
 
185
        if ready, optionally restart them.
 
186
 
 
187
        If no service names are given, reconfigures all registered services.
 
188
        """
 
189
        for service_name in service_names or self.services.keys():
 
190
            if self.is_ready(service_name):
 
191
                self.fire_event('data_ready', service_name)
 
192
                self.fire_event('start', service_name, default=[
 
193
                    service_restart,
 
194
                    manage_ports])
 
195
                self.save_ready(service_name)
 
196
            else:
 
197
                if self.was_ready(service_name):
 
198
                    self.fire_event('data_lost', service_name)
 
199
                self.fire_event('stop', service_name, default=[
 
200
                    manage_ports,
 
201
                    service_stop])
 
202
                self.save_lost(service_name)
 
203
 
 
204
    def stop_services(self, *service_names):
 
205
        """
 
206
        Stop one or more registered services, by name.
 
207
 
 
208
        If no service names are given, stops all registered services.
 
209
        """
 
210
        for service_name in service_names or self.services.keys():
 
211
            self.fire_event('stop', service_name, default=[
 
212
                manage_ports,
 
213
                service_stop])
 
214
 
 
215
    def get_service(self, service_name):
 
216
        """
 
217
        Given the name of a registered service, return its service definition.
 
218
        """
 
219
        service = self.services.get(service_name)
 
220
        if not service:
 
221
            raise KeyError('Service not registered: %s' % service_name)
 
222
        return service
 
223
 
 
224
    def fire_event(self, event_name, service_name, default=None):
 
225
        """
 
226
        Fire a data_ready, data_lost, start, or stop event on a given service.
 
227
        """
 
228
        service = self.get_service(service_name)
 
229
        callbacks = service.get(event_name, default)
 
230
        if not callbacks:
 
231
            return
 
232
        if not isinstance(callbacks, Iterable):
 
233
            callbacks = [callbacks]
 
234
        for callback in callbacks:
 
235
            if isinstance(callback, ManagerCallback):
 
236
                callback(self, service_name, event_name)
 
237
            else:
 
238
                callback(service_name)
 
239
 
 
240
    def is_ready(self, service_name):
 
241
        """
 
242
        Determine if a registered service is ready, by checking its 'required_data'.
 
243
 
 
244
        A 'required_data' item can be any mapping type, and is considered ready
 
245
        if `bool(item)` evaluates as True.
 
246
        """
 
247
        service = self.get_service(service_name)
 
248
        reqs = service.get('required_data', [])
 
249
        return all(bool(req) for req in reqs)
 
250
 
 
251
    def _load_ready_file(self):
 
252
        if self._ready is not None:
 
253
            return
 
254
        if os.path.exists(self._ready_file):
 
255
            with open(self._ready_file) as fp:
 
256
                self._ready = set(json.load(fp))
 
257
        else:
 
258
            self._ready = set()
 
259
 
 
260
    def _save_ready_file(self):
 
261
        if self._ready is None:
 
262
            return
 
263
        with open(self._ready_file, 'w') as fp:
 
264
            json.dump(list(self._ready), fp)
 
265
 
 
266
    def save_ready(self, service_name):
 
267
        """
 
268
        Save an indicator that the given service is now data_ready.
 
269
        """
 
270
        self._load_ready_file()
 
271
        self._ready.add(service_name)
 
272
        self._save_ready_file()
 
273
 
 
274
    def save_lost(self, service_name):
 
275
        """
 
276
        Save an indicator that the given service is no longer data_ready.
 
277
        """
 
278
        self._load_ready_file()
 
279
        self._ready.discard(service_name)
 
280
        self._save_ready_file()
 
281
 
 
282
    def was_ready(self, service_name):
 
283
        """
 
284
        Determine if the given service was previously data_ready.
 
285
        """
 
286
        self._load_ready_file()
 
287
        return service_name in self._ready
 
288
 
 
289
 
 
290
class ManagerCallback(object):
 
291
    """
 
292
    Special case of a callback that takes the `ServiceManager` instance
 
293
    in addition to the service name.
 
294
 
 
295
    Subclasses should implement `__call__` which should accept three parameters:
 
296
 
 
297
        * `manager`       The `ServiceManager` instance
 
298
        * `service_name`  The name of the service it's being triggered for
 
299
        * `event_name`    The name of the event that this callback is handling
 
300
    """
 
301
    def __call__(self, manager, service_name, event_name):
 
302
        raise NotImplementedError()
 
303
 
 
304
 
 
305
class PortManagerCallback(ManagerCallback):
 
306
    """
 
307
    Callback class that will open or close ports, for use as either
 
308
    a start or stop action.
 
309
    """
 
310
    def __call__(self, manager, service_name, event_name):
 
311
        service = manager.get_service(service_name)
 
312
        new_ports = service.get('ports', [])
 
313
        port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
 
314
        if os.path.exists(port_file):
 
315
            with open(port_file) as fp:
 
316
                old_ports = fp.read().split(',')
 
317
            for old_port in old_ports:
 
318
                if bool(old_port):
 
319
                    old_port = int(old_port)
 
320
                    if old_port not in new_ports:
 
321
                        hookenv.close_port(old_port)
 
322
        with open(port_file, 'w') as fp:
 
323
            fp.write(','.join(str(port) for port in new_ports))
 
324
        for port in new_ports:
 
325
            if event_name == 'start':
 
326
                hookenv.open_port(port)
 
327
            elif event_name == 'stop':
 
328
                hookenv.close_port(port)
 
329
 
 
330
 
 
331
def service_stop(service_name):
 
332
    """
 
333
    Wrapper around host.service_stop to prevent spurious "unknown service"
 
334
    messages in the logs.
 
335
    """
 
336
    if host.service_running(service_name):
 
337
        host.service_stop(service_name)
 
338
 
 
339
 
 
340
def service_restart(service_name):
 
341
    """
 
342
    Wrapper around host.service_restart to prevent spurious "unknown service"
 
343
    messages in the logs.
 
344
    """
 
345
    if host.service_available(service_name):
 
346
        if host.service_running(service_name):
 
347
            host.service_restart(service_name)
 
348
        else:
 
349
            host.service_start(service_name)
 
350
 
 
351
 
 
352
# Convenience aliases
 
353
open_ports = close_ports = manage_ports = PortManagerCallback()