~charmers/charms/precise/postgresql/trunk

« back to all changes in this revision

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

  • Committer: Charles Butler
  • Date: 2014-10-23 19:55:51 UTC
  • mfrom: (101.2.17 integration)
  • Revision ID: chuck@dasroot.net-20141023195551-c8i0ese8em4whbzh
[R=lazypower,S=Stub]  Stuart Bishop 2014-10-14 Ignore magic synced charmhelpers
    Stuart Bishop 2014-10-14 Allow charm store charms, fixing rsyslog test
    Stuart Bishop 2014-10-14 [merge] Embed test client charm and remove JUJU_REPOSITORY requirement
    Stuart Bishop 2014-10-14 Embed test client charm and remove requirement of JUJU_REPOSITORY bei...
    Stuart Bishop 2014-10-07 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-10-07 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-10-07 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-10-07 [merge] Merged bug-1281600-log_temp_files into bug-1276024-fix-log-sh...
    Stuart Bishop 2014-10-07 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-10-07 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-10-07 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-10-07 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-10-07 [merge] Merged trunk-dev into charm-helpers.
    Stuart Bishop 2014-10-07 [merge] Merged postgresql into trunk-dev.
    Stuart Bishop 2014-09-23 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-09-23 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-09-23 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-09-23 [merge] Merged bug-1281600-log_temp_files into bug-1276024-fix-log-sh...
    Stuart Bishop 2014-09-23 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-09-23 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-09-23 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-09-23 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-09-23 [merge] Merged trunk-dev into charm-helpers.
    Stuart Bishop 2014-09-23 [merge] Merged postgresql into trunk-dev.
    Stuart Bishop 2014-10-10 Make hooks.py executable
    Stuart Bishop 2014-10-08 [merge] Merge trunk
    Stuart Bishop 2014-10-03 Fix failing tests
    Stuart Bishop 2014-10-02 Better guarding on credentials resetting
    Stuart Bishop 2014-10-02 Don't introspect relations we haven't joined yet
    Stuart Bishop 2014-10-02 [merge] Merge trunk
    Stuart Bishop 2014-10-02 Review feedback
    Stuart Bishop 2014-09-23 Split full test run
    Stuart Bishop 2014-09-22 Timeouts
    Stuart Bishop 2014-09-22 Do not attempt to publish client connection details until the master ...
    Stuart Bishop 2014-09-19 timings
    Stuart Bishop 2014-09-19 Increase timeout, and ignore all failures of juju-run commands since ...
    Stuart Bishop 2014-09-19 [merge] delint
    Adam Israel 2014-09-17 Fix lint errors
    Stuart Bishop 2014-09-08 [merge] Merge development pipeline up to data-checksums
    Stuart Bishop 2014-08-27 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-08-27 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1281600-log_temp_files into bug-1276024-fix-log-sh...
    Stuart Bishop 2014-08-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-08-27 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-08-27 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-08-27 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-08-27 Add missing charm-helpers files
    Stuart Bishop 2014-08-27 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-08-27 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1281600-log_temp_files into bug-1276024-fix-log-sh...
    Stuart Bishop 2014-08-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-08-27 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-08-27 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-08-27 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-08-27 [merge] Merged trunk-dev into charm-helpers.
    Stuart Bishop 2014-08-27 [merge] Merged postgresql into trunk-dev.
    Stuart Bishop 2014-08-27 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-08-27 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1281600-log_temp_files into bug-1276024-fix-log-sh...
    Stuart Bishop 2014-08-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-08-27 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-08-27 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-08-27 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-08-27 Update charm-helpers, really
    Stuart Bishop 2014-08-27 Update charm-helpers branch
    Stuart Bishop 2014-08-27 [merge] Merged test-upgrade-charm into data-checksums.
    Stuart Bishop 2014-08-27 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-08-27 [merge] Merged bug-1276024-fix-log-shipping into wal-e.
    Stuart Bishop 2014-08-27 [merge] Resolve conflicts
    Stuart Bishop 2014-08-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-08-27 [merge] Merged tests into bug-1329816-backup-log-spurious-alert.
    Stuart Bishop 2014-08-27 [merge] Merged cleanups into tests.
    Stuart Bishop 2014-08-27 [merge] Merged charm-helpers into cleanups.
    Stuart Bishop 2014-08-27 Update charm-helpers
    Stuart Bishop 2014-08-27 charm-helpers sync target
    Stuart Bishop 2014-06-30 Unused and incorrect global
    Stuart Bishop 2014-06-30 Dead code
    Stuart Bishop 2014-06-30 Dead code
    Stuart Bishop 2014-08-23 Turn on data checksums by default for new PG 9.3+ installations
    Stuart Bishop 2014-08-22 [merge] Merged wal-e into test-upgrade-charm.
    Stuart Bishop 2014-08-21 Document experimental wal-e support
    Stuart Bishop 2014-08-07 Test upgrade-charm does not reset the database
    Stuart Bishop 2014-07-14 delint
    Stuart Bishop 2014-07-14 Cope when config items are not set
    Stuart Bishop 2014-07-07 WAL-E Swift tests passing
    Stuart Bishop 2014-07-04 WIP
    Stuart Bishop 2014-07-04 WAL-E support, WIP
    Stuart Bishop 2014-07-02 SwiftWAL tweaks and a test
    Stuart Bishop 2014-07-01 Manual runs working
    Stuart Bishop 2014-06-30 WIP
    Stuart Bishop 2014-06-30 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-06-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-06-27 [merge] Merged bug-1329816-backup-log-spurious-alert into bug-1281600...
    Stuart Bishop 2014-06-24 Add log_temp_files configuration option

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