~openstack-charmers-archive/charms/trusty/cinder-ceph/old-1501

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2014-08-13 14:18:42 UTC
  • mfrom: (16.1.1 cinder-ceph)
  • Revision ID: liam.young@canonical.com-20140813141842-ayugbafi6zy5odnr
[gnuoy, rs=jamespage] Sync charm-helpers from lp:charm-helpers

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os
 
2
import re
 
3
import json
 
4
from collections import Iterable
 
5
 
 
6
from charmhelpers.core import host
 
7
from charmhelpers.core import hookenv
 
8
 
 
9
 
 
10
__all__ = ['ServiceManager', 'ManagerCallback',
 
11
           'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
 
12
           'service_restart', 'service_stop']
 
13
 
 
14
 
 
15
class ServiceManager(object):
 
16
    def __init__(self, services=None):
 
17
        """
 
18
        Register a list of services, given their definitions.
 
19
 
 
20
        Traditional charm authoring is focused on implementing hooks.  That is,
 
21
        the charm author is thinking in terms of "What hook am I handling; what
 
22
        does this hook need to do?"  However, in most cases, the real question
 
23
        should be "Do I have the information I need to configure and start this
 
24
        piece of software and, if so, what are the steps for doing so?"  The
 
25
        ServiceManager framework tries to bring the focus to the data and the
 
26
        setup tasks, in the most declarative way possible.
 
27
 
 
28
        Service definitions are dicts in the following formats (all keys except
 
29
        'service' are optional)::
 
30
 
 
31
            {
 
32
                "service": <service name>,
 
33
                "required_data": <list of required data contexts>,
 
34
                "data_ready": <one or more callbacks>,
 
35
                "data_lost": <one or more callbacks>,
 
36
                "start": <one or more callbacks>,
 
37
                "stop": <one or more callbacks>,
 
38
                "ports": <list of ports to manage>,
 
39
            }
 
40
 
 
41
        The 'required_data' list should contain dicts of required data (or
 
42
        dependency managers that act like dicts and know how to collect the data).
 
43
        Only when all items in the 'required_data' list are populated are the list
 
44
        of 'data_ready' and 'start' callbacks executed.  See `is_ready()` for more
 
45
        information.
 
46
 
 
47
        The 'data_ready' value should be either a single callback, or a list of
 
48
        callbacks, to be called when all items in 'required_data' pass `is_ready()`.
 
49
        Each callback will be called with the service name as the only parameter.
 
50
        After all of the 'data_ready' callbacks are called, the 'start' callbacks
 
51
        are fired.
 
52
 
 
53
        The 'data_lost' value should be either a single callback, or a list of
 
54
        callbacks, to be called when a 'required_data' item no longer passes
 
55
        `is_ready()`.  Each callback will be called with the service name as the
 
56
        only parameter.  After all of the 'data_lost' callbacks are called,
 
57
        the 'stop' callbacks are fired.
 
58
 
 
59
        The 'start' value should be either a single callback, or a list of
 
60
        callbacks, to be called when starting the service, after the 'data_ready'
 
61
        callbacks are complete.  Each callback will be called with the service
 
62
        name as the only parameter.  This defaults to
 
63
        `[host.service_start, services.open_ports]`.
 
64
 
 
65
        The 'stop' value should be either a single callback, or a list of
 
66
        callbacks, to be called when stopping the service.  If the service is
 
67
        being stopped because it no longer has all of its 'required_data', this
 
68
        will be called after all of the 'data_lost' callbacks are complete.
 
69
        Each callback will be called with the service name as the only parameter.
 
70
        This defaults to `[services.close_ports, host.service_stop]`.
 
71
 
 
72
        The 'ports' value should be a list of ports to manage.  The default
 
73
        'start' handler will open the ports after the service is started,
 
74
        and the default 'stop' handler will close the ports prior to stopping
 
75
        the service.
 
76
 
 
77
 
 
78
        Examples:
 
79
 
 
80
        The following registers an Upstart service called bingod that depends on
 
81
        a mongodb relation and which runs a custom `db_migrate` function prior to
 
82
        restarting the service, and a Runit service called spadesd::
 
83
 
 
84
            manager = services.ServiceManager([
 
85
                {
 
86
                    'service': 'bingod',
 
87
                    'ports': [80, 443],
 
88
                    'required_data': [MongoRelation(), config(), {'my': 'data'}],
 
89
                    'data_ready': [
 
90
                        services.template(source='bingod.conf'),
 
91
                        services.template(source='bingod.ini',
 
92
                                          target='/etc/bingod.ini',
 
93
                                          owner='bingo', perms=0400),
 
94
                    ],
 
95
                },
 
96
                {
 
97
                    'service': 'spadesd',
 
98
                    'data_ready': services.template(source='spadesd_run.j2',
 
99
                                                    target='/etc/sv/spadesd/run',
 
100
                                                    perms=0555),
 
101
                    'start': runit_start,
 
102
                    'stop': runit_stop,
 
103
                },
 
104
            ])
 
105
            manager.manage()
 
106
        """
 
