30
29
from charmhelpers.core.host import (
35
33
from charmhelpers.contrib.hahelpers.cluster import (
36
34
determine_apache_port,
37
35
determine_api_port,
42
39
from charmhelpers.contrib.hahelpers.apache import (
48
44
from charmhelpers.contrib.openstack.neutron import (
49
45
neutron_plugin_attribute,
52
47
from charmhelpers.contrib.network.ip import (
53
48
get_address_in_network,
55
50
get_netmask_for_address,
52
is_address_in_network,
60
54
from charmhelpers.contrib.openstack.utils import get_host_ip
62
56
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
57
ADDRESS_TYPES = ['admin', 'internal', 'public']
65
60
class OSContextError(Exception):
76
71
def context_complete(ctxt):
78
for k, v in ctxt.iteritems():
73
for k, v in six.iteritems(ctxt):
79
74
if v is None or v == '':
82
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
78
log('Missing required data: %s' % ' '.join(_missing), level=INFO)
87
84
def config_flags_parser(config_flags):
85
"""Parses config flags string into dict.
87
The provided config_flags string may be a list of comma-separated values
88
which themselves may be comma-separated list of values.
88
90
if config_flags.find('==') >= 0:
89
log("config_flags is not in expected format (key=value)",
91
log("config_flags is not in expected format (key=value)", level=ERROR)
91
92
raise OSContextError
92
94
# strip the following from each value.
93
95
post_strippers = ' ,'
94
96
# we strip any leading/trailing '=' or ' ' from the string then
134
137
def __init__(self,
135
138
database=None, user=None, relation_prefix=None, ssl_dir=None):
137
Allows inspecting relation for settings prefixed with relation_prefix.
138
This is useful for parsing access for multiple databases returned via
139
the shared-db interface (eg, nova_password, quantum_password)
139
"""Allows inspecting relation for settings prefixed with
140
relation_prefix. This is useful for parsing access for multiple
141
databases returned via the shared-db interface (eg, nova_password,
141
144
self.relation_prefix = relation_prefix
142
145
self.database = database
202
204
def __call__(self):
203
205
self.database = self.database or config('database')
204
206
if self.database is None:
205
log('Could not generate postgresql_db context. '
206
'Missing required charm config options. '
207
log('Could not generate postgresql_db context. Missing required '
208
'charm config options. (database name)', level=ERROR)
208
209
raise OSContextError
211
212
for rid in relation_ids(self.interfaces[0]):
212
213
for unit in related_units(rid):
214
'database_host': relation_get('host', rid=rid, unit=unit),
215
'database': self.database,
216
'database_user': relation_get('user', rid=rid, unit=unit),
217
'database_password': relation_get('password', rid=rid, unit=unit),
218
'database_type': 'postgresql',
214
rel_host = relation_get('host', rid=rid, unit=unit)
215
rel_user = relation_get('user', rid=rid, unit=unit)
216
rel_passwd = relation_get('password', rid=rid, unit=unit)
217
ctxt = {'database_host': rel_host,
218
'database': self.database,
219
'database_user': rel_user,
220
'database_password': rel_passwd,
221
'database_type': 'postgresql'}
220
222
if context_complete(ctxt):
227
230
ca_path = os.path.join(ssl_dir, 'db-client.ca')
228
231
with open(ca_path, 'w') as fh:
229
232
fh.write(b64decode(rdata['ssl_ca']))
230
234
ctxt['database_ssl_ca'] = ca_path
231
235
elif 'ssl_ca' in rdata:
232
log("Charm not setup for ssl support but ssl ca found")
236
log("Charm not setup for ssl support but ssl ca found", level=INFO)
234
239
if 'ssl_cert' in rdata:
235
240
cert_path = os.path.join(
236
241
ssl_dir, 'db-client.cert')
237
242
if not os.path.exists(cert_path):
238
log("Waiting 1m for ssl client cert validity")
243
log("Waiting 1m for ssl client cert validity", level=INFO)
240
246
with open(cert_path, 'w') as fh:
241
247
fh.write(b64decode(rdata['ssl_cert']))
242
249
ctxt['database_ssl_cert'] = cert_path
243
250
key_path = os.path.join(ssl_dir, 'db-client.key')
244
251
with open(key_path, 'w') as fh:
245
252
fh.write(b64decode(rdata['ssl_key']))
246
254
ctxt['database_ssl_key'] = key_path
261
269
serv_host = format_ipv6_addr(serv_host) or serv_host
262
270
auth_host = rdata.get('auth_host')
263
271
auth_host = format_ipv6_addr(auth_host) or auth_host
266
'service_port': rdata.get('service_port'),
267
'service_host': serv_host,
268
'auth_host': auth_host,
269
'auth_port': rdata.get('auth_port'),
270
'admin_tenant_name': rdata.get('service_tenant'),
271
'admin_user': rdata.get('service_username'),
272
'admin_password': rdata.get('service_password'),
274
rdata.get('service_protocol') or 'http',
276
rdata.get('auth_protocol') or 'http',
272
svc_protocol = rdata.get('service_protocol') or 'http'
273
auth_protocol = rdata.get('auth_protocol') or 'http'
274
ctxt = {'service_port': rdata.get('service_port'),
275
'service_host': serv_host,
276
'auth_host': auth_host,
277
'auth_port': rdata.get('auth_port'),
278
'admin_tenant_name': rdata.get('service_tenant'),
279
'admin_user': rdata.get('service_username'),
280
'admin_password': rdata.get('service_password'),
281
'service_protocol': svc_protocol,
282
'auth_protocol': auth_protocol}
278
283
if context_complete(ctxt):
279
284
# NOTE(jamespage) this is required for >= icehouse
280
285
# so a missing value just indicates keystone needs
282
287
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
293
299
self.interfaces = [rel_name]
295
301
def __call__(self):
296
log('Generating template context for amqp')
302
log('Generating template context for amqp', level=DEBUG)
298
user_setting = 'rabbit-user'
299
vhost_setting = 'rabbit-vhost'
300
304
if self.relation_prefix:
301
user_setting = self.relation_prefix + '-rabbit-user'
302
vhost_setting = self.relation_prefix + '-rabbit-vhost'
305
user_setting = '%s-rabbit-user' % (self.relation_prefix)
306
vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
308
user_setting = 'rabbit-user'
309
vhost_setting = 'rabbit-vhost'
305
312
username = conf[user_setting]
306
313
vhost = conf[vhost_setting]
307
314
except KeyError as e:
308
log('Could not generate shared_db context. '
309
'Missing required charm config options: %s.' % e)
315
log('Could not generate shared_db context. Missing required charm '
316
'config options: %s.' % e, level=ERROR)
310
317
raise OSContextError
312
320
for rid in relation_ids(self.rel_name):
313
321
ha_vip_only = False
344
354
if context_complete(ctxt):
345
355
if 'rabbit_ssl_ca' in ctxt:
346
356
if not self.ssl_dir:
347
log(("Charm not setup for ssl support "
357
log("Charm not setup for ssl support but ssl ca "
350
361
ca_path = os.path.join(
351
362
self.ssl_dir, 'rabbit-client-ca.pem')
352
363
with open(ca_path, 'w') as fh:
353
364
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
354
365
ctxt['rabbit_ssl_ca'] = ca_path
355
367
# Sufficient information found = break out!
357
370
# Used for active/active rabbitmq >= grizzly
358
if ('clustered' not in ctxt or ha_vip_only) \
359
and len(related_units(rid)) > 1:
371
if (('clustered' not in ctxt or ha_vip_only) and
372
len(related_units(rid)) > 1):
360
373
rabbitmq_hosts = []
361
374
for unit in related_units(rid):
362
375
host = relation_get('private-address', rid=rid, unit=unit)
363
376
host = format_ipv6_addr(host) or host
364
377
rabbitmq_hosts.append(host)
365
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
379
ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
366
381
if not context_complete(ctxt):
372
387
class CephContext(OSContextGenerator):
388
"""Generates context for /etc/ceph/ceph.conf templates."""
373
389
interfaces = ['ceph']
375
391
def __call__(self):
376
'''This generates context for /etc/ceph/ceph.conf templates'''
377
392
if not relation_ids('ceph'):
380
log('Generating template context for ceph')
395
log('Generating template context for ceph', level=DEBUG)
387
401
for unit in related_units(rid):
388
402
auth = relation_get('auth', rid=rid, unit=unit)
389
403
key = relation_get('key', rid=rid, unit=unit)
391
relation_get('ceph-public-address', rid=rid, unit=unit) or \
392
relation_get('private-address', rid=rid, unit=unit)
404
ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
406
unit_priv_addr = relation_get('private-address', rid=rid,
408
ceph_addr = ceph_pub_addr or unit_priv_addr
393
409
ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
394
410
mon_hosts.append(ceph_addr)
397
'mon_hosts': ' '.join(mon_hosts),
400
'use_syslog': use_syslog
412
ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
415
'use_syslog': use_syslog}
403
417
if not os.path.isdir('/etc/ceph'):
404
418
os.mkdir('/etc/ceph')
409
423
ensure_packages(['ceph-common'])
414
ADDRESS_TYPES = ['admin', 'internal', 'public']
417
427
class HAProxyContext(OSContextGenerator):
428
"""Provides half a context for the haproxy template, which describes
429
all peers to be included in the cluster. Each charm needs to include
430
its own context generator that describes the port mapping.
418
432
interfaces = ['cluster']
434
def __init__(self, singlenode_mode=False):
435
self.singlenode_mode = singlenode_mode
420
437
def __call__(self):
422
Builds half a context for the haproxy template, which describes
423
all peers to be included in the cluster. Each charm needs to include
424
its own context generator that describes the port mapping.
426
if not relation_ids('cluster'):
438
if not relation_ids('cluster') and not self.singlenode_mode:
429
l_unit = local_unit().replace('/', '-')
431
441
if config('prefer-ipv6'):
432
442
addr = get_ipv6_addr(exc_list=[config('vip')])[0]
434
444
addr = get_host_ip(unit_get('private-address'))
446
l_unit = local_unit().replace('/', '-')
436
447
cluster_hosts = {}
438
449
# NOTE(jamespage): build out map of configured network endpoints
439
450
# and associated backends
440
451
for addr_type in ADDRESS_TYPES:
441
laddr = get_address_in_network(
442
config('os-{}-network'.format(addr_type)))
452
cfg_opt = 'os-{}-network'.format(addr_type)
453
laddr = get_address_in_network(config(cfg_opt))
444
cluster_hosts[laddr] = {}
445
cluster_hosts[laddr]['network'] = "{}/{}".format(
447
get_netmask_for_address(laddr)
449
cluster_hosts[laddr]['backends'] = {}
450
cluster_hosts[laddr]['backends'][l_unit] = laddr
455
netmask = get_netmask_for_address(laddr)
456
cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
458
'backends': {l_unit: laddr}}
451
459
for rid in relation_ids('cluster'):
452
460
for unit in related_units(rid):
453
_unit = unit.replace('/', '-')
454
461
_laddr = relation_get('{}-address'.format(addr_type),
455
462
rid=rid, unit=unit)
464
_unit = unit.replace('/', '-')
457
465
cluster_hosts[laddr]['backends'][_unit] = _laddr
459
467
# NOTE(jamespage) no split configurations found, just use
460
468
# private addresses
461
469
if not cluster_hosts:
462
cluster_hosts[addr] = {}
463
cluster_hosts[addr]['network'] = "{}/{}".format(
465
get_netmask_for_address(addr)
467
cluster_hosts[addr]['backends'] = {}
468
cluster_hosts[addr]['backends'][l_unit] = addr
470
netmask = get_netmask_for_address(addr)
471
cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
472
'backends': {l_unit: addr}}
469
473
for rid in relation_ids('cluster'):
470
474
for unit in related_units(rid):
471
_unit = unit.replace('/', '-')
472
475
_laddr = relation_get('private-address',
473
476
rid=rid, unit=unit)
478
_unit = unit.replace('/', '-')
475
479
cluster_hosts[addr]['backends'][_unit] = _laddr
478
'frontends': cluster_hosts,
481
ctxt = {'frontends': cluster_hosts}
481
483
if config('haproxy-server-timeout'):
482
484
ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
483
486
if config('haproxy-client-timeout'):
484
487
ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
493
496
ctxt['stat_port'] = ':8888'
495
498
for frontend in cluster_hosts:
496
if len(cluster_hosts[frontend]['backends']) > 1:
499
if (len(cluster_hosts[frontend]['backends']) > 1 or
500
self.singlenode_mode):
497
501
# Enable haproxy when we have enough peers.
498
log('Ensuring haproxy enabled in /etc/default/haproxy.')
502
log('Ensuring haproxy enabled in /etc/default/haproxy.',
499
504
with open('/etc/default/haproxy', 'w') as out:
500
505
out.write('ENABLED=1\n')
502
log('HAProxy context is incomplete, this unit has no peers.')
509
log('HAProxy context is incomplete, this unit has no peers.',
507
515
interfaces = ['image-service']
509
517
def __call__(self):
511
Obtains the glance API server from the image-service relation. Useful
512
in nova and cinder (currently).
514
log('Generating template context for image-service.')
518
"""Obtains the glance API server from the image-service relation.
519
Useful in nova and cinder (currently).
521
log('Generating template context for image-service.', level=DEBUG)
515
522
rids = relation_ids('image-service')
519
527
for unit in related_units(rid):
520
528
api_server = relation_get('glance-api-server',
521
529
rid=rid, unit=unit)
523
531
return {'glance_api_servers': api_server}
524
log('ImageService context is incomplete. '
525
'Missing required relation data.')
533
log("ImageService context is incomplete. Missing required relation "
529
538
class ApacheSSLContext(OSContextGenerator):
532
Generates a context for an apache vhost configuration that configures
539
"""Generates a context for an apache vhost configuration that configures
533
540
HTTPS reverse proxying for one or many endpoints. Generated context
534
541
looks something like::
583
592
if k.startswith('ssl_key_'):
584
593
cns.append(k.lstrip('ssl_key_'))
585
return list(set(cns))
595
return sorted(list(set(cns)))
597
def get_network_addresses(self):
598
"""For each network configured, return corresponding address and vip
601
Returns a list of tuples of the form:
603
[(address_in_net_a, vip_in_net_a),
604
(address_in_net_b, vip_in_net_b),
607
or, if no vip(s) available:
609
[(address_in_net_a, address_in_net_a),
610
(address_in_net_b, address_in_net_b),
615
vips = config('vip').split()
619
for net_type in ['os-internal-network', 'os-admin-network',
620
'os-public-network']:
621
addr = get_address_in_network(config(net_type),
622
unit_get('private-address'))
623
if len(vips) > 1 and is_clustered():
624
if not config(net_type):
625
log("Multiple networks configured but net_type "
626
"is None (%s)." % net_type, level=WARNING)
630
if is_address_in_network(config(net_type), vip):
631
addresses.append((addr, vip))
634
elif is_clustered() and config('vip'):
635
addresses.append((addr, config('vip')))
637
addresses.append((addr, addr))
639
return sorted(addresses)
587
641
def __call__(self):
588
if isinstance(self.external_ports, basestring):
642
if isinstance(self.external_ports, six.string_types):
589
643
self.external_ports = [self.external_ports]
590
if (not self.external_ports or not https()):
645
if not self.external_ports or not https():
593
648
self.configure_ca()
594
649
self.enable_modules()
597
'namespace': self.service_namespace,
651
ctxt = {'namespace': self.service_namespace,
602
655
for cn in self.canonical_names():
603
656
self.configure_cert(cn)
608
vips = config('vip').split()
610
for network_type in ['os-internal-network',
612
'os-public-network']:
613
address = get_address_in_network(config(network_type),
614
unit_get('private-address'))
615
if len(vips) > 0 and is_clustered():
617
if is_address_in_network(config(network_type),
619
addresses.append((address, vip))
622
addresses.append((address, config('vip')))
624
addresses.append((address, address))
626
for address, endpoint in set(addresses):
658
addresses = self.get_network_addresses()
659
for address, endpoint in sorted(set(addresses)):
627
660
for api_port in self.external_ports:
628
661
ext_port = determine_apache_port(api_port)
629
662
int_port = determine_api_port(api_port)
630
663
portmap = (address, endpoint, int(ext_port), int(int_port))
631
664
ctxt['endpoints'].append(portmap)
632
665
ctxt['ext_ports'].append(int(ext_port))
633
ctxt['ext_ports'] = list(set(ctxt['ext_ports']))
667
ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
649
683
def packages(self):
650
return neutron_plugin_attribute(
651
self.plugin, 'packages', self.network_manager)
684
return neutron_plugin_attribute(self.plugin, 'packages',
685
self.network_manager)
654
688
def neutron_security_groups(self):
657
691
def _ensure_packages(self):
658
[ensure_packages(pkgs) for pkgs in self.packages]
692
for pkgs in self.packages:
693
ensure_packages(pkgs)
660
695
def _save_flag_file(self):
661
696
if self.network_manager == 'quantum':
662
697
_file = '/etc/nova/quantum_plugin.conf'
664
699
_file = '/etc/nova/neutron_plugin.conf'
665
701
with open(_file, 'wb') as out:
666
702
out.write(self.plugin + '\n')
700
732
self.network_manager)
701
733
n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
702
734
self.network_manager)
704
'core_plugin': driver,
705
'neutron_plugin': 'n1kv',
706
'neutron_security_groups': self.neutron_security_groups,
707
'local_ip': unit_private_ip(),
708
'config': n1kv_config,
709
'vsm_ip': config('n1kv-vsm-ip'),
710
'vsm_username': config('n1kv-vsm-username'),
711
'vsm_password': config('n1kv-vsm-password'),
712
'restrict_policy_profiles': config(
713
'n1kv_restrict_policy_profiles'),
735
n1kv_user_config_flags = config('n1kv-config-flags')
736
restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
737
n1kv_ctxt = {'core_plugin': driver,
738
'neutron_plugin': 'n1kv',
739
'neutron_security_groups': self.neutron_security_groups,
740
'local_ip': unit_private_ip(),
741
'config': n1kv_config,
742
'vsm_ip': config('n1kv-vsm-ip'),
743
'vsm_username': config('n1kv-vsm-username'),
744
'vsm_password': config('n1kv-vsm-password'),
745
'restrict_policy_profiles': restrict_policy_profiles}
747
if n1kv_user_config_flags:
748
flags = config_flags_parser(n1kv_user_config_flags)
749
n1kv_ctxt['user_config_flags'] = flags
753
def calico_ctxt(self):
754
driver = neutron_plugin_attribute(self.plugin, 'driver',
755
self.network_manager)
756
config = neutron_plugin_attribute(self.plugin, 'config',
757
self.network_manager)
758
calico_ctxt = {'core_plugin': driver,
759
'neutron_plugin': 'Calico',
760
'neutron_security_groups': self.neutron_security_groups,
761
'local_ip': unit_private_ip(),
718
766
def neutron_ctxt(self):
723
772
if is_clustered():
724
773
host = config('vip')
726
775
host = unit_get('private-address')
727
url = '%s://%s:%s' % (proto, host, '9696')
729
'network_manager': self.network_manager,
777
ctxt = {'network_manager': self.network_manager,
778
'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
734
781
def __call__(self):
761
810
class OSConfigFlagContext(OSContextGenerator):
764
Responsible for adding user-defined config-flags in charm config to a
811
"""Provides support for user-defined config flags.
813
Users can define a comma-seperated list of key=value pairs
814
in the charm configuration and apply them at any point in
815
any file by using a template flag.
817
Sometimes users might want config flags inserted within a
818
specific section so this class allows users to specify the
819
template flag name, allowing for multiple template flags
820
(sections) within the same context.
767
822
NOTE: the value of config-flags may be a comma-separated list of
768
823
key=value pairs and some Openstack config files support
769
824
comma-separated lists as values.
827
def __init__(self, charm_flag='config-flags',
828
template_flag='user_config_flags'):
830
:param charm_flag: config flags in charm configuration.
831
:param template_flag: insert point for user-defined flags in template
834
super(OSConfigFlagContext, self).__init__()
835
self._charm_flag = charm_flag
836
self._template_flag = template_flag
772
838
def __call__(self):
773
config_flags = config('config-flags')
839
config_flags = config(self._charm_flag)
774
840
if not config_flags:
777
flags = config_flags_parser(config_flags)
778
return {'user_config_flags': flags}
843
return {self._template_flag:
844
config_flags_parser(config_flags)}
781
847
class SubordinateConfigContext(OSContextGenerator):
850
915
if self.service not in sub_config:
851
916
log('Found subordinate_config on %s but it contained'
852
'nothing for %s service' % (rid, self.service))
917
'nothing for %s service' % (rid, self.service),
855
921
sub_config = sub_config[self.service]
856
922
if self.config_file not in sub_config:
857
923
log('Found subordinate_config on %s but it contained'
858
'nothing for %s' % (rid, self.config_file))
924
'nothing for %s' % (rid, self.config_file),
861
928
sub_config = sub_config[self.config_file]
862
for k, v in sub_config.iteritems():
929
for k, v in six.iteritems(sub_config):
863
930
if k == 'sections':
864
for section, config_dict in v.iteritems():
865
log("adding section '%s'" % (section))
931
for section, config_dict in six.iteritems(v):
932
log("adding section '%s'" % (section),
866
934
ctxt[k][section] = config_dict
870
log("%d section(s) found" % (len(ctxt['sections'])), level=INFO)
938
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
914
976
except ImportError:
915
977
apt_install('python-psutil', fatal=True)
916
978
from psutil import NUM_CPUS
919
982
def __call__(self):
920
multiplier = config('worker-multiplier') or 1
922
"workers": self.num_cpus * multiplier
983
multiplier = config('worker-multiplier') or 0
984
ctxt = {"workers": self.num_cpus * multiplier}
988
class ZeroMQContext(OSContextGenerator):
989
interfaces = ['zeromq-configuration']
993
if is_relation_made('zeromq-configuration', 'host'):
994
for rid in relation_ids('zeromq-configuration'):
995
for unit in related_units(rid):
996
ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
997
ctxt['zmq_host'] = relation_get('host', unit, rid)
1002
class NotificationDriverContext(OSContextGenerator):
1004
def __init__(self, zmq_relation='zeromq-configuration',
1005
amqp_relation='amqp'):
1007
:param zmq_relation: Name of Zeromq relation to check
1009
self.zmq_relation = zmq_relation
1010
self.amqp_relation = amqp_relation
1013
ctxt = {'notifications': 'False'}
1014
if is_relation_made(self.amqp_relation):
1015
ctxt['notifications'] = "True"