4
from base64 import b64decode
6
from subprocess import (
11
from charmhelpers.fetch import (
13
filter_installed_packages,
16
from charmhelpers.core.hookenv import (
29
from charmhelpers.contrib.hahelpers.cluster import (
31
determine_haproxy_port,
37
from charmhelpers.contrib.hahelpers.apache import (
42
from charmhelpers.contrib.openstack.neutron import (
43
neutron_plugin_attribute,
46
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
49
class OSContextError(Exception):
53
def ensure_packages(packages):
54
'''Install but do not upgrade required plugin packages'''
55
required = filter_installed_packages(packages)
57
apt_install(required, fatal=True)
60
def context_complete(ctxt):
62
for k, v in ctxt.iteritems():
63
if v is None or v == '':
66
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
71
class OSContextGenerator(object):
75
raise NotImplementedError
78
class SharedDBContext(OSContextGenerator):
79
interfaces = ['shared-db']
81
def __init__(self, database=None, user=None, relation_prefix=None):
83
Allows inspecting relation for settings prefixed with relation_prefix.
84
This is useful for parsing access for multiple databases returned via
85
the shared-db interface (eg, nova_password, quantum_password)
87
self.relation_prefix = relation_prefix
88
self.database = database
92
self.database = self.database or config('database')
93
self.user = self.user or config('database-user')
94
if None in [self.database, self.user]:
95
log('Could not generate shared_db context. '
96
'Missing required charm config options. '
97
'(database name and user)')
101
password_setting = 'password'
102
if self.relation_prefix:
103
password_setting = self.relation_prefix + '_password'
105
for rid in relation_ids('shared-db'):
106
for unit in related_units(rid):
107
passwd = relation_get(password_setting, rid=rid, unit=unit)
109
'database_host': relation_get('db_host', rid=rid,
111
'database': self.database,
112
'database_user': self.user,
113
'database_password': passwd,
115
if context_complete(ctxt):
120
class IdentityServiceContext(OSContextGenerator):
121
interfaces = ['identity-service']
124
log('Generating template context for identity-service')
127
for rid in relation_ids('identity-service'):
128
for unit in related_units(rid):
130
'service_port': relation_get('service_port', rid=rid,
132
'service_host': relation_get('service_host', rid=rid,
134
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
135
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
136
'admin_tenant_name': relation_get('service_tenant',
138
'admin_user': relation_get('service_username', rid=rid,
140
'admin_password': relation_get('service_password', rid=rid,
142
# XXX: Hard-coded http.
143
'service_protocol': 'http',
144
'auth_protocol': 'http',
146
if context_complete(ctxt):
151
class AMQPContext(OSContextGenerator):
152
interfaces = ['amqp']
155
log('Generating template context for amqp')
158
username = conf['rabbit-user']
159
vhost = conf['rabbit-vhost']
160
except KeyError as e:
161
log('Could not generate shared_db context. '
162
'Missing required charm config options: %s.' % e)
166
for rid in relation_ids('amqp'):
167
for unit in related_units(rid):
168
if relation_get('clustered', rid=rid, unit=unit):
169
ctxt['clustered'] = True
170
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
173
ctxt['rabbitmq_host'] = relation_get('private-address',
176
'rabbitmq_user': username,
177
'rabbitmq_password': relation_get('password', rid=rid,
179
'rabbitmq_virtual_host': vhost,
181
if context_complete(ctxt):
182
# Sufficient information found = break out!
184
# Used for active/active rabbitmq >= grizzly
185
ctxt['rabbitmq_hosts'] = []
186
for unit in related_units(rid):
187
ctxt['rabbitmq_hosts'].append(relation_get('private-address',
189
if not context_complete(ctxt):
195
class CephContext(OSContextGenerator):
196
interfaces = ['ceph']
199
'''This generates context for /etc/ceph/ceph.conf templates'''
200
if not relation_ids('ceph'):
202
log('Generating template context for ceph')
206
for rid in relation_ids('ceph'):
207
for unit in related_units(rid):
208
mon_hosts.append(relation_get('private-address', rid=rid,
210
auth = relation_get('auth', rid=rid, unit=unit)
211
key = relation_get('key', rid=rid, unit=unit)
214
'mon_hosts': ' '.join(mon_hosts),
219
if not os.path.isdir('/etc/ceph'):
220
os.mkdir('/etc/ceph')
222
if not context_complete(ctxt):
225
ensure_packages(['ceph-common'])
230
class HAProxyContext(OSContextGenerator):
231
interfaces = ['cluster']
235
Builds half a context for the haproxy template, which describes
236
all peers to be included in the cluster. Each charm needs to include
237
its own context generator that describes the port mapping.
239
if not relation_ids('cluster'):
243
l_unit = local_unit().replace('/', '-')
244
cluster_hosts[l_unit] = unit_get('private-address')
246
for rid in relation_ids('cluster'):
247
for unit in related_units(rid):
248
_unit = unit.replace('/', '-')
249
addr = relation_get('private-address', rid=rid, unit=unit)
250
cluster_hosts[_unit] = addr
253
'units': cluster_hosts,
255
if len(cluster_hosts.keys()) > 1:
256
# Enable haproxy when we have enough peers.
257
log('Ensuring haproxy enabled in /etc/default/haproxy.')
258
with open('/etc/default/haproxy', 'w') as out:
259
out.write('ENABLED=1\n')
261
log('HAProxy context is incomplete, this unit has no peers.')
265
class ImageServiceContext(OSContextGenerator):
266
interfaces = ['image-service']
270
Obtains the glance API server from the image-service relation. Useful
271
in nova and cinder (currently).
273
log('Generating template context for image-service.')
274
rids = relation_ids('image-service')
278
for unit in related_units(rid):
279
api_server = relation_get('glance-api-server',
282
return {'glance_api_servers': api_server}
283
log('ImageService context is incomplete. '
284
'Missing required relation data.')
288
class ApacheSSLContext(OSContextGenerator):
290
Generates a context for an apache vhost configuration that configures
291
HTTPS reverse proxying for one or many endpoints. Generated context
292
looks something like:
294
'namespace': 'cinder',
295
'private_address': 'iscsi.mycinderhost.com',
296
'endpoints': [(8776, 8766), (8777, 8767)]
299
The endpoints list consists of a tuples mapping external ports
302
interfaces = ['https']
304
# charms should inherit this context and set external ports
305
# and service namespace accordingly.
307
service_namespace = None
309
def enable_modules(self):
310
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
313
def configure_cert(self):
314
if not os.path.isdir('/etc/apache2/ssl'):
315
os.mkdir('/etc/apache2/ssl')
316
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
317
if not os.path.isdir(ssl_dir):
319
cert, key = get_cert()
320
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
321
cert_out.write(b64decode(cert))
322
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
323
key_out.write(b64decode(key))
324
ca_cert = get_ca_cert()
326
with open(CA_CERT_PATH, 'w') as ca_out:
327
ca_out.write(b64decode(ca_cert))
328
check_call(['update-ca-certificates'])
331
if isinstance(self.external_ports, basestring):
332
self.external_ports = [self.external_ports]
333
if (not self.external_ports or not https()):
336
self.configure_cert()
337
self.enable_modules()
340
'namespace': self.service_namespace,
341
'private_address': unit_get('private-address'),
344
for ext_port in self.external_ports:
345
if peer_units() or is_clustered():
346
int_port = determine_haproxy_port(ext_port)
348
int_port = determine_api_port(ext_port)
349
portmap = (int(ext_port), int(int_port))
350
ctxt['endpoints'].append(portmap)
354
class NeutronContext(object):
362
def network_manager(self):
367
return neutron_plugin_attribute(
368
self.plugin, 'packages', self.network_manager)
371
def neutron_security_groups(self):
374
def _ensure_packages(self):
375
[ensure_packages(pkgs) for pkgs in self.packages]
377
def _save_flag_file(self):
378
if self.network_manager == 'quantum':
379
_file = '/etc/nova/quantum_plugin.conf'
381
_file = '/etc/nova/neutron_plugin.conf'
382
with open(_file, 'wb') as out:
383
out.write(self.plugin + '\n')
386
driver = neutron_plugin_attribute(self.plugin, 'driver',
387
self.network_manager)
390
'core_plugin': driver,
391
'neutron_plugin': 'ovs',
392
'neutron_security_groups': self.neutron_security_groups,
393
'local_ip': unit_private_ip(),
399
self._ensure_packages()
401
if self.network_manager not in ['quantum', 'neutron']:
407
ctxt = {'network_manager': self.network_manager}
409
if self.plugin == 'ovs':
410
ctxt.update(self.ovs_ctxt())
412
self._save_flag_file()
416
class OSConfigFlagContext(OSContextGenerator):
418
Responsible adding user-defined config-flags in charm config to a
419
to a template context.
422
config_flags = config('config-flags')
423
if not config_flags or config_flags in ['None', '']:
425
config_flags = config_flags.split(',')
427
for flag in config_flags:
429
log('Improperly formatted config-flag, expected k=v '
430
'got %s' % flag, level=WARNING)
432
k, v = flag.split('=')
434
ctxt = {'user_config_flags': flags}
438
class SubordinateConfigContext(OSContextGenerator):
440
Responsible for inspecting relations to subordinates that
441
may be exporting required config via a json blob.
443
The subordinate interface allows subordinates to export their
444
configuration requirements to the principle for multiple config
445
files and multiple serivces. Ie, a subordinate that has interfaces
446
to both glance and nova may export to following yaml blob as json:
449
/etc/glance/glance-api.conf:
453
/etc/glance/glance-registry.conf:
463
It is then up to the principle charms to subscribe this context to
464
the service+config file it is interestd in. Configuration data will
465
be available in the template context, in glance's case, as:
467
... other context ...
468
'subordinate_config': {
479
def __init__(self, service, config_file, interface):
481
:param service : Service name key to query in any subordinate
483
:param config_file : Service's config file to query sections
484
:param interface : Subordinate interface to inspect
486
self.service = service
487
self.config_file = config_file
488
self.interface = interface
492
for rid in relation_ids(self.interface):
493
for unit in related_units(rid):
494
sub_config = relation_get('subordinate_configuration',
496
if sub_config and sub_config != '':
498
sub_config = json.loads(sub_config)
500
log('Could not parse JSON from subordinate_config '
501
'setting from %s' % rid, level=ERROR)
504
if self.service not in sub_config:
505
log('Found subordinate_config on %s but it contained'
506
'nothing for %s service' % (rid, self.service))
509
sub_config = sub_config[self.service]
510
if self.config_file not in sub_config:
511
log('Found subordinate_config on %s but it contained'
512
'nothing for %s' % (rid, self.config_file))
515
sub_config = sub_config[self.config_file]
516
for k, v in sub_config.iteritems():
520
ctxt['sections'] = {}