]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: auto wrap and unwrap ipv6 addresses
authorMatthew Oliver <moliver@suse.com>
Mon, 17 Aug 2020 01:08:56 +0000 (11:08 +1000)
committerSebastian Wagner <sebastian.wagner@suse.com>
Thu, 10 Sep 2020 09:00:43 +0000 (11:00 +0200)
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 <moliver@suse.com>
(cherry picked from commit 09eac4bef0f04f5db7118f94dd9679f3295bddf8)

Conflicts:
src/pybind/mgr/dashboard/tools.py
src/python-common/ceph/deployment/service_spec.py

src/cephadm/cephadm
src/cephadm/tests/test_cephadm.py
src/pybind/mgr/cephadm/services/cephadmservice.py
src/pybind/mgr/dashboard/tools.py
src/python-common/ceph/deployment/service_spec.py
src/python-common/ceph/deployment/utils.py [new file with mode: 0644]
src/python-common/ceph/tests/test_utils.py [new file with mode: 0644]

index c93f8e48c61f7ee34db28626a7622e2862e55bef..3f67f09cf407cb4dd2513a171a4379e5954572a4 100755 (executable)
@@ -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])
index ef23a260454fa721f8c1df9cf5b5c9494793c6da..19aa8e254882af7d7a3140016b0f24a9fa9aa097 100644 (file)
@@ -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):
index 52b08a21afaeb009741de0346eccc934ba688072..fee1fb059777f4ef3b31376d943a9cd774f839e2 100644 (file)
@@ -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:
index 2b6d92ca55f7d1ab90e4dadf2c380d4f7f2e79d7..ec88cbbff74c6165a2f069f4dab1c37517214bbd 100644 (file)
@@ -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(
index a2518867a0721e3b9b7e04ed479470cb961eea63..f204f9a18b11536bc650ad43950518558f8107e0 100644 (file)
@@ -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 (file)
index 0000000..33c84ca
--- /dev/null
@@ -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 (file)
index 0000000..fff67e1
--- /dev/null
@@ -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)