~celebdor/charms/trusty/midonet-gateway/fake

« back to all changes in this revision

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

  • Committer: Antoni Segura Puimedon
  • Date: 2015-12-21 01:14:08 UTC
  • Revision ID: toni@midokura.com-20151221011408-tdcv1pec67th7ckr
ultra bare bones charm for just pulling midonet-agent

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 re
19
 
import json
20
 
from collections import Iterable
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 = {}
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
 
        hook_name = hookenv.hook_name()
132
 
        if hook_name == 'stop':
133
 
            self.stop_services()
134
 
        else:
135
 
            self.provide_data()
136
 
            self.reconfigure_services()
137
 
        cfg = hookenv.config()
138
 
        if cfg.implicit_save:
139
 
            cfg.save()
140
 
 
141
 
    def provide_data(self):
142
 
        """
143
 
        Set the relation data for each provider in the ``provided_data`` list.
144
 
 
145
 
        A provider must have a `name` attribute, which indicates which relation
146
 
        to set data on, and a `provide_data()` method, which returns a dict of
147
 
        data to set.
148
 
        """
149
 
        hook_name = hookenv.hook_name()
150
 
        for service in self.services.values():
151
 
            for provider in service.get('provided_data', []):
152
 
                if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
153
 
                    data = provider.provide_data()
154
 
                    _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
155
 
                    if _ready:
156
 
                        hookenv.relation_set(None, data)
157
 
 
158
 
    def reconfigure_services(self, *service_names):
159
 
        """
160
 
        Update all files for one or more registered services, and,
161
 
        if ready, optionally restart them.
162
 
 
163
 
        If no service names are given, reconfigures all registered services.
164
 
        """
165
 
        for service_name in service_names or self.services.keys():
166
 
            if self.is_ready(service_name):
167
 
                self.fire_event('data_ready', service_name)
168
 
                self.fire_event('start', service_name, default=[
169
 
                    service_restart,
170
 
                    manage_ports])
171
 
                self.save_ready(service_name)
172
 
            else:
173
 
                if self.was_ready(service_name):
174
 
                    self.fire_event('data_lost', service_name)
175
 
                self.fire_event('stop', service_name, default=[
176
 
                    manage_ports,
177
 
                    service_stop])
178
 
                self.save_lost(service_name)
179
 
 
180
 
    def stop_services(self, *service_names):
181
 
        """
182
 
        Stop one or more registered services, by name.
183
 
 
184
 
        If no service names are given, stops all registered services.
185
 
        """
186
 
        for service_name in service_names or self.services.keys():
187
 
            self.fire_event('stop', service_name, default=[
188
 
                manage_ports,
189
 
                service_stop])
190
 
 
191
 
    def get_service(self, service_name):
192
 
        """
193
 
        Given the name of a registered service, return its service definition.
194
 
        """
195
 
        service = self.services.get(service_name)
196
 
        if not service:
197
 
            raise KeyError('Service not registered: %s' % service_name)
198
 
        return service
199
 
 
200
 
    def fire_event(self, event_name, service_name, default=None):
201
 
        """
202
 
        Fire a data_ready, data_lost, start, or stop event on a given service.
203
 
        """
204
 
        service = self.get_service(service_name)
205
 
        callbacks = service.get(event_name, default)
206
 
        if not callbacks:
207
 
            return
208
 
        if not isinstance(callbacks, Iterable):
209
 
            callbacks = [callbacks]
210
 
        for callback in callbacks:
211
 
            if isinstance(callback, ManagerCallback):
212
 
                callback(self, service_name, event_name)
213
 
            else:
214
 
                callback(service_name)
215
 
 
216
 
    def is_ready(self, service_name):
217
 
        """
218
 
        Determine if a registered service is ready, by checking its 'required_data'.
219
 
 
220
 
        A 'required_data' item can be any mapping type, and is considered ready
221
 
        if `bool(item)` evaluates as True.
222
 
        """
223
 
        service = self.get_service(service_name)
224
 
        reqs = service.get('required_data', [])
225
 
        return all(bool(req) for req in reqs)
226
 
 
227
 
    def _load_ready_file(self):
228
 
        if self._ready is not None:
229
 
            return
230
 
        if os.path.exists(self._ready_file):
231
 
            with open(self._ready_file) as fp:
232
 
                self._ready = set(json.load(fp))
233
 
        else:
234
 
            self._ready = set()
235
 
 
236
 
    def _save_ready_file(self):
237
 
        if self._ready is None:
238
 
            return
239
 
        with open(self._ready_file, 'w') as fp:
240
 
            json.dump(list(self._ready), fp)
241
 
 
242
 
    def save_ready(self, service_name):
243
 
        """
244
 
        Save an indicator that the given service is now data_ready.
245
 
        """
246
 
        self._load_ready_file()
247
 
        self._ready.add(service_name)
248
 
        self._save_ready_file()
249
 
 
250
 
    def save_lost(self, service_name):
251
 
        """
252
 
        Save an indicator that the given service is no longer data_ready.
253
 
        """
254
 
        self._load_ready_file()
255
 
        self._ready.discard(service_name)
256
 
        self._save_ready_file()
257
 
 
258
 
    def was_ready(self, service_name):
259
 
        """
260
 
        Determine if the given service was previously data_ready.
261
 
        """
262
 
        self._load_ready_file()
263
 
        return service_name in self._ready
264
 
 
265
 
 
266
 
class ManagerCallback(object):
267
 
    """
268
 
    Special case of a callback that takes the `ServiceManager` instance
269
 
    in addition to the service name.
270
 
 
271
 
    Subclasses should implement `__call__` which should accept three parameters:
272
 
 
273
 
        * `manager`       The `ServiceManager` instance
274
 
        * `service_name`  The name of the service it's being triggered for
275
 
        * `event_name`    The name of the event that this callback is handling
276
 
    """
277
 
    def __call__(self, manager, service_name, event_name):
278
 
        raise NotImplementedError()
279
 
 
280
 
 
281
 
class PortManagerCallback(ManagerCallback):
282
 
    """
283
 
    Callback class that will open or close ports, for use as either
284
 
    a start or stop action.
285
 
    """
286
 
    def __call__(self, manager, service_name, event_name):
287
 
        service = manager.get_service(service_name)
288
 
        new_ports = service.get('ports', [])
289
 
        port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
290
 
        if os.path.exists(port_file):
291
 
            with open(port_file) as fp:
292
 
                old_ports = fp.read().split(',')
293
 
            for old_port in old_ports:
294
 
                if bool(old_port):
295
 
                    old_port = int(old_port)
296
 
                    if old_port not in new_ports:
297
 
                        hookenv.close_port(old_port)
298
 
        with open(port_file, 'w') as fp:
299
 
            fp.write(','.join(str(port) for port in new_ports))
300
 
        for port in new_ports:
301
 
            if event_name == 'start':
302
 
                hookenv.open_port(port)
303
 
            elif event_name == 'stop':
304
 
                hookenv.close_port(port)
305
 
 
306
 
 
307
 
def service_stop(service_name):
308
 
    """
309
 
    Wrapper around host.service_stop to prevent spurious "unknown service"
310
 
    messages in the logs.
311
 
    """
312
 
    if host.service_running(service_name):
313
 
        host.service_stop(service_name)
314
 
 
315
 
 
316
 
def service_restart(service_name):
317
 
    """
318
 
    Wrapper around host.service_restart to prevent spurious "unknown service"
319
 
    messages in the logs.
320
 
    """
321
 
    if host.service_available(service_name):
322
 
        if host.service_running(service_name):
323
 
            host.service_restart(service_name)
324
 
        else:
325
 
            host.service_start(service_name)
326
 
 
327
 
 
328
 
# Convenience aliases
329
 
open_ports = close_ports = manage_ports = PortManagerCallback()