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 (
58
from charmhelpers.contrib.hahelpers.cluster import (
59
determine_apache_port,
64
from charmhelpers.contrib.hahelpers.apache import (
69
from charmhelpers.contrib.openstack.neutron import (
70
neutron_plugin_attribute,
71
parse_data_port_mappings,
73
from charmhelpers.contrib.openstack.ip import (
77
from charmhelpers.contrib.network.ip import (
78
get_address_in_network,
81
get_netmask_for_address,
83
is_address_in_network,
86
from charmhelpers.contrib.openstack.utils import get_host_ip
87
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
88
ADDRESS_TYPES = ['admin', 'internal', 'public']
91
class OSContextError(Exception):
95
def ensure_packages(packages):
96
"""Install but do not upgrade required plugin packages."""
97
required = filter_installed_packages(packages)
99
apt_install(required, fatal=True)
102
def context_complete(ctxt):
104
for k, v in six.iteritems(ctxt):
105
if v is None or v == '':
109
log('Missing required data: %s' % ' '.join(_missing), level=INFO)
115
def config_flags_parser(config_flags):
116
"""Parses config flags string into dict.
118
This parsing method supports a few different formats for the config
119
flag values to be parsed:
121
1. A string in the simple format of key=value pairs, with the possibility
122
of specifying multiple key value pairs within the same string. For
123
example, a string in the format of 'key1=value1, key2=value2' will
128
2. A string in the above format, but supporting a comma-delimited list
129
of values for the same key. For example, a string in the format of
130
'key1=value1, key2=value3,value4,value5' will return a dict of:
132
'key2', 'value2,value3,value4'}
134
3. A string containing a colon character (:) prior to an equal
135
character (=) will be treated as yaml and parsed as such. This can be
136
used to specify more complex key value pairs. For example,
137
a string in the format of 'key1: subkey1=value1, subkey2=value2' will
139
{'key1', 'subkey1=value1, subkey2=value2'}
141
The provided config_flags string may be a list of comma-separated values
142
which themselves may be comma-separated list of values.
144
# If we find a colon before an equals sign then treat it as yaml.
145
# Note: limit it to finding the colon first since this indicates assignment
147
colon = config_flags.find(':')
148
equals = config_flags.find('=')
150
if colon < equals or equals < 0:
151
return yaml.safe_load(config_flags)
153
if config_flags.find('==') >= 0:
154
log("config_flags is not in expected format (key=value)", level=ERROR)
157
# strip the following from each value.
158
post_strippers = ' ,'
159
# we strip any leading/trailing '=' or ' ' from the string then
161
split = config_flags.strip(' =').split('=')
164
for i in range(0, limit - 1):
167
vindex = next.rfind(',')
168
if (i == limit - 2) or (vindex < 0):
171
value = next[:vindex]
176
# if this not the first entry, expect an embedded key.
177
index = current.rfind(',')
179
log("Invalid config value(s) at index %s" % (i), level=ERROR)
181
key = current[index + 1:]
184
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
189
class OSContextGenerator(object):
190
"""Base class for all context generators."""
194
raise NotImplementedError
197
class SharedDBContext(OSContextGenerator):
198
interfaces = ['shared-db']
201
database=None, user=None, relation_prefix=None, ssl_dir=None):
202
"""Allows inspecting relation for settings prefixed with
203
relation_prefix. This is useful for parsing access for multiple
204
databases returned via the shared-db interface (eg, nova_password,
207
self.relation_prefix = relation_prefix
208
self.database = database
210
self.ssl_dir = ssl_dir
213
self.database = self.database or config('database')
214
self.user = self.user or config('database-user')
215
if None in [self.database, self.user]:
216
log("Could not generate shared_db context. Missing required charm "
217
"config options. (database name and user)", level=ERROR)
222
# NOTE(jamespage) if mysql charm provides a network upon which
223
# access to the database should be made, reconfigure relation
224
# with the service units local address and defer execution
225
access_network = relation_get('access-network')
226
if access_network is not None:
227
if self.relation_prefix is not None:
228
hostname_key = "{}_hostname".format(self.relation_prefix)
230
hostname_key = "hostname"
231
access_hostname = get_address_in_network(access_network,
232
unit_get('private-address'))
233
set_hostname = relation_get(attribute=hostname_key,
235
if set_hostname != access_hostname:
236
relation_set(relation_settings={hostname_key: access_hostname})
237
return None # Defer any further hook execution for now....
239
password_setting = 'password'
240
if self.relation_prefix:
241
password_setting = self.relation_prefix + '_password'
243
for rid in relation_ids(self.interfaces[0]):
244
for unit in related_units(rid):
245
rdata = relation_get(rid=rid, unit=unit)
246
host = rdata.get('db_host')
247
host = format_ipv6_addr(host) or host
249
'database_host': host,
250
'database': self.database,
251
'database_user': self.user,
252
'database_password': rdata.get(password_setting),
253
'database_type': 'mysql'
255
if context_complete(ctxt):
256
db_ssl(rdata, ctxt, self.ssl_dir)
261
class PostgresqlDBContext(OSContextGenerator):
262
interfaces = ['pgsql-db']
264
def __init__(self, database=None):
265
self.database = database
268
self.database = self.database or config('database')
269
if self.database is None:
270
log('Could not generate postgresql_db context. Missing required '
271
'charm config options. (database name)', level=ERROR)
275
for rid in relation_ids(self.interfaces[0]):
276
for unit in related_units(rid):
277
rel_host = relation_get('host', rid=rid, unit=unit)
278
rel_user = relation_get('user', rid=rid, unit=unit)
279
rel_passwd = relation_get('password', rid=rid, unit=unit)
280
ctxt = {'database_host': rel_host,
281
'database': self.database,
282
'database_user': rel_user,
283
'database_password': rel_passwd,
284
'database_type': 'postgresql'}
285
if context_complete(ctxt):
291
def db_ssl(rdata, ctxt, ssl_dir):
292
if 'ssl_ca' in rdata and ssl_dir:
293
ca_path = os.path.join(ssl_dir, 'db-client.ca')
294
with open(ca_path, 'w') as fh:
295
fh.write(b64decode(rdata['ssl_ca']))
297
ctxt['database_ssl_ca'] = ca_path
298
elif 'ssl_ca' in rdata:
299
log("Charm not setup for ssl support but ssl ca found", level=INFO)
302
if 'ssl_cert' in rdata:
303
cert_path = os.path.join(
304
ssl_dir, 'db-client.cert')
305
if not os.path.exists(cert_path):
306
log("Waiting 1m for ssl client cert validity", level=INFO)
309
with open(cert_path, 'w') as fh:
310
fh.write(b64decode(rdata['ssl_cert']))
312
ctxt['database_ssl_cert'] = cert_path
313
key_path = os.path.join(ssl_dir, 'db-client.key')
314
with open(key_path, 'w') as fh:
315
fh.write(b64decode(rdata['ssl_key']))
317
ctxt['database_ssl_key'] = key_path
322
class IdentityServiceContext(OSContextGenerator):
324
def __init__(self, service=None, service_user=None, rel_name='identity-service'):
325
self.service = service
326
self.service_user = service_user
327
self.rel_name = rel_name
328
self.interfaces = [self.rel_name]
331
log('Generating template context for ' + self.rel_name, level=DEBUG)
334
if self.service and self.service_user:
335
# This is required for pki token signing if we don't want /tmp to
337
cachedir = '/var/cache/%s' % (self.service)
338
if not os.path.isdir(cachedir):
339
log("Creating service cache dir %s" % (cachedir), level=DEBUG)
340
mkdir(path=cachedir, owner=self.service_user,
341
group=self.service_user, perms=0o700)
343
ctxt['signing_dir'] = cachedir
345
for rid in relation_ids(self.rel_name):
346
for unit in related_units(rid):
347
rdata = relation_get(rid=rid, unit=unit)
348
serv_host = rdata.get('service_host')
349
serv_host = format_ipv6_addr(serv_host) or serv_host
350
auth_host = rdata.get('auth_host')
351
auth_host = format_ipv6_addr(auth_host) or auth_host
352
svc_protocol = rdata.get('service_protocol') or 'http'
353
auth_protocol = rdata.get('auth_protocol') or 'http'
354
ctxt.update({'service_port': rdata.get('service_port'),
355
'service_host': serv_host,
356
'auth_host': auth_host,
357
'auth_port': rdata.get('auth_port'),
358
'admin_tenant_name': rdata.get('service_tenant'),
359
'admin_user': rdata.get('service_username'),
360
'admin_password': rdata.get('service_password'),
361
'service_protocol': svc_protocol,
362
'auth_protocol': auth_protocol})
364
if context_complete(ctxt):
365
# NOTE(jamespage) this is required for >= icehouse
366
# so a missing value just indicates keystone needs
368
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
374
class AMQPContext(OSContextGenerator):
376
def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
377
self.ssl_dir = ssl_dir
378
self.rel_name = rel_name
379
self.relation_prefix = relation_prefix
380
self.interfaces = [rel_name]
383
log('Generating template context for amqp', level=DEBUG)
385
if self.relation_prefix:
386
user_setting = '%s-rabbit-user' % (self.relation_prefix)
387
vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
389
user_setting = 'rabbit-user'
390
vhost_setting = 'rabbit-vhost'
393
username = conf[user_setting]
394
vhost = conf[vhost_setting]
395
except KeyError as e:
396
log('Could not generate shared_db context. Missing required charm '
397
'config options: %s.' % e, level=ERROR)
401
for rid in relation_ids(self.rel_name):
403
for unit in related_units(rid):
404
if relation_get('clustered', rid=rid, unit=unit):
405
ctxt['clustered'] = True
406
vip = relation_get('vip', rid=rid, unit=unit)
407
vip = format_ipv6_addr(vip) or vip
408
ctxt['rabbitmq_host'] = vip
410
host = relation_get('private-address', rid=rid, unit=unit)
411
host = format_ipv6_addr(host) or host
412
ctxt['rabbitmq_host'] = host
415
'rabbitmq_user': username,
416
'rabbitmq_password': relation_get('password', rid=rid,
418
'rabbitmq_virtual_host': vhost,
421
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
423
ctxt['rabbit_ssl_port'] = ssl_port
425
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
427
ctxt['rabbit_ssl_ca'] = ssl_ca
429
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
430
ctxt['rabbitmq_ha_queues'] = True
432
ha_vip_only = relation_get('ha-vip-only',
433
rid=rid, unit=unit) is not None
435
if context_complete(ctxt):
436
if 'rabbit_ssl_ca' in ctxt:
438
log("Charm not setup for ssl support but ssl ca "
442
ca_path = os.path.join(
443
self.ssl_dir, 'rabbit-client-ca.pem')
444
with open(ca_path, 'w') as fh:
445
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
446
ctxt['rabbit_ssl_ca'] = ca_path
448
# Sufficient information found = break out!
451
# Used for active/active rabbitmq >= grizzly
452
if (('clustered' not in ctxt or ha_vip_only) and
453
len(related_units(rid)) > 1):
455
for unit in related_units(rid):
456
host = relation_get('private-address', rid=rid, unit=unit)
457
host = format_ipv6_addr(host) or host
458
rabbitmq_hosts.append(host)
460
ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
462
oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
463
if oslo_messaging_flags:
464
ctxt['oslo_messaging_flags'] = config_flags_parser(
465
oslo_messaging_flags)
467
if not context_complete(ctxt):
473
class CephContext(OSContextGenerator):
474
"""Generates context for /etc/ceph/ceph.conf templates."""
475
interfaces = ['ceph']
478
if not relation_ids('ceph'):
481
log('Generating template context for ceph', level=DEBUG)
485
use_syslog = str(config('use-syslog')).lower()
486
for rid in relation_ids('ceph'):
487
for unit in related_units(rid):
488
auth = relation_get('auth', rid=rid, unit=unit)
489
key = relation_get('key', rid=rid, unit=unit)
490
ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
492
unit_priv_addr = relation_get('private-address', rid=rid,
494
ceph_addr = ceph_pub_addr or unit_priv_addr
495
ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
496
mon_hosts.append(ceph_addr)
498
ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
501
'use_syslog': use_syslog}
503
if not os.path.isdir('/etc/ceph'):
504
os.mkdir('/etc/ceph')
506
if not context_complete(ctxt):
509
ensure_packages(['ceph-common'])
513
class HAProxyContext(OSContextGenerator):
514
"""Provides half a context for the haproxy template, which describes
515
all peers to be included in the cluster. Each charm needs to include
516
its own context generator that describes the port mapping.
518
interfaces = ['cluster']
520
def __init__(self, singlenode_mode=False):
521
self.singlenode_mode = singlenode_mode
524
if not relation_ids('cluster') and not self.singlenode_mode:
527
if config('prefer-ipv6'):
528
addr = get_ipv6_addr(exc_list=[config('vip')])[0]
530
addr = get_host_ip(unit_get('private-address'))
532
l_unit = local_unit().replace('/', '-')
535
# NOTE(jamespage): build out map of configured network endpoints
536
# and associated backends
537
for addr_type in ADDRESS_TYPES:
538
cfg_opt = 'os-{}-network'.format(addr_type)
539
laddr = get_address_in_network(config(cfg_opt))
541
netmask = get_netmask_for_address(laddr)
542
cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
544
'backends': {l_unit: laddr}}
545
for rid in relation_ids('cluster'):
546
for unit in related_units(rid):
547
_laddr = relation_get('{}-address'.format(addr_type),
550
_unit = unit.replace('/', '-')
551
cluster_hosts[laddr]['backends'][_unit] = _laddr
553
# NOTE(jamespage) add backend based on private address - this
554
# with either be the only backend or the fallback if no acls
555
# match in the frontend
556
cluster_hosts[addr] = {}
557
netmask = get_netmask_for_address(addr)
558
cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
559
'backends': {l_unit: addr}}
560
for rid in relation_ids('cluster'):
561
for unit in related_units(rid):
562
_laddr = relation_get('private-address',
565
_unit = unit.replace('/', '-')
566
cluster_hosts[addr]['backends'][_unit] = _laddr
569
'frontends': cluster_hosts,
570
'default_backend': addr
573
if config('haproxy-server-timeout'):
574
ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
576
if config('haproxy-client-timeout'):
577
ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
579
if config('prefer-ipv6'):
581
ctxt['local_host'] = 'ip6-localhost'
582
ctxt['haproxy_host'] = '::'
583
ctxt['stat_port'] = ':::8888'
585
ctxt['local_host'] = '127.0.0.1'
586
ctxt['haproxy_host'] = '0.0.0.0'
587
ctxt['stat_port'] = ':8888'
589
for frontend in cluster_hosts:
590
if (len(cluster_hosts[frontend]['backends']) > 1 or
591
self.singlenode_mode):
592
# Enable haproxy when we have enough peers.
593
log('Ensuring haproxy enabled in /etc/default/haproxy.',
595
with open('/etc/default/haproxy', 'w') as out:
596
out.write('ENABLED=1\n')
600
log('HAProxy context is incomplete, this unit has no peers.',
605
class ImageServiceContext(OSContextGenerator):
606
interfaces = ['image-service']
609
"""Obtains the glance API server from the image-service relation.
610
Useful in nova and cinder (currently).
612
log('Generating template context for image-service.', level=DEBUG)
613
rids = relation_ids('image-service')
618
for unit in related_units(rid):
619
api_server = relation_get('glance-api-server',
622
return {'glance_api_servers': api_server}
624
log("ImageService context is incomplete. Missing required relation "
629
class ApacheSSLContext(OSContextGenerator):
630
"""Generates a context for an apache vhost configuration that configures
631
HTTPS reverse proxying for one or many endpoints. Generated context
632
looks something like::
635
'namespace': 'cinder',
636
'private_address': 'iscsi.mycinderhost.com',
637
'endpoints': [(8776, 8766), (8777, 8767)]
640
The endpoints list consists of a tuples mapping external ports
643
interfaces = ['https']
645
# charms should inherit this context and set external ports
646
# and service namespace accordingly.
648
service_namespace = None
650
def enable_modules(self):
651
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
654
def configure_cert(self, cn=None):
655
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
657
cert, key = get_cert(cn)
659
cert_filename = 'cert_{}'.format(cn)
660
key_filename = 'key_{}'.format(cn)
662
cert_filename = 'cert'
665
write_file(path=os.path.join(ssl_dir, cert_filename),
666
content=b64decode(cert))
667
write_file(path=os.path.join(ssl_dir, key_filename),
668
content=b64decode(key))
670
def configure_ca(self):
671
ca_cert = get_ca_cert()
673
install_ca_cert(b64decode(ca_cert))
675
def canonical_names(self):
676
"""Figure out which canonical names clients will access this service.
679
for r_id in relation_ids('identity-service'):
680
for unit in related_units(r_id):
681
rdata = relation_get(rid=r_id, unit=unit)
683
if k.startswith('ssl_key_'):
684
cns.append(k.lstrip('ssl_key_'))
686
return sorted(list(set(cns)))
688
def get_network_addresses(self):
689
"""For each network configured, return corresponding address and vip
692
Returns a list of tuples of the form:
694
[(address_in_net_a, vip_in_net_a),
695
(address_in_net_b, vip_in_net_b),
698
or, if no vip(s) available:
700
[(address_in_net_a, address_in_net_a),
701
(address_in_net_b, address_in_net_b),
706
vips = config('vip').split()
710
for net_type in ['os-internal-network', 'os-admin-network',
711
'os-public-network']:
712
addr = get_address_in_network(config(net_type),
713
unit_get('private-address'))
714
if len(vips) > 1 and is_clustered():
715
if not config(net_type):
716
log("Multiple networks configured but net_type "
717
"is None (%s)." % net_type, level=WARNING)
721
if is_address_in_network(config(net_type), vip):
722
addresses.append((addr, vip))
725
elif is_clustered() and config('vip'):
726
addresses.append((addr, config('vip')))
728
addresses.append((addr, addr))
730
return sorted(addresses)
733
if isinstance(self.external_ports, six.string_types):
734
self.external_ports = [self.external_ports]
736
if not self.external_ports or not https():
740
self.enable_modules()
742
ctxt = {'namespace': self.service_namespace,
746
cns = self.canonical_names()
749
self.configure_cert(cn)
751
# Expect cert/key provided in config (currently assumed that ca
753
cn = resolve_address(endpoint_type=INTERNAL)
754
self.configure_cert(cn)
756
addresses = self.get_network_addresses()
757
for address, endpoint in sorted(set(addresses)):
758
for api_port in self.external_ports:
759
ext_port = determine_apache_port(api_port,
760
singlenode_mode=True)
761
int_port = determine_api_port(api_port, singlenode_mode=True)
762
portmap = (address, endpoint, int(ext_port), int(int_port))
763
ctxt['endpoints'].append(portmap)
764
ctxt['ext_ports'].append(int(ext_port))
766
ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
770
class NeutronContext(OSContextGenerator):
778
def network_manager(self):
783
return neutron_plugin_attribute(self.plugin, 'packages',
784
self.network_manager)
787
def neutron_security_groups(self):
790
def _ensure_packages(self):
791
for pkgs in self.packages:
792
ensure_packages(pkgs)
794
def _save_flag_file(self):
795
if self.network_manager == 'quantum':
796
_file = '/etc/nova/quantum_plugin.conf'
798
_file = '/etc/nova/neutron_plugin.conf'
800
with open(_file, 'wb') as out:
801
out.write(self.plugin + '\n')
804
driver = neutron_plugin_attribute(self.plugin, 'driver',
805
self.network_manager)
806
config = neutron_plugin_attribute(self.plugin, 'config',
807
self.network_manager)
808
ovs_ctxt = {'core_plugin': driver,
809
'neutron_plugin': 'ovs',
810
'neutron_security_groups': self.neutron_security_groups,
811
'local_ip': unit_private_ip(),
816
def nuage_ctxt(self):
817
driver = neutron_plugin_attribute(self.plugin, 'driver',
818
self.network_manager)
819
config = neutron_plugin_attribute(self.plugin, 'config',
820
self.network_manager)
821
nuage_ctxt = {'core_plugin': driver,
822
'neutron_plugin': 'vsp',
823
'neutron_security_groups': self.neutron_security_groups,
824
'local_ip': unit_private_ip(),
830
driver = neutron_plugin_attribute(self.plugin, 'driver',
831
self.network_manager)
832
config = neutron_plugin_attribute(self.plugin, 'config',
833
self.network_manager)
834
nvp_ctxt = {'core_plugin': driver,
835
'neutron_plugin': 'nvp',
836
'neutron_security_groups': self.neutron_security_groups,
837
'local_ip': unit_private_ip(),
843
driver = neutron_plugin_attribute(self.plugin, 'driver',
844
self.network_manager)
845
n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
846
self.network_manager)
847
n1kv_user_config_flags = config('n1kv-config-flags')
848
restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
849
n1kv_ctxt = {'core_plugin': driver,
850
'neutron_plugin': 'n1kv',
851
'neutron_security_groups': self.neutron_security_groups,
852
'local_ip': unit_private_ip(),
853
'config': n1kv_config,
854
'vsm_ip': config('n1kv-vsm-ip'),
855
'vsm_username': config('n1kv-vsm-username'),
856
'vsm_password': config('n1kv-vsm-password'),
857
'restrict_policy_profiles': restrict_policy_profiles}
859
if n1kv_user_config_flags:
860
flags = config_flags_parser(n1kv_user_config_flags)
861
n1kv_ctxt['user_config_flags'] = flags
865
def calico_ctxt(self):
866
driver = neutron_plugin_attribute(self.plugin, 'driver',
867
self.network_manager)
868
config = neutron_plugin_attribute(self.plugin, 'config',
869
self.network_manager)
870
calico_ctxt = {'core_plugin': driver,
871
'neutron_plugin': 'Calico',
872
'neutron_security_groups': self.neutron_security_groups,
873
'local_ip': unit_private_ip(),
878
def neutron_ctxt(self):
887
host = unit_get('private-address')
889
ctxt = {'network_manager': self.network_manager,
890
'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
894
self._ensure_packages()
896
if self.network_manager not in ['quantum', 'neutron']:
902
ctxt = self.neutron_ctxt()
904
if self.plugin == 'ovs':
905
ctxt.update(self.ovs_ctxt())
906
elif self.plugin in ['nvp', 'nsx']:
907
ctxt.update(self.nvp_ctxt())
908
elif self.plugin == 'n1kv':
909
ctxt.update(self.n1kv_ctxt())
910
elif self.plugin == 'Calico':
911
ctxt.update(self.calico_ctxt())
912
elif self.plugin == 'vsp':
913
ctxt.update(self.nuage_ctxt())
915
alchemy_flags = config('neutron-alchemy-flags')
917
flags = config_flags_parser(alchemy_flags)
918
ctxt['neutron_alchemy_flags'] = flags
920
self._save_flag_file()
924
class NeutronPortContext(OSContextGenerator):
925
NIC_PREFIXES = ['eth', 'bond']
927
def resolve_ports(self, ports):
928
"""Resolve NICs not yet bound to bridge(s)
930
If hwaddress provided then returns resolved hwaddress otherwise NIC.
937
for nic in list_nics(self.NIC_PREFIXES):
938
hwaddr = get_nic_hwaddr(nic)
939
hwaddr_to_nic[hwaddr] = nic
940
addresses = get_ipv4_addr(nic, fatal=False)
941
addresses += get_ipv6_addr(iface=nic, fatal=False)
942
hwaddr_to_ip[hwaddr] = addresses
945
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
947
if re.match(mac_regex, entry):
948
# NIC is in known NICs and does NOT hace an IP address
949
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
950
# If the nic is part of a bridge then don't use it
951
if is_bridge_member(hwaddr_to_nic[entry]):
954
# Entry is a MAC address for a valid interface that doesn't
955
# have an IP address assigned yet.
956
resolved.append(hwaddr_to_nic[entry])
958
# If the passed entry is not a MAC address, assume it's a valid
959
# interface, and that the user put it there on purpose (we can
960
# trust it to be the real external network).
961
resolved.append(entry)
966
class OSConfigFlagContext(OSContextGenerator):
967
"""Provides support for user-defined config flags.
969
Users can define a comma-seperated list of key=value pairs
970
in the charm configuration and apply them at any point in
971
any file by using a template flag.
973
Sometimes users might want config flags inserted within a
974
specific section so this class allows users to specify the
975
template flag name, allowing for multiple template flags
976
(sections) within the same context.
978
NOTE: the value of config-flags may be a comma-separated list of
979
key=value pairs and some Openstack config files support
980
comma-separated lists as values.
983
def __init__(self, charm_flag='config-flags',
984
template_flag='user_config_flags'):
986
:param charm_flag: config flags in charm configuration.
987
:param template_flag: insert point for user-defined flags in template
990
super(OSConfigFlagContext, self).__init__()
991
self._charm_flag = charm_flag
992
self._template_flag = template_flag
995
config_flags = config(self._charm_flag)
999
return {self._template_flag:
1000
config_flags_parser(config_flags)}
1003
class SubordinateConfigContext(OSContextGenerator):
1006
Responsible for inspecting relations to subordinates that
1007
may be exporting required config via a json blob.
1009
The subordinate interface allows subordinates to export their
1010
configuration requirements to the principle for multiple config
1011
files and multiple serivces. Ie, a subordinate that has interfaces
1012
to both glance and nova may export to following yaml blob as json::
1015
/etc/glance/glance-api.conf:
1019
/etc/glance/glance-registry.conf:
1023
/etc/nova/nova.conf:
1029
It is then up to the principle charms to subscribe this context to
1030
the service+config file it is interestd in. Configuration data will
1031
be available in the template context, in glance's case, as::
1034
... other context ...
1035
'subordinate_config': {
1046
def __init__(self, service, config_file, interface):
1048
:param service : Service name key to query in any subordinate
1050
:param config_file : Service's config file to query sections
1051
:param interface : Subordinate interface to inspect
1053
self.service = service
1054
self.config_file = config_file
1055
self.interface = interface
1058
ctxt = {'sections': {}}
1059
for rid in relation_ids(self.interface):
1060
for unit in related_units(rid):
1061
sub_config = relation_get('subordinate_configuration',
1063
if sub_config and sub_config != '':
1065
sub_config = json.loads(sub_config)
1067
log('Could not parse JSON from subordinate_config '
1068
'setting from %s' % rid, level=ERROR)
1071
if self.service not in sub_config:
1072
log('Found subordinate_config on %s but it contained'
1073
'nothing for %s service' % (rid, self.service),
1077
sub_config = sub_config[self.service]
1078
if self.config_file not in sub_config:
1079
log('Found subordinate_config on %s but it contained'
1080
'nothing for %s' % (rid, self.config_file),
1084
sub_config = sub_config[self.config_file]
1085
for k, v in six.iteritems(sub_config):
1087
for section, config_dict in six.iteritems(v):
1088
log("adding section '%s'" % (section),
1090
ctxt[k][section] = config_dict
1094
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
1098
class LogLevelContext(OSContextGenerator):
1103
False if config('debug') is None else config('debug')
1105
False if config('verbose') is None else config('verbose')
1110
class SyslogContext(OSContextGenerator):
1113
ctxt = {'use_syslog': config('use-syslog')}
1117
class BindHostContext(OSContextGenerator):
1120
if config('prefer-ipv6'):
1121
return {'bind_host': '::'}
1123
return {'bind_host': '0.0.0.0'}
1126
class WorkerConfigContext(OSContextGenerator):
1131
from psutil import NUM_CPUS
1133
apt_install('python-psutil', fatal=True)
1134
from psutil import NUM_CPUS
1139
multiplier = config('worker-multiplier') or 0
1140
ctxt = {"workers": self.num_cpus * multiplier}
1144
class ZeroMQContext(OSContextGenerator):
1145
interfaces = ['zeromq-configuration']
1149
if is_relation_made('zeromq-configuration', 'host'):
1150
for rid in relation_ids('zeromq-configuration'):
1151
for unit in related_units(rid):
1152
ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
1153
ctxt['zmq_host'] = relation_get('host', unit, rid)
1154
ctxt['zmq_redis_address'] = relation_get(
1155
'zmq_redis_address', unit, rid)
1160
class NotificationDriverContext(OSContextGenerator):
1162
def __init__(self, zmq_relation='zeromq-configuration',
1163
amqp_relation='amqp'):
1165
:param zmq_relation: Name of Zeromq relation to check
1167
self.zmq_relation = zmq_relation
1168
self.amqp_relation = amqp_relation
1171
ctxt = {'notifications': 'False'}
1172
if is_relation_made(self.amqp_relation):
1173
ctxt['notifications'] = "True"
1178
class SysctlContext(OSContextGenerator):
1179
"""This context check if the 'sysctl' option exists on configuration
1180
then creates a file with the loaded contents"""
1182
sysctl_dict = config('sysctl')
1184
sysctl_create(sysctl_dict,
1185
'/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
1186
return {'sysctl': sysctl_dict}
1189
class NeutronAPIContext(OSContextGenerator):
1191
Inspects current neutron-plugin-api relation for neutron settings. Return
1192
defaults if it is not present.
1194
interfaces = ['neutron-plugin-api']
1197
self.neutron_defaults = {
1199
'rel_key': 'l2-population',
1202
'overlay_network_type': {
1203
'rel_key': 'overlay-network-type',
1206
'neutron_security_groups': {
1207
'rel_key': 'neutron-security-groups',
1210
'network_device_mtu': {
1211
'rel_key': 'network-device-mtu',
1215
'rel_key': 'enable-dvr',
1219
'rel_key': 'enable-l3ha',
1223
ctxt = self.get_neutron_options({})
1224
for rid in relation_ids('neutron-plugin-api'):
1225
for unit in related_units(rid):
1226
rdata = relation_get(rid=rid, unit=unit)
1227
if 'l2-population' in rdata:
1228
ctxt.update(self.get_neutron_options(rdata))
1232
def get_neutron_options(self, rdata):
1234
for nkey in self.neutron_defaults.keys():
1235
defv = self.neutron_defaults[nkey]['default']
1236
rkey = self.neutron_defaults[nkey]['rel_key']
1237
if rkey in rdata.keys():
1238
if type(defv) is bool:
1239
settings[nkey] = bool_from_string(rdata[rkey])
1241
settings[nkey] = rdata[rkey]
1243
settings[nkey] = defv
1247
class ExternalPortContext(NeutronPortContext):
1251
ports = config('ext-port')
1253
ports = [p.strip() for p in ports.split()]
1254
ports = self.resolve_ports(ports)
1256
ctxt = {"ext_port": ports[0]}
1257
napi_settings = NeutronAPIContext()()
1258
mtu = napi_settings.get('network_device_mtu')
1260
ctxt['ext_port_mtu'] = mtu
1265
class DataPortContext(NeutronPortContext):
1268
ports = config('data-port')
1270
portmap = parse_data_port_mappings(ports)
1271
ports = portmap.values()
1272
resolved = self.resolve_ports(ports)
1273
normalized = {get_nic_hwaddr(port): port for port in resolved
1274
if port not in ports}
1275
normalized.update({port: port for port in resolved
1278
return {bridge: normalized[port] for bridge, port in
1279
six.iteritems(portmap) if port in normalized.keys()}
1284
class PhyNICMTUContext(DataPortContext):
1288
mappings = super(PhyNICMTUContext, self).__call__()
1289
if mappings and mappings.values():
1290
ports = mappings.values()
1291
napi_settings = NeutronAPIContext()()
1292
mtu = napi_settings.get('network_device_mtu')
1294
ctxt["devs"] = '\\n'.join(ports)
1300
class NetworkServiceContext(OSContextGenerator):
1302
def __init__(self, rel_name='quantum-network-service'):
1303
self.rel_name = rel_name
1304
self.interfaces = [rel_name]
1307
for rid in relation_ids(self.rel_name):
1308
for unit in related_units(rid):
1309
rdata = relation_get(rid=rid, unit=unit)
1311
'keystone_host': rdata.get('keystone_host'),
1312
'service_port': rdata.get('service_port'),
1313
'auth_port': rdata.get('auth_port'),
1314
'service_tenant': rdata.get('service_tenant'),
1315
'service_username': rdata.get('service_username'),
1316
'service_password': rdata.get('service_password'),
1317
'quantum_host': rdata.get('quantum_host'),
1318
'quantum_port': rdata.get('quantum_port'),
1319
'quantum_url': rdata.get('quantum_url'),
1320
'region': rdata.get('region'),
1322
rdata.get('service_protocol') or 'http',
1324
rdata.get('auth_protocol') or 'http',
1326
if context_complete(ctxt):