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)
152
if context_complete(ctxt):
153
db_ssl(rdata, ctxt, self.ssl_dir)
158
def db_ssl(rdata, ctxt, ssl_dir):
159
if 'ssl_ca' in rdata and ssl_dir:
160
ca_path = os.path.join(ssl_dir, 'db-client.ca')
161
with open(ca_path, 'w') as fh:
162
fh.write(b64decode(rdata['ssl_ca']))
163
ctxt['database_ssl_ca'] = ca_path
164
elif 'ssl_ca' in rdata:
165
log("Charm not setup for ssl support but ssl ca found")
167
if 'ssl_cert' in rdata:
168
cert_path = os.path.join(
169
ssl_dir, 'db-client.cert')
170
if not os.path.exists(cert_path):
171
log("Waiting 1m for ssl client cert validity")
173
with open(cert_path, 'w') as fh:
174
fh.write(b64decode(rdata['ssl_cert']))
175
ctxt['database_ssl_cert'] = cert_path
176
key_path = os.path.join(ssl_dir, 'db-client.key')
177
with open(key_path, 'w') as fh:
178
fh.write(b64decode(rdata['ssl_key']))
179
ctxt['database_ssl_key'] = key_path
183
class IdentityServiceContext(OSContextGenerator):
184
interfaces = ['identity-service']
187
log('Generating template context for identity-service')
190
for rid in relation_ids('identity-service'):
191
for unit in related_units(rid):
192
rdata = relation_get(rid=rid, unit=unit)
194
'service_port': rdata.get('service_port'),
195
'service_host': rdata.get('service_host'),
196
'auth_host': rdata.get('auth_host'),
197
'auth_port': rdata.get('auth_port'),
198
'admin_tenant_name': rdata.get('service_tenant'),
199
'admin_user': rdata.get('service_username'),
200
'admin_password': rdata.get('service_password'),
202
rdata.get('service_protocol') or 'http',
204
rdata.get('auth_protocol') or 'http',
206
if context_complete(ctxt):
211
class AMQPContext(OSContextGenerator):
212
interfaces = ['amqp']
214
def __init__(self, ssl_dir=None):
215
self.ssl_dir = ssl_dir
218
log('Generating template context for amqp')
221
username = conf['rabbit-user']
222
vhost = conf['rabbit-vhost']
223
except KeyError as e:
224
log('Could not generate shared_db context. '
225
'Missing required charm config options: %s.' % e)
228
for rid in relation_ids('amqp'):
230
for unit in related_units(rid):
231
if relation_get('clustered', rid=rid, unit=unit):
232
ctxt['clustered'] = True
233
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
236
ctxt['rabbitmq_host'] = relation_get('private-address',
239
'rabbitmq_user': username,
240
'rabbitmq_password': relation_get('password', rid=rid,
242
'rabbitmq_virtual_host': vhost,
245
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
247
ctxt['rabbit_ssl_port'] = ssl_port
248
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
250
ctxt['rabbit_ssl_ca'] = ssl_ca
252
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
253
ctxt['rabbitmq_ha_queues'] = True
255
ha_vip_only = relation_get('ha-vip-only',
256
rid=rid, unit=unit) is not None
258
if context_complete(ctxt):
259
if 'rabbit_ssl_ca' in ctxt:
261
log(("Charm not setup for ssl support "
264
ca_path = os.path.join(
265
self.ssl_dir, 'rabbit-client-ca.pem')
266
with open(ca_path, 'w') as fh:
267
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
268
ctxt['rabbit_ssl_ca'] = ca_path
269
# Sufficient information found = break out!
271
# Used for active/active rabbitmq >= grizzly
272
if ('clustered' not in ctxt or ha_vip_only) \
273
and len(related_units(rid)) > 1:
275
for unit in related_units(rid):
276
rabbitmq_hosts.append(relation_get('private-address',
278
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
279
if not context_complete(ctxt):
285
class CephContext(OSContextGenerator):
286
interfaces = ['ceph']
289
'''This generates context for /etc/ceph/ceph.conf templates'''
290
if not relation_ids('ceph'):
293
log('Generating template context for ceph')
298
use_syslog = str(config('use-syslog')).lower()
299
for rid in relation_ids('ceph'):
300
for unit in related_units(rid):
301
mon_hosts.append(relation_get('private-address', rid=rid,
303
auth = relation_get('auth', rid=rid, unit=unit)
304
key = relation_get('key', rid=rid, unit=unit)
307
'mon_hosts': ' '.join(mon_hosts),
310
'use_syslog': use_syslog
313
if not os.path.isdir('/etc/ceph'):
314
os.mkdir('/etc/ceph')
316
if not context_complete(ctxt):
319
ensure_packages(['ceph-common'])
324
class HAProxyContext(OSContextGenerator):
325
interfaces = ['cluster']
329
Builds half a context for the haproxy template, which describes
330
all peers to be included in the cluster. Each charm needs to include
331
its own context generator that describes the port mapping.
333
if not relation_ids('cluster'):
337
l_unit = local_unit().replace('/', '-')
338
cluster_hosts[l_unit] = unit_get('private-address')
340
for rid in relation_ids('cluster'):
341
for unit in related_units(rid):
342
_unit = unit.replace('/', '-')
343
addr = relation_get('private-address', rid=rid, unit=unit)
344
cluster_hosts[_unit] = addr
347
'units': cluster_hosts,
349
if len(cluster_hosts.keys()) > 1:
350
# Enable haproxy when we have enough peers.
351
log('Ensuring haproxy enabled in /etc/default/haproxy.')
352
with open('/etc/default/haproxy', 'w') as out:
353
out.write('ENABLED=1\n')
355
log('HAProxy context is incomplete, this unit has no peers.')
359
class ImageServiceContext(OSContextGenerator):
360
interfaces = ['image-service']
364
Obtains the glance API server from the image-service relation. Useful
365
in nova and cinder (currently).
367
log('Generating template context for image-service.')
368
rids = relation_ids('image-service')
372
for unit in related_units(rid):
373
api_server = relation_get('glance-api-server',
376
return {'glance_api_servers': api_server}
377
log('ImageService context is incomplete. '
378
'Missing required relation data.')
382
class ApacheSSLContext(OSContextGenerator):
385
Generates a context for an apache vhost configuration that configures
386
HTTPS reverse proxying for one or many endpoints. Generated context
387
looks something like:
389
'namespace': 'cinder',
390
'private_address': 'iscsi.mycinderhost.com',
391
'endpoints': [(8776, 8766), (8777, 8767)]
394
The endpoints list consists of a tuples mapping external ports
397
interfaces = ['https']
399
# charms should inherit this context and set external ports
400
# and service namespace accordingly.
402
service_namespace = None
404
def enable_modules(self):
405
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
408
def configure_cert(self):
409
if not os.path.isdir('/etc/apache2/ssl'):
410
os.mkdir('/etc/apache2/ssl')
411
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
412
if not os.path.isdir(ssl_dir):
414
cert, key = get_cert()
415
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
416
cert_out.write(b64decode(cert))
417
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
418
key_out.write(b64decode(key))
419
ca_cert = get_ca_cert()
421
with open(CA_CERT_PATH, 'w') as ca_out:
422
ca_out.write(b64decode(ca_cert))
423
check_call(['update-ca-certificates'])
426
if isinstance(self.external_ports, basestring):
427
self.external_ports = [self.external_ports]
428
if (not self.external_ports or not https()):
431
self.configure_cert()
432
self.enable_modules()
435
'namespace': self.service_namespace,
436
'private_address': unit_get('private-address'),
440
ctxt['private_address'] = config('vip')
441
for api_port in self.external_ports:
442
ext_port = determine_apache_port(api_port)
443
int_port = determine_api_port(api_port)
444
portmap = (int(ext_port), int(int_port))
445
ctxt['endpoints'].append(portmap)
449
class NeutronContext(OSContextGenerator):
457
def network_manager(self):
462
return neutron_plugin_attribute(
463
self.plugin, 'packages', self.network_manager)
466
def neutron_security_groups(self):
469
def _ensure_packages(self):
470
[ensure_packages(pkgs) for pkgs in self.packages]
472
def _save_flag_file(self):
473
if self.network_manager == 'quantum':
474
_file = '/etc/nova/quantum_plugin.conf'
476
_file = '/etc/nova/neutron_plugin.conf'
477
with open(_file, 'wb') as out:
478
out.write(self.plugin + '\n')
481
driver = neutron_plugin_attribute(self.plugin, 'driver',
482
self.network_manager)
483
config = neutron_plugin_attribute(self.plugin, 'config',
484
self.network_manager)
486
'core_plugin': driver,
487
'neutron_plugin': 'ovs',
488
'neutron_security_groups': self.neutron_security_groups,
489
'local_ip': unit_private_ip(),
496
driver = neutron_plugin_attribute(self.plugin, 'driver',
497
self.network_manager)
498
config = neutron_plugin_attribute(self.plugin, 'config',
499
self.network_manager)
501
'core_plugin': driver,
502
'neutron_plugin': 'nvp',
503
'neutron_security_groups': self.neutron_security_groups,
504
'local_ip': unit_private_ip(),
510
def neutron_ctxt(self):
518
host = unit_get('private-address')
519
url = '%s://%s:%s' % (proto, host, '9696')
521
'network_manager': self.network_manager,
527
self._ensure_packages()
529
if self.network_manager not in ['quantum', 'neutron']:
535
ctxt = self.neutron_ctxt()
537
if self.plugin == 'ovs':
538
ctxt.update(self.ovs_ctxt())
539
elif self.plugin == 'nvp':
540
ctxt.update(self.nvp_ctxt())
542
alchemy_flags = config('neutron-alchemy-flags')
544
flags = config_flags_parser(alchemy_flags)
545
ctxt['neutron_alchemy_flags'] = flags
547
self._save_flag_file()
551
class OSConfigFlagContext(OSContextGenerator):
554
Responsible for adding user-defined config-flags in charm config to a
557
NOTE: the value of config-flags may be a comma-separated list of
558
key=value pairs and some Openstack config files support
559
comma-separated lists as values.
563
config_flags = config('config-flags')
567
flags = config_flags_parser(config_flags)
568
return {'user_config_flags': flags}
571
class SubordinateConfigContext(OSContextGenerator):
574
Responsible for inspecting relations to subordinates that
575
may be exporting required config via a json blob.
577
The subordinate interface allows subordinates to export their
578
configuration requirements to the principle for multiple config
579
files and multiple serivces. Ie, a subordinate that has interfaces
580
to both glance and nova may export to following yaml blob as json:
583
/etc/glance/glance-api.conf:
587
/etc/glance/glance-registry.conf:
597
It is then up to the principle charms to subscribe this context to
598
the service+config file it is interestd in. Configuration data will
599
be available in the template context, in glance's case, as:
601
... other context ...
602
'subordinate_config': {
614
def __init__(self, service, config_file, interface):
616
:param service : Service name key to query in any subordinate
618
:param config_file : Service's config file to query sections
619
:param interface : Subordinate interface to inspect
621
self.service = service
622
self.config_file = config_file
623
self.interface = interface
627
for rid in relation_ids(self.interface):
628
for unit in related_units(rid):
629
sub_config = relation_get('subordinate_configuration',
631
if sub_config and sub_config != '':
633
sub_config = json.loads(sub_config)
635
log('Could not parse JSON from subordinate_config '
636
'setting from %s' % rid, level=ERROR)
639
if self.service not in sub_config:
640
log('Found subordinate_config on %s but it contained'
641
'nothing for %s service' % (rid, self.service))
644
sub_config = sub_config[self.service]
645
if self.config_file not in sub_config:
646
log('Found subordinate_config on %s but it contained'
647
'nothing for %s' % (rid, self.config_file))
650
sub_config = sub_config[self.config_file]
651
for k, v in sub_config.iteritems():
655
ctxt['sections'] = {}
660
class SyslogContext(OSContextGenerator):
664
'use_syslog': config('use-syslog')