4
from collections import Iterable
6
from charmhelpers.core import host
7
from charmhelpers.core import hookenv
10
__all__ = ['ServiceManager', 'ManagerCallback',
11
'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
12
'service_restart', 'service_stop']
15
class ServiceManager(object):
16
def __init__(self, services=None):
18
Register a list of services, given their definitions.
20
Service definitions are dicts in the following formats (all keys except
21
'service' are optional)::
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>,
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
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.
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
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.
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]`.
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]`.
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
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::
81
manager = services.ServiceManager([
85
'required_data': [MongoRelation(), config(), {'my': 'data'}],
87
services.template(source='bingod.conf'),
88
services.template(source='bingod.ini',
89
target='/etc/bingod.ini',
90
owner='bingo', perms=0400),
95
'data_ready': services.template(source='spadesd_run.j2',
96
target='/etc/sv/spadesd/run',
104
self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
107
for service in services or []:
108
service_name = service['service']
109
self.services[service_name] = service
113
Handle the current hook by doing The Right Thing with the registered services.
115
hook_name = hookenv.hook_name()
116
if hook_name == 'stop':
120
self.reconfigure_services()
121
cfg = hookenv.config()
122
if cfg.implicit_save:
125
def provide_data(self):
127
Set the relation data for each provider in the ``provided_data`` list.
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
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
140
hookenv.relation_set(None, data)
142
def reconfigure_services(self, *service_names):
144
Update all files for one or more registered services, and,
145
if ready, optionally restart them.
147
If no service names are given, reconfigures all registered services.
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=[
155
self.save_ready(service_name)
157
if self.was_ready(service_name):
158
self.fire_event('data_lost', service_name)
159
self.fire_event('stop', service_name, default=[
162
self.save_lost(service_name)
164
def stop_services(self, *service_names):
166
Stop one or more registered services, by name.
168
If no service names are given, stops all registered services.
170
for service_name in service_names or self.services.keys():
171
self.fire_event('stop', service_name, default=[
175
def get_service(self, service_name):
177
Given the name of a registered service, return its service definition.
179
service = self.services.get(service_name)
181
raise KeyError('Service not registered: %s' % service_name)
184
def fire_event(self, event_name, service_name, default=None):
186
Fire a data_ready, data_lost, start, or stop event on a given service.
188
service = self.get_service(service_name)
189
callbacks = service.get(event_name, default)
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)
198
callback(service_name)
200
def is_ready(self, service_name):
202
Determine if a registered service is ready, by checking its 'required_data'.
204
A 'required_data' item can be any mapping type, and is considered ready
205
if `bool(item)` evaluates as True.
207
service = self.get_service(service_name)
208
reqs = service.get('required_data', [])
209
return all(bool(req) for req in reqs)
211
def _load_ready_file(self):
212
if self._ready is not None:
214
if os.path.exists(self._ready_file):
215
with open(self._ready_file) as fp:
216
self._ready = set(json.load(fp))
220
def _save_ready_file(self):
221
if self._ready is None:
223
with open(self._ready_file, 'w') as fp:
224
json.dump(list(self._ready), fp)
226
def save_ready(self, service_name):
228
Save an indicator that the given service is now data_ready.
230
self._load_ready_file()
231
self._ready.add(service_name)
232
self._save_ready_file()
234
def save_lost(self, service_name):
236
Save an indicator that the given service is no longer data_ready.
238
self._load_ready_file()
239
self._ready.discard(service_name)
240
self._save_ready_file()
242
def was_ready(self, service_name):
244
Determine if the given service was previously data_ready.
246
self._load_ready_file()
247
return service_name in self._ready
250
class ManagerCallback(object):
252
Special case of a callback that takes the `ServiceManager` instance
253
in addition to the service name.
255
Subclasses should implement `__call__` which should accept three parameters:
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
261
def __call__(self, manager, service_name, event_name):
262
raise NotImplementedError()
265
class PortManagerCallback(ManagerCallback):
267
Callback class that will open or close ports, for use as either
268
a start or stop action.
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:
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)
291
def service_stop(service_name):
293
Wrapper around host.service_stop to prevent spurious "unknown service"
294
messages in the logs.
296
if host.service_running(service_name):
297
host.service_stop(service_name)
300
def service_restart(service_name):
302
Wrapper around host.service_restart to prevent spurious "unknown service"
303
messages in the logs.
305
if host.service_available(service_name):
306
if host.service_running(service_name):
307
host.service_restart(service_name)
309
host.service_start(service_name)
312
# Convenience aliases
313
open_ports = close_ports = manage_ports = PortManagerCallback()