cloud-init/backport-Fix-IPv6-netmask-format-for-sysconfig-1215.patch
2023-08-26 14:46:22 +08:00

550 lines
21 KiB
Diff

From b97a30f0a05c1dea918c46ca9c05c869d15fe2d5 Mon Sep 17 00:00:00 2001
From: Harald <hjensas@redhat.com>
Date: Tue, 8 Feb 2022 15:49:00 +0100
Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215)
Reference:https://github.com/canonical/cloud-init/commit/b97a30f0a05c1dea918c46ca9c05c869d15fe2d5
Conflict:format diffs.
This change converts the IPv6 netmask from the network_data.json[1]
format to the CIDR style, <IPv6_addr>/<prefix>.
Using an IPv6 address like ffff:ffff:ffff:ffff:: does not work with
NetworkManager, nor networkscripts.
NetworkManager will ignore the route, logging:
ifcfg-rh: ignoring invalid route at \
"::/:: via fd00:fd00:fd00:2::fffe dev " \
(/etc/sysconfig/network-scripts/route6-:3): \
Argument for "::/::" is not ADDR/PREFIX format
Similarly if using networkscripts, ip route fail with error:
Error: inet6 prefix is expected rather than \
"fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".
Also a bit of refactoring ...
cloudinit.net.sysconfig.Route.to_string:
* Move a couple of lines around to reduce repeated code.
* if "ADDRESS" not in key -> continute, so that the
code block following it can be de-indented.
cloudinit.net.network_state:
* Refactors the ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix
removes mask_to_net_prefix methods. Utilize ipaddress library to
do some of the heavy lifting.
LP: #1959148
---
cloudinit/net/__init__.py | 7 +-
cloudinit/net/network_state.py | 101 +++++++-----------
cloudinit/net/sysconfig.py | 91 +++++++++-------
cloudinit/sources/DataSourceOpenNebula.py | 2 +-
.../sources/helpers/vmware/imc/config_nic.py | 4 +-
cloudinit/net/tests/test_init.py | 4 +-
cloudinit/net/tests/test_network_state.py | 58 +++++++++-
tests/unittests/test_net.py | 78 +++++++++++++-
8 files changed, 233 insertions(+), 112 deletions(-)
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index a503210..d3ac4c8 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -15,7 +15,7 @@ from typing import Any, Dict
from cloudinit import subp
from cloudinit import util
-from cloudinit.net.network_state import mask_to_net_prefix
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
from cloudinit.url_helper import UrlError, readurl
LOG = logging.getLogger(__name__)
@@ -1140,10 +1140,11 @@ class EphemeralIPv4Network(object):
'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
interface, ip, prefix_or_mask, broadcast))
try:
- self.prefix = mask_to_net_prefix(prefix_or_mask)
+ self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask)
except ValueError as e:
raise ValueError(
- 'Cannot setup network: {0}'.format(e)
+ "Cannot setup network, invalid prefix or "
+ "netmask: {0}".format(e)
) from e
self.connectivity_url_data = connectivity_url_data
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index ac44304..0a4e0c6 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -6,6 +6,7 @@
import copy
import functools
+import ipaddress
import logging
import socket
import struct
@@ -878,10 +879,16 @@ def _normalize_net_keys(network, address_keys=()):
try:
prefix = int(maybe_prefix)
except ValueError:
- # this supports input of <address>/255.255.255.0
- prefix = mask_to_net_prefix(maybe_prefix)
- elif netmask:
- prefix = mask_to_net_prefix(netmask)
+ if ipv6:
+ # this supports input of ffff:ffff:ffff::
+ prefix = ipv6_mask_to_net_prefix(maybe_prefix)
+ else:
+ # this supports input of 255.255.255.0
+ prefix = ipv4_mask_to_net_prefix(maybe_prefix)
+ elif netmask and not ipv6:
+ prefix = ipv4_mask_to_net_prefix(netmask)
+ elif netmask and ipv6:
+ prefix = ipv6_mask_to_net_prefix(netmask)
elif 'prefix' in net:
prefix = int(net['prefix'])
else:
@@ -978,73 +985,41 @@ def ipv4_mask_to_net_prefix(mask):
str(24) => 24
"24" => 24
"""
- if isinstance(mask, int):
- return mask
- if isinstance(mask, str):
- try:
- return int(mask)
- except ValueError:
- pass
- else:
- raise TypeError("mask '%s' is not a string or int")
-
- if '.' not in mask:
- raise ValueError("netmask '%s' does not contain a '.'" % mask)
-
- toks = mask.split(".")
- if len(toks) != 4:
- raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks)))
-
- return sum([bin(int(x)).count('1') for x in toks])
-
+ return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen
def ipv6_mask_to_net_prefix(mask):
"""Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix.
- If 'mask' is an integer or string representation of one then
- int(mask) will be returned.
+ If the input is already an integer or a string representation of
+ an integer, then int(mask) will be returned.
+ "ffff:ffff:ffff::" => 48
+ "48" => 48
"""
-
- if isinstance(mask, int):
- return mask
- if isinstance(mask, str):
- try:
- return int(mask)
- except ValueError:
- pass
- else:
- raise TypeError("mask '%s' is not a string or int")
-
- if ':' not in mask:
- raise ValueError("mask '%s' does not have a ':'")
-
- bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00,
- 0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc,
- 0xfffe, 0xffff]
- prefix = 0
- for word in mask.split(':'):
- if not word or int(word, 16) == 0:
- break
- prefix += bitCount.index(int(word, 16))
-
- return prefix
-
-
-def mask_to_net_prefix(mask):
- """Return the network prefix for the netmask provided.
-
- Supports ipv4 or ipv6 netmasks."""
try:
- # if 'mask' is a prefix that is an integer.
- # then just return it.
- return int(mask)
+ # In the case the mask is already a prefix
+ prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen
+ return prefixlen
except ValueError:
+ # ValueError means mask is an IPv6 address representation and need
+ # conversion.
pass
- if is_ipv6_addr(mask):
- return ipv6_mask_to_net_prefix(mask)
- else:
- return ipv4_mask_to_net_prefix(mask)
-
+
+ netmask = ipaddress.ip_address(mask)
+ mask_int = int(netmask)
+ # If the mask is all zeroes, just return it
+ if mask_int == 0:
+ return mask_int
+
+ trailing_zeroes = min(
+ ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length()
+ )
+ leading_ones = mask_int >> trailing_zeroes
+ prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes
+ all_ones = (1 << prefixlen) - 1
+ if leading_ones != all_ones:
+ raise ValueError("Invalid network mask '%s'" % mask)
+
+ return prefixlen
def mask_and_ipv4_to_bcast_addr(mask, ip):
"""Calculate the broadcast address from the subnet mask and ip addr.
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 32a2d5a..4c6caef 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -12,6 +12,7 @@ from cloudinit import util
from cloudinit import subp
from cloudinit.distros.parsers import networkmanager_conf
from cloudinit.distros.parsers import resolv_conf
+from cloudinit.net import network_state
from . import renderer
from .network_state import (
@@ -172,44 +173,60 @@ class Route(ConfigMap):
# (because Route can contain a mix of IPv4 and IPv6)
reindex = -1
for key in sorted(self._conf.keys()):
- if 'ADDRESS' in key:
- index = key.replace('ADDRESS', '')
- address_value = str(self._conf[key])
- # only accept combinations:
- # if proto ipv6 only display ipv6 routes
- # if proto ipv4 only display ipv4 routes
- # do not add ipv6 routes if proto is ipv4
- # do not add ipv4 routes if proto is ipv6
- # (this array will contain a mix of ipv4 and ipv6)
- if proto == "ipv4" and not self.is_ipv6_route(address_value):
- netmask_value = str(self._conf['NETMASK' + index])
- gateway_value = str(self._conf['GATEWAY' + index])
- # increase IPv4 index
- reindex = reindex + 1
- buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
- _quote_value(address_value)))
- buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
- _quote_value(gateway_value)))
- buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
- _quote_value(netmask_value)))
- metric_key = 'METRIC' + index
- if metric_key in self._conf:
- metric_value = str(self._conf['METRIC' + index])
- buf.write("%s=%s\n" % ('METRIC' + str(reindex),
- _quote_value(metric_value)))
- elif proto == "ipv6" and self.is_ipv6_route(address_value):
- netmask_value = str(self._conf['NETMASK' + index])
- gateway_value = str(self._conf['GATEWAY' + index])
- metric_value = (
- 'metric ' + str(self._conf['METRIC' + index])
- if 'METRIC' + index in self._conf else '')
- buf.write(
- "%s/%s via %s %s dev %s\n" % (address_value,
- netmask_value,
- gateway_value,
- metric_value,
- self._route_name))
+ if "ADDRESS" not in key:
+ continue
+ index = key.replace("ADDRESS", "")
+ address_value = str(self._conf[key])
+ netmask_value = str(self._conf["NETMASK" + index])
+ gateway_value = str(self._conf["GATEWAY" + index])
+
+ # only accept combinations:
+ # if proto ipv6 only display ipv6 routes
+ # if proto ipv4 only display ipv4 routes
+ # do not add ipv6 routes if proto is ipv4
+ # do not add ipv4 routes if proto is ipv6
+ # (this array will contain a mix of ipv4 and ipv6)
+ if proto == "ipv4" and not self.is_ipv6_route(address_value):
+ # increase IPv4 index
+ reindex = reindex + 1
+ buf.write(
+ "%s=%s\n"
+ % ("ADDRESS" + str(reindex), _quote_value(address_value))
+ )
+ buf.write(
+ "%s=%s\n"
+ % ("GATEWAY" + str(reindex), _quote_value(gateway_value))
+ )
+ buf.write(
+ "%s=%s\n"
+ % ("NETMASK" + str(reindex), _quote_value(netmask_value))
+ )
+ metric_key = "METRIC" + index
+ if metric_key in self._conf:
+ metric_value = str(self._conf["METRIC" + index])
+ buf.write("%s=%s\n"
+ % ("METRIC" + str(reindex), _quote_value(metric_value))
+ )
+ elif proto == "ipv6" and self.is_ipv6_route(address_value):
+ prefix_value = network_state.ipv6_mask_to_net_prefix(
+ netmask_value
+ )
+ metric_value = (
+ "metric " + str(self._conf["METRIC" + index])
+ if "METRIC" + index in self._conf
+ else ""
+ )
+ buf.write(
+ "%s/%s via %s %s dev %s\n"
+ % (
+ address_value,
+ prefix_value,
+ gateway_value,
+ metric_value,
+ self._route_name,
+ )
+ )
return buf.getvalue()
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index 21603fb..8b37f39 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -237,7 +237,7 @@ class OpenNebulaNetwork(object):
# Set IPv4 address
devconf['addresses'] = []
mask = self.get_mask(c_dev)
- prefix = str(net.mask_to_net_prefix(mask))
+ prefix = str(net.ipv4_mask_to_net_prefix(mask))
devconf['addresses'].append(
self.get_ip(c_dev, mac) + '/' + prefix)
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
index 9cd2c0c..3a45c67 100644
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
@@ -9,7 +9,7 @@ import logging
import os
import re
-from cloudinit.net.network_state import mask_to_net_prefix
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
from cloudinit import subp
from cloudinit import util
@@ -180,7 +180,7 @@ class NicConfigurator(object):
"""
route_list = []
- cidr = mask_to_net_prefix(netmask)
+ cidr = ipv4_mask_to_net_prefix(netmask)
for gateway in gateways:
destination = "%s/%d" % (gen_subnet(gateway, netmask), cidr)
diff --git a/cloudinit/net/tests/test_init.py b/cloudinit/net/tests/test_init.py
index 2ef5ab7..215cabf 100644
--- a/cloudinit/net/tests/test_init.py
+++ b/cloudinit/net/tests/test_init.py
@@ -588,7 +588,9 @@ class TestEphemeralIPV4Network(CiTestCase):
with net.EphemeralIPv4Network(**params):
pass
error = context_manager.exception
- self.assertIn('Cannot setup network: netmask', str(error))
+ self.assertIn(
+ "Cannot setup network, invalid prefix or netmask: ", str(error)
+ )
self.assertEqual(0, m_subp.call_count)
def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
diff --git a/cloudinit/net/tests/test_network_state.py b/cloudinit/net/tests/test_network_state.py
index 45e9917..f03db50 100644
--- a/cloudinit/net/tests/test_network_state.py
+++ b/cloudinit/net/tests/test_network_state.py
@@ -1,5 +1,5 @@
# This file is part of cloud-init. See LICENSE file for license information.
-
+import ipaddress
from unittest import mock
import pytest
@@ -161,4 +161,60 @@ class TestNetworkStateParseNameservers:
'spam.local',
] == sorted(config.dns_searchdomains)
+class TestNetworkStateHelperFunctions(CiTestCase):
+ def test_mask_to_net_prefix_ipv4(self):
+ netmask_value = "255.255.255.0"
+ expected = 24
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+ def test_mask_to_net_prefix_all_bits_ipv4(self):
+ netmask_value = "255.255.255.255"
+ expected = 32
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+ def test_mask_to_net_prefix_to_many_bits_ipv4(self):
+ netmask_value = "33"
+ self.assertRaises(
+ ValueError, network_state.ipv4_mask_to_net_prefix, netmask_value
+ )
+
+ def test_mask_to_net_prefix_all_bits_ipv6(self):
+ netmask_value = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ expected = 128
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+ def test_mask_to_net_prefix_ipv6(self):
+ netmask_value = "ffff:ffff:ffff:ffff::"
+ expected = 64
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+ def test_mask_to_net_prefix_raises_value_error(self):
+ netmask_value = "ff:ff:ff:ff::"
+ self.assertRaises(
+ ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value
+ )
+
+ def test_mask_to_net_prefix_to_many_bits_ipv6(self):
+ netmask_value = "129"
+ self.assertRaises(
+ ValueError, network_state.ipv6_mask_to_net_prefix, netmask_value
+ )
+
+ def test_mask_to_net_prefix_ipv4_object(self):
+ netmask_value = ipaddress.IPv4Address("255.255.255.255")
+ expected = 32
+ prefix_value = network_state.ipv4_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+ def test_mask_to_net_prefix_ipv6_object(self):
+ netmask_value = ipaddress.IPv6Address("ffff:ffff:ffff::")
+ expected = 48
+ prefix_value = network_state.ipv6_mask_to_net_prefix(netmask_value)
+ assert prefix_value == expected
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 764e1c7..5013277 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -2237,10 +2237,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
routes:
- gateway: 2001:67c:1562:1
network: 2001:67c:1
- netmask: ffff:ffff:0
+ netmask: "ffff:ffff::"
- gateway: 3001:67c:1562:1
network: 3001:67c:1
- netmask: ffff:ffff:0
+ netmask: "ffff:ffff::"
metric: 10000
"""),
'expected_netplan': textwrap.dedent("""
@@ -2507,8 +2507,8 @@ iface bond0 inet6 static
'route6-bond0': textwrap.dedent("""\
# Created by cloud-init on instance boot automatically, do not edit.
#
- 2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1 dev bond0
- 3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0
+ 2001:67c:1/32 via 2001:67c:1562:1 dev bond0
+ 3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0
"""),
'route-bond0': textwrap.dedent("""\
ADDRESS0=10.1.3.0
@@ -3297,6 +3297,76 @@ USERCTL=no
renderer.render_network_state(ns, target=render_dir)
self.assertEqual([], os.listdir(render_dir))
+ def test_invalid_network_mask_ipv6(self):
+ net_json = {
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
+ "networks": [
+ {
+ "network_id": "public-ipv6",
+ "type": "ipv6",
+ "netmask": "",
+ "link": "tap1a81968a-79",
+ "routes": [
+ {
+ "gateway": "2001:DB8::1",
+ "netmask": "ff:ff:ff:ff::",
+ "network": "2001:DB8:1::1",
+ },
+ ],
+ "ip_address": "2001:DB8::10",
+ "id": "network1",
+ }
+ ],
+ "links": [
+ {
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+ "mtu": None,
+ "type": "bridge",
+ "id": "tap1a81968a-79",
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
+ },
+ ],
+ }
+ macs = {"fa:16:3e:ed:9a:59": "eth0"}
+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
+ with self.assertRaises(ValueError):
+ network_state.parse_net_config_data(network_cfg, skip_broken=False)
+
+ def test_invalid_network_mask_ipv4(self):
+ net_json = {
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
+ "networks": [
+ {
+ "network_id": "public-ipv4",
+ "type": "ipv4",
+ "netmask": "",
+ "link": "tap1a81968a-79",
+ "routes": [
+ {
+ "gateway": "172.20.0.1",
+ "netmask": "255.234.255.0",
+ "network": "172.19.0.0",
+ },
+ ],
+ "ip_address": "172.20.0.10",
+ "id": "network1",
+ }
+ ],
+ "links": [
+ {
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+ "mtu": None,
+ "type": "bridge",
+ "id": "tap1a81968a-79",
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
+ },
+ ],
+ }
+ macs = {"fa:16:3e:ed:9a:59": "eth0"}
+ network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
+ with self.assertRaises(ValueError):
+ network_state.parse_net_config_data(network_cfg, skip_broken=False)
+
def test_openstack_rendering_samples(self):
for os_sample in OS_SAMPLES:
render_dir = self.tmp_dir()
--
2.33.0