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()
122
def provide_data(self):
124
Set the relation data for each provider in the ``provided_data`` list.
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
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
137
hookenv.relation_set(None, data)
139
def reconfigure_services(self, *service_names):
141
Update all files for one or more registered services, and,
142
if ready, optionally restart them.
144
If no service names are given, reconfigures all registered services.
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=[
152
self.save_ready(service_name)
154
if self.was_ready(service_name):
155
self.fire_event('data_lost', service_name)
156
self.fire_event('stop', service_name, default=[
159
self.save_lost(service_name)
161
def stop_services(self, *service_names):
163
Stop one or more registered services, by name.
165
If no service names are given, stops all registered services.
167
for service_name in service_names or self.services.keys():
168
self.fire_event('stop', service_name, default=[
172
def get_service(self, service_name):
174
Given the name of a registered service, return its service definition.
176
service = self.services.get(service_name)
178
raise KeyError('Service not registered: %s' % service_name)
181
def fire_event(self, event_name, service_name, default=None):
183
Fire a data_ready, data_lost, start, or stop event on a given service.
185
service = self.get_service(service_name)
186
callbacks = service.get(event_name, default)
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)
195
callback(service_name)
197
def is_ready(self, service_name):
199
Determine if a registered service is ready, by checking its 'required_data'.
201
A 'required_data' item can be any mapping type, and is considered ready
202
if `bool(item)` evaluates as True.
204
service = self.get_service(service_name)
205
reqs = service.get('required_data', [])
206
return all(bool(req) for req in reqs)
208
def _load_ready_file(self):
209
if self._ready is not None:
211
if os.path.exists(self._ready_file):
212
with open(self._ready_file) as fp:
213
self._ready = set(json.load(fp))
217
def _save_ready_file(self):
218
if self._ready is None:
220
with open(self._ready_file, 'w') as fp:
221
json.dump(list(self._ready), fp)
223
def save_ready(self, service_name):
225
Save an indicator that the given service is now data_ready.
227
self._load_ready_file()
228
self._ready.add(service_name)
229
self._save_ready_file()
231
def save_lost(self, service_name):
233
Save an indicator that the given service is no longer data_ready.
235
self._load_ready_file()
236
self._ready.discard(service_name)
237
self._save_ready_file()
239
def was_ready(self, service_name):
241
Determine if the given service was previously data_ready.
243
self._load_ready_file()
244
return service_name in self._ready
247
class ManagerCallback(object):
249
Special case of a callback that takes the `ServiceManager` instance
250
in addition to the service name.
252
Subclasses should implement `__call__` which should accept three parameters:
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
258
def __call__(self, manager, service_name, event_name):
259
raise NotImplementedError()
262
class PortManagerCallback(ManagerCallback):
264
Callback class that will open or close ports, for use as either
265
a start or stop action.
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:
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)
288
def service_stop(service_name):
290
Wrapper around host.service_stop to prevent spurious "unknown service"
291
messages in the logs.
293
if host.service_running(service_name):
294
host.service_stop(service_name)
297
def service_restart(service_name):
299
Wrapper around host.service_restart to prevent spurious "unknown service"
300
messages in the logs.
302
if host.service_available(service_name):
303
if host.service_running(service_name):
304
host.service_restart(service_name)
306
host.service_start(service_name)
309
# Convenience aliases
310
open_ports = close_ports = manage_ports = PortManagerCallback()