diff --git a/charm-helpers-hooks.yaml b/charm-helpers-hooks.yaml index dbc1bac2..779e2142 100644 --- a/charm-helpers-hooks.yaml +++ b/charm-helpers-hooks.yaml @@ -1,4 +1,4 @@ -branch: lp:~james-page/charm-helpers/multiple-https-networks +branch: lp:charm-helpers destination: hooks/charmhelpers include: - core diff --git a/config.yaml b/config.yaml index 5269853e..019ba6b2 100644 --- a/config.yaml +++ b/config.yaml @@ -115,4 +115,15 @@ options: 192.168.0.0/24) . This network will be used for public endpoints. - + prefer-ipv6: + type: boolean + default: False + description: | + If True enables IPv6 support. The charm will expect network interfaces + to be configured with an IPv6 address. If set to False (default) IPv4 + is expected. + . + NOTE: these charms do not currently support IPv6 privacy extension. In + order for this charm to function correctly, the privacy extension must be + disabled and a non-temporary address must be configured/available on + your network interface. diff --git a/hooks/charmhelpers/contrib/network/ip.py b/hooks/charmhelpers/contrib/network/ip.py index b859a097..9a3c2bfa 100644 --- a/hooks/charmhelpers/contrib/network/ip.py +++ b/hooks/charmhelpers/contrib/network/ip.py @@ -1,11 +1,16 @@ import glob +import re +import subprocess import sys from functools import partial +from charmhelpers.core.hookenv import unit_get from charmhelpers.fetch import apt_install from charmhelpers.core.hookenv import ( - ERROR, log, + WARNING, + ERROR, + log ) try: @@ -164,13 +169,14 @@ def format_ipv6_addr(address): if is_ipv6(address): address = "[%s]" % address else: - log("Not an valid ipv6 address: %s" % address, - level=ERROR) + log("Not a valid ipv6 address: %s" % address, level=WARNING) address = None + return address -def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=True, exc_list=None): +def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, + fatal=True, exc_list=None): """ Return the assigned IP address for a given interface, if any, or []. """ @@ -210,26 +216,105 @@ def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, fatal=T if 'addr' in entry and entry['addr'] not in exc_list: addresses.append(entry['addr']) if fatal and not addresses: - raise Exception("Interface '%s' doesn't have any %s addresses." % (iface, inet_type)) + raise Exception("Interface '%s' doesn't have any %s addresses." % + (iface, inet_type)) return addresses get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET') -def get_ipv6_addr(iface='eth0', inc_aliases=False, fatal=True, exc_list=None): +def get_iface_from_addr(addr): + """Work out on which interface the provided address is configured.""" + for iface in netifaces.interfaces(): + addresses = netifaces.ifaddresses(iface) + for inet_type in addresses: + for _addr in addresses[inet_type]: + _addr = _addr['addr'] + # link local + ll_key = re.compile("(.+)%.*") + raw = re.match(ll_key, _addr) + if raw: + _addr = raw.group(1) + if _addr == addr: + log("Address '%s' is configured on iface '%s'" % + (addr, iface)) + return iface + + msg = "Unable to infer net iface on which '%s' is configured" % (addr) + raise Exception(msg) + + +def sniff_iface(f): + """If no iface provided, inject net iface inferred from unit private + address. """ - Return the assigned IPv6 address for a given interface, if any, or []. + def iface_sniffer(*args, **kwargs): + if not kwargs.get('iface', None): + kwargs['iface'] = get_iface_from_addr(unit_get('private-address')) + + return f(*args, **kwargs) + + return iface_sniffer + + +@sniff_iface +def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None, + dynamic_only=True): + """Get assigned IPv6 address for a given interface. + + Returns list of addresses found. If no address found, returns empty list. + + If iface is None, we infer the current primary interface by doing a reverse + lookup on the unit private-address. + + We currently only support scope global IPv6 addresses i.e. non-temporary + addresses. If no global IPv6 address is found, return the first one found + in the ipv6 address list. """ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6', inc_aliases=inc_aliases, fatal=fatal, exc_list=exc_list) - remotly_addressable = [] - for address in addresses: - if not address.startswith('fe80'): - remotly_addressable.append(address) - if fatal and not remotly_addressable: - raise Exception("Interface '%s' doesn't have global ipv6 address." % iface) - return remotly_addressable + + if addresses: + global_addrs = [] + for addr in addresses: + key_scope_link_local = re.compile("^fe80::..(.+)%(.+)") + m = re.match(key_scope_link_local, addr) + if m: + eui_64_mac = m.group(1) + iface = m.group(2) + else: + global_addrs.append(addr) + + if global_addrs: + # Make sure any found global addresses are not temporary + cmd = ['ip', 'addr', 'show', iface] + out = subprocess.check_output(cmd) + if dynamic_only: + key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*") + else: + key = re.compile("inet6 (.+)/[0-9]+ scope global.*") + + addrs = [] + for line in out.split('\n'): + line = line.strip() + m = re.match(key, line) + if m and 'temporary' not in line: + # Return the first valid address we find + for addr in global_addrs: + if m.group(1) == addr: + if not dynamic_only or \ + m.group(1).endswith(eui_64_mac): + addrs.append(addr) + + if addrs: + return addrs + + if fatal: + raise Exception("Interface '%s' doesn't have a scope global " + "non-temporary ipv6 address." % iface) + + return [] def get_bridges(vnic_dir='/sys/devices/virtual/net'): diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py index 9179eeb1..3c7f422a 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py @@ -10,32 +10,62 @@ class OpenStackAmuletDeployment(AmuletDeployment): that is specifically for use by OpenStack charms. """ - def __init__(self, series=None, openstack=None, source=None): + def __init__(self, series=None, openstack=None, source=None, stable=True): """Initialize the deployment environment.""" super(OpenStackAmuletDeployment, self).__init__(series) self.openstack = openstack self.source = source + self.stable = stable + # Note(coreycb): this needs to be changed when new next branches come + # out. + self.current_next = "trusty" + + def _determine_branch_locations(self, other_services): + """Determine the branch locations for the other services. + + Determine if the local branch being tested is derived from its + stable or next (dev) branch, and based on this, use the corresonding + stable or next branches for the other_services.""" + base_charms = ['mysql', 'mongodb', 'rabbitmq-server'] + + if self.stable: + for svc in other_services: + temp = 'lp:charms/{}' + svc['location'] = temp.format(svc['name']) + else: + for svc in other_services: + if svc['name'] in base_charms: + temp = 'lp:charms/{}' + svc['location'] = temp.format(svc['name']) + else: + temp = 'lp:~openstack-charmers/charms/{}/{}/next' + svc['location'] = temp.format(self.current_next, + svc['name']) + return other_services def _add_services(self, this_service, other_services): - """Add services to the deployment and set openstack-origin.""" + """Add services to the deployment and set openstack-origin/source.""" + other_services = self._determine_branch_locations(other_services) + super(OpenStackAmuletDeployment, self)._add_services(this_service, other_services) - name = 0 + services = other_services services.append(this_service) - use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph'] + use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', + 'ceph-osd', 'ceph-radosgw'] if self.openstack: for svc in services: - if svc[name] not in use_source: + if svc['name'] not in use_source: config = {'openstack-origin': self.openstack} - self.d.configure(svc[name], config) + self.d.configure(svc['name'], config) if self.source: for svc in services: - if svc[name] in use_source: + if svc['name'] in use_source: config = {'source': self.source} - self.d.configure(svc[name], config) + self.d.configure(svc['name'], config) def _configure_services(self, configs): """Configure all of the services.""" diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py index bd327bdc..0f312b99 100644 --- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py +++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py @@ -187,15 +187,16 @@ class OpenStackAmuletUtils(AmuletUtils): f = opener.open("http://download.cirros-cloud.net/version/released") version = f.read().strip() - cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version) + cirros_img = "cirros-{}-x86_64-disk.img".format(version) + local_path = os.path.join('tests', cirros_img) - if not os.path.exists(cirros_img): + if not os.path.exists(local_path): cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net", version, cirros_img) - opener.retrieve(cirros_url, cirros_img) + opener.retrieve(cirros_url, local_path) f.close() - with open(cirros_img) as f: + with open(local_path) as f: image = glance.images.create(name=image_name, is_public=True, disk_format='qcow2', container_format='bare', data=f) diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py index 3af65252..94f8bbe1 100644 --- a/hooks/charmhelpers/contrib/openstack/context.py +++ b/hooks/charmhelpers/contrib/openstack/context.py @@ -52,8 +52,9 @@ from charmhelpers.contrib.openstack.neutron import ( from charmhelpers.contrib.network.ip import ( get_address_in_network, get_ipv6_addr, - is_address_in_network, - get_netmask_for_address + get_netmask_for_address, + format_ipv6_addr, + is_address_in_network ) CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' @@ -175,8 +176,10 @@ class SharedDBContext(OSContextGenerator): for rid in relation_ids('shared-db'): for unit in related_units(rid): rdata = relation_get(rid=rid, unit=unit) + host = rdata.get('db_host') + host = format_ipv6_addr(host) or host ctxt = { - 'database_host': rdata.get('db_host'), + 'database_host': host, 'database': self.database, 'database_user': self.user, 'database_password': rdata.get(password_setting), @@ -252,10 +255,15 @@ class IdentityServiceContext(OSContextGenerator): for rid in relation_ids('identity-service'): for unit in related_units(rid): rdata = relation_get(rid=rid, unit=unit) + serv_host = rdata.get('service_host') + serv_host = format_ipv6_addr(serv_host) or serv_host + auth_host = rdata.get('auth_host') + auth_host = format_ipv6_addr(auth_host) or auth_host + ctxt = { 'service_port': rdata.get('service_port'), - 'service_host': rdata.get('service_host'), - 'auth_host': rdata.get('auth_host'), + 'service_host': serv_host, + 'auth_host': auth_host, 'auth_port': rdata.get('auth_port'), 'admin_tenant_name': rdata.get('service_tenant'), 'admin_user': rdata.get('service_username'), @@ -304,11 +312,13 @@ class AMQPContext(OSContextGenerator): for unit in related_units(rid): if relation_get('clustered', rid=rid, unit=unit): ctxt['clustered'] = True - ctxt['rabbitmq_host'] = relation_get('vip', rid=rid, - unit=unit) + vip = relation_get('vip', rid=rid, unit=unit) + vip = format_ipv6_addr(vip) or vip + ctxt['rabbitmq_host'] = vip else: - ctxt['rabbitmq_host'] = relation_get('private-address', - rid=rid, unit=unit) + host = relation_get('private-address', rid=rid, unit=unit) + host = format_ipv6_addr(host) or host + ctxt['rabbitmq_host'] = host ctxt.update({ 'rabbitmq_user': username, 'rabbitmq_password': relation_get('password', rid=rid, @@ -347,8 +357,9 @@ class AMQPContext(OSContextGenerator): and len(related_units(rid)) > 1: rabbitmq_hosts = [] for unit in related_units(rid): - rabbitmq_hosts.append(relation_get('private-address', - rid=rid, unit=unit)) + host = relation_get('private-address', rid=rid, unit=unit) + host = format_ipv6_addr(host) or host + rabbitmq_hosts.append(host) ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts) if not context_complete(ctxt): return {} @@ -377,6 +388,7 @@ class CephContext(OSContextGenerator): ceph_addr = \ relation_get('ceph-public-address', rid=rid, unit=unit) or \ relation_get('private-address', rid=rid, unit=unit) + ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr mon_hosts.append(ceph_addr) ctxt = { @@ -413,8 +425,9 @@ class HAProxyContext(OSContextGenerator): return {} l_unit = local_unit().replace('/', '-') + if config('prefer-ipv6'): - addr = get_ipv6_addr() + addr = get_ipv6_addr(exc_list=[config('vip')])[0] else: addr = unit_get('private-address') @@ -443,7 +456,7 @@ class HAProxyContext(OSContextGenerator): # NOTE(jamespage) no split configurations found, just use # private addresses - if len(cluster_hosts) < 1: + if not cluster_hosts: cluster_hosts[addr] = {} cluster_hosts[addr]['network'] = "{}/{}".format( addr, @@ -463,6 +476,11 @@ class HAProxyContext(OSContextGenerator): 'frontends': cluster_hosts, } + if config('haproxy-server-timeout'): + ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout') + if config('haproxy-client-timeout'): + ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout') + if config('prefer-ipv6'): ctxt['local_host'] = 'ip6-localhost' ctxt['haproxy_host'] = '::' @@ -870,3 +888,16 @@ class SyslogContext(OSContextGenerator): 'use_syslog': config('use-syslog') } return ctxt + + +class BindHostContext(OSContextGenerator): + + def __call__(self): + if config('prefer-ipv6'): + return { + 'bind_host': '::' + } + else: + return { + 'bind_host': '0.0.0.0' + } diff --git a/hooks/charmhelpers/contrib/openstack/ip.py b/hooks/charmhelpers/contrib/openstack/ip.py index affe8cd1..bc84fc45 100644 --- a/hooks/charmhelpers/contrib/openstack/ip.py +++ b/hooks/charmhelpers/contrib/openstack/ip.py @@ -66,7 +66,7 @@ def resolve_address(endpoint_type=PUBLIC): resolved_address = vip else: if config('prefer-ipv6'): - fallback_addr = get_ipv6_addr() + fallback_addr = get_ipv6_addr(exc_list=[config('vip')])[0] else: fallback_addr = unit_get(_address_map[endpoint_type]['fallback']) resolved_address = get_address_in_network( diff --git a/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg b/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg index f6bfb65b..19c9b856 100644 --- a/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg +++ b/hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg @@ -14,8 +14,17 @@ defaults retries 3 timeout queue 1000 timeout connect 1000 +{% if haproxy_client_timeout -%} + timeout client {{ haproxy_client_timeout }} +{% else -%} timeout client 30000 +{% endif -%} + +{% if haproxy_server_timeout -%} + timeout server {{ haproxy_server_timeout }} +{% else -%} timeout server 30000 +{% endif -%} listen stats {{ stat_port }} mode http diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 23d237de..91b8c7b8 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -4,6 +4,7 @@ from collections import OrderedDict import subprocess +import json import os import socket import sys @@ -13,7 +14,9 @@ from charmhelpers.core.hookenv import ( log as juju_log, charm_dir, ERROR, - INFO + INFO, + relation_ids, + relation_set ) from charmhelpers.contrib.storage.linux.lvm import ( @@ -22,6 +25,10 @@ from charmhelpers.contrib.storage.linux.lvm import ( remove_lvm_physical_volume, ) +from charmhelpers.contrib.network.ip import ( + get_ipv6_addr +) + from charmhelpers.core.host import lsb_release, mounts, umount from charmhelpers.fetch import apt_install, apt_cache from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk @@ -457,3 +464,21 @@ def get_hostname(address, fqdn=True): return result else: return result.split('.')[0] + + +def sync_db_with_multi_ipv6_addresses(database, database_user, + relation_prefix=None): + hosts = get_ipv6_addr(dynamic_only=False) + + kwargs = {'database': database, + 'username': database_user, + 'hostname': json.dumps(hosts)} + + if relation_prefix: + keys = kwargs.keys() + for key in keys: + kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key] + del kwargs[key] + + for rid in relation_ids('shared-db'): + relation_set(relation_id=rid, **kwargs) diff --git a/hooks/glance_contexts.py b/hooks/glance_contexts.py index 01d73e45..505ce239 100644 --- a/hooks/glance_contexts.py +++ b/hooks/glance_contexts.py @@ -8,6 +8,7 @@ from charmhelpers.core.hookenv import ( from charmhelpers.contrib.openstack.context import ( OSContextGenerator, ApacheSSLContext as SSLContext, + BindHostContext ) from charmhelpers.contrib.hahelpers.cluster import ( @@ -81,3 +82,15 @@ class LoggingConfigContext(OSContextGenerator): def __call__(self): return {'debug': config('debug'), 'verbose': config('verbose')} + + +class GlanceIPv6Context(BindHostContext): + + def __call__(self): + ctxt = super(GlanceIPv6Context, self).__call__() + if config('prefer-ipv6'): + ctxt['registry_host'] = '[::]' + else: + ctxt['registry_host'] = '0.0.0.0' + + return ctxt diff --git a/hooks/glance_relations.py b/hooks/glance_relations.py index 064927a6..396e12e0 100755 --- a/hooks/glance_relations.py +++ b/hooks/glance_relations.py @@ -16,7 +16,8 @@ from glance_utils import ( GLANCE_API_CONF, GLANCE_API_PASTE_INI, HAPROXY_CONF, - ceph_config_file) + ceph_config_file, + setup_ipv6) from charmhelpers.core.hookenv import ( config, @@ -53,7 +54,8 @@ from charmhelpers.contrib.openstack.utils import ( configure_installation_source, get_os_codename_package, openstack_upgrade_available, - lsb_release, ) + lsb_release, + sync_db_with_multi_ipv6_addresses) from charmhelpers.contrib.storage.linux.ceph import ensure_ceph_keyring from charmhelpers.payload.execd import execd_preinstall @@ -61,6 +63,8 @@ from charmhelpers.contrib.network.ip import ( get_address_in_network, get_netmask_for_address, get_iface_for_address, + get_ipv6_addr, + is_ipv6 ) from charmhelpers.contrib.openstack.ip import ( canonical_url, @@ -105,8 +109,14 @@ def db_joined(): juju_log(e, level=ERROR) raise Exception(e) - relation_set(database=config('database'), username=config('database-user'), - hostname=unit_get('private-address')) + if config('prefer-ipv6'): + sync_db_with_multi_ipv6_addresses(config('database'), + config('database-user')) + else: + host = unit_get('private-address') + relation_set(database=config('database'), + username=config('database-user'), + hostname=host) @hooks.hook('pgsql-db-relation-joined') @@ -275,6 +285,11 @@ def keystone_changed(): @hooks.hook('config-changed') @restart_on_change(restart_map(), stopstart=True) def config_changed(): + if config('prefer-ipv6'): + setup_ipv6() + sync_db_with_multi_ipv6_addresses(config('database'), + config('database-user')) + if openstack_upgrade_available('glance-common'): juju_log('Upgrading OpenStack release') do_openstack_upgrade(CONFIGS) @@ -300,6 +315,10 @@ def cluster_joined(relation_id=None): relation_id=relation_id, relation_settings={'{}-address'.format(addr_type): address} ) + if config('prefer-ipv6'): + private_addr = get_ipv6_addr(exc_list=[config('vip')])[0] + relation_set(relation_id=relation_id, + relation_settings={'private-address': private_addr}) @hooks.hook('cluster-relation-changed') @@ -320,7 +339,7 @@ def upgrade_charm(): @hooks.hook('ha-relation-joined') def ha_relation_joined(): - config = get_hacluster_config() + cluster_config = get_hacluster_config() resources = { 'res_glance_haproxy': 'lsb:haproxy' @@ -331,14 +350,22 @@ def ha_relation_joined(): } vip_group = [] - for vip in config['vip'].split(): + for vip in cluster_config['vip'].split(): + if is_ipv6(vip): + res_ks_vip = 'ocf:heartbeat:IPv6addr' + vip_params = 'ipv6addr' + else: + res_ks_vip = 'ocf:heartbeat:IPaddr2' + vip_params = 'ip' + iface = get_iface_for_address(vip) if iface is not None: vip_key = 'res_glance_{}_vip'.format(iface) - resources[vip_key] = 'ocf:heartbeat:IPaddr2' + resources[vip_key] = res_ks_vip resource_params[vip_key] = ( - 'params ip="{vip}" cidr_netmask="{netmask}"' - ' nic="{iface}"'.format(vip=vip, + 'params {ip}="{vip}" cidr_netmask="{netmask}"' + ' nic="{iface}"'.format(ip=vip_params, + vip=vip, iface=iface, netmask=get_netmask_for_address(vip)) ) @@ -356,8 +383,8 @@ def ha_relation_joined(): } relation_set(init_services=init_services, - corosync_bindiface=config['ha-bindiface'], - corosync_mcastport=config['ha-mcastport'], + corosync_bindiface=cluster_config['ha-bindiface'], + corosync_mcastport=cluster_config['ha-mcastport'], resources=resources, resource_params=resource_params, clones=clones) diff --git a/hooks/glance_utils.py b/hooks/glance_utils.py index 28737bad..139d8a7e 100755 --- a/hooks/glance_utils.py +++ b/hooks/glance_utils.py @@ -10,7 +10,8 @@ from collections import OrderedDict from charmhelpers.fetch import ( apt_upgrade, apt_update, - apt_install, ) + apt_install, + add_source) from charmhelpers.core.hookenv import ( config, @@ -21,7 +22,8 @@ from charmhelpers.core.hookenv import ( from charmhelpers.core.host import ( mkdir, service_stop, - service_start + service_start, + lsb_release ) from charmhelpers.contrib.openstack import ( @@ -80,7 +82,8 @@ CONFIG_FILES = OrderedDict([ context.PostgresqlDBContext(), context.IdentityServiceContext(), context.SyslogContext(), - glance_contexts.LoggingConfigContext()], + glance_contexts.LoggingConfigContext(), + glance_contexts.GlanceIPv6Context()], 'services': ['glance-registry'] }), (GLANCE_API_CONF, { @@ -92,7 +95,8 @@ CONFIG_FILES = OrderedDict([ glance_contexts.ObjectStoreContext(), glance_contexts.HAProxyContext(), context.SyslogContext(), - glance_contexts.LoggingConfigContext()], + glance_contexts.LoggingConfigContext(), + glance_contexts.GlanceIPv6Context()], 'services': ['glance-api'] }), (GLANCE_API_PASTE_INI, { @@ -236,3 +240,19 @@ def services(): for v in restart_map().values(): _services = _services + v return list(set(_services)) + + +def setup_ipv6(): + ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower() + if ubuntu_rel < "trusty": + raise Exception("IPv6 is not supported in the charms for Ubuntu " + "versions less than Trusty 14.04") + + # NOTE(xianghui): Need to install haproxy(1.5.3) from trusty-backports + # to support ipv6 address, so check is required to make sure not + # breaking other versions, IPv6 only support for >= Trusty + if ubuntu_rel == 'trusty': + add_source('deb http://archive.ubuntu.com/ubuntu trusty-backports' + ' main') + apt_update() + apt_install('haproxy/trusty-backports', fatal=True) diff --git a/templates/icehouse/glance-api.conf b/templates/icehouse/glance-api.conf index 61495266..0b2e42f7 100644 --- a/templates/icehouse/glance-api.conf +++ b/templates/icehouse/glance-api.conf @@ -11,7 +11,7 @@ default_store = swift default_store = file {% endif -%} -bind_host = 0.0.0.0 +bind_host = {{ bind_host }} {% if ext -%} bind_port = {{ ext }} @@ -26,7 +26,7 @@ backlog = 4096 sql_idle_timeout = 3600 workers = 1 -registry_host = 0.0.0.0 +registry_host = {{ registry_host }} registry_port = 9191 registry_client_protocol = http diff --git a/templates/icehouse/glance-registry.conf b/templates/icehouse/glance-registry.conf index 1908f562..bbd1c31f 100644 --- a/templates/icehouse/glance-registry.conf +++ b/templates/icehouse/glance-registry.conf @@ -3,7 +3,7 @@ verbose = {{ verbose }} use_syslog = {{ use_syslog }} debug = {{ debug }} -bind_host = 0.0.0.0 +bind_host = {{ bind_host }} bind_port = 9191 log_file = /var/log/glance/registry.log backlog = 4096 diff --git a/tests/charmhelpers/contrib/openstack/amulet/deployment.py b/tests/charmhelpers/contrib/openstack/amulet/deployment.py index 495ebdb6..3c7f422a 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/deployment.py +++ b/tests/charmhelpers/contrib/openstack/amulet/deployment.py @@ -16,7 +16,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): self.openstack = openstack self.source = source self.stable = stable - # Note(coreycb): this needs to be changed when new next branches come out. + # Note(coreycb): this needs to be changed when new next branches come + # out. self.current_next = "trusty" def _determine_branch_locations(self, other_services): @@ -51,7 +52,8 @@ class OpenStackAmuletDeployment(AmuletDeployment): services = other_services services.append(this_service) - use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph'] + use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph', + 'ceph-osd', 'ceph-radosgw'] if self.openstack: for svc in services: diff --git a/unit_tests/test_glance_contexts.py b/unit_tests/test_glance_contexts.py index 5a40ee2f..c60494db 100644 --- a/unit_tests/test_glance_contexts.py +++ b/unit_tests/test_glance_contexts.py @@ -70,3 +70,23 @@ class TestGlanceContexts(CharmTestCase): 'namespace': 'glance'}) self.assertTrue(mock_https.called) mock_unit_get.assert_called_with('private-address') + + @patch('charmhelpers.contrib.openstack.context.config') + @patch('glance_contexts.config') + def test_glance_ipv6_context_service_enabled(self, mock_config, + mock_context_config): + mock_config.return_value = True + mock_context_config.return_value = True + ctxt = contexts.GlanceIPv6Context() + self.assertEquals(ctxt(), {'bind_host': '::', + 'registry_host': '[::]'}) + + @patch('charmhelpers.contrib.openstack.context.config') + @patch('glance_contexts.config') + def test_glance_ipv6_context_service_disabled(self, mock_config, + mock_context_config): + mock_config.return_value = False + mock_context_config.return_value = False + ctxt = contexts.GlanceIPv6Context() + self.assertEquals(ctxt(), {'bind_host': '0.0.0.0', + 'registry_host': '0.0.0.0'}) diff --git a/unit_tests/test_glance_relations.py b/unit_tests/test_glance_relations.py index 2beb3c52..bb61b1ba 100644 --- a/unit_tests/test_glance_relations.py +++ b/unit_tests/test_glance_relations.py @@ -60,7 +60,9 @@ TO_PATCH = [ 'filter_installed_packages', 'get_hacluster_config', 'get_netmask_for_address', - 'get_iface_for_address' + 'get_iface_for_address', + 'get_ipv6_addr', + 'sync_db_with_multi_ipv6_addresses', ] @@ -87,7 +89,8 @@ class GlanceRelationTests(CharmTestCase): def test_install_hook_precise_distro(self): self.test_config.set('openstack-origin', 'distro') - self.lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'} + self.lsb_release.return_value = {'DISTRIB_RELEASE': 12.04, + 'DISTRIB_CODENAME': 'precise'} self.service_stop.return_value = True relations.install_hook() self.configure_installation_source.assert_called_with( @@ -103,6 +106,25 @@ class GlanceRelationTests(CharmTestCase): hostname='glance.foohost.com') self.unit_get.assert_called_with('private-address') + @patch.object(relations, 'sync_db_with_multi_ipv6_addresses') + @patch.object(relations, 'get_ipv6_addr') + def test_db_joined_with_ipv6(self, mock_get_ipv6_addr, + mock_sync_db): + self.test_config.set('prefer-ipv6', True) + mock_get_ipv6_addr.return_value = ['2001:db8:1::1'] + mock_sync_db.return_value = MagicMock() + self.is_relation_made.return_value = False + relations.db_joined() + relation_data = { + 'database': 'glance', + 'username': 'glance', + } + relation_data['hostname'] = '2001:db8:1::1' + + self.sync_db_with_multi_ipv6_addresses.assert_called_with_once( + 'glance', 'glance') + self.get_ipv6_addr.assert_called_once() + def test_postgresql_db_joined(self): self.unit_get.return_value = 'glance.foohost.com' self.is_relation_made.return_value = False @@ -420,6 +442,7 @@ class GlanceRelationTests(CharmTestCase): @patch.object(relations, 'CONFIGS') def test_cluster_changed(self, configs): + self.test_config.set('prefer-ipv6', False) configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = ['cluster'] configs.write = MagicMock() @@ -428,6 +451,20 @@ class GlanceRelationTests(CharmTestCase): call('/etc/haproxy/haproxy.cfg')], configs.write.call_args_list) + @patch.object(relations, 'relation_set') + @patch.object(relations, 'CONFIGS') + def test_cluster_changed_with_ipv6(self, configs, relation_set): + self.test_config.set('prefer-ipv6', True) + configs.complete_contexts = MagicMock() + configs.complete_contexts.return_value = ['cluster'] + configs.write = MagicMock() + self.get_ipv6_addr.return_value = '2001:db8:1::1' + self.relation_ids.return_value = ['cluster:0'] + relations.cluster_changed() + self.assertEquals([call('/etc/glance/glance-api.conf'), + call('/etc/haproxy/haproxy.cfg')], + configs.write.call_args_list) + @patch.object(relations, 'CONFIGS') def test_upgrade_charm(self, configs): self.filter_installed_packages.return_value = ['test'] @@ -461,6 +498,30 @@ class GlanceRelationTests(CharmTestCase): call(**args), ]) + def test_ha_relation_joined_with_ipv6(self): + self.test_config.set('prefer-ipv6', True) + self.get_hacluster_config.return_value = { + 'ha-bindiface': 'em0', + 'ha-mcastport': '8080', + 'vip': '2001:db8:1::1', + } + self.get_iface_for_address.return_value = 'eth1' + self.get_netmask_for_address.return_value = '64' + relations.ha_relation_joined() + args = { + 'corosync_bindiface': 'em0', + 'corosync_mcastport': '8080', + 'init_services': {'res_glance_haproxy': 'haproxy'}, + 'resources': {'res_glance_eth1_vip': 'ocf:heartbeat:IPv6addr', + 'res_glance_haproxy': 'lsb:haproxy'}, + 'resource_params': { + 'res_glance_eth1_vip': 'params ipv6addr="2001:db8:1::1"' + ' cidr_netmask="64" nic="eth1"', + 'res_glance_haproxy': 'op monitor interval="5s"'}, + 'clones': {'cl_glance_haproxy': 'res_glance_haproxy'} + } + self.relation_set.assert_called_with(**args) + def test_ha_relation_changed_not_clustered(self): self.relation_get.return_value = False relations.ha_relation_changed()