1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
21
from base64 import b64decode
22
from subprocess import check_call
27
from charmhelpers.fetch import (
29
filter_installed_packages,
31
from charmhelpers.core.hookenv import (
49
from charmhelpers.core.sysctl import create as sysctl_create
50
from charmhelpers.core.strutils import bool_from_string
52
from charmhelpers.core.host import (
60
from charmhelpers.contrib.hahelpers.cluster import (
61
determine_apache_port,
66
from charmhelpers.contrib.hahelpers.apache import (
71
from charmhelpers.contrib.openstack.neutron import (
72
neutron_plugin_attribute,
73
parse_data_port_mappings,
75
from charmhelpers.contrib.openstack.ip import (
79
from charmhelpers.contrib.network.ip import (
80
get_address_in_network,
83
get_netmask_for_address,
85
is_address_in_network,
88
from charmhelpers.contrib.openstack.utils import get_host_ip
89
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
90
ADDRESS_TYPES = ['admin', 'internal', 'public']
93
class OSContextError(Exception):
97
def ensure_packages(packages):
98
"""Install but do not upgrade required plugin packages."""
99
required = filter_installed_packages(packages)
101
apt_install(required, fatal=True)
104
def context_complete(ctxt):
106
for k, v in six.iteritems(ctxt):
107
if v is None or v == '':
111
log('Missing required data: %s' % ' '.join(_missing), level=INFO)
117
def config_flags_parser(config_flags):
118
"""Parses config flags string into dict.
120
This parsing method supports a few different formats for the config
121
flag values to be parsed:
123
1. A string in the simple format of key=value pairs, with the possibility
124
of specifying multiple key value pairs within the same string. For
125
example, a string in the format of 'key1=value1, key2=value2' will
131
2. A string in the above format, but supporting a comma-delimited list
132
of values for the same key. For example, a string in the format of
133
'key1=value1, key2=value3,value4,value5' will return a dict of:
136
'key2', 'value2,value3,value4'}
138
3. A string containing a colon character (:) prior to an equal
139
character (=) will be treated as yaml and parsed as such. This can be
140
used to specify more complex key value pairs. For example,
141
a string in the format of 'key1: subkey1=value1, subkey2=value2' will
144
{'key1', 'subkey1=value1, subkey2=value2'}
146
The provided config_flags string may be a list of comma-separated values
147
which themselves may be comma-separated list of values.
149
# If we find a colon before an equals sign then treat it as yaml.
150
# Note: limit it to finding the colon first since this indicates assignment
152
colon = config_flags.find(':')
153
equals = config_flags.find('=')
155
if colon < equals or equals < 0:
156
return yaml.safe_load(config_flags)
158
if config_flags.find('==') >= 0:
159
log("config_flags is not in expected format (key=value)", level=ERROR)
162
# strip the following from each value.
163
post_strippers = ' ,'
164
# we strip any leading/trailing '=' or ' ' from the string then
166
split = config_flags.strip(' =').split('=')
169
for i in range(0, limit - 1):
172
vindex = next.rfind(',')
173
if (i == limit - 2) or (vindex < 0):
176
value = next[:vindex]
181
# if this not the first entry, expect an embedded key.
182
index = current.rfind(',')
184
log("Invalid config value(s) at index %s" % (i), level=ERROR)
186
key = current[index + 1:]
189
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
194
class OSContextGenerator(object):
195
"""Base class for all context generators."""
199
raise NotImplementedError
202
class SharedDBContext(OSContextGenerator):
203
interfaces = ['shared-db']
206
database=None, user=None, relation_prefix=None, ssl_dir=None):
207
"""Allows inspecting relation for settings prefixed with
208
relation_prefix. This is useful for parsing access for multiple
209
databases returned via the shared-db interface (eg, nova_password,
212
self.relation_prefix = relation_prefix
213
self.database = database
215
self.ssl_dir = ssl_dir
218
self.database = self.database or config('database')
219
self.user = self.user or config('database-user')
220
if None in [self.database, self.user]:
221
log("Could not generate shared_db context. Missing required charm "
222
"config options. (database name and user)", level=ERROR)
227
# NOTE(jamespage) if mysql charm provides a network upon which
228
# access to the database should be made, reconfigure relation
229
# with the service units local address and defer execution
230
access_network = relation_get('access-network')
231
if access_network is not None:
232
if self.relation_prefix is not None:
233
hostname_key = "{}_hostname".format(self.relation_prefix)
235
hostname_key = "hostname"
236
access_hostname = get_address_in_network(access_network,
237
unit_get('private-address'))
238
set_hostname = relation_get(attribute=hostname_key,
240
if set_hostname != access_hostname:
241
relation_set(relation_settings={hostname_key: access_hostname})
242
return None # Defer any further hook execution for now....
244
password_setting = 'password'
245
if self.relation_prefix:
246
password_setting = self.relation_prefix + '_password'
248
for rid in relation_ids(self.interfaces[0]):
249
for unit in related_units(rid):
250
rdata = relation_get(rid=rid, unit=unit)
251
host = rdata.get('db_host')
252
host = format_ipv6_addr(host) or host
254
'database_host': host,
255
'database': self.database,
256
'database_user': self.user,
257
'database_password': rdata.get(password_setting),
258
'database_type': 'mysql'
260
if context_complete(ctxt):
261
db_ssl(rdata, ctxt, self.ssl_dir)
266
class PostgresqlDBContext(OSContextGenerator):
267
interfaces = ['pgsql-db']
269
def __init__(self, database=None):
270
self.database = database
273
self.database = self.database or config('database')
274
if self.database is None:
275
log('Could not generate postgresql_db context. Missing required '
276
'charm config options. (database name)', level=ERROR)
280
for rid in relation_ids(self.interfaces[0]):
281
for unit in related_units(rid):
282
rel_host = relation_get('host', rid=rid, unit=unit)
283
rel_user = relation_get('user', rid=rid, unit=unit)
284
rel_passwd = relation_get('password', rid=rid, unit=unit)
285
ctxt = {'database_host': rel_host,
286
'database': self.database,
287
'database_user': rel_user,
288
'database_password': rel_passwd,
289
'database_type': 'postgresql'}
290
if context_complete(ctxt):
296
def db_ssl(rdata, ctxt, ssl_dir):
297
if 'ssl_ca' in rdata and ssl_dir:
298
ca_path = os.path.join(ssl_dir, 'db-client.ca')
299
with open(ca_path, 'w') as fh:
300
fh.write(b64decode(rdata['ssl_ca']))
302
ctxt['database_ssl_ca'] = ca_path
303
elif 'ssl_ca' in rdata:
304
log("Charm not setup for ssl support but ssl ca found", level=INFO)
307
if 'ssl_cert' in rdata:
308
cert_path = os.path.join(
309
ssl_dir, 'db-client.cert')
310
if not os.path.exists(cert_path):
311
log("Waiting 1m for ssl client cert validity", level=INFO)
314
with open(cert_path, 'w') as fh:
315
fh.write(b64decode(rdata['ssl_cert']))
317
ctxt['database_ssl_cert'] = cert_path
318
key_path = os.path.join(ssl_dir, 'db-client.key')
319
with open(key_path, 'w') as fh:
320
fh.write(b64decode(rdata['ssl_key']))
322
ctxt['database_ssl_key'] = key_path
327
class IdentityServiceContext(OSContextGenerator):
329
def __init__(self, service=None, service_user=None, rel_name='identity-service'):
330
self.service = service
331
self.service_user = service_user
332
self.rel_name = rel_name
333
self.interfaces = [self.rel_name]
336
log('Generating template context for ' + self.rel_name, level=DEBUG)
339
if self.service and self.service_user:
340
# This is required for pki token signing if we don't want /tmp to
342
cachedir = '/var/cache/%s' % (self.service)
343
if not os.path.isdir(cachedir):
344
log("Creating service cache dir %s" % (cachedir), level=DEBUG)
345
mkdir(path=cachedir, owner=self.service_user,
346
group=self.service_user, perms=0o700)
348
ctxt['signing_dir'] = cachedir
350
for rid in relation_ids(self.rel_name):
351
for unit in related_units(rid):
352
rdata = relation_get(rid=rid, unit=unit)
353
serv_host = rdata.get('service_host')
354
serv_host = format_ipv6_addr(serv_host) or serv_host
355
auth_host = rdata.get('auth_host')
356
auth_host = format_ipv6_addr(auth_host) or auth_host
357
svc_protocol = rdata.get('service_protocol') or 'http'
358
auth_protocol = rdata.get('auth_protocol') or 'http'
359
ctxt.update({'service_port': rdata.get('service_port'),
360
'service_host': serv_host,
361
'auth_host': auth_host,
362
'auth_port': rdata.get('auth_port'),
363
'admin_tenant_name': rdata.get('service_tenant'),
364
'admin_user': rdata.get('service_username'),
365
'admin_password': rdata.get('service_password'),
366
'service_protocol': svc_protocol,
367
'auth_protocol': auth_protocol})
369
if context_complete(ctxt):
370
# NOTE(jamespage) this is required for >= icehouse
371
# so a missing value just indicates keystone needs
373
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
379
class AMQPContext(OSContextGenerator):
381
def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
382
self.ssl_dir = ssl_dir
383
self.rel_name = rel_name
384
self.relation_prefix = relation_prefix
385
self.interfaces = [rel_name]
388
log('Generating template context for amqp', level=DEBUG)
390
if self.relation_prefix:
391
user_setting = '%s-rabbit-user' % (self.relation_prefix)
392
vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
394
user_setting = 'rabbit-user'
395
vhost_setting = 'rabbit-vhost'
398
username = conf[user_setting]
399
vhost = conf[vhost_setting]
400
except KeyError as e:
401
log('Could not generate shared_db context. Missing required charm '
402
'config options: %s.' % e, level=ERROR)
406
for rid in relation_ids(self.rel_name):
408
for unit in related_units(rid):
409
if relation_get('clustered', rid=rid, unit=unit):
410
ctxt['clustered'] = True
411
vip = relation_get('vip', rid=rid, unit=unit)
412
vip = format_ipv6_addr(vip) or vip
413
ctxt['rabbitmq_host'] = vip
415
host = relation_get('private-address', rid=rid, unit=unit)
416
host = format_ipv6_addr(host) or host
417
ctxt['rabbitmq_host'] = host
420
'rabbitmq_user': username,
421
'rabbitmq_password': relation_get('password', rid=rid,
423
'rabbitmq_virtual_host': vhost,
426
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
428
ctxt['rabbit_ssl_port'] = ssl_port
430
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
432
ctxt['rabbit_ssl_ca'] = ssl_ca
434
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
435
ctxt['rabbitmq_ha_queues'] = True
437
ha_vip_only = relation_get('ha-vip-only',
438
rid=rid, unit=unit) is not None
440
if context_complete(ctxt):
441
if 'rabbit_ssl_ca' in ctxt:
443
log("Charm not setup for ssl support but ssl ca "
447
ca_path = os.path.join(
448
self.ssl_dir, 'rabbit-client-ca.pem')
449
with open(ca_path, 'w') as fh:
450
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
451
ctxt['rabbit_ssl_ca'] = ca_path
453
# Sufficient information found = break out!
456
# Used for active/active rabbitmq >= grizzly
457
if (('clustered' not in ctxt or ha_vip_only) and
458
len(related_units(rid)) > 1):
460
for unit in related_units(rid):
461
host = relation_get('private-address', rid=rid, unit=unit)
462
host = format_ipv6_addr(host) or host
463
rabbitmq_hosts.append(host)
465
ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
467
oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
468
if oslo_messaging_flags:
469
ctxt['oslo_messaging_flags'] = config_flags_parser(
470
oslo_messaging_flags)
472
if not context_complete(ctxt):
478
class CephContext(OSContextGenerator):
479
"""Generates context for /etc/ceph/ceph.conf templates."""
480
interfaces = ['ceph']
483
if not relation_ids('ceph'):
486
log('Generating template context for ceph', level=DEBUG)
490
use_syslog = str(config('use-syslog')).lower()
491
for rid in relation_ids('ceph'):
492
for unit in related_units(rid):
493
auth = relation_get('auth', rid=rid, unit=unit)
494
key = relation_get('key', rid=rid, unit=unit)
495
ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
497
unit_priv_addr = relation_get('private-address', rid=rid,
499
ceph_addr = ceph_pub_addr or unit_priv_addr
500
ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
501
mon_hosts.append(ceph_addr)
503
ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
506
'use_syslog': use_syslog}
508
if not os.path.isdir('/etc/ceph'):
509
os.mkdir('/etc/ceph')
511
if not context_complete(ctxt):
514
ensure_packages(['ceph-common'])
518
class HAProxyContext(OSContextGenerator):
519
"""Provides half a context for the haproxy template, which describes
520
all peers to be included in the cluster. Each charm needs to include
521
its own context generator that describes the port mapping.
523
interfaces = ['cluster']
525
def __init__(self, singlenode_mode=False):
526
self.singlenode_mode = singlenode_mode
529
if not relation_ids('cluster') and not self.singlenode_mode:
532
if config('prefer-ipv6'):
533
addr = get_ipv6_addr(exc_list=[config('vip')])[0]
535
addr = get_host_ip(unit_get('private-address'))
537
l_unit = local_unit().replace('/', '-')
540
# NOTE(jamespage): build out map of configured network endpoints
541
# and associated backends
542
for addr_type in ADDRESS_TYPES:
543
cfg_opt = 'os-{}-network'.format(addr_type)
544
laddr = get_address_in_network(config(cfg_opt))
546
netmask = get_netmask_for_address(laddr)
547
cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
549
'backends': {l_unit: laddr}}
550
for rid in relation_ids('cluster'):
551
for unit in related_units(rid):
552
_laddr = relation_get('{}-address'.format(addr_type),
555
_unit = unit.replace('/', '-')
556
cluster_hosts[laddr]['backends'][_unit] = _laddr
558
# NOTE(jamespage) add backend based on private address - this
559
# with either be the only backend or the fallback if no acls
560
# match in the frontend
561
cluster_hosts[addr] = {}
562
netmask = get_netmask_for_address(addr)
563
cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
564
'backends': {l_unit: addr}}
565
for rid in relation_ids('cluster'):
566
for unit in related_units(rid):
567
_laddr = relation_get('private-address',
570
_unit = unit.replace('/', '-')
571
cluster_hosts[addr]['backends'][_unit] = _laddr
574
'frontends': cluster_hosts,
575
'default_backend': addr
578
if config('haproxy-server-timeout'):
579
ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
581
if config('haproxy-client-timeout'):
582
ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
584
if config('prefer-ipv6'):
586
ctxt['local_host'] = 'ip6-localhost'
587
ctxt['haproxy_host'] = '::'
588
ctxt['stat_port'] = ':::8888'
590
ctxt['local_host'] = '127.0.0.1'
591
ctxt['haproxy_host'] = '0.0.0.0'
592
ctxt['stat_port'] = ':8888'
594
for frontend in cluster_hosts:
595
if (len(cluster_hosts[frontend]['backends']) > 1 or
596
self.singlenode_mode):
597
# Enable haproxy when we have enough peers.
598
log('Ensuring haproxy enabled in /etc/default/haproxy.',
600
with open('/etc/default/haproxy', 'w') as out:
601
out.write('ENABLED=1\n')
605
log('HAProxy context is incomplete, this unit has no peers.',
610
class ImageServiceContext(OSContextGenerator):
611
interfaces = ['image-service']
614
"""Obtains the glance API server from the image-service relation.
615
Useful in nova and cinder (currently).
617
log('Generating template context for image-service.', level=DEBUG)
618
rids = relation_ids('image-service')
623
for unit in related_units(rid):
624
api_server = relation_get('glance-api-server',
627
return {'glance_api_servers': api_server}
629
log("ImageService context is incomplete. Missing required relation "
634
class ApacheSSLContext(OSContextGenerator):
635
"""Generates a context for an apache vhost configuration that configures
636
HTTPS reverse proxying for one or many endpoints. Generated context
637
looks something like::
640
'namespace': 'cinder',
641
'private_address': 'iscsi.mycinderhost.com',
642
'endpoints': [(8776, 8766), (8777, 8767)]
645
The endpoints list consists of a tuples mapping external ports
648
interfaces = ['https']
650
# charms should inherit this context and set external ports
651
# and service namespace accordingly.
653
service_namespace = None
655
def enable_modules(self):
656
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
659
def configure_cert(self, cn=None):
660
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
662
cert, key = get_cert(cn)
664
cert_filename = 'cert_{}'.format(cn)
665
key_filename = 'key_{}'.format(cn)
667
cert_filename = 'cert'
670
write_file(path=os.path.join(ssl_dir, cert_filename),
671
content=b64decode(cert))
672
write_file(path=os.path.join(ssl_dir, key_filename),
673
content=b64decode(key))
675
def configure_ca(self):
676
ca_cert = get_ca_cert()
678
install_ca_cert(b64decode(ca_cert))
680
def canonical_names(self):
681
"""Figure out which canonical names clients will access this service.
684
for r_id in relation_ids('identity-service'):
685
for unit in related_units(r_id):
686
rdata = relation_get(rid=r_id, unit=unit)
688
if k.startswith('ssl_key_'):
689
cns.append(k.lstrip('ssl_key_'))
691
return sorted(list(set(cns)))
693
def get_network_addresses(self):
694
"""For each network configured, return corresponding address and vip
697
Returns a list of tuples of the form:
699
[(address_in_net_a, vip_in_net_a),
700
(address_in_net_b, vip_in_net_b),
703
or, if no vip(s) available:
705
[(address_in_net_a, address_in_net_a),
706
(address_in_net_b, address_in_net_b),
711
vips = config('vip').split()
715
for net_type in ['os-internal-network', 'os-admin-network',
716
'os-public-network']:
717
addr = get_address_in_network(config(net_type),
718
unit_get('private-address'))
719
if len(vips) > 1 and is_clustered():
720
if not config(net_type):
721
log("Multiple networks configured but net_type "
722
"is None (%s)." % net_type, level=WARNING)
726
if is_address_in_network(config(net_type), vip):
727
addresses.append((addr, vip))
730
elif is_clustered() and config('vip'):
731
addresses.append((addr, config('vip')))
733
addresses.append((addr, addr))
735
return sorted(addresses)
738
if isinstance(self.external_ports, six.string_types):
739
self.external_ports = [self.external_ports]
741
if not self.external_ports or not https():
745
self.enable_modules()
747
ctxt = {'namespace': self.service_namespace,
751
cns = self.canonical_names()
754
self.configure_cert(cn)
756
# Expect cert/key provided in config (currently assumed that ca
758
cn = resolve_address(endpoint_type=INTERNAL)
759
self.configure_cert(cn)
761
addresses = self.get_network_addresses()
762
for address, endpoint in sorted(set(addresses)):
763
for api_port in self.external_ports:
764
ext_port = determine_apache_port(api_port,
765
singlenode_mode=True)
766
int_port = determine_api_port(api_port, singlenode_mode=True)
767
portmap = (address, endpoint, int(ext_port), int(int_port))
768
ctxt['endpoints'].append(portmap)
769
ctxt['ext_ports'].append(int(ext_port))
771
ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
775
class NeutronContext(OSContextGenerator):
783
def network_manager(self):
788
return neutron_plugin_attribute(self.plugin, 'packages',
789
self.network_manager)
792
def neutron_security_groups(self):
795
def _ensure_packages(self):
796
for pkgs in self.packages:
797
ensure_packages(pkgs)
799
def _save_flag_file(self):
800
if self.network_manager == 'quantum':
801
_file = '/etc/nova/quantum_plugin.conf'
803
_file = '/etc/nova/neutron_plugin.conf'
805
with open(_file, 'wb') as out:
806
out.write(self.plugin + '\n')
809
driver = neutron_plugin_attribute(self.plugin, 'driver',
810
self.network_manager)
811
config = neutron_plugin_attribute(self.plugin, 'config',
812
self.network_manager)
813
ovs_ctxt = {'core_plugin': driver,
814
'neutron_plugin': 'ovs',
815
'neutron_security_groups': self.neutron_security_groups,
816
'local_ip': unit_private_ip(),
821
def nuage_ctxt(self):
822
driver = neutron_plugin_attribute(self.plugin, 'driver',
823
self.network_manager)
824
config = neutron_plugin_attribute(self.plugin, 'config',
825
self.network_manager)
826
nuage_ctxt = {'core_plugin': driver,
827
'neutron_plugin': 'vsp',
828
'neutron_security_groups': self.neutron_security_groups,
829
'local_ip': unit_private_ip(),
835
driver = neutron_plugin_attribute(self.plugin, 'driver',
836
self.network_manager)
837
config = neutron_plugin_attribute(self.plugin, 'config',
838
self.network_manager)
839
nvp_ctxt = {'core_plugin': driver,
840
'neutron_plugin': 'nvp',
841
'neutron_security_groups': self.neutron_security_groups,
842
'local_ip': unit_private_ip(),
848
driver = neutron_plugin_attribute(self.plugin, 'driver',
849
self.network_manager)
850
n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
851
self.network_manager)
852
n1kv_user_config_flags = config('n1kv-config-flags')
853
restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
854
n1kv_ctxt = {'core_plugin': driver,
855
'neutron_plugin': 'n1kv',
856
'neutron_security_groups': self.neutron_security_groups,
857
'local_ip': unit_private_ip(),
858
'config': n1kv_config,
859
'vsm_ip': config('n1kv-vsm-ip'),
860
'vsm_username': config('n1kv-vsm-username'),
861
'vsm_password': config('n1kv-vsm-password'),
862
'restrict_policy_profiles': restrict_policy_profiles}
864
if n1kv_user_config_flags:
865
flags = config_flags_parser(n1kv_user_config_flags)
866
n1kv_ctxt['user_config_flags'] = flags
870
def calico_ctxt(self):
871
driver = neutron_plugin_attribute(self.plugin, 'driver',
872
self.network_manager)
873
config = neutron_plugin_attribute(self.plugin, 'config',
874
self.network_manager)
875
calico_ctxt = {'core_plugin': driver,
876
'neutron_plugin': 'Calico',
877
'neutron_security_groups': self.neutron_security_groups,
878
'local_ip': unit_private_ip(),
883
def neutron_ctxt(self):
892
host = unit_get('private-address')
894
ctxt = {'network_manager': self.network_manager,
895
'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
899
if self.network_manager not in ['quantum', 'neutron']:
905
ctxt = self.neutron_ctxt()
907
if self.plugin == 'ovs':
908
ctxt.update(self.ovs_ctxt())
909
elif self.plugin in ['nvp', 'nsx']:
910
ctxt.update(self.nvp_ctxt())
911
elif self.plugin == 'n1kv':
912
ctxt.update(self.n1kv_ctxt())
913
elif self.plugin == 'Calico':
914
ctxt.update(self.calico_ctxt())
915
elif self.plugin == 'vsp':
916
ctxt.update(self.nuage_ctxt())
918
alchemy_flags = config('neutron-alchemy-flags')
920
flags = config_flags_parser(alchemy_flags)
921
ctxt['neutron_alchemy_flags'] = flags
923
self._save_flag_file()
927
class NeutronPortContext(OSContextGenerator):
929
def resolve_ports(self, ports):
930
"""Resolve NICs not yet bound to bridge(s)
932
If hwaddress provided then returns resolved hwaddress otherwise NIC.
939
for nic in list_nics():
940
# Ignore virtual interfaces (bond masters will be identified from
942
if not is_phy_iface(nic):
945
_nic = get_bond_master(nic)
947
log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
951
hwaddr = get_nic_hwaddr(nic)
952
hwaddr_to_nic[hwaddr] = nic
953
addresses = get_ipv4_addr(nic, fatal=False)
954
addresses += get_ipv6_addr(iface=nic, fatal=False)
955
hwaddr_to_ip[hwaddr] = addresses
958
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
960
if re.match(mac_regex, entry):
961
# NIC is in known NICs and does NOT hace an IP address
962
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
963
# If the nic is part of a bridge then don't use it
964
if is_bridge_member(hwaddr_to_nic[entry]):
967
# Entry is a MAC address for a valid interface that doesn't
968
# have an IP address assigned yet.
969
resolved.append(hwaddr_to_nic[entry])
971
# If the passed entry is not a MAC address, assume it's a valid
972
# interface, and that the user put it there on purpose (we can
973
# trust it to be the real external network).
974
resolved.append(entry)
976
# Ensure no duplicates
977
return list(set(resolved))
980
class OSConfigFlagContext(OSContextGenerator):
981
"""Provides support for user-defined config flags.
983
Users can define a comma-seperated list of key=value pairs
984
in the charm configuration and apply them at any point in
985
any file by using a template flag.
987
Sometimes users might want config flags inserted within a
988
specific section so this class allows users to specify the
989
template flag name, allowing for multiple template flags
990
(sections) within the same context.
992
NOTE: the value of config-flags may be a comma-separated list of
993
key=value pairs and some Openstack config files support
994
comma-separated lists as values.
997
def __init__(self, charm_flag='config-flags',
998
template_flag='user_config_flags'):
1000
:param charm_flag: config flags in charm configuration.
1001
:param template_flag: insert point for user-defined flags in template
1004
super(OSConfigFlagContext, self).__init__()
1005
self._charm_flag = charm_flag
1006
self._template_flag = template_flag
1009
config_flags = config(self._charm_flag)
1010
if not config_flags:
1013
return {self._template_flag:
1014
config_flags_parser(config_flags)}
1017
class SubordinateConfigContext(OSContextGenerator):
1020
Responsible for inspecting relations to subordinates that
1021
may be exporting required config via a json blob.
1023
The subordinate interface allows subordinates to export their
1024
configuration requirements to the principle for multiple config
1025
files and multiple serivces. Ie, a subordinate that has interfaces
1026
to both glance and nova may export to following yaml blob as json::
1029
/etc/glance/glance-api.conf:
1033
/etc/glance/glance-registry.conf:
1037
/etc/nova/nova.conf:
1043
It is then up to the principle charms to subscribe this context to
1044
the service+config file it is interestd in. Configuration data will
1045
be available in the template context, in glance's case, as::
1048
... other context ...
1049
'subordinate_config': {
1060
def __init__(self, service, config_file, interface):
1062
:param service : Service name key to query in any subordinate
1064
:param config_file : Service's config file to query sections
1065
:param interface : Subordinate interface to inspect
1067
self.config_file = config_file
1068
if isinstance(service, list):
1069
self.services = service
1071
self.services = [service]
1072
if isinstance(interface, list):
1073
self.interfaces = interface
1075
self.interfaces = [interface]
1078
ctxt = {'sections': {}}
1080
for interface in self.interfaces:
1081
rids.extend(relation_ids(interface))
1083
for unit in related_units(rid):
1084
sub_config = relation_get('subordinate_configuration',
1086
if sub_config and sub_config != '':
1088
sub_config = json.loads(sub_config)
1090
log('Could not parse JSON from subordinate_config '
1091
'setting from %s' % rid, level=ERROR)
1094
for service in self.services:
1095
if service not in sub_config:
1096
log('Found subordinate_config on %s but it contained'
1097
'nothing for %s service' % (rid, service),
1101
sub_config = sub_config[service]
1102
if self.config_file not in sub_config:
1103
log('Found subordinate_config on %s but it contained'
1104
'nothing for %s' % (rid, self.config_file),
1108
sub_config = sub_config[self.config_file]
1109
for k, v in six.iteritems(sub_config):
1111
for section, config_list in six.iteritems(v):
1112
log("adding section '%s'" % (section),
1114
if ctxt[k].get(section):
1115
ctxt[k][section].extend(config_list)
1117
ctxt[k][section] = config_list
1120
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
1124
class LogLevelContext(OSContextGenerator):
1129
False if config('debug') is None else config('debug')
1131
False if config('verbose') is None else config('verbose')
1136
class SyslogContext(OSContextGenerator):
1139
ctxt = {'use_syslog': config('use-syslog')}
1143
class BindHostContext(OSContextGenerator):
1146
if config('prefer-ipv6'):
1147
return {'bind_host': '::'}
1149
return {'bind_host': '0.0.0.0'}
1152
class WorkerConfigContext(OSContextGenerator):
1157
from psutil import NUM_CPUS
1159
apt_install('python-psutil', fatal=True)
1160
from psutil import NUM_CPUS
1165
multiplier = config('worker-multiplier') or 0
1166
ctxt = {"workers": self.num_cpus * multiplier}
1170
class ZeroMQContext(OSContextGenerator):
1171
interfaces = ['zeromq-configuration']
1175
if is_relation_made('zeromq-configuration', 'host'):
1176
for rid in relation_ids('zeromq-configuration'):
1177
for unit in related_units(rid):
1178
ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
1179
ctxt['zmq_host'] = relation_get('host', unit, rid)
1180
ctxt['zmq_redis_address'] = relation_get(
1181
'zmq_redis_address', unit, rid)
1186
class NotificationDriverContext(OSContextGenerator):
1188
def __init__(self, zmq_relation='zeromq-configuration',
1189
amqp_relation='amqp'):
1191
:param zmq_relation: Name of Zeromq relation to check
1193
self.zmq_relation = zmq_relation
1194
self.amqp_relation = amqp_relation
1197
ctxt = {'notifications': 'False'}
1198
if is_relation_made(self.amqp_relation):
1199
ctxt['notifications'] = "True"
1204
class SysctlContext(OSContextGenerator):
1205
"""This context check if the 'sysctl' option exists on configuration
1206
then creates a file with the loaded contents"""
1208
sysctl_dict = config('sysctl')
1210
sysctl_create(sysctl_dict,
1211
'/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
1212
return {'sysctl': sysctl_dict}
1215
class NeutronAPIContext(OSContextGenerator):
1217
Inspects current neutron-plugin-api relation for neutron settings. Return
1218
defaults if it is not present.
1220
interfaces = ['neutron-plugin-api']
1223
self.neutron_defaults = {
1225
'rel_key': 'l2-population',
1228
'overlay_network_type': {
1229
'rel_key': 'overlay-network-type',
1232
'neutron_security_groups': {
1233
'rel_key': 'neutron-security-groups',
1236
'network_device_mtu': {
1237
'rel_key': 'network-device-mtu',
1241
'rel_key': 'enable-dvr',
1245
'rel_key': 'enable-l3ha',
1249
ctxt = self.get_neutron_options({})
1250
for rid in relation_ids('neutron-plugin-api'):
1251
for unit in related_units(rid):
1252
rdata = relation_get(rid=rid, unit=unit)
1253
if 'l2-population' in rdata:
1254
ctxt.update(self.get_neutron_options(rdata))
1258
def get_neutron_options(self, rdata):
1260
for nkey in self.neutron_defaults.keys():
1261
defv = self.neutron_defaults[nkey]['default']
1262
rkey = self.neutron_defaults[nkey]['rel_key']
1263
if rkey in rdata.keys():
1264
if type(defv) is bool:
1265
settings[nkey] = bool_from_string(rdata[rkey])
1267
settings[nkey] = rdata[rkey]
1269
settings[nkey] = defv
1273
class ExternalPortContext(NeutronPortContext):
1277
ports = config('ext-port')
1279
ports = [p.strip() for p in ports.split()]
1280
ports = self.resolve_ports(ports)
1282
ctxt = {"ext_port": ports[0]}
1283
napi_settings = NeutronAPIContext()()
1284
mtu = napi_settings.get('network_device_mtu')
1286
ctxt['ext_port_mtu'] = mtu
1291
class DataPortContext(NeutronPortContext):
1294
ports = config('data-port')
1296
# Map of {port/mac:bridge}
1297
portmap = parse_data_port_mappings(ports)
1298
ports = portmap.keys()
1299
# Resolve provided ports or mac addresses and filter out those
1300
# already attached to a bridge.
1301
resolved = self.resolve_ports(ports)
1302
# FIXME: is this necessary?
1303
normalized = {get_nic_hwaddr(port): port for port in resolved
1304
if port not in ports}
1305
normalized.update({port: port for port in resolved
1308
return {bridge: normalized[port] for port, bridge in
1309
six.iteritems(portmap) if port in normalized.keys()}
1314
class PhyNICMTUContext(DataPortContext):
1318
mappings = super(PhyNICMTUContext, self).__call__()
1319
if mappings and mappings.values():
1320
ports = mappings.values()
1321
napi_settings = NeutronAPIContext()()
1322
mtu = napi_settings.get('network_device_mtu')
1324
ctxt["devs"] = '\\n'.join(ports)
1330
class NetworkServiceContext(OSContextGenerator):
1332
def __init__(self, rel_name='quantum-network-service'):
1333
self.rel_name = rel_name
1334
self.interfaces = [rel_name]
1337
for rid in relation_ids(self.rel_name):
1338
for unit in related_units(rid):
1339
rdata = relation_get(rid=rid, unit=unit)
1341
'keystone_host': rdata.get('keystone_host'),
1342
'service_port': rdata.get('service_port'),
1343
'auth_port': rdata.get('auth_port'),
1344
'service_tenant': rdata.get('service_tenant'),
1345
'service_username': rdata.get('service_username'),
1346
'service_password': rdata.get('service_password'),
1347
'quantum_host': rdata.get('quantum_host'),
1348
'quantum_port': rdata.get('quantum_port'),
1349
'quantum_url': rdata.get('quantum_url'),
1350
'region': rdata.get('region'),
1352
rdata.get('service_protocol') or 'http',
1354
rdata.get('auth_protocol') or 'http',
1356
if context_complete(ctxt):