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
Traditional charm authoring is focused on implementing hooks. That is,
21
the charm author is thinking in terms of "What hook am I handling; what
22
does this hook need to do?" However, in most cases, the real question
23
should be "Do I have the information I need to configure and start this
24
piece of software and, if so, what are the steps for doing so?" The
25
ServiceManager framework tries to bring the focus to the data and the
26
setup tasks, in the most declarative way possible.
28
Service definitions are dicts in the following formats (all keys except
29
'service' are optional)::
32
"service": <service name>,
33
"required_data": <list of required data contexts>,
34
"data_ready": <one or more callbacks>,
35
"data_lost": <one or more callbacks>,
36
"start": <one or more callbacks>,
37
"stop": <one or more callbacks>,
38
"ports": <list of ports to manage>,
41
The 'required_data' list should contain dicts of required data (or
42
dependency managers that act like dicts and know how to collect the data).
43
Only when all items in the 'required_data' list are populated are the list
44
of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
47
The 'data_ready' value should be either a single callback, or a list of
48
callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49
Each callback will be called with the service name as the only parameter.
50
After all of the 'data_ready' callbacks are called, the 'start' callbacks
53
The 'data_lost' value should be either a single callback, or a list of
54
callbacks, to be called when a 'required_data' item no longer passes
55
`is_ready()`. Each callback will be called with the service name as the
56
only parameter. After all of the 'data_lost' callbacks are called,
57
the 'stop' callbacks are fired.
59
The 'start' value should be either a single callback, or a list of
60
callbacks, to be called when starting the service, after the 'data_ready'
61
callbacks are complete. Each callback will be called with the service
62
name as the only parameter. This defaults to
63
`[host.service_start, services.open_ports]`.
65
The 'stop' value should be either a single callback, or a list of
66
callbacks, to be called when stopping the service. If the service is
67
being stopped because it no longer has all of its 'required_data', this
68
will be called after all of the 'data_lost' callbacks are complete.
69
Each callback will be called with the service name as the only parameter.
70
This defaults to `[services.close_ports, host.service_stop]`.
72
The 'ports' value should be a list of ports to manage. The default
73
'start' handler will open the ports after the service is started,
74
and the default 'stop' handler will close the ports prior to stopping
80
The following registers an Upstart service called bingod that depends on
81
a mongodb relation and which runs a custom `db_migrate` function prior to
82
restarting the service, and a Runit service called spadesd::
84
manager = services.ServiceManager([
88
'required_data': [MongoRelation(), config(), {'my': 'data'}],
90
services.template(source='bingod.conf'),
91
services.template(source='bingod.ini',
92
target='/etc/bingod.ini',
93
owner='bingo', perms=0400),
98
'data_ready': services.template(source='spadesd_run.j2',
99
target='/etc/sv/spadesd/run',
101
'start': runit_start,
107
self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
110
for service in services or []:
111
service_name = service['service']
112
self.services[service_name] = service
116
Handle the current hook by doing The Right Thing with the registered services.
118
hook_name = hookenv.hook_name()
119
if hook_name == 'stop':
123
self.reconfigure_services()
125
def provide_data(self):
126
hook_name = hookenv.hook_name()
127
for service in self.services.values():
128
for provider in service.get('provided_data', []):
129
if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130
data = provider.provide_data()
131
if provider._is_ready(data):
132
hookenv.relation_set(None, data)
134
def reconfigure_services(self, *service_names):
136
Update all files for one or more registered services, and,
137
if ready, optionally restart them.
139
If no service names are given, reconfigures all registered services.
141
for service_name in service_names or self.services.keys():
142
if self.is_ready(service_name):
143
self.fire_event('data_ready', service_name)
144
self.fire_event('start', service_name, default=[
147
self.save_ready(service_name)
149
if self.was_ready(service_name):
150
self.fire_event('data_lost', service_name)
151
self.fire_event('stop', service_name, default=[
154
self.save_lost(service_name)
156
def stop_services(self, *service_names):
158
Stop one or more registered services, by name.
160
If no service names are given, stops all registered services.
162
for service_name in service_names or self.services.keys():
163
self.fire_event('stop', service_name, default=[
167
def get_service(self, service_name):
169
Given the name of a registered service, return its service definition.
171
service = self.services.get(service_name)
173
raise KeyError('Service not registered: %s' % service_name)
176
def fire_event(self, event_name, service_name, default=None):
178
Fire a data_ready, data_lost, start, or stop event on a given service.
180
service = self.get_service(service_name)
181
callbacks = service.get(event_name, default)
184
if not isinstance(callbacks, Iterable):
185
callbacks = [callbacks]
186
for callback in callbacks:
187
if isinstance(callback, ManagerCallback):
188
callback(self, service_name, event_name)
190
callback(service_name)
192
def is_ready(self, service_name):
194
Determine if a registered service is ready, by checking its 'required_data'.
196
A 'required_data' item can be any mapping type, and is considered ready
197
if `bool(item)` evaluates as True.
199
service = self.get_service(service_name)
200
reqs = service.get('required_data', [])
201
return all(bool(req) for req in reqs)
203
def _load_ready_file(self):
204
if self._ready is not None:
206
if os.path.exists(self._ready_file):
207
with open(self._ready_file) as fp:
208
self._ready = set(json.load(fp))
212
def _save_ready_file(self):
213
if self._ready is None:
215
with open(self._ready_file, 'w') as fp:
216
json.dump(list(self._ready), fp)
218
def save_ready(self, service_name):
220
Save an indicator that the given service is now data_ready.
222
self._load_ready_file()
223
self._ready.add(service_name)
224
self._save_ready_file()
226
def save_lost(self, service_name):
228
Save an indicator that the given service is no longer data_ready.
230
self._load_ready_file()
231
self._ready.discard(service_name)
232
self._save_ready_file()
234
def was_ready(self, service_name):
236
Determine if the given service was previously data_ready.
238
self._load_ready_file()
239
return service_name in self._ready
242
class ManagerCallback(object):
244
Special case of a callback that takes the `ServiceManager` instance
245
in addition to the service name.
247
Subclasses should implement `__call__` which should accept three parameters:
249
* `manager` The `ServiceManager` instance
250
* `service_name` The name of the service it's being triggered for
251
* `event_name` The name of the event that this callback is handling
253
def __call__(self, manager, service_name, event_name):
254
raise NotImplementedError()
257
class PortManagerCallback(ManagerCallback):
259
Callback class that will open or close ports, for use as either
260
a start or stop action.
262
def __call__(self, manager, service_name, event_name):
263
service = manager.get_service(service_name)
264
new_ports = service.get('ports', [])
265
port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
266
if os.path.exists(port_file):
267
with open(port_file) as fp:
268
old_ports = fp.read().split(',')
269
for old_port in old_ports:
271
old_port = int(old_port)
272
if old_port not in new_ports:
273
hookenv.close_port(old_port)
274
with open(port_file, 'w') as fp:
275
fp.write(','.join(str(port) for port in new_ports))
276
for port in new_ports:
277
if event_name == 'start':
278
hookenv.open_port(port)
279
elif event_name == 'stop':
280
hookenv.close_port(port)
283
def service_stop(service_name):
285
Wrapper around host.service_stop to prevent spurious "unknown service"
286
messages in the logs.
288
if host.service_running(service_name):
289
host.service_stop(service_name)
292
def service_restart(service_name):
294
Wrapper around host.service_restart to prevent spurious "unknown service"
295
messages in the logs.
297
if host.service_available(service_name):
298
if host.service_running(service_name):
299
host.service_restart(service_name)
301
host.service_start(service_name)
304
# Convenience aliases
305
open_ports = close_ports = manage_ports = PortManagerCallback()