31
from charmhelpers.core.sysctl import create as sysctl_create
30
33
from charmhelpers.core.host import (
35
37
from charmhelpers.contrib.hahelpers.cluster import (
36
38
determine_apache_port,
37
39
determine_api_port,
42
43
from charmhelpers.contrib.hahelpers.apache import (
48
48
from charmhelpers.contrib.openstack.neutron import (
49
49
neutron_plugin_attribute,
52
51
from charmhelpers.contrib.network.ip import (
53
52
get_address_in_network,
55
54
get_netmask_for_address,
56
is_address_in_network,
60
58
from charmhelpers.contrib.openstack.utils import get_host_ip
62
60
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
61
ADDRESS_TYPES = ['admin', 'internal', 'public']
65
64
class OSContextError(Exception):
76
75
def context_complete(ctxt):
78
for k, v in ctxt.iteritems():
77
for k, v in six.iteritems(ctxt):
79
78
if v is None or v == '':
82
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
82
log('Missing required data: %s' % ' '.join(_missing), level=INFO)
87
88
def config_flags_parser(config_flags):
89
"""Parses config flags string into dict.
91
The provided config_flags string may be a list of comma-separated values
92
which themselves may be comma-separated list of values.
88
94
if config_flags.find('==') >= 0:
89
log("config_flags is not in expected format (key=value)",
95
log("config_flags is not in expected format (key=value)", level=ERROR)
91
96
raise OSContextError
92
98
# strip the following from each value.
93
99
post_strippers = ' ,'
94
100
# we strip any leading/trailing '=' or ' ' from the string then
134
141
def __init__(self,
135
142
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)
143
"""Allows inspecting relation for settings prefixed with
144
relation_prefix. This is useful for parsing access for multiple
145
databases returned via the shared-db interface (eg, nova_password,
141
148
self.relation_prefix = relation_prefix
142
149
self.database = database
202
208
def __call__(self):
203
209
self.database = self.database or config('database')
204
210
if self.database is None:
205
log('Could not generate postgresql_db context. '
206
'Missing required charm config options. '
211
log('Could not generate postgresql_db context. Missing required '
212
'charm config options. (database name)', level=ERROR)
208
213
raise OSContextError
211
216
for rid in relation_ids(self.interfaces[0]):
212
217
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',
218
rel_host = relation_get('host', rid=rid, unit=unit)
219
rel_user = relation_get('user', rid=rid, unit=unit)
220
rel_passwd = relation_get('password', rid=rid, unit=unit)
221
ctxt = {'database_host': rel_host,
222
'database': self.database,
223
'database_user': rel_user,
224
'database_password': rel_passwd,
225
'database_type': 'postgresql'}
220
226
if context_complete(ctxt):
227
234
ca_path = os.path.join(ssl_dir, 'db-client.ca')
228
235
with open(ca_path, 'w') as fh:
229
236
fh.write(b64decode(rdata['ssl_ca']))
230
238
ctxt['database_ssl_ca'] = ca_path
231
239
elif 'ssl_ca' in rdata:
232
log("Charm not setup for ssl support but ssl ca found")
240
log("Charm not setup for ssl support but ssl ca found", level=INFO)
234
243
if 'ssl_cert' in rdata:
235
244
cert_path = os.path.join(
236
245
ssl_dir, 'db-client.cert')
237
246
if not os.path.exists(cert_path):
238
log("Waiting 1m for ssl client cert validity")
247
log("Waiting 1m for ssl client cert validity", level=INFO)
240
250
with open(cert_path, 'w') as fh:
241
251
fh.write(b64decode(rdata['ssl_cert']))
242
253
ctxt['database_ssl_cert'] = cert_path
243
254
key_path = os.path.join(ssl_dir, 'db-client.key')
244
255
with open(key_path, 'w') as fh:
245
256
fh.write(b64decode(rdata['ssl_key']))
246
258
ctxt['database_ssl_key'] = key_path
261
273
serv_host = format_ipv6_addr(serv_host) or serv_host
262
274
auth_host = rdata.get('auth_host')
263
275
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',
276
svc_protocol = rdata.get('service_protocol') or 'http'
277
auth_protocol = rdata.get('auth_protocol') or 'http'
278
ctxt = {'service_port': rdata.get('service_port'),
279
'service_host': serv_host,
280
'auth_host': auth_host,
281
'auth_port': rdata.get('auth_port'),
282
'admin_tenant_name': rdata.get('service_tenant'),
283
'admin_user': rdata.get('service_username'),
284
'admin_password': rdata.get('service_password'),
285
'service_protocol': svc_protocol,
286
'auth_protocol': auth_protocol}
278
287
if context_complete(ctxt):
279
288
# NOTE(jamespage) this is required for >= icehouse
280
289
# so a missing value just indicates keystone needs
282
291
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
293
303
self.interfaces = [rel_name]
295
305
def __call__(self):
296
log('Generating template context for amqp')
306
log('Generating template context for amqp', level=DEBUG)
298
user_setting = 'rabbit-user'
299
vhost_setting = 'rabbit-vhost'
300
308
if self.relation_prefix:
301
user_setting = self.relation_prefix + '-rabbit-user'
302
vhost_setting = self.relation_prefix + '-rabbit-vhost'
309
user_setting = '%s-rabbit-user' % (self.relation_prefix)
310
vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
312
user_setting = 'rabbit-user'
313
vhost_setting = 'rabbit-vhost'
305
316
username = conf[user_setting]
306
317
vhost = conf[vhost_setting]
307
318
except KeyError as e:
308
log('Could not generate shared_db context. '
309
'Missing required charm config options: %s.' % e)
319
log('Could not generate shared_db context. Missing required charm '
320
'config options: %s.' % e, level=ERROR)
310
321
raise OSContextError
312
324
for rid in relation_ids(self.rel_name):
313
325
ha_vip_only = False
344
358
if context_complete(ctxt):
345
359
if 'rabbit_ssl_ca' in ctxt:
346
360
if not self.ssl_dir:
347
log(("Charm not setup for ssl support "
361
log("Charm not setup for ssl support but ssl ca "
350
365
ca_path = os.path.join(
351
366
self.ssl_dir, 'rabbit-client-ca.pem')
352
367
with open(ca_path, 'w') as fh:
353
368
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
354
369
ctxt['rabbit_ssl_ca'] = ca_path
355
371
# Sufficient information found = break out!
357
374
# Used for active/active rabbitmq >= grizzly
358
if ('clustered' not in ctxt or ha_vip_only) \
359
and len(related_units(rid)) > 1:
375
if (('clustered' not in ctxt or ha_vip_only) and
376
len(related_units(rid)) > 1):
360
377
rabbitmq_hosts = []
361
378
for unit in related_units(rid):
362
379
host = relation_get('private-address', rid=rid, unit=unit)
363
380
host = format_ipv6_addr(host) or host
364
381
rabbitmq_hosts.append(host)
365
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
383
ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
366
385
if not context_complete(ctxt):
372
391
class CephContext(OSContextGenerator):
392
"""Generates context for /etc/ceph/ceph.conf templates."""
373
393
interfaces = ['ceph']
375
395
def __call__(self):
376
'''This generates context for /etc/ceph/ceph.conf templates'''
377
396
if not relation_ids('ceph'):
380
log('Generating template context for ceph')
399
log('Generating template context for ceph', level=DEBUG)
387
405
for unit in related_units(rid):
388
406
auth = relation_get('auth', rid=rid, unit=unit)
389
407
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)
408
ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
410
unit_priv_addr = relation_get('private-address', rid=rid,
412
ceph_addr = ceph_pub_addr or unit_priv_addr
393
413
ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
394
414
mon_hosts.append(ceph_addr)
397
'mon_hosts': ' '.join(mon_hosts),
400
'use_syslog': use_syslog
416
ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
419
'use_syslog': use_syslog}
403
421
if not os.path.isdir('/etc/ceph'):
404
422
os.mkdir('/etc/ceph')
409
427
ensure_packages(['ceph-common'])
414
ADDRESS_TYPES = ['admin', 'internal', 'public']
417
431
class HAProxyContext(OSContextGenerator):
432
"""Provides half a context for the haproxy template, which describes
433
all peers to be included in the cluster. Each charm needs to include
434
its own context generator that describes the port mapping.
418
436
interfaces = ['cluster']
438
def __init__(self, singlenode_mode=False):
439
self.singlenode_mode = singlenode_mode
420
441
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'):
442
if not relation_ids('cluster') and not self.singlenode_mode:
429
l_unit = local_unit().replace('/', '-')
431
445
if config('prefer-ipv6'):
432
446
addr = get_ipv6_addr(exc_list=[config('vip')])[0]
434
448
addr = get_host_ip(unit_get('private-address'))
450
l_unit = local_unit().replace('/', '-')
436
451
cluster_hosts = {}
438
453
# NOTE(jamespage): build out map of configured network endpoints
439
454
# and associated backends
440
455
for addr_type in ADDRESS_TYPES:
441
laddr = get_address_in_network(
442
config('os-{}-network'.format(addr_type)))
456
cfg_opt = 'os-{}-network'.format(addr_type)
457
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
459
netmask = get_netmask_for_address(laddr)
460
cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
462
'backends': {l_unit: laddr}}
451
463
for rid in relation_ids('cluster'):
452
464
for unit in related_units(rid):
453
_unit = unit.replace('/', '-')
454
465
_laddr = relation_get('{}-address'.format(addr_type),
455
466
rid=rid, unit=unit)
468
_unit = unit.replace('/', '-')
457
469
cluster_hosts[laddr]['backends'][_unit] = _laddr
459
471
# NOTE(jamespage) no split configurations found, just use
460
472
# private addresses
461
473
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
474
netmask = get_netmask_for_address(addr)
475
cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
476
'backends': {l_unit: addr}}
469
477
for rid in relation_ids('cluster'):
470
478
for unit in related_units(rid):
471
_unit = unit.replace('/', '-')
472
479
_laddr = relation_get('private-address',
473
480
rid=rid, unit=unit)
482
_unit = unit.replace('/', '-')
475
483
cluster_hosts[addr]['backends'][_unit] = _laddr
478
'frontends': cluster_hosts,
485
ctxt = {'frontends': cluster_hosts}
481
487
if config('haproxy-server-timeout'):
482
488
ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
483
490
if config('haproxy-client-timeout'):
484
491
ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
486
493
if config('prefer-ipv6'):
487
495
ctxt['local_host'] = 'ip6-localhost'
488
496
ctxt['haproxy_host'] = '::'
489
497
ctxt['stat_port'] = ':::8888'
493
501
ctxt['stat_port'] = ':8888'
495
503
for frontend in cluster_hosts:
496
if len(cluster_hosts[frontend]['backends']) > 1:
504
if (len(cluster_hosts[frontend]['backends']) > 1 or
505
self.singlenode_mode):
497
506
# Enable haproxy when we have enough peers.
498
log('Ensuring haproxy enabled in /etc/default/haproxy.')
507
log('Ensuring haproxy enabled in /etc/default/haproxy.',
499
509
with open('/etc/default/haproxy', 'w') as out:
500
510
out.write('ENABLED=1\n')
502
log('HAProxy context is incomplete, this unit has no peers.')
514
log('HAProxy context is incomplete, this unit has no peers.',
507
520
interfaces = ['image-service']
509
522
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.')
523
"""Obtains the glance API server from the image-service relation.
524
Useful in nova and cinder (currently).
526
log('Generating template context for image-service.', level=DEBUG)
515
527
rids = relation_ids('image-service')
519
532
for unit in related_units(rid):
520
533
api_server = relation_get('glance-api-server',
521
534
rid=rid, unit=unit)
523
536
return {'glance_api_servers': api_server}
524
log('ImageService context is incomplete. '
525
'Missing required relation data.')
538
log("ImageService context is incomplete. Missing required relation "
529
543
class ApacheSSLContext(OSContextGenerator):
532
Generates a context for an apache vhost configuration that configures
544
"""Generates a context for an apache vhost configuration that configures
533
545
HTTPS reverse proxying for one or many endpoints. Generated context
534
546
looks something like::
583
597
if k.startswith('ssl_key_'):
584
598
cns.append(k.lstrip('ssl_key_'))
585
return list(set(cns))
600
return sorted(list(set(cns)))
602
def get_network_addresses(self):
603
"""For each network configured, return corresponding address and vip
606
Returns a list of tuples of the form:
608
[(address_in_net_a, vip_in_net_a),
609
(address_in_net_b, vip_in_net_b),
612
or, if no vip(s) available:
614
[(address_in_net_a, address_in_net_a),
615
(address_in_net_b, address_in_net_b),
620
vips = config('vip').split()
624
for net_type in ['os-internal-network', 'os-admin-network',
625
'os-public-network']:
626
addr = get_address_in_network(config(net_type),
627
unit_get('private-address'))
628
if len(vips) > 1 and is_clustered():
629
if not config(net_type):
630
log("Multiple networks configured but net_type "
631
"is None (%s)." % net_type, level=WARNING)
635
if is_address_in_network(config(net_type), vip):
636
addresses.append((addr, vip))
639
elif is_clustered() and config('vip'):
640
addresses.append((addr, config('vip')))
642
addresses.append((addr, addr))
644
return sorted(addresses)
587
646
def __call__(self):
588
if isinstance(self.external_ports, basestring):
647
if isinstance(self.external_ports, six.string_types):
589
648
self.external_ports = [self.external_ports]
590
if (not self.external_ports or not https()):
650
if not self.external_ports or not https():
593
653
self.configure_ca()
594
654
self.enable_modules()
597
'namespace': self.service_namespace,
656
ctxt = {'namespace': self.service_namespace,
602
660
for cn in self.canonical_names():
603
661
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):
663
addresses = self.get_network_addresses()
664
for address, endpoint in sorted(set(addresses)):
627
665
for api_port in self.external_ports:
628
666
ext_port = determine_apache_port(api_port)
629
667
int_port = determine_api_port(api_port)
630
668
portmap = (address, endpoint, int(ext_port), int(int_port))
631
669
ctxt['endpoints'].append(portmap)
632
670
ctxt['ext_ports'].append(int(ext_port))
633
ctxt['ext_ports'] = list(set(ctxt['ext_ports']))
672
ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
649
688
def packages(self):
650
return neutron_plugin_attribute(
651
self.plugin, 'packages', self.network_manager)
689
return neutron_plugin_attribute(self.plugin, 'packages',
690
self.network_manager)
654
693
def neutron_security_groups(self):
657
696
def _ensure_packages(self):
658
[ensure_packages(pkgs) for pkgs in self.packages]
697
for pkgs in self.packages:
698
ensure_packages(pkgs)
660
700
def _save_flag_file(self):
661
701
if self.network_manager == 'quantum':
662
702
_file = '/etc/nova/quantum_plugin.conf'
664
704
_file = '/etc/nova/neutron_plugin.conf'
665
706
with open(_file, 'wb') as out:
666
707
out.write(self.plugin + '\n')
700
737
self.network_manager)
701
738
n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
702
739
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'),
740
n1kv_user_config_flags = config('n1kv-config-flags')
741
restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
742
n1kv_ctxt = {'core_plugin': driver,
743
'neutron_plugin': 'n1kv',
744
'neutron_security_groups': self.neutron_security_groups,
745
'local_ip': unit_private_ip(),
746
'config': n1kv_config,
747
'vsm_ip': config('n1kv-vsm-ip'),
748
'vsm_username': config('n1kv-vsm-username'),
749
'vsm_password': config('n1kv-vsm-password'),
750
'restrict_policy_profiles': restrict_policy_profiles}
752
if n1kv_user_config_flags:
753
flags = config_flags_parser(n1kv_user_config_flags)
754
n1kv_ctxt['user_config_flags'] = flags
758
def calico_ctxt(self):
759
driver = neutron_plugin_attribute(self.plugin, 'driver',
760
self.network_manager)
761
config = neutron_plugin_attribute(self.plugin, 'config',
762
self.network_manager)
763
calico_ctxt = {'core_plugin': driver,
764
'neutron_plugin': 'Calico',
765
'neutron_security_groups': self.neutron_security_groups,
766
'local_ip': unit_private_ip(),
718
771
def neutron_ctxt(self):
723
777
if is_clustered():
724
778
host = config('vip')
726
780
host = unit_get('private-address')
727
url = '%s://%s:%s' % (proto, host, '9696')
729
'network_manager': self.network_manager,
782
ctxt = {'network_manager': self.network_manager,
783
'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
734
786
def __call__(self):
761
815
class OSConfigFlagContext(OSContextGenerator):
764
Responsible for adding user-defined config-flags in charm config to a
816
"""Provides support for user-defined config flags.
818
Users can define a comma-seperated list of key=value pairs
819
in the charm configuration and apply them at any point in
820
any file by using a template flag.
822
Sometimes users might want config flags inserted within a
823
specific section so this class allows users to specify the
824
template flag name, allowing for multiple template flags
825
(sections) within the same context.
767
827
NOTE: the value of config-flags may be a comma-separated list of
768
828
key=value pairs and some Openstack config files support
769
829
comma-separated lists as values.
832
def __init__(self, charm_flag='config-flags',
833
template_flag='user_config_flags'):
835
:param charm_flag: config flags in charm configuration.
836
:param template_flag: insert point for user-defined flags in template
839
super(OSConfigFlagContext, self).__init__()
840
self._charm_flag = charm_flag
841
self._template_flag = template_flag
772
843
def __call__(self):
773
config_flags = config('config-flags')
844
config_flags = config(self._charm_flag)
774
845
if not config_flags:
777
flags = config_flags_parser(config_flags)
778
return {'user_config_flags': flags}
848
return {self._template_flag:
849
config_flags_parser(config_flags)}
781
852
class SubordinateConfigContext(OSContextGenerator):
850
920
if self.service not in sub_config:
851
921
log('Found subordinate_config on %s but it contained'
852
'nothing for %s service' % (rid, self.service))
922
'nothing for %s service' % (rid, self.service),
855
926
sub_config = sub_config[self.service]
856
927
if self.config_file not in sub_config:
857
928
log('Found subordinate_config on %s but it contained'
858
'nothing for %s' % (rid, self.config_file))
929
'nothing for %s' % (rid, self.config_file),
861
933
sub_config = sub_config[self.config_file]
862
for k, v in sub_config.iteritems():
934
for k, v in six.iteritems(sub_config):
863
935
if k == 'sections':
864
for section, config_dict in v.iteritems():
865
log("adding section '%s'" % (section))
936
for section, config_dict in six.iteritems(v):
937
log("adding section '%s'" % (section),
866
939
ctxt[k][section] = config_dict
870
log("%d section(s) found" % (len(ctxt['sections'])), level=INFO)
943
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
914
981
except ImportError:
915
982
apt_install('python-psutil', fatal=True)
916
983
from psutil import NUM_CPUS
919
987
def __call__(self):
920
multiplier = config('worker-multiplier') or 1
922
"workers": self.num_cpus * multiplier
988
multiplier = config('worker-multiplier') or 0
989
ctxt = {"workers": self.num_cpus * multiplier}
993
class ZeroMQContext(OSContextGenerator):
994
interfaces = ['zeromq-configuration']
998
if is_relation_made('zeromq-configuration', 'host'):
999
for rid in relation_ids('zeromq-configuration'):
1000
for unit in related_units(rid):
1001
ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
1002
ctxt['zmq_host'] = relation_get('host', unit, rid)
1007
class NotificationDriverContext(OSContextGenerator):
1009
def __init__(self, zmq_relation='zeromq-configuration',
1010
amqp_relation='amqp'):
1012
:param zmq_relation: Name of Zeromq relation to check
1014
self.zmq_relation = zmq_relation
1015
self.amqp_relation = amqp_relation
1018
ctxt = {'notifications': 'False'}
1019
if is_relation_made(self.amqp_relation):
1020
ctxt['notifications'] = "True"
1025
class SysctlContext(OSContextGenerator):
1026
"""This context check if the 'sysctl' option exists on configuration
1027
then creates a file with the loaded contents"""
1029
sysctl_dict = config('sysctl')
1031
sysctl_create(sysctl_dict,
1032
'/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
1033
return {'sysctl': sysctl_dict}