From 3a81b1576da7df2a095e1bb50a60c143526e5efd Mon Sep 17 00:00:00 2001 From: Matthew Oliver Date: Mon, 17 Aug 2020 11:08:56 +1000 Subject: [PATCH] cephadm: auto wrap and unwrap ipv6 addresses This patch attempts to simplify IPv6 support in cephadm by automatically wrapping and unwrapping IPv6 addresses when required. There are some asumptions though, if you are supplyings an IPv6 addrv then it needs to be wrapped. But because you are specifiying, you should know what your doing. But in general, it means in bootstrap you should be able to supply ipv6 addresses wrapped or not so long as there isn't a post appended. Fixes: https://tracker.ceph.com/issues/46922 Signed-off-by: Matthew Oliver (cherry picked from commit 09eac4bef0f04f5db7118f94dd9679f3295bddf8) Conflicts: src/pybind/mgr/dashboard/tools.py src/python-common/ceph/deployment/service_spec.py --- src/cephadm/cephadm | 23 ++++++++++-- src/cephadm/tests/test_cephadm.py | 14 +++++++ .../mgr/cephadm/services/cephadmservice.py | 3 ++ src/pybind/mgr/dashboard/tools.py | 15 ++------ .../ceph/deployment/service_spec.py | 8 +++- src/python-common/ceph/deployment/utils.py | 36 ++++++++++++++++++ src/python-common/ceph/tests/test_utils.py | 37 +++++++++++++++++++ 7 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 src/python-common/ceph/deployment/utils.py create mode 100644 src/python-common/ceph/tests/test_utils.py diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm index c93f8e48c61f7..3f67f09cf407c 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -534,10 +534,9 @@ def check_ip_port(ip, port): # type: (str, int) -> None if not args.skip_ping_check: logger.info('Verifying IP %s port %d ...' % (ip, port)) - if ip.startswith('[') or '::' in ip: + if is_ipv6(ip): s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - if ip.startswith('[') and ip.endswith(']'): - ip = ip[1:-1] + ip = unwrap_ipv6(ip) else: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -2488,6 +2487,7 @@ def command_inspect_image(): ################################## + def unwrap_ipv6(address): # type: (str) -> str if address.startswith('[') and address.endswith(']'): @@ -2495,6 +2495,21 @@ def unwrap_ipv6(address): return address +def wrap_ipv6(address): + # type: (str) -> str + + # We cannot assume it's already wrapped or even an IPv6 address if + # it's already wrapped it'll not pass (like if it's a hostname) and trigger + # the ValueError + try: + if ipaddress.ip_address(unicode(address)).version == 6: + return f"[{address}]" + except ValueError: + pass + + return address + + def is_ipv6(address): # type: (str) -> bool address = unwrap_ipv6(address) @@ -2550,6 +2565,8 @@ def command_bootstrap(): base_ip = '' if args.mon_ip: ipv6 = is_ipv6(args.mon_ip) + if ipv6: + args.mon_ip = wrap_ipv6(args.mon_ip) hasport = r.findall(args.mon_ip) if hasport: port = int(hasport[0]) diff --git a/src/cephadm/tests/test_cephadm.py b/src/cephadm/tests/test_cephadm.py index ef23a260454fa..19aa8e254882a 100644 --- a/src/cephadm/tests/test_cephadm.py +++ b/src/cephadm/tests/test_cephadm.py @@ -177,6 +177,20 @@ default via fe80::2480:28ec:5097:3fe2 dev wlp2s0 proto ra metric 20600 pref medi for address, expected in tests: unwrap_test(address, expected) + def test_wrap_ipv6(self): + def wrap_test(address, expected): + assert cd.wrap_ipv6(address) == expected + + tests = [ + ('::1', '[::1]'), ('[::1]', '[::1]'), + ('fde4:8dba:82e1:0:5054:ff:fe6a:357', + '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'), + ('myhost.example.com', 'myhost.example.com'), + ('192.168.0.1', '192.168.0.1'), + ('', ''), ('fd00::1::1', 'fd00::1::1')] + for address, expected in tests: + wrap_test(address, expected) + @mock.patch('cephadm.call_throws') @mock.patch('cephadm.get_parm') def test_registry_login(self, get_parm, call_throws): diff --git a/src/pybind/mgr/cephadm/services/cephadmservice.py b/src/pybind/mgr/cephadm/services/cephadmservice.py index 52b08a21afaeb..fee1fb059777f 100644 --- a/src/pybind/mgr/cephadm/services/cephadmservice.py +++ b/src/pybind/mgr/cephadm/services/cephadmservice.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, List, Callable, Any, TypeVar, Generic, Option from mgr_module import HandleCommandResult, MonCommandFailed from ceph.deployment.service_spec import ServiceSpec, RGWSpec +from ceph.deployment.utils import is_ipv6, unwrap_ipv6 from orchestrator import OrchestratorError, DaemonDescription from cephadm import utils @@ -250,6 +251,8 @@ class MonService(CephadmService): extra_config += 'public network = %s\n' % network elif network.startswith('[v') and network.endswith(']'): extra_config += 'public addrv = %s\n' % network + elif is_ipv6(network): + extra_config += 'public addr = %s\n' % unwrap_ipv6(network) elif ':' not in network: extra_config += 'public addr = %s\n' % network else: diff --git a/src/pybind/mgr/dashboard/tools.py b/src/pybind/mgr/dashboard/tools.py index 2b6d92ca55f7d..ec88cbbff74c6 100644 --- a/src/pybind/mgr/dashboard/tools.py +++ b/src/pybind/mgr/dashboard/tools.py @@ -5,7 +5,6 @@ import sys import inspect import json import functools -import ipaddress import logging import collections @@ -14,7 +13,6 @@ from distutils.util import strtobool import fnmatch import time import threading -import six from six.moves import urllib import cherrypy @@ -23,6 +21,8 @@ try: except ImportError: from urllib.parse import urljoin +from ceph.deployment.utils import wrap_ipv6 + from . import mgr from .exceptions import ViewCacheNoDataException from .settings import Settings @@ -693,16 +693,7 @@ def build_url(host, scheme=None, port=None): :type port: int :rtype: str """ - try: - try: - u_host = six.u(host) - except TypeError: - u_host = host - - ipaddress.IPv6Address(u_host) - netloc = '[{}]'.format(host) - except ValueError: - netloc = host + netloc = wrap_ipv6(host) if port: netloc += ':{}'.format(port) pr = urllib.parse.ParseResult( diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index a2518867a0721..f204f9a18b115 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -9,6 +9,7 @@ import six import yaml from ceph.deployment.hostspec import HostSpec +from ceph.deployment.utils import unwrap_ipv6 class ServiceSpecValidationError(Exception): @@ -122,13 +123,16 @@ class HostPlacementSpec(namedtuple('HostPlacementSpec', ['hostname', 'network', for network in networks: # only if we have versioned network configs if network.startswith('v') or network.startswith('[v'): - network = network.split(':')[1] + # if this is ipv6 we can't just simply split on ':' so do + # a split once and rsplit once to leave us with just ipv6 addr + network = network.split(':', 1)[1] + network = network.rsplit(':', 1)[0] try: # if subnets are defined, also verify the validity if '/' in network: ip_network(six.text_type(network)) else: - ip_address(six.text_type(network)) + ip_address(unwrap_ipv6(network)) except ValueError as e: # logging? raise e diff --git a/src/python-common/ceph/deployment/utils.py b/src/python-common/ceph/deployment/utils.py new file mode 100644 index 0000000000000..33c84cae10178 --- /dev/null +++ b/src/python-common/ceph/deployment/utils.py @@ -0,0 +1,36 @@ +import ipaddress +import sys + +if sys.version_info > (3, 0): + unicode = str + + +def unwrap_ipv6(address): + # type: (str) -> str + if address.startswith('[') and address.endswith(']'): + return address[1:-1] + return address + + +def wrap_ipv6(address): + # type: (str) -> str + + # We cannot assume it's already wrapped or even an IPv6 address if + # it's already wrapped it'll not pass (like if it's a hostname) and trigger + # the ValueError + try: + if ipaddress.ip_address(unicode(address)).version == 6: + return f"[{address}]" + except ValueError: + pass + + return address + + +def is_ipv6(address): + # type: (str) -> bool + address = unwrap_ipv6(address) + try: + return ipaddress.ip_address(unicode(address)).version == 6 + except ValueError: + return False diff --git a/src/python-common/ceph/tests/test_utils.py b/src/python-common/ceph/tests/test_utils.py new file mode 100644 index 0000000000000..fff67e1702b10 --- /dev/null +++ b/src/python-common/ceph/tests/test_utils.py @@ -0,0 +1,37 @@ +from ceph.deployment.utils import is_ipv6, unwrap_ipv6, wrap_ipv6 + + +def test_is_ipv6(): + for good in ("[::1]", "::1", + "fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"): + assert is_ipv6(good) + for bad in ("127.0.0.1", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffg", + "1:2:3:4:5:6:7:8:9", "fd00::1::1", "[fg::1]"): + assert not is_ipv6(bad) + + +def test_unwrap_ipv6(): + def unwrap_test(address, expected): + assert unwrap_ipv6(address) == expected + + tests = [ + ('::1', '::1'), ('[::1]', '::1'), + ('[fde4:8dba:82e1:0:5054:ff:fe6a:357]', 'fde4:8dba:82e1:0:5054:ff:fe6a:357'), + ('can actually be any string', 'can actually be any string'), + ('[but needs to be stripped] ', '[but needs to be stripped] ')] + for address, expected in tests: + unwrap_test(address, expected) + + +def test_wrap_ipv6(): + def wrap_test(address, expected): + assert wrap_ipv6(address) == expected + + tests = [ + ('::1', '[::1]'), ('[::1]', '[::1]'), + ('fde4:8dba:82e1:0:5054:ff:fe6a:357', '[fde4:8dba:82e1:0:5054:ff:fe6a:357]'), + ('myhost.example.com', 'myhost.example.com'), ('192.168.0.1', '192.168.0.1'), + ('', ''), ('fd00::1::1', 'fd00::1::1')] + for address, expected in tests: + wrap_test(address, expected) -- 2.39.5