diff --git a/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch b/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch new file mode 100644 index 0000000..a99e43f --- /dev/null +++ b/SOURCES/ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch @@ -0,0 +1,484 @@ +From e38ff212eb35943961b79f0d30cdceffc1bc0905 Mon Sep 17 00:00:00 2001 +From: Emanuele Giuseppe Esposito +Date: Wed, 2 Mar 2022 10:18:02 +0100 +Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215) + +RH-Author: Emanuele Giuseppe Esposito +RH-MergeRequest: 49: Fix IPv6 netmask format for sysconfig (#1215) +RH-Commit: [1/1] 7a97580791fc03f6ae878a699cf92f620f58a237 +RH-Bugzilla: 2060026 +RH-Acked-by: Eduardo Otubo +RH-Acked-by: Mohamed Gamal Morsy + +commit b97a30f0a05c1dea918c46ca9c05c869d15fe2d5 +Author: Harald +Date: Tue Feb 8 15:49:00 2022 +0100 + + Fix IPv6 netmask format for sysconfig (#1215) + + This change converts the IPv6 netmask from the network_data.json[1] + format to the CIDR style, /. + + 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 $DEV" \ + (/etc/sysconfig/network-scripts/route6-$DEV: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 + +Conflicts (most related to different format style): + cloudinit/net/__init__.py + cloudinit/net/network_state.py + cloudinit/net/sysconfig.py + cloudinit/sources/DataSourceOpenNebula.py + cloudinit/sources/helpers/vmware/imc/config_nic.py + tests/unittests/net/test_init.py (file not backported) + tests/unittests/net/test_network_state.py (file not backported) + tests/unittests/test_net.py + +Signed-off-by: Emanuele Giuseppe Esposito +--- + cloudinit/net/__init__.py | 7 +- + cloudinit/net/network_state.py | 103 +++++++----------- + cloudinit/net/sysconfig.py | 91 ++++++++++------ + cloudinit/sources/DataSourceOpenNebula.py | 2 +- + .../sources/helpers/vmware/imc/config_nic.py | 4 +- + tests/unittests/test_net.py | 78 ++++++++++++- + 6 files changed, 176 insertions(+), 109 deletions(-) + +diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py +index 003efa2a..12bf64de 100644 +--- a/cloudinit/net/__init__.py ++++ b/cloudinit/net/__init__.py +@@ -14,7 +14,7 @@ import re + + 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__) +@@ -1048,10 +1048,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 = connectivity_url +diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py +index e8bf9e39..2768ef94 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 +@@ -872,12 +873,18 @@ def _normalize_net_keys(network, address_keys=()): + try: + prefix = int(maybe_prefix) + except ValueError: +- # this supports input of
/255.255.255.0 +- prefix = mask_to_net_prefix(maybe_prefix) +- elif netmask: +- prefix = mask_to_net_prefix(netmask) +- elif 'prefix' in net: +- prefix = int(net['prefix']) ++ 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: + prefix = 64 if ipv6 else 24 + +@@ -972,72 +979,42 @@ 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): +diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py +index d5440998..7ecbe1c3 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 ( +@@ -171,43 +172,61 @@ 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 '') ++ 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 via %s %s dev %s\n" % (address_value, +- netmask_value, +- gateway_value, +- metric_value, +- self._route_name)) ++ "%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 730ec586..e7980ab1 100644 +--- a/cloudinit/sources/DataSourceOpenNebula.py ++++ b/cloudinit/sources/DataSourceOpenNebula.py +@@ -233,7 +233,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 9cd2c0c0..3a45c67e 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/tests/unittests/test_net.py b/tests/unittests/test_net.py +index 14d3462f..a7f6a1f7 100644 +--- a/tests/unittests/test_net.py ++++ b/tests/unittests/test_net.py +@@ -2025,10 +2025,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(""" +@@ -2295,8 +2295,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 +@@ -3088,6 +3088,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.27.0 + diff --git a/SPECS/cloud-init.spec b/SPECS/cloud-init.spec index ac098a0..d8eaae4 100644 --- a/SPECS/cloud-init.spec +++ b/SPECS/cloud-init.spec @@ -6,7 +6,7 @@ Name: cloud-init Version: 21.1 -Release: 7%{?dist}.4.alma +Release: 7%{?dist}.5.alma Summary: Cloud instance init scripts Group: System Environment/Base @@ -50,6 +50,8 @@ Patch20: ci-Change-netifaces-dependency-to-0.10.4-965.patch Patch21: ci-Update-dscheck_VMware-s-rpctool-check-970.patch # For bz#2040704 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' [rhel-8.5.0.z] Patch22: ci-Revert-unnecesary-lcase-in-ds-identify-978.patch +# For bz#2060026 - cloud-init writes route6-$DEVICE config with a HEX netmask. ip route does not like : Error: inet6 prefix is expected rather than "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::". [rhel-8.5.0.z] +Patch23: ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch # For bz#2040704 - [cloud-init][RHEL8] Support for cloud-init datasource 'cloud-init-vmware-guestinfo' [rhel-8.5.0.z] #Patch23: ci-Add-netifaces-package-as-a-Requires-in-cloud-init.sp.patch @@ -250,9 +252,14 @@ fi %config(noreplace) %{_sysconfdir}/rsyslog.d/21-cloudinit.conf %changelog -* Fri Mar 25 2022 Andrew Lukoshko - 21.1-7.el8_5.4.alma +* Tue Apr 26 2022 Andrew Lukoshko - 21.1-7.el8_5.5.alma - AlmaLinux support +* Thu Mar 03 2022 Jon Maloy - 21.1-7.el8_5.5 +- ci-Fix-IPv6-netmask-format-for-sysconfig-1215.patch [bz#2060026] +- Resolves: bz#2060026 + (cloud-init writes route6-$DEVICE config with a HEX netmask. ip route does not like : Error: inet6 prefix is expected rather than "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::". [rhel-8.5.0.z]) + * Wed Jan 19 2022 Jon Maloy - 21.1-7.el8_5.4 - ci-Add-gdisk-and-openssl-as-deps-to-fix-UEFI-Azure-init.patch [bz#2040690] - ci-Datasource-for-VMware-953.patch [bz#2040704]