5
from base64 import b64decode
7
from subprocess import (
12
from charmhelpers.fetch import (
14
filter_installed_packages,
17
from charmhelpers.core.hookenv import (
29
from charmhelpers.contrib.hahelpers.cluster import (
30
determine_apache_port,
36
from charmhelpers.contrib.hahelpers.apache import (
41
from charmhelpers.contrib.openstack.neutron import (
42
neutron_plugin_attribute,
45
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
48
class OSContextError(Exception):
52
def ensure_packages(packages):
53
'''Install but do not upgrade required plugin packages'''
54
required = filter_installed_packages(packages)
56
apt_install(required, fatal=True)
59
def context_complete(ctxt):
61
for k, v in ctxt.iteritems():
62
if v is None or v == '':
65
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
70
def config_flags_parser(config_flags):
71
if config_flags.find('==') >= 0:
72
log("config_flags is not in expected format (key=value)",
75
# strip the following from each value.
77
# we strip any leading/trailing '=' or ' ' from the string then
79
split = config_flags.strip(' =').split('=')
82
for i in xrange(0, limit - 1):
85
vindex = next.rfind(',')
86
if (i == limit - 2) or (vindex < 0):
94
# if this not the first entry, expect an embedded key.
95
index = current.rfind(',')
97
log("invalid config value(s) at index %s" % (i),
100
key = current[index + 1:]
103
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
107
class OSContextGenerator(object):
111
raise NotImplementedError
114
class SharedDBContext(OSContextGenerator):
115
interfaces = ['shared-db']
118
database=None, user=None, relation_prefix=None, ssl_dir=None):
120
Allows inspecting relation for settings prefixed with relation_prefix.
121
This is useful for parsing access for multiple databases returned via
122
the shared-db interface (eg, nova_password, quantum_password)
124
self.relation_prefix = relation_prefix
125
self.database = database
127
self.ssl_dir = ssl_dir
130
self.database = self.database or config('database')
131
self.user = self.user or config('database-user')
132
if None in [self.database, self.user]:
133
log('Could not generate shared_db context. '
134
'Missing required charm config options. '
135
'(database name and user)')
139
password_setting = 'password'
140
if self.relation_prefix:
141
password_setting = self.relation_prefix + '_password'
143
for rid in relation_ids('shared-db'):
144
for unit in related_units(rid):
145
rdata = relation_get(rid=rid, unit=unit)
147
'database_host': rdata.get('db_host'),
148
'database': self.database,
149
'database_user': self.user,
150
'database_password': rdata.get(password_setting),
151
'database_type': 'mysql'
153
if context_complete(ctxt):
154
db_ssl(rdata, ctxt, self.ssl_dir)
159
class PostgresqlDBContext(OSContextGenerator):
160
interfaces = ['pgsql-db']
162
def __init__(self, database=None):
163
self.database = database
166
self.database = self.database or config('database')
167
if self.database is None:
168
log('Could not generate postgresql_db context. '
169
'Missing required charm config options. '
174
for rid in relation_ids(self.interfaces[0]):
175
for unit in related_units(rid):
177
'database_host': relation_get('host', rid=rid, unit=unit),
178
'database': self.database,
179
'database_user': relation_get('user', rid=rid, unit=unit),
180
'database_password': relation_get('password', rid=rid, unit=unit),
181
'database_type': 'postgresql',
183
if context_complete(ctxt):
188
def db_ssl(rdata, ctxt, ssl_dir):
189
if 'ssl_ca' in rdata and ssl_dir:
190
ca_path = os.path.join(ssl_dir, 'db-client.ca')
191
with open(ca_path, 'w') as fh:
192
fh.write(b64decode(rdata['ssl_ca']))
193
ctxt['database_ssl_ca'] = ca_path
194
elif 'ssl_ca' in rdata:
195
log("Charm not setup for ssl support but ssl ca found")
197
if 'ssl_cert' in rdata:
198
cert_path = os.path.join(
199
ssl_dir, 'db-client.cert')
200
if not os.path.exists(cert_path):
201
log("Waiting 1m for ssl client cert validity")
203
with open(cert_path, 'w') as fh:
204
fh.write(b64decode(rdata['ssl_cert']))
205
ctxt['database_ssl_cert'] = cert_path
206
key_path = os.path.join(ssl_dir, 'db-client.key')
207
with open(key_path, 'w') as fh:
208
fh.write(b64decode(rdata['ssl_key']))
209
ctxt['database_ssl_key'] = key_path
213
class IdentityServiceContext(OSContextGenerator):
214
interfaces = ['identity-service']
217
log('Generating template context for identity-service')
220
for rid in relation_ids('identity-service'):
221
for unit in related_units(rid):
222
rdata = relation_get(rid=rid, unit=unit)
224
'service_port': rdata.get('service_port'),
225
'service_host': rdata.get('service_host'),
226
'auth_host': rdata.get('auth_host'),
227
'auth_port': rdata.get('auth_port'),
228
'admin_tenant_name': rdata.get('service_tenant'),
229
'admin_user': rdata.get('service_username'),
230
'admin_password': rdata.get('service_password'),
232
rdata.get('service_protocol') or 'http',
234
rdata.get('auth_protocol') or 'http',
236
if context_complete(ctxt):
237
# NOTE(jamespage) this is required for >= icehouse
238
# so a missing value just indicates keystone needs
240
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
245
class AMQPContext(OSContextGenerator):
246
interfaces = ['amqp']
248
def __init__(self, ssl_dir=None):
249
self.ssl_dir = ssl_dir
252
log('Generating template context for amqp')
255
username = conf['rabbit-user']
256
vhost = conf['rabbit-vhost']
257
except KeyError as e:
258
log('Could not generate shared_db context. '
259
'Missing required charm config options: %s.' % e)
262
for rid in relation_ids('amqp'):
264
for unit in related_units(rid):
265
if relation_get('clustered', rid=rid, unit=unit):
266
ctxt['clustered'] = True
267
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
270
ctxt['rabbitmq_host'] = relation_get('private-address',
273
'rabbitmq_user': username,
274
'rabbitmq_password': relation_get('password', rid=rid,
276
'rabbitmq_virtual_host': vhost,
279
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
281
ctxt['rabbit_ssl_port'] = ssl_port
282
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
284
ctxt['rabbit_ssl_ca'] = ssl_ca
286
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
287
ctxt['rabbitmq_ha_queues'] = True
289
ha_vip_only = relation_get('ha-vip-only',
290
rid=rid, unit=unit) is not None
292
if context_complete(ctxt):
293
if 'rabbit_ssl_ca' in ctxt:
295
log(("Charm not setup for ssl support "
298
ca_path = os.path.join(
299
self.ssl_dir, 'rabbit-client-ca.pem')
300
with open(ca_path, 'w') as fh:
301
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
302
ctxt['rabbit_ssl_ca'] = ca_path
303
# Sufficient information found = break out!
305
# Used for active/active rabbitmq >= grizzly
306
if ('clustered' not in ctxt or ha_vip_only) \
307
and len(related_units(rid)) > 1:
309
for unit in related_units(rid):
310
rabbitmq_hosts.append(relation_get('private-address',
312
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
313
if not context_complete(ctxt):
319
class CephContext(OSContextGenerator):
320
interfaces = ['ceph']
323
'''This generates context for /etc/ceph/ceph.conf templates'''
324
if not relation_ids('ceph'):
327
log('Generating template context for ceph')
332
use_syslog = str(config('use-syslog')).lower()
333
for rid in relation_ids('ceph'):
334
for unit in related_units(rid):
335
mon_hosts.append(relation_get('private-address', rid=rid,
337
auth = relation_get('auth', rid=rid, unit=unit)
338
key = relation_get('key', rid=rid, unit=unit)
341
'mon_hosts': ' '.join(mon_hosts),
344
'use_syslog': use_syslog
347
if not os.path.isdir('/etc/ceph'):
348
os.mkdir('/etc/ceph')
350
if not context_complete(ctxt):
353
ensure_packages(['ceph-common'])
358
class HAProxyContext(OSContextGenerator):
359
interfaces = ['cluster']
363
Builds half a context for the haproxy template, which describes
364
all peers to be included in the cluster. Each charm needs to include
365
its own context generator that describes the port mapping.
367
if not relation_ids('cluster'):
371
l_unit = local_unit().replace('/', '-')
372
cluster_hosts[l_unit] = unit_get('private-address')
374
for rid in relation_ids('cluster'):
375
for unit in related_units(rid):
376
_unit = unit.replace('/', '-')
377
addr = relation_get('private-address', rid=rid, unit=unit)
378
cluster_hosts[_unit] = addr
381
'units': cluster_hosts,
383
if len(cluster_hosts.keys()) > 1:
384
# Enable haproxy when we have enough peers.
385
log('Ensuring haproxy enabled in /etc/default/haproxy.')
386
with open('/etc/default/haproxy', 'w') as out:
387
out.write('ENABLED=1\n')
389
log('HAProxy context is incomplete, this unit has no peers.')
393
class ImageServiceContext(OSContextGenerator):
394
interfaces = ['image-service']
398
Obtains the glance API server from the image-service relation. Useful
399
in nova and cinder (currently).
401
log('Generating template context for image-service.')
402
rids = relation_ids('image-service')
406
for unit in related_units(rid):
407
api_server = relation_get('glance-api-server',
410
return {'glance_api_servers': api_server}
411
log('ImageService context is incomplete. '
412
'Missing required relation data.')
416
class ApacheSSLContext(OSContextGenerator):
419
Generates a context for an apache vhost configuration that configures
420
HTTPS reverse proxying for one or many endpoints. Generated context
421
looks something like:
423
'namespace': 'cinder',
424
'private_address': 'iscsi.mycinderhost.com',
425
'endpoints': [(8776, 8766), (8777, 8767)]
428
The endpoints list consists of a tuples mapping external ports
431
interfaces = ['https']
433
# charms should inherit this context and set external ports
434
# and service namespace accordingly.
436
service_namespace = None
438
def enable_modules(self):
439
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
442
def configure_cert(self):
443
if not os.path.isdir('/etc/apache2/ssl'):
444
os.mkdir('/etc/apache2/ssl')
445
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
446
if not os.path.isdir(ssl_dir):
448
cert, key = get_cert()
449
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
450
cert_out.write(b64decode(cert))
451
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
452
key_out.write(b64decode(key))
453
ca_cert = get_ca_cert()
455
with open(CA_CERT_PATH, 'w') as ca_out:
456
ca_out.write(b64decode(ca_cert))
457
check_call(['update-ca-certificates'])
460
if isinstance(self.external_ports, basestring):
461
self.external_ports = [self.external_ports]
462
if (not self.external_ports or not https()):
465
self.configure_cert()
466
self.enable_modules()
469
'namespace': self.service_namespace,
470
'private_address': unit_get('private-address'),
474
ctxt['private_address'] = config('vip')
475
for api_port in self.external_ports:
476
ext_port = determine_apache_port(api_port)
477
int_port = determine_api_port(api_port)
478
portmap = (int(ext_port), int(int_port))
479
ctxt['endpoints'].append(portmap)
483
class NeutronContext(OSContextGenerator):
491
def network_manager(self):
496
return neutron_plugin_attribute(
497
self.plugin, 'packages', self.network_manager)
500
def neutron_security_groups(self):
503
def _ensure_packages(self):
504
[ensure_packages(pkgs) for pkgs in self.packages]
506
def _save_flag_file(self):
507
if self.network_manager == 'quantum':
508
_file = '/etc/nova/quantum_plugin.conf'
510
_file = '/etc/nova/neutron_plugin.conf'
511
with open(_file, 'wb') as out:
512
out.write(self.plugin + '\n')
515
driver = neutron_plugin_attribute(self.plugin, 'driver',
516
self.network_manager)
517
config = neutron_plugin_attribute(self.plugin, 'config',
518
self.network_manager)
520
'core_plugin': driver,
521
'neutron_plugin': 'ovs',
522
'neutron_security_groups': self.neutron_security_groups,
523
'local_ip': unit_private_ip(),
530
driver = neutron_plugin_attribute(self.plugin, 'driver',
531
self.network_manager)
532
config = neutron_plugin_attribute(self.plugin, 'config',
533
self.network_manager)
535
'core_plugin': driver,
536
'neutron_plugin': 'nvp',
537
'neutron_security_groups': self.neutron_security_groups,
538
'local_ip': unit_private_ip(),
544
def neutron_ctxt(self):
552
host = unit_get('private-address')
553
url = '%s://%s:%s' % (proto, host, '9696')
555
'network_manager': self.network_manager,
561
self._ensure_packages()
563
if self.network_manager not in ['quantum', 'neutron']:
569
ctxt = self.neutron_ctxt()
571
if self.plugin == 'ovs':
572
ctxt.update(self.ovs_ctxt())
573
elif self.plugin == 'nvp':
574
ctxt.update(self.nvp_ctxt())
576
alchemy_flags = config('neutron-alchemy-flags')
578
flags = config_flags_parser(alchemy_flags)
579
ctxt['neutron_alchemy_flags'] = flags
581
self._save_flag_file()
585
class OSConfigFlagContext(OSContextGenerator):
588
Responsible for adding user-defined config-flags in charm config to a
591
NOTE: the value of config-flags may be a comma-separated list of
592
key=value pairs and some Openstack config files support
593
comma-separated lists as values.
597
config_flags = config('config-flags')
601
flags = config_flags_parser(config_flags)
602
return {'user_config_flags': flags}
605
class SubordinateConfigContext(OSContextGenerator):
608
Responsible for inspecting relations to subordinates that
609
may be exporting required config via a json blob.
611
The subordinate interface allows subordinates to export their
612
configuration requirements to the principle for multiple config
613
files and multiple serivces. Ie, a subordinate that has interfaces
614
to both glance and nova may export to following yaml blob as json:
617
/etc/glance/glance-api.conf:
621
/etc/glance/glance-registry.conf:
631
It is then up to the principle charms to subscribe this context to
632
the service+config file it is interestd in. Configuration data will
633
be available in the template context, in glance's case, as:
635
... other context ...
636
'subordinate_config': {
648
def __init__(self, service, config_file, interface):
650
:param service : Service name key to query in any subordinate
652
:param config_file : Service's config file to query sections
653
:param interface : Subordinate interface to inspect
655
self.service = service
656
self.config_file = config_file
657
self.interface = interface
661
for rid in relation_ids(self.interface):
662
for unit in related_units(rid):
663
sub_config = relation_get('subordinate_configuration',
665
if sub_config and sub_config != '':
667
sub_config = json.loads(sub_config)
669
log('Could not parse JSON from subordinate_config '
670
'setting from %s' % rid, level=ERROR)
673
if self.service not in sub_config:
674
log('Found subordinate_config on %s but it contained'
675
'nothing for %s service' % (rid, self.service))
678
sub_config = sub_config[self.service]
679
if self.config_file not in sub_config:
680
log('Found subordinate_config on %s but it contained'
681
'nothing for %s' % (rid, self.config_file))
684
sub_config = sub_config[self.config_file]
685
for k, v in sub_config.iteritems():
689
ctxt['sections'] = {}
694
class SyslogContext(OSContextGenerator):
698
'use_syslog': config('use-syslog')