parser_list_networks = subparsers.add_parser(
'list-networks', help='list IP networks')
+ parser_list_networks.add_argument(
+ '--allow-lo-routes',
+ action='store_true',
+ default=False,
+ help='Reserved for future filtering of loopback-related routes',
+ )
+ parser_list_networks.add_argument(
+ '--allow-bgp-routes',
+ action='store_true',
+ default=False,
+ help='Reserved for future filtering of BGP-derived routes',
+ )
parser_list_networks.set_defaults(func=command_list_networks)
parser_list_rdma = subparsers.add_parser(
def list_networks(ctx):
# type: (CephadmContext) -> Dict[str,Dict[str, Set[str]]]
- # sadly, 18.04's iproute2 4.15.0-2ubun doesn't support the -j flag,
- # so we'll need to use a regex to parse 'ip' command output.
- #
- # out, _, _ = call_throws(['ip', '-j', 'route', 'ls'])
- # j = json.loads(out)
- # for x in j:
- res = _list_ipv4_networks(ctx)
- res.update(_list_ipv6_networks(ctx))
+ # Main route tables use text ``ip route ls`` (regex parsers). BGP supplements
+ # use ``ip -j route ls proto bgp`` on supported platforms (iproute2 >= 4.18).
+ allow_lo = getattr(ctx, 'allow_lo_routes', False)
+ allow_bgp = getattr(ctx, 'allow_bgp_routes', False)
+ res = _list_ipv4_networks(
+ ctx, allow_lo_routes=allow_lo, allow_bgp_routes=allow_bgp
+ )
+ res.update(
+ _list_ipv6_networks(
+ ctx, allow_lo_routes=allow_lo, allow_bgp_routes=allow_bgp
+ )
+ )
return res
return result
-def _list_ipv4_networks(
- ctx: CephadmContext,
-) -> Dict[str, Dict[str, Set[str]]]:
- execstr: Optional[str] = find_executable('ip')
- if not execstr:
- raise FileNotFoundError("unable to find 'ip' command")
- out, _, _ = call_throws(
- ctx,
- [execstr, 'route', 'ls'],
- verbosity=CallVerbosity.QUIET_UNLESS_ERROR,
+_IPV4_LOOPBACK_SPACE = ipaddress.ip_network('127.0.0.0/8')
+_IPV6_LOOPBACK_HOST = ipaddress.ip_network('::1/128')
+
+
+def _is_ipv4_loopback(net_s: str) -> bool:
+ """True if *net_s* is an IPv4 prefix wholly within host loopback 127.0.0.0/8."""
+ try:
+ n = ipaddress.ip_network(net_s, strict=False)
+ except ValueError:
+ return False
+ return (
+ n.version == 4
+ and n.network_address in _IPV4_LOOPBACK_SPACE
+ and n.broadcast_address in _IPV4_LOOPBACK_SPACE
)
- return _parse_ipv4_route(out)
-def _parse_ipv4_route(out: str) -> Dict[str, Dict[str, Set[str]]]:
+def _is_ipv6_loopback(net_s: str) -> bool:
+ """True if *net_s* is exactly the host loopback prefix ::1/128."""
+ try:
+ n = ipaddress.ip_network(net_s, strict=False)
+ except ValueError:
+ return False
+ if n.version != 6:
+ return False
+ return n == _IPV6_LOOPBACK_HOST
+
+
+def _merge_ipv4_network_dicts(
+ dst: Dict[str, Dict[str, Set[str]]], add: Dict[str, Dict[str, Set[str]]]
+) -> None:
+ """Merge *add* into *dst* (nested sets)."""
+ for net, ifaces in add.items():
+ if net not in dst:
+ dst[net] = {}
+ for iface, ips in ifaces.items():
+ if iface not in dst[net]:
+ dst[net][iface] = set()
+ dst[net][iface].update(ips)
+
+
+def _parse_ipv4_route(
+ out: str, allow_lo_routes: bool = False
+) -> Dict[str, Dict[str, Set[str]]]:
r = {} # type: Dict[str, Dict[str, Set[str]]]
p = re.compile(
r'^(\S+) (?:via \S+)? ?dev (\S+) (.*)scope link (.*)src (\S+)'
net = m[0][0]
if '/' not in net: # aggregate /32 mask for single host sub-networks
net += '/32'
- iface = m[0][1]
+ # Strip iproute2 ``@`` suffix (e.g. ``brx.0@eno1`` → ``brx.0``).
+ iface = m[0][1].split('@')[0]
+ if iface == 'lo' and not allow_lo_routes:
+ continue
ip = m[0][4]
+ if _is_ipv4_loopback(net):
+ continue
+ if net not in r:
+ r[net] = {}
+ if iface not in r[net]:
+ r[net][iface] = set()
+ r[net][iface].add(ip)
+ return r
+
+
+def _parse_ipv4_lo_route(out: str) -> Dict[str, Dict[str, Set[str]]]:
+ """``lo`` /32 without ``src``, and ``scope host`` + ``src`` (e.g. dummy /32)."""
+ r = {} # type: Dict[str, Dict[str, Set[str]]]
+ p_lo_no_src = re.compile(
+ r'^(\S+) (?:via \S+)? ?dev (lo(?:@\S+)?) (.*)scope (?:link|host)\s*(?!src\b).*$'
+ )
+ p_host_src = re.compile(
+ r'^(\S+) (?:via \S+)? ?dev (\S+) (.*)scope host (.*)src (\S+)'
+ )
+
+ def put(net: str, iface: str, ip: str) -> None:
+ if _is_ipv4_loopback(net):
+ return
if net not in r:
r[net] = {}
if iface not in r[net]:
r[net][iface] = set()
r[net][iface].add(ip)
+
+ for line in out.splitlines():
+ m = p_lo_no_src.findall(line)
+ if not m:
+ continue
+ net = m[0][0]
+ if '/' not in net:
+ net += '/32'
+ iface = m[0][1].split('@')[0]
+ try:
+ n = ipaddress.ip_network(net, strict=False)
+ except ValueError:
+ continue
+ if n.prefixlen != 32:
+ continue
+ put(net, iface, str(n.network_address))
+
+ for line in out.splitlines():
+ m = p_host_src.findall(line)
+ if not m:
+ continue
+ net = m[0][0]
+ if '/' not in net:
+ net += '/32'
+ iface = m[0][1].split('@')[0]
+ ip = m[0][4]
+ put(net, iface, ip)
+
return r
+def _route_iface_name(dev: str) -> str:
+ """Strip iproute2 ``child@parent`` interface suffix."""
+ return dev.split('@')[0]
+
+
+def _route_dst_to_network(dst: str, version: int) -> str:
+ if '/' in dst:
+ return dst
+ return f'{dst}/{"32" if version == 4 else "128"}'
+
+
+def _parse_bgp_routes_json(
+ out: str,
+ version: int,
+ allow_lo_routes: bool = False,
+) -> Dict[str, Dict[str, Set[str]]]:
+ """Parse JSON from ``ip -j route ls proto bgp`` (or ``ip -6 -j route ls proto bgp``)."""
+ r: Dict[str, Dict[str, Set[str]]] = {}
+ try:
+ routes = json.loads(out)
+ except (json.JSONDecodeError, TypeError):
+ logger.debug('failed to parse ip -j route output as JSON')
+ return r
+ if not isinstance(routes, list):
+ return r
+
+ is_v4 = version == 4
+ host_plen = 32 if is_v4 else 128
+
+ def put(net: str, iface: str, ip: str) -> None:
+ if _is_ipv4_loopback(net) if is_v4 else _is_ipv6_loopback(net):
+ return
+ if net not in r:
+ r[net] = {}
+ if iface not in r[net]:
+ r[net][iface] = set()
+ r[net][iface].add(ip)
+
+ for route in routes:
+ if not isinstance(route, dict):
+ continue
+ dst = route.get('dst')
+ if not dst:
+ continue
+
+ net = _route_dst_to_network(dst, version)
+ nexthops = route.get('nexthops')
+ prefsrc = route.get('prefsrc')
+
+ if nexthops and prefsrc:
+ for nh in nexthops:
+ if not isinstance(nh, dict):
+ continue
+ dev = nh.get('dev')
+ if not dev:
+ continue
+ iface = _route_iface_name(dev)
+ if iface == 'lo' and not allow_lo_routes:
+ continue
+ put(net, iface, prefsrc)
+ continue
+
+ dev = route.get('dev', '')
+ iface = _route_iface_name(dev) if dev else ''
+
+ if iface == 'lo' and not prefsrc:
+ if not allow_lo_routes:
+ continue
+ try:
+ n = ipaddress.ip_network(net, strict=False)
+ except ValueError:
+ continue
+ if n.prefixlen != host_plen:
+ continue
+ put(net, 'lo', str(n.network_address))
+ continue
+
+ if prefsrc and iface:
+ if iface == 'lo' and not allow_lo_routes:
+ continue
+ put(net, iface, prefsrc)
+
+ return r
+
+
+def _parse_ipv4_bgp_route(
+ out: str, allow_lo_routes: bool = False
+) -> Dict[str, Dict[str, Set[str]]]:
+ """Parse JSON from ``ip -j route ls proto bgp``."""
+ return _parse_bgp_routes_json(out, 4, allow_lo_routes)
+
+
+def _parse_ipv6_bgp_route(
+ out: str, allow_lo_routes: bool = False
+) -> Dict[str, Dict[str, Set[str]]]:
+ """Parse JSON from ``ip -6 -j route ls proto bgp``."""
+ return _parse_bgp_routes_json(out, 6, allow_lo_routes)
+
+
+def _list_ipv4_networks(
+ ctx: CephadmContext,
+ allow_lo_routes: bool = False,
+ allow_bgp_routes: bool = False,
+) -> Dict[str, Dict[str, Set[str]]]:
+ """IPv4 networks from ``ip route ls`` (and optional ``proto bgp`` supplement).
+
+ When *allow_lo_routes* is false, routes on ``lo`` are ignored by the parsers.
+ When true, ``_parse_ipv4_lo_route`` supplements the base table.
+ """
+ execstr: Optional[str] = find_executable('ip')
+ if not execstr:
+ raise FileNotFoundError("unable to find 'ip' command")
+ out, _, _ = call_throws(
+ ctx,
+ [execstr, 'route', 'ls'],
+ verbosity=CallVerbosity.QUIET_UNLESS_ERROR,
+ )
+ res = _parse_ipv4_route(out, allow_lo_routes)
+ if allow_lo_routes:
+ _merge_ipv4_network_dicts(res, _parse_ipv4_lo_route(out))
+ if allow_bgp_routes:
+ bgp_out, _, _ = call_throws(
+ ctx,
+ [execstr, '-j', 'route', 'ls', 'proto', 'bgp'],
+ verbosity=CallVerbosity.QUIET_UNLESS_ERROR,
+ )
+ _merge_ipv4_network_dicts(
+ res, _parse_ipv4_bgp_route(bgp_out, allow_lo_routes)
+ )
+ return res
+
+
def _list_ipv6_networks(
ctx: CephadmContext,
+ allow_lo_routes: bool = False,
+ allow_bgp_routes: bool = False,
) -> Dict[str, Dict[str, Set[str]]]:
+ """IPv6 networks from ``ip -6 route`` and ``ip -6 addr``.
+
+ When *allow_lo_routes* is false, routes on ``lo`` are ignored so loopback globals
+ are not listed. When *allow_bgp_routes* is true, ``ip -6 route ls proto bgp``
+ is merged into the same shape as the main table (respecting *allow_lo_routes*
+ for ``lo`` BGP paths).
+ """
execstr: Optional[str] = find_executable('ip')
if not execstr:
raise FileNotFoundError("unable to find 'ip' command")
[execstr, '-6', 'addr', 'ls'],
verbosity=CallVerbosity.QUIET_UNLESS_ERROR,
)
- return _parse_ipv6_route(routes, ips)
+ res = _parse_ipv6_route(routes, ips, allow_lo_routes)
+ if allow_bgp_routes:
+ bgp_out, _, _ = call_throws(
+ ctx,
+ [execstr, '-6', '-j', 'route', 'ls', 'proto', 'bgp'],
+ verbosity=CallVerbosity.QUIET_UNLESS_ERROR,
+ )
+ _merge_ipv4_network_dicts(
+ res, _parse_ipv6_bgp_route(bgp_out, allow_lo_routes)
+ )
+ return res
def _parse_ipv6_route(
- routes: str, ips: str
+ routes: str,
+ ips: str,
+ allow_lo_routes: bool = False,
) -> Dict[str, Dict[str, Set[str]]]:
r = {} # type: Dict[str, Dict[str, Set[str]]]
route_p = re.compile(
net = m[0][0]
if '/' not in net: # aggregate /128 mask for single host sub-networks
net += '/128'
- iface = m[0][1]
- if iface == 'lo': # skip loopback devices
+ iface = m[0][1].split('@')[0]
+ if iface == 'lo' and not allow_lo_routes:
+ continue
+ if _is_ipv6_loopback(net):
continue
if net not in r:
r[net] = {}
class TestCommandListNetworks:
+ # fmt: off
+ # Keep table-style parametrization unchanged for reviewable diffs (see PR discussion).
@pytest.mark.parametrize("test_input, expected", [
(
dedent("""
}
),
])
+ # fmt: on
def test_parse_ipv4_route(self, test_input, expected):
assert _parse_ipv4_route(test_input) == expected
+ # fmt: off
@pytest.mark.parametrize("test_routes, test_ips, expected", [
(
dedent("""
::1 dev lo proto kernel metric 256 pref medium
fe80::/64 dev ceph-brx proto kernel metric 256 pref medium
fe80::/64 dev brx.0 proto kernel metric 256 pref medium
- default via fe80::327c:5e00:6487:71e0 dev enp3s0f1 proto ra metric 1024 expires 1790sec hoplimit 64 pref medium """),
+ default via fe80::327c:5e00:6487:71e0 dev enp3s0f1 proto ra metric 1024 expires 1790sec hoplimit 64 pref medium
+ """),
dedent("""
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
}
),
])
+ # fmt: on
def test_parse_ipv6_route(self, test_routes, test_ips, expected):
assert _parse_ipv6_route(test_routes, test_ips) == expected
+ def test_parse_ipv6_route_includes_lo_global_not_slaac(self):
+ test_routes = dedent(
+ """
+ ::1 dev lo proto kernel metric 256 pref medium
+ 2620:52:0:1304::71 dev lo proto kernel metric 256 pref medium
+ """
+ )
+ test_ips = dedent(
+ """
+ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
+ inet6 ::1/128 scope host
+ valid_lft forever preferred_lft forever
+ inet6 2620:52:0:1304::71/128 scope global
+ valid_lft forever preferred_lft forever
+ """
+ )
+ expected = {
+ '2620:52:0:1304::71/128': {'lo': {'2620:52:0:1304::71'}},
+ }
+ assert (
+ _parse_ipv6_route(test_routes, test_ips, allow_lo_routes=False)
+ == {}
+ )
+ assert (
+ _parse_ipv6_route(test_routes, test_ips, allow_lo_routes=True)
+ == expected
+ )
+
@mock.patch('cephadmlib.net_utils.read_file')
def test_get_ipv6_addr(self, _read_file):
proc_net_if_net6 = """fe80000000000000505400fffe347999 02 40 20 80 eth0
@mock.patch('cephadmlib.host_facts.call_throws')
@mock.patch('cephadmlib.host_facts.find_executable')
- def test_command_list_networks(self, _find_exe, _call_throws, cephadm_fs, capsys):
- _call_throws.return_value = ('10.4.0.1 dev tun0 proto kernel scope link src 10.4.0.2 metric 50\n', '', '')
+ def test_command_list_networks(
+ self, _find_exe, _call_throws, cephadm_fs, capsys
+ ):
+ _call_throws.side_effect = [
+ (
+ '10.4.0.1 dev tun0 proto kernel scope link src 10.4.0.2 metric 50\n',
+ '',
+ '',
+ ),
+ ('', '', ''),
+ ('', '', ''),
+ ]
_find_exe.return_value = 'ip'
with with_cephadm_ctx([]) as ctx:
_cephadm.command_list_networks(ctx)
--- /dev/null
+import json
+from textwrap import dedent
+
+from cephadmlib.host_facts import (
+ _merge_ipv4_network_dicts,
+ _parse_ipv4_bgp_route,
+ _parse_ipv4_lo_route,
+ _parse_ipv4_route,
+ _parse_ipv6_bgp_route,
+)
+
+
+def _merged_ipv4(out: str, *, allow_lo: bool = False):
+ r = _parse_ipv4_route(out, allow_lo)
+ if allow_lo:
+ _merge_ipv4_network_dicts(r, _parse_ipv4_lo_route(out))
+ return r
+
+
+class TestLoopbackRouteParsing:
+ def test_parse_ipv4_route_omits_127_on_lo(self):
+ test_input = dedent(
+ """
+ 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
+ 127.0.0.0/8 dev lo proto kernel scope link src 127.0.0.1
+ """
+ )
+ expected = {'192.168.1.0/24': {'eth0': {'192.168.1.1'}}}
+ assert _parse_ipv4_route(test_input) == expected
+
+ def test_parse_ipv4_route_includes_non_loopback_on_lo(self):
+ test_input = dedent(
+ """
+ 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
+ 10.168.100.0/24 dev lo proto kernel scope link src 10.168.100.10
+ """
+ )
+ expected_with_lo = {
+ '192.168.1.0/24': {'eth0': {'192.168.1.1'}},
+ '10.168.100.0/24': {'lo': {'10.168.100.10'}},
+ }
+ assert _parse_ipv4_route(test_input) == {'192.168.1.0/24': {'eth0': {'192.168.1.1'}}}
+ assert _parse_ipv4_route(test_input, allow_lo_routes=True) == expected_with_lo
+
+ def test_parse_ipv4_lo_route_adds_lo_host_route_without_src(self):
+ # ``ip route add …/32 dev lo proto bgp`` prints no ``src`` line.
+ test_input = dedent(
+ """
+ 192.168.100.0/24 dev ens3 proto kernel scope link src 192.168.100.100
+ 10.168.100.10 dev lo proto bgp scope link
+ """
+ )
+ expected = {
+ '192.168.100.0/24': {'ens3': {'192.168.100.100'}},
+ '10.168.100.10/32': {'lo': {'10.168.100.10'}},
+ }
+ assert _merged_ipv4(test_input, allow_lo=True) == expected
+
+ def test_parse_ipv4_lo_route_scope_host_on_lo(self):
+ test_input = dedent(
+ """
+ 192.168.100.0/24 dev ens3 proto kernel scope link src 192.168.100.100
+ 10.10.10.90 dev lo proto kernel scope host
+ """
+ )
+ assert _parse_ipv4_route(test_input) == {
+ '192.168.100.0/24': {'ens3': {'192.168.100.100'}},
+ }
+ assert _merged_ipv4(test_input, allow_lo=True) == {
+ '192.168.100.0/24': {'ens3': {'192.168.100.100'}},
+ '10.10.10.90/32': {'lo': {'10.10.10.90'}},
+ }
+
+ def test_parse_ipv4_lo_route_bgp_scope_host_on_lo(self):
+ test_input = dedent(
+ """
+ 10.10.10.10 dev lo proto bgp scope host
+ """
+ )
+ assert _parse_ipv4_route(test_input) == {}
+ assert _parse_ipv4_lo_route(test_input) == {
+ '10.10.10.10/32': {'lo': {'10.10.10.10'}},
+ }
+
+ def test_parse_ipv4_lo_route_dummy_scope_host(self):
+ # Host-scoped /32 on dummy (tracker.ceph.com/issues/76229).
+ test_input = dedent(
+ """
+ 192.168.100.0/24 dev ens3 proto kernel scope link src 192.168.100.100
+ 10.1.0.40 dev dummy0 proto kernel scope host src 10.1.0.40
+ """
+ )
+ expected = {
+ '192.168.100.0/24': {'ens3': {'192.168.100.100'}},
+ '10.1.0.40/32': {'dummy0': {'10.1.0.40'}},
+ }
+ assert _merged_ipv4(test_input, allow_lo=True) == expected
+
+ def test_parse_ipv4_bgp_route_ecmp_nexthops(self):
+ # From ``ip -j route ls proto bgp`` on a BGP host (ceph-node-0).
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '192.168.100.5',
+ 'protocol': 'bgp',
+ 'prefsrc': '192.168.100.11',
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ ],
+ },
+ {
+ 'dst': '192.168.100.6',
+ 'protocol': 'bgp',
+ 'prefsrc': '192.168.100.11',
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ ],
+ },
+ {
+ 'dst': '10.0.1.11',
+ 'protocol': 'bgp',
+ 'prefsrc': '10.0.1.11',
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ ],
+ },
+ ]
+ )
+ expected = {
+ '192.168.100.5/32': {
+ 'ens1f0np0': {'192.168.100.11'},
+ 'ens1f1np1': {'192.168.100.11'},
+ },
+ '192.168.100.6/32': {
+ 'ens1f0np0': {'192.168.100.11'},
+ 'ens1f1np1': {'192.168.100.11'},
+ },
+ '10.0.1.11/32': {
+ 'ens1f0np0': {'10.0.1.11'},
+ 'ens1f1np1': {'10.0.1.11'},
+ },
+ }
+ assert _parse_ipv4_bgp_route(bgp_input) == expected
+
+ def test_parse_ipv4_bgp_route_ecmp_without_protocol_field(self):
+ # ``ip -j route ls proto bgp`` may omit ``protocol`` on filtered output.
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '192.168.100.5',
+ 'prefsrc': '192.168.100.11',
+ 'metric': 20,
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ ],
+ },
+ {
+ 'dst': '192.168.100.6',
+ 'prefsrc': '192.168.100.11',
+ 'metric': 20,
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ {
+ 'gateway': '169.254.0.1',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': ['onlink'],
+ },
+ ],
+ },
+ ]
+ )
+ expected = {
+ '192.168.100.5/32': {
+ 'ens1f0np0': {'192.168.100.11'},
+ 'ens1f1np1': {'192.168.100.11'},
+ },
+ '192.168.100.6/32': {
+ 'ens1f0np0': {'192.168.100.11'},
+ 'ens1f1np1': {'192.168.100.11'},
+ },
+ }
+ assert _parse_ipv4_bgp_route(bgp_input) == expected
+
+ def test_parse_ipv4_bgp_route_lo_without_src(self):
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '10.168.100.10',
+ 'dev': 'lo',
+ 'protocol': 'bgp',
+ 'scope': 'link',
+ 'flags': [],
+ }
+ ]
+ )
+ assert _parse_ipv4_bgp_route(bgp_input) == {}
+ assert _parse_ipv4_bgp_route(bgp_input, allow_lo_routes=True) == {
+ '10.168.100.10/32': {'lo': {'10.168.100.10'}},
+ }
+
+ def test_parse_ipv4_bgp_route_lo_from_full_table(self):
+ # ``ip -j route ls`` entry for ``10.10.10.10 dev lo proto bgp``.
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '10.10.10.10',
+ 'dev': 'lo',
+ 'protocol': 'bgp',
+ 'scope': 'link',
+ 'flags': [],
+ }
+ ]
+ )
+ assert _parse_ipv4_bgp_route(bgp_input, allow_lo_routes=True) == {
+ '10.10.10.10/32': {'lo': {'10.10.10.10'}},
+ }
+
+ def test_parse_ipv6_bgp_route_ecmp_nexthops(self):
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '2001:db8:100::5',
+ 'protocol': 'bgp',
+ 'prefsrc': '2001:db8:100::11',
+ 'metric': 20,
+ 'pref': 'medium',
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': 'fe80::1',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': [],
+ },
+ {
+ 'gateway': 'fe80::2',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': [],
+ },
+ ],
+ },
+ {
+ 'dst': '2001:db8:200::1:11',
+ 'protocol': 'bgp',
+ 'prefsrc': '2001:db8:200::1:11',
+ 'metric': 20,
+ 'pref': 'medium',
+ 'flags': [],
+ 'nexthops': [
+ {
+ 'gateway': 'fe80::a',
+ 'dev': 'ens1f0np0',
+ 'weight': 1,
+ 'flags': [],
+ },
+ {
+ 'gateway': 'fe80::b',
+ 'dev': 'ens1f1np1',
+ 'weight': 1,
+ 'flags': [],
+ },
+ ],
+ },
+ ]
+ )
+ expected = {
+ '2001:db8:100::5/128': {
+ 'ens1f0np0': {'2001:db8:100::11'},
+ 'ens1f1np1': {'2001:db8:100::11'},
+ },
+ '2001:db8:200::1:11/128': {
+ 'ens1f0np0': {'2001:db8:200::1:11'},
+ 'ens1f1np1': {'2001:db8:200::1:11'},
+ },
+ }
+ assert _parse_ipv6_bgp_route(bgp_input) == expected
+
+ def test_parse_ipv6_bgp_route_lo_without_src(self):
+ bgp_input = json.dumps(
+ [
+ {
+ 'dst': '2001:db8:bad:10::10',
+ 'dev': 'lo',
+ 'protocol': 'bgp',
+ 'scope': 'link',
+ 'flags': [],
+ }
+ ]
+ )
+ assert _parse_ipv6_bgp_route(bgp_input) == {}
+ assert _parse_ipv6_bgp_route(bgp_input, allow_lo_routes=True) == {
+ '2001:db8:bad:10::10/128': {'lo': {'2001:db8:bad:10::10'}},
+ }
default=1 * 60,
desc='Seconds for which to cache host facts data',
),
+ Option(
+ 'allow_lo_routes',
+ type='bool',
+ default=False,
+ desc='If true, cephadm list-networks is run with --allow-lo-routes so '
+ 'loopback (lo) routes are included in host network facts; if false, '
+ 'they are omitted (default).',
+ ),
+ Option(
+ 'allow_bgp_routes',
+ type='bool',
+ default=False,
+ desc='If true, cephadm list-networks is run with --allow-bgp-routes so '
+ 'BGP routes from ``ip route ls proto bgp`` and '
+ '``ip -6 route ls proto bgp`` are merged into host network facts; if '
+ 'false (default), only the main IPv4 and IPv6 tables are used.',
+ ),
Option(
'host_check_interval',
type='secs',
self.device_cache_timeout = 0
self.daemon_cache_timeout = 0
self.facts_cache_timeout = 0
+ self.allow_lo_routes = False
+ self.allow_bgp_routes = False
self.host_check_interval = 0
self.stray_daemon_check_interval = 0
self.max_count_per_host = 0
def _refresh_host_networks(self, host: str) -> Optional[str]:
try:
+ list_net_args: List[str] = []
+ if self.mgr.allow_lo_routes:
+ list_net_args.append('--allow-lo-routes')
+ if self.mgr.allow_bgp_routes:
+ list_net_args.append('--allow-bgp-routes')
with self.mgr.async_timeout_handler(host, 'cephadm list-networks'):
networks = self.mgr.wait_async(self._run_cephadm_json(
- host, 'mon', 'list-networks', [], no_fsid=True, log_output=self.mgr.log_refresh_metadata))
+ host,
+ 'mon',
+ 'list-networks',
+ list_net_args,
+ no_fsid=True,
+ log_output=self.mgr.log_refresh_metadata,
+ ))
except OrchestratorError as e:
return str(e)