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('shared-db'):
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(),
879
driver = neutron_plugin_attribute(self.plugin, 'driver',
880
self.network_manager)
881
config = neutron_plugin_attribute(self.plugin, 'config',
882
self.network_manager)
883
ovs_ctxt = {'core_plugin': driver,
884
'neutron_plugin': 'plumgrid',
885
'neutron_security_groups': self.neutron_security_groups,
886
'local_ip': unit_private_ip(),
891
def neutron_ctxt(self):
900
host = unit_get('private-address')
902
ctxt = {'network_manager': self.network_manager,
903
'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
907
self._ensure_packages()
909
if self.network_manager not in ['quantum', 'neutron']:
915
ctxt = self.neutron_ctxt()
917
if self.plugin == 'ovs':
918
ctxt.update(self.ovs_ctxt())
919
elif self.plugin in ['nvp', 'nsx']:
920
ctxt.update(self.nvp_ctxt())
921
elif self.plugin == 'n1kv':
922
ctxt.update(self.n1kv_ctxt())
923
elif self.plugin == 'Calico':
924
ctxt.update(self.calico_ctxt())
925
elif self.plugin == 'vsp':
926
ctxt.update(self.nuage_ctxt())
927
elif self.plugin == 'plumgrid':
928
ctxt.update(self.pg_ctxt())
930
alchemy_flags = config('neutron-alchemy-flags')
932
flags = config_flags_parser(alchemy_flags)
933
ctxt['neutron_alchemy_flags'] = flags
935
self._save_flag_file()
939
class NeutronPortContext(OSContextGenerator):
940
NIC_PREFIXES = ['eth', 'bond']
942
def resolve_ports(self, ports):
943
"""Resolve NICs not yet bound to bridge(s)
945
If hwaddress provided then returns resolved hwaddress otherwise NIC.
952
for nic in list_nics(self.NIC_PREFIXES):
953
hwaddr = get_nic_hwaddr(nic)
954
hwaddr_to_nic[hwaddr] = nic
955
addresses = get_ipv4_addr(nic, fatal=False)
956
addresses += get_ipv6_addr(iface=nic, fatal=False)
957
hwaddr_to_ip[hwaddr] = addresses
960
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
962
if re.match(mac_regex, entry):
963
# NIC is in known NICs and does NOT hace an IP address
964
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
965
# If the nic is part of a bridge then don't use it
966
if is_bridge_member(hwaddr_to_nic[entry]):
969
# Entry is a MAC address for a valid interface that doesn't
970
# have an IP address assigned yet.
971
resolved.append(hwaddr_to_nic[entry])
973
# If the passed entry is not a MAC address, assume it's a valid
974
# interface, and that the user put it there on purpose (we can
975
# trust it to be the real external network).
976
resolved.append(entry)
981
class OSConfigFlagContext(OSContextGenerator):
982
"""Provides support for user-defined config flags.
984
Users can define a comma-seperated list of key=value pairs
985
in the charm configuration and apply them at any point in
986
any file by using a template flag.
988
Sometimes users might want config flags inserted within a
989
specific section so this class allows users to specify the
990
template flag name, allowing for multiple template flags
991
(sections) within the same context.
993
NOTE: the value of config-flags may be a comma-separated list of
994
key=value pairs and some Openstack config files support
995
comma-separated lists as values.
998
def __init__(self, charm_flag='config-flags',
999
template_flag='user_config_flags'):
1001
:param charm_flag: config flags in charm configuration.
1002
:param template_flag: insert point for user-defined flags in template
1005
super(OSConfigFlagContext, self).__init__()
1006
self._charm_flag = charm_flag
1007
self._template_flag = template_flag
1010
config_flags = config(self._charm_flag)
1011
if not config_flags:
1014
return {self._template_flag:
1015
config_flags_parser(config_flags)}
1018
class SubordinateConfigContext(OSContextGenerator):
1021
Responsible for inspecting relations to subordinates that
1022
may be exporting required config via a json blob.
1024
The subordinate interface allows subordinates to export their
1025
configuration requirements to the principle for multiple config
1026
files and multiple serivces. Ie, a subordinate that has interfaces
1027
to both glance and nova may export to following yaml blob as json::
1030
/etc/glance/glance-api.conf:
1034
/etc/glance/glance-registry.conf:
1038
/etc/nova/nova.conf:
1044
It is then up to the principle charms to subscribe this context to
1045
the service+config file it is interestd in. Configuration data will
1046
be available in the template context, in glance's case, as::
1049
... other context ...
1050
'subordinate_config': {
1061
def __init__(self, service, config_file, interface):
1063
:param service : Service name key to query in any subordinate
1065
:param config_file : Service's config file to query sections
1066
:param interface : Subordinate interface to inspect
1068
self.service = service
1069
self.config_file = config_file
1070
self.interface = interface
1073
ctxt = {'sections': {}}
1074
for rid in relation_ids(self.interface):
1075
for unit in related_units(rid):
1076
sub_config = relation_get('subordinate_configuration',
1078
if sub_config and sub_config != '':
1080
sub_config = json.loads(sub_config)
1082
log('Could not parse JSON from subordinate_config '
1083
'setting from %s' % rid, level=ERROR)
1086
if self.service not in sub_config:
1087
log('Found subordinate_config on %s but it contained'
1088
'nothing for %s service' % (rid, self.service),
1092
sub_config = sub_config[self.service]
1093
if self.config_file not in sub_config:
1094
log('Found subordinate_config on %s but it contained'
1095
'nothing for %s' % (rid, self.config_file),
1099
sub_config = sub_config[self.config_file]
1100
for k, v in six.iteritems(sub_config):
1102
for section, config_dict in six.iteritems(v):
1103
log("adding section '%s'" % (section),
1105
ctxt[k][section] = config_dict
1109
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
1113
class LogLevelContext(OSContextGenerator):
1118
False if config('debug') is None else config('debug')
1120
False if config('verbose') is None else config('verbose')
1125
class SyslogContext(OSContextGenerator):
1128
ctxt = {'use_syslog': config('use-syslog')}
1132
class BindHostContext(OSContextGenerator):
1135
if config('prefer-ipv6'):
1136
return {'bind_host': '::'}
1138
return {'bind_host': '0.0.0.0'}
1141
class WorkerConfigContext(OSContextGenerator):
1146
from psutil import NUM_CPUS
1148
apt_install('python-psutil', fatal=True)
1149
from psutil import NUM_CPUS
1154
multiplier = config('worker-multiplier') or 0
1155
ctxt = {"workers": self.num_cpus * multiplier}
1159
class ZeroMQContext(OSContextGenerator):
1160
interfaces = ['zeromq-configuration']
1164
if is_relation_made('zeromq-configuration', 'host'):
1165
for rid in relation_ids('zeromq-configuration'):
1166
for unit in related_units(rid):
1167
ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
1168
ctxt['zmq_host'] = relation_get('host', unit, rid)
1169
ctxt['zmq_redis_address'] = relation_get(
1170
'zmq_redis_address', unit, rid)
1175
class NotificationDriverContext(OSContextGenerator):
1177
def __init__(self, zmq_relation='zeromq-configuration',
1178
amqp_relation='amqp'):
1180
:param zmq_relation: Name of Zeromq relation to check
1182
self.zmq_relation = zmq_relation
1183
self.amqp_relation = amqp_relation
1186
ctxt = {'notifications': 'False'}
1187
if is_relation_made(self.amqp_relation):
1188
ctxt['notifications'] = "True"
1193
class SysctlContext(OSContextGenerator):
1194
"""This context check if the 'sysctl' option exists on configuration
1195
then creates a file with the loaded contents"""
1197
sysctl_dict = config('sysctl')
1199
sysctl_create(sysctl_dict,
1200
'/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
1201
return {'sysctl': sysctl_dict}
1204
class NeutronAPIContext(OSContextGenerator):
1206
Inspects current neutron-plugin-api relation for neutron settings. Return
1207
defaults if it is not present.
1209
interfaces = ['neutron-plugin-api']
1212
self.neutron_defaults = {
1214
'rel_key': 'l2-population',
1217
'overlay_network_type': {
1218
'rel_key': 'overlay-network-type',
1221
'neutron_security_groups': {
1222
'rel_key': 'neutron-security-groups',
1225
'network_device_mtu': {
1226
'rel_key': 'network-device-mtu',
1230
'rel_key': 'enable-dvr',
1234
'rel_key': 'enable-l3ha',
1238
ctxt = self.get_neutron_options({})
1239
for rid in relation_ids('neutron-plugin-api'):
1240
for unit in related_units(rid):
1241
rdata = relation_get(rid=rid, unit=unit)
1242
if 'l2-population' in rdata:
1243
ctxt.update(self.get_neutron_options(rdata))
1247
def get_neutron_options(self, rdata):
1249
for nkey in self.neutron_defaults.keys():
1250
defv = self.neutron_defaults[nkey]['default']
1251
rkey = self.neutron_defaults[nkey]['rel_key']
1252
if rkey in rdata.keys():
1253
if type(defv) is bool:
1254
settings[nkey] = bool_from_string(rdata[rkey])
1256
settings[nkey] = rdata[rkey]
1258
settings[nkey] = defv
1262
class ExternalPortContext(NeutronPortContext):
1266
ports = config('ext-port')
1268
ports = [p.strip() for p in ports.split()]
1269
ports = self.resolve_ports(ports)
1271
ctxt = {"ext_port": ports[0]}
1272
napi_settings = NeutronAPIContext()()
1273
mtu = napi_settings.get('network_device_mtu')
1275
ctxt['ext_port_mtu'] = mtu
1280
class DataPortContext(NeutronPortContext):
1283
ports = config('data-port')
1285
portmap = parse_data_port_mappings(ports)
1286
ports = portmap.values()
1287
resolved = self.resolve_ports(ports)
1288
normalized = {get_nic_hwaddr(port): port for port in resolved
1289
if port not in ports}
1290
normalized.update({port: port for port in resolved
1293
return {bridge: normalized[port] for bridge, port in
1294
six.iteritems(portmap) if port in normalized.keys()}
1299
class PhyNICMTUContext(DataPortContext):
1303
mappings = super(PhyNICMTUContext, self).__call__()
1304
if mappings and mappings.values():
1305
ports = mappings.values()
1306
napi_settings = NeutronAPIContext()()
1307
mtu = napi_settings.get('network_device_mtu')
1309
ctxt["devs"] = '\\n'.join(ports)
1315
class NetworkServiceContext(OSContextGenerator):
1317
def __init__(self, rel_name='quantum-network-service'):
1318
self.rel_name = rel_name
1319
self.interfaces = [rel_name]
1322
for rid in relation_ids(self.rel_name):
1323
for unit in related_units(rid):
1324
rdata = relation_get(rid=rid, unit=unit)
1326
'keystone_host': rdata.get('keystone_host'),
1327
'service_port': rdata.get('service_port'),
1328
'auth_port': rdata.get('auth_port'),
1329
'service_tenant': rdata.get('service_tenant'),
1330
'service_username': rdata.get('service_username'),
1331
'service_password': rdata.get('service_password'),
1332
'quantum_host': rdata.get('quantum_host'),
1333
'quantum_port': rdata.get('quantum_port'),
1334
'quantum_url': rdata.get('quantum_url'),
1335
'region': rdata.get('region'),
1337
rdata.get('service_protocol') or 'http',
1339
rdata.get('auth_protocol') or 'http',
1341
if context_complete(ctxt):