~openstack-charmers-archive/charms/precise/swift-proxy/trunk

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2015-10-22 13:24:57 UTC
  • Revision ID: james.page@ubuntu.com-20151022132457-4p14oifelnzjz5n3
Tags: 15.10
15.10 Charm release

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()