~free.ekanayaka/landscape-charm/run-schema-script

« back to all changes in this revision

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

  • Committer: Free Ekanayaka
  • Date: 2015-01-28 11:53:32 UTC
  • mfrom: (222.1.5 testable-install-hook)
  • Revision ID: free.ekanayaka@canonical.com-20150128115332-4fjkh28z3lqf136x
MergeĀ fromĀ testable-install-hook

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