~openstack-charmers-archive/charms/trusty/ceph/next

« back to all changes in this revision

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

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