107
        self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
 
108
        self._ready = None
 
109
        self.services = {}
 
110
        for service in services or []:
 
111
            service_name = service['service']
 
112
            self.services[service_name] = service
 
113
 
 
114
    def manage(self):
 
115
        """
 
116
        Handle the current hook by doing The Right Thing with the registered services.
 
117
        """
 
118
        hook_name = hookenv.hook_name()
 
119
        if hook_name == 'stop':
 
120
            self.stop_services()
 
121
        else:
 
122
            self.provide_data()
 
123
            self.reconfigure_services()
 
124
 
 
125
    def provide_data(self):
 
126
        hook_name = hookenv.hook_name()
 
127
        for service in self.services.values():
 
128
            for provider in service.get('provided_data', []):
 
129
                if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
 
130
                    data = provider.provide_data()
 
131
                    if provider._is_ready(data):
 
132
                        hookenv.relation_set(None, data)
 
133
 
 
134
    def reconfigure_services(self, *service_names):
 
135
        """
 
136
        Update all files for one or more registered services, and,
 
137
        if ready, optionally restart them.
 
138
 
 
139
        If no service names are given, reconfigures all registered services.
 
140
        """
 
141
        for service_name in service_names or self.services.keys():
 
142
            if self.is_ready(service_name):
 
143
                self.fire_event('data_ready', service_name)
 
144
                self.fire_event('start', service_name, default=[
 
145
                    service_restart,
 
146
                    manage_ports])
 
147
                self.save_ready(service_name)
 
148
            else:
 
149
                if self.was_ready(service_name):
 
150
                    self.fire_event('data_lost', service_name)
 
151
                self.fire_event('stop', service_name, default=[
 
152
                    manage_ports,
 
153
                    service_stop])
 
154
                self.save_lost(service_name)
 
155
 
 
156
    def stop_services(self, *service_names):
 
157
        """
 
158
        Stop one or more registered services, by name.
 
159
 
 
160
        If no service names are given, stops all registered services.
 
161
        """
 
162
        for service_name in service_names or self.services.keys():
 
163
            self.fire_event('stop', service_name, default=[
 
164
                manage_ports,
 
165
                service_stop])
 
166
 
 
167
    def get_service(self, service_name):
 
168
        """
 
169
        Given the name of a registered service, return its service definition.
 
170
        """
 
171
        service = self.services.get(service_name)
 
172
        if not service:
 
173
            raise KeyError('Service not registered: %s' % service_name)
 
174
        return service
 
175
 
 
176
    def fire_event(self, event_name, service_name, default=None):
 
177
        """
 
178
        Fire a data_ready, data_lost, start, or stop event on a given service.
 
179
        """
 
180
        service = self.get_service(service_name)
 
181
        callbacks = service.get(event_name, default)
 
182
        if not callbacks:
 
183
            return
 
184
        if not isinstance(callbacks, Iterable):
 
185
            callbacks = [callbacks]
 
186
        for callback in callbacks:
 
187
            if isinstance(callback, ManagerCallback):
 
188
                callback(self, service_name, event_name)
 
189
            else:
 
190
                callback(service_name)
 
191
 
 
192
    def is_ready(self, service_name):
 
193
        """
 
194
        Determine if a registered service is ready, by checking its 'required_data'.
 
195
 
 
196
        A 'required_data' item can be any mapping type, and is considered ready
 
197
        if `bool(item)` evaluates as True.
 
198
        """
 
199
        service = self.get_service(service_name)
 
200
        reqs = service.get('required_data', [])
 
201
        return all(bool(req) for req in reqs)
 
202
 
 
203
    def _load_ready_file(self):
 
204
        if self._ready is not None:
 
205
            return
 
206
        if os.path.exists(self._ready_file):
 
207
            with open(self._ready_file) as fp:
 
208
                self._ready = set(json.load(fp))
 
209
        else:
 
210
            self._ready = set()
 
211
 
 
212
    def _save_ready_file(self):
 
213
        if self._ready is None:
 
214
            return
 
215
        with open(self._ready_file, 'w') as fp:
 
216
            json.dump(list(self._ready), fp)
 
217
 
 
218
    def save_ready(self, service_name):
 
219
        """
 
220
        Save an indicator that the given service is now data_ready.
 
221
        """
 
222
        self._load_ready_file()
 
223
        self._ready.add(service_name)
 
224
        self._save_ready_file()
 
225
 
 
226
    def save_lost(self, service_name):
 
227
        """
 
228
        Save an indicator that the given service is no longer data_ready.
 
229
        """
 
230
        self._load_ready_file()
 
231
        self._ready.discard(service_name)
 
232
        self._save_ready_file()
 
233
 
 
234
    def was_ready(self, service_name):
 
235
        """
 
236
        Determine if the given service was previously data_ready.
 
237
        """
 
238
        self._load_ready_file()
 
239
        return service_name in self._ready
 
240
 
 
241
 
 
242
class ManagerCallback(object):
 
243
    """
 
244
    Special case of a callback that takes the `ServiceManager` instance
 
245
    in addition to the service name.
 
246
 
 
247
    Subclasses should implement `__call__` which should accept three parameters:
 
248
 
 
249
        * `manager`       The `ServiceManager` instance
 
250
        * `service_name`  The name of the service it's being triggered for
 
251
        * `event_name`    The name of the event that this callback is handling
 
252
    """
 
253
    def __call__(self, manager, service_name, event_name):
 
254
        raise NotImplementedError()
 
255
 
 
256
 
 
257
class PortManagerCallback(ManagerCallback):
 
258
    """
 
259
    Callback class that will open or close ports, for use as either
 
260
    a start or stop action.
 
261
    """
 
262
    def __call__(self, manager, service_name, event_name):
 
263
        service = manager.get_service(service_name)
 
264
        new_ports = service.get('ports', [])
 
265
        port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
 
266
        if os.path.exists(port_file):
 
267
            with open(port_file) as fp:
 
268
                old_ports = fp.read().split(',')
 
269
            for old_port in old_ports:
 
270
                if bool(old_port):
 
271
                    old_port = int(old_port)
 
272
                    if old_port not in new_ports:
 
273
                        hookenv.close_port(old_port)
 
274
        with open(port_file, 'w') as fp:
 
275
            fp.write(','.join(str(port) for port in new_ports))
 
276
        for port in new_ports:
 
277
            if event_name == 'start':
 
278
                hookenv.open_port(port)
 
279
            elif event_name == 'stop':
 
280
                hookenv.close_port(port)
 
281
 
 
282
 
 
283
def service_stop(service_name):
 
284
    """
 
285
    Wrapper around host.service_stop to prevent spurious "unknown service"
 
286
    messages in the logs.
 
287
    """
 
288
    if host.service_running(service_name):
 
289
        host.service_stop(service_name)
 
290
 
 
291
 
 
292
def service_restart(service_name):
 
293
    """
 
294
    Wrapper around host.service_restart to prevent spurious "unknown service"
 
295
    messages in the logs.
 
296
    """
 
297
    if host.service_available(service_name):
 
298
        if host.service_running(service_name):
 
299
            host.service_restart(service_name)
 
300
        else:
 
301
            host.service_start(service_name)
 
302
 
 
303
 
 
304
# Convenience aliases
 
305
open_ports = close_ports = manage_ports = PortManagerCallback()