4
from base64 import b64decode
6
from subprocess import (
11
from charmhelpers.fetch import (
13
filter_installed_packages,
16
from charmhelpers.core.hookenv import (
28
from charmhelpers.contrib.hahelpers.cluster import (
29
determine_apache_port,
35
from charmhelpers.contrib.hahelpers.apache import (
40
from charmhelpers.contrib.openstack.neutron import (
41
neutron_plugin_attribute,
44
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
47
class OSContextError(Exception):
51
def ensure_packages(packages):
52
'''Install but do not upgrade required plugin packages'''
53
required = filter_installed_packages(packages)
55
apt_install(required, fatal=True)
58
def context_complete(ctxt):
60
for k, v in ctxt.iteritems():
61
if v is None or v == '':
64
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
69
def config_flags_parser(config_flags):
70
if config_flags.find('==') >= 0:
71
log("config_flags is not in expected format (key=value)",
74
# strip the following from each value.
76
# we strip any leading/trailing '=' or ' ' from the string then
78
split = config_flags.strip(' =').split('=')
81
for i in xrange(0, limit - 1):
84
vindex = next.rfind(',')
85
if (i == limit - 2) or (vindex < 0):
93
# if this not the first entry, expect an embedded key.
94
index = current.rfind(',')
96
log("invalid config value(s) at index %s" % (i),
99
key = current[index + 1:]
102
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
106
class OSContextGenerator(object):
110
raise NotImplementedError
113
class SharedDBContext(OSContextGenerator):
114
interfaces = ['shared-db']
116
def __init__(self, database=None, user=None, relation_prefix=None):
118
Allows inspecting relation for settings prefixed with relation_prefix.
119
This is useful for parsing access for multiple databases returned via
120
the shared-db interface (eg, nova_password, quantum_password)
122
self.relation_prefix = relation_prefix
123
self.database = database
127
self.database = self.database or config('database')
128
self.user = self.user or config('database-user')
129
if None in [self.database, self.user]:
130
log('Could not generate shared_db context. '
131
'Missing required charm config options. '
132
'(database name and user)')
136
password_setting = 'password'
137
if self.relation_prefix:
138
password_setting = self.relation_prefix + '_password'
140
for rid in relation_ids('shared-db'):
141
for unit in related_units(rid):
142
passwd = relation_get(password_setting, rid=rid, unit=unit)
144
'database_host': relation_get('db_host', rid=rid,
146
'database': self.database,
147
'database_user': self.user,
148
'database_password': passwd,
150
if context_complete(ctxt):
155
class IdentityServiceContext(OSContextGenerator):
156
interfaces = ['identity-service']
159
log('Generating template context for identity-service')
162
for rid in relation_ids('identity-service'):
163
for unit in related_units(rid):
165
'service_port': relation_get('service_port', rid=rid,
167
'service_host': relation_get('service_host', rid=rid,
169
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
170
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
171
'admin_tenant_name': relation_get('service_tenant',
173
'admin_user': relation_get('service_username', rid=rid,
175
'admin_password': relation_get('service_password', rid=rid,
177
# XXX: Hard-coded http.
178
'service_protocol': 'http',
179
'auth_protocol': 'http',
181
if context_complete(ctxt):
186
class AMQPContext(OSContextGenerator):
187
interfaces = ['amqp']
190
log('Generating template context for amqp')
193
username = conf['rabbit-user']
194
vhost = conf['rabbit-vhost']
195
except KeyError as e:
196
log('Could not generate shared_db context. '
197
'Missing required charm config options: %s.' % e)
201
for rid in relation_ids('amqp'):
203
for unit in related_units(rid):
204
if relation_get('clustered', rid=rid, unit=unit):
205
ctxt['clustered'] = True
206
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
209
ctxt['rabbitmq_host'] = relation_get('private-address',
212
'rabbitmq_user': username,
213
'rabbitmq_password': relation_get('password', rid=rid,
215
'rabbitmq_virtual_host': vhost,
217
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
218
ctxt['rabbitmq_ha_queues'] = True
220
ha_vip_only = relation_get('ha-vip-only',
221
rid=rid, unit=unit) is not None
223
if context_complete(ctxt):
224
# Sufficient information found = break out!
226
# Used for active/active rabbitmq >= grizzly
227
if ('clustered' not in ctxt or ha_vip_only) \
228
and len(related_units(rid)) > 1:
230
for unit in related_units(rid):
231
rabbitmq_hosts.append(relation_get('private-address',
233
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
234
if not context_complete(ctxt):
240
class CephContext(OSContextGenerator):
241
interfaces = ['ceph']
244
'''This generates context for /etc/ceph/ceph.conf templates'''
245
if not relation_ids('ceph'):
248
log('Generating template context for ceph')
253
use_syslog = str(config('use-syslog')).lower()
254
for rid in relation_ids('ceph'):
255
for unit in related_units(rid):
256
mon_hosts.append(relation_get('private-address', rid=rid,
258
auth = relation_get('auth', rid=rid, unit=unit)
259
key = relation_get('key', rid=rid, unit=unit)
262
'mon_hosts': ' '.join(mon_hosts),
265
'use_syslog': use_syslog
268
if not os.path.isdir('/etc/ceph'):
269
os.mkdir('/etc/ceph')
271
if not context_complete(ctxt):
274
ensure_packages(['ceph-common'])
279
class HAProxyContext(OSContextGenerator):
280
interfaces = ['cluster']
284
Builds half a context for the haproxy template, which describes
285
all peers to be included in the cluster. Each charm needs to include
286
its own context generator that describes the port mapping.
288
if not relation_ids('cluster'):
292
l_unit = local_unit().replace('/', '-')
293
cluster_hosts[l_unit] = unit_get('private-address')
295
for rid in relation_ids('cluster'):
296
for unit in related_units(rid):
297
_unit = unit.replace('/', '-')
298
addr = relation_get('private-address', rid=rid, unit=unit)
299
cluster_hosts[_unit] = addr
302
'units': cluster_hosts,
304
if len(cluster_hosts.keys()) > 1:
305
# Enable haproxy when we have enough peers.
306
log('Ensuring haproxy enabled in /etc/default/haproxy.')
307
with open('/etc/default/haproxy', 'w') as out:
308
out.write('ENABLED=1\n')
310
log('HAProxy context is incomplete, this unit has no peers.')
314
class ImageServiceContext(OSContextGenerator):
315
interfaces = ['image-service']
319
Obtains the glance API server from the image-service relation. Useful
320
in nova and cinder (currently).
322
log('Generating template context for image-service.')
323
rids = relation_ids('image-service')
327
for unit in related_units(rid):
328
api_server = relation_get('glance-api-server',
331
return {'glance_api_servers': api_server}
332
log('ImageService context is incomplete. '
333
'Missing required relation data.')
337
class ApacheSSLContext(OSContextGenerator):
340
Generates a context for an apache vhost configuration that configures
341
HTTPS reverse proxying for one or many endpoints. Generated context
342
looks something like:
344
'namespace': 'cinder',
345
'private_address': 'iscsi.mycinderhost.com',
346
'endpoints': [(8776, 8766), (8777, 8767)]
349
The endpoints list consists of a tuples mapping external ports
352
interfaces = ['https']
354
# charms should inherit this context and set external ports
355
# and service namespace accordingly.
357
service_namespace = None
359
def enable_modules(self):
360
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
363
def configure_cert(self):
364
if not os.path.isdir('/etc/apache2/ssl'):
365
os.mkdir('/etc/apache2/ssl')
366
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
367
if not os.path.isdir(ssl_dir):
369
cert, key = get_cert()
370
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
371
cert_out.write(b64decode(cert))
372
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
373
key_out.write(b64decode(key))
374
ca_cert = get_ca_cert()
376
with open(CA_CERT_PATH, 'w') as ca_out:
377
ca_out.write(b64decode(ca_cert))
378
check_call(['update-ca-certificates'])
381
if isinstance(self.external_ports, basestring):
382
self.external_ports = [self.external_ports]
383
if (not self.external_ports or not https()):
386
self.configure_cert()
387
self.enable_modules()
390
'namespace': self.service_namespace,
391
'private_address': unit_get('private-address'),
394
for api_port in self.external_ports:
395
ext_port = determine_apache_port(api_port)
396
int_port = determine_api_port(api_port)
397
portmap = (int(ext_port), int(int_port))
398
ctxt['endpoints'].append(portmap)
402
class NeutronContext(OSContextGenerator):
410
def network_manager(self):
415
return neutron_plugin_attribute(
416
self.plugin, 'packages', self.network_manager)
419
def neutron_security_groups(self):
422
def _ensure_packages(self):
423
[ensure_packages(pkgs) for pkgs in self.packages]
425
def _save_flag_file(self):
426
if self.network_manager == 'quantum':
427
_file = '/etc/nova/quantum_plugin.conf'
429
_file = '/etc/nova/neutron_plugin.conf'
430
with open(_file, 'wb') as out:
431
out.write(self.plugin + '\n')
434
driver = neutron_plugin_attribute(self.plugin, 'driver',
435
self.network_manager)
436
config = neutron_plugin_attribute(self.plugin, 'config',
437
self.network_manager)
439
'core_plugin': driver,
440
'neutron_plugin': 'ovs',
441
'neutron_security_groups': self.neutron_security_groups,
442
'local_ip': unit_private_ip(),
449
driver = neutron_plugin_attribute(self.plugin, 'driver',
450
self.network_manager)
451
config = neutron_plugin_attribute(self.plugin, 'config',
452
self.network_manager)
454
'core_plugin': driver,
455
'neutron_plugin': 'nvp',
456
'neutron_security_groups': self.neutron_security_groups,
457
'local_ip': unit_private_ip(),
463
def neutron_ctxt(self):
471
host = unit_get('private-address')
472
url = '%s://%s:%s' % (proto, host, '9696')
474
'network_manager': self.network_manager,
480
self._ensure_packages()
482
if self.network_manager not in ['quantum', 'neutron']:
488
ctxt = self.neutron_ctxt()
490
if self.plugin == 'ovs':
491
ctxt.update(self.ovs_ctxt())
492
elif self.plugin == 'nvp':
493
ctxt.update(self.nvp_ctxt())
495
alchemy_flags = config('neutron-alchemy-flags')
497
flags = config_flags_parser(alchemy_flags)
498
ctxt['neutron_alchemy_flags'] = flags
500
self._save_flag_file()
504
class OSConfigFlagContext(OSContextGenerator):
507
Responsible for adding user-defined config-flags in charm config to a
510
NOTE: the value of config-flags may be a comma-separated list of
511
key=value pairs and some Openstack config files support
512
comma-separated lists as values.
516
config_flags = config('config-flags')
520
flags = config_flags_parser(config_flags)
521
return {'user_config_flags': flags}
524
class SubordinateConfigContext(OSContextGenerator):
527
Responsible for inspecting relations to subordinates that
528
may be exporting required config via a json blob.
530
The subordinate interface allows subordinates to export their
531
configuration requirements to the principle for multiple config
532
files and multiple serivces. Ie, a subordinate that has interfaces
533
to both glance and nova may export to following yaml blob as json:
536
/etc/glance/glance-api.conf:
540
/etc/glance/glance-registry.conf:
550
It is then up to the principle charms to subscribe this context to
551
the service+config file it is interestd in. Configuration data will
552
be available in the template context, in glance's case, as:
554
... other context ...
555
'subordinate_config': {
567
def __init__(self, service, config_file, interface):
569
:param service : Service name key to query in any subordinate
571
:param config_file : Service's config file to query sections
572
:param interface : Subordinate interface to inspect
574
self.service = service
575
self.config_file = config_file
576
self.interface = interface
580
for rid in relation_ids(self.interface):
581
for unit in related_units(rid):
582
sub_config = relation_get('subordinate_configuration',
584
if sub_config and sub_config != '':
586
sub_config = json.loads(sub_config)
588
log('Could not parse JSON from subordinate_config '
589
'setting from %s' % rid, level=ERROR)
592
if self.service not in sub_config:
593
log('Found subordinate_config on %s but it contained'
594
'nothing for %s service' % (rid, self.service))
597
sub_config = sub_config[self.service]
598
if self.config_file not in sub_config:
599
log('Found subordinate_config on %s but it contained'
600
'nothing for %s' % (rid, self.config_file))
603
sub_config = sub_config[self.config_file]
604
for k, v in sub_config.iteritems():
608
ctxt['sections'] = {}
613
class SyslogContext(OSContextGenerator):
617
'use_syslog': config('use-syslog')