diff --git a/.bzrignore b/.bzrignore new file mode 100644 index 0000000..a2c7a09 --- /dev/null +++ b/.bzrignore @@ -0,0 +1,2 @@ +bin +.coverage diff --git a/config.yaml b/config.yaml index 007fb4e..a0404a1 100644 --- a/config.yaml +++ b/config.yaml @@ -134,3 +134,15 @@ options: description: | The IP address and netmask of the cluster (back-side) network (e.g., 192.168.0.0/24) + 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 b859a09..9a3c2bf 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/hooks.py b/hooks/hooks.py index ce72a58..8a6c26c 100755 --- a/hooks/hooks.py +++ b/hooks/hooks.py @@ -40,11 +40,15 @@ from charmhelpers.fetch import ( ) from charmhelpers.payload.execd import execd_preinstall from charmhelpers.contrib.openstack.alternatives import install_alternative -from charmhelpers.contrib.network.ip import is_ipv6 +from charmhelpers.contrib.network.ip import ( + get_ipv6_addr, + format_ipv6_addr +) from utils import ( render_template, get_public_addr, + assert_charm_supports_ipv6 ) hooks = Hooks() @@ -77,6 +81,14 @@ def emit_cephconf(): 'ceph_public_network': config('ceph-public-network'), 'ceph_cluster_network': config('ceph-cluster-network'), } + + if config('prefer-ipv6'): + dynamic_ipv6_address = get_ipv6_addr()[0] + if not config('ceph-public-network'): + cephcontext['public_addr'] = dynamic_ipv6_address + if not config('ceph-cluster-network'): + cephcontext['cluster_addr'] = dynamic_ipv6_address + # Install ceph.conf as an alternative to support # co-existence with other charms that write this file charm_ceph_conf = "/var/lib/charm/{}/ceph.conf".format(service_name()) @@ -91,6 +103,9 @@ JOURNAL_ZAPPED = '/var/lib/ceph/journal_zapped' @hooks.hook('config-changed') def config_changed(): + if config('prefer-ipv6'): + assert_charm_supports_ipv6() + log('Monitor hosts are ' + repr(get_mon_hosts())) # Pre-flight checks @@ -132,19 +147,14 @@ def config_changed(): def get_mon_hosts(): hosts = [] addr = get_public_addr() - if is_ipv6(addr): - hosts.append('[{}]:6789'.format(addr)) - else: - hosts.append('{}:6789'.format(addr)) + hosts.append('{}:6789'.format(format_ipv6_addr(addr) or addr)) for relid in relation_ids('mon'): for unit in related_units(relid): addr = relation_get('ceph-public-address', unit, relid) if addr is not None: - if is_ipv6(addr): - hosts.append('[{}]:6789'.format(addr)) - else: - hosts.append('{}:6789'.format(addr)) + hosts.append('{}:6789'.format( + format_ipv6_addr(addr) or addr)) hosts.sort() return hosts diff --git a/hooks/utils.py b/hooks/utils.py index 21fd1a5..ada3563 100644 --- a/hooks/utils.py +++ b/hooks/utils.py @@ -19,7 +19,14 @@ from charmhelpers.fetch import ( filter_installed_packages ) +from charmhelpers.core.host import ( + lsb_release +) + from charmhelpers.contrib.network import ip +from charmhelpers.contrib.network.ip import ( + get_ipv6_addr +) TEMPLATES_DIR = 'templates' @@ -64,6 +71,9 @@ def get_unit_hostname(): @cached def get_host_ip(hostname=None): + if config('prefer-ipv6'): + return get_ipv6_addr()[0] + hostname = hostname or unit_get('private-address') try: # Test to see if already an IPv4 address @@ -81,3 +91,10 @@ def get_host_ip(hostname=None): def get_public_addr(): return ip.get_address_in_network(config('ceph-public-network'), fallback=get_host_ip()) + + +def assert_charm_supports_ipv6(): + """Check whether we are able to support charms ipv6.""" + if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty": + raise Exception("IPv6 is not supported in the charms for Ubuntu " + "versions less than Trusty 14.04") diff --git a/templates/ceph.conf b/templates/ceph.conf index 3b0d91f..e168f54 100644 --- a/templates/ceph.conf +++ b/templates/ceph.conf @@ -22,6 +22,13 @@ public network = {{ ceph_public_network }} cluster network = {{ ceph_cluster_network }} {%- endif %} +{% if public_addr %} +public addr = {{ public_addr }} +{% endif %} +{% if cluster_addr %} +cluster addr = {{ cluster_addr }} +{%- endif %} + [mon] keyring = /var/lib/ceph/mon/$cluster-$id/keyring