cloud-init/backport-Fix-KeyError-when-rendering-sysconfig-IPv6-routes.patch
2023-08-26 14:46:22 +08:00

292 lines
12 KiB
Diff

From 9fabfff5aac6e1b398e18429e6dfad54f59e7f75 Mon Sep 17 00:00:00 2001
From: James Falcon <james.falcon@canonical.com>
Date: Thu, 14 Apr 2022 09:31:17 -0500
Subject: [PATCH] Fix KeyError when rendering sysconfig IPv6 routes (#1380)
Reference:https://github.com/canonical/cloud-init/commit/9fabfff5aac6e1b398e18429e6dfad54f59e7f75
Conflict:(1)do not delete ipv6_mask_to_net_prefix.
(2)do not add "NM_CONTROLLED=no" in test because of cloud-init-20.4-nm-controlled.patch.
(3)format diffs.
Route rendering code was expecting a netmask rather than using the
prefix. A prefix is provided to the renderer, but was being hidden
from the route rendering code. This commit exposes the prefix and
prefers it for IPv6, given how uncommon netmasks are for IPv6.
LP: #1958506
---
cloudinit/net/network_state.py | 4 +-
cloudinit/net/renderer.py | 4 +-
cloudinit/net/sysconfig.py | 41 +++++++-------
tests/unittests/test_net.py | 100 +++++++++++++++++++++++++++++++++
4 files changed, 124 insertions(+), 25 deletions(-)
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 0a4e0c6..0d81291 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -885,12 +885,12 @@ def _normalize_net_keys(network, address_keys=()):
else:
# this supports input of 255.255.255.0
prefix = ipv4_mask_to_net_prefix(maybe_prefix)
+ elif "prefix" in net:
+ prefix = int(net["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
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index 54a83b5..e387173 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -8,7 +8,7 @@
import abc
import io
-from cloudinit.net.network_state import parse_net_config_data
+from cloudinit.net.network_state import NetworkState, parse_net_config_data
from cloudinit.net.udev import generate_udev_rule
@@ -32,7 +32,7 @@ class Renderer(object):
pass
@staticmethod
- def _render_persistent_net(network_state):
+ def _render_persistent_net(network_state: NetworkState):
"""Given state, emit udev rules to map mac to ifname."""
# TODO(harlowja): this seems shared between eni renderer and
# this, so move it to a shared location.
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
index 4c6caef..6d17d69 100644
--- a/cloudinit/net/sysconfig.py
+++ b/cloudinit/net/sysconfig.py
@@ -4,6 +4,7 @@ import copy
import io
import os
import re
+from typing import Mapping
from configobj import ConfigObj
@@ -17,6 +18,7 @@ from cloudinit.net import network_state
from . import renderer
from .network_state import (
is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES)
+from cloudinit.net.network_state import NetworkState
LOG = logging.getLogger(__name__)
KNOWN_DISTROS = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora',
@@ -178,7 +180,6 @@ class Route(ConfigMap):
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:
@@ -188,6 +189,7 @@ class Route(ConfigMap):
# 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])
# increase IPv4 index
reindex = reindex + 1
buf.write(
@@ -209,9 +211,7 @@ class Route(ConfigMap):
% ("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
- )
+ prefix_value = str(self._conf[f"PREFIX{index}"])
metric_value = (
"metric " + str(self._conf["METRIC" + index])
if "METRIC" + index in self._conf
@@ -604,12 +604,9 @@ class Renderer(renderer.Renderer):
raise ValueError("Duplicate declaration of default "
"route found for interface '%s'"
% (iface_cfg.name))
- # NOTE(harlowja): ipv6 and ipv4 default gateways
- gw_key = 'GATEWAY0'
- nm_key = 'NETMASK0'
- addr_key = 'ADDRESS0'
- # The owning interface provides the default route.
- #
+ # NOTE that instead of defining the route0 settings,
+ # the owning interface provides the default route.
+
# TODO(harlowja): add validation that no other iface has
# also provided the default route?
iface_cfg['DEFROUTE'] = True
@@ -626,19 +623,19 @@ class Renderer(renderer.Renderer):
iface_cfg['METRIC'] = route['metric']
else:
- gw_key = 'GATEWAY%s' % route_cfg.last_idx
- nm_key = 'NETMASK%s' % route_cfg.last_idx
- addr_key = 'ADDRESS%s' % route_cfg.last_idx
- metric_key = 'METRIC%s' % route_cfg.last_idx
- route_cfg.last_idx += 1
# add default routes only to ifcfg files, not
# to route-* or route6-*
- for (old_key, new_key) in [('gateway', gw_key),
- ('metric', metric_key),
- ('netmask', nm_key),
- ('network', addr_key)]:
+ for old_key, new_name in [
+ ("gateway", "GATEWAY"),
+ ("metric", "METRIC"),
+ ("prefix", "PREFIX"),
+ ("netmask", "NETMASK"),
+ ("network", "ADDRESS"),
+ ]:
if old_key in route:
+ new_key = f"{new_name}{route_cfg.last_idx}"
route_cfg[new_key] = route[old_key]
+ route_cfg.last_idx += 1
@classmethod
def _render_bonding_opts(cls, iface_cfg, iface, flavor):
@@ -890,7 +887,7 @@ class Renderer(renderer.Renderer):
'''Given state, return /etc/sysconfig files + contents'''
if not templates:
templates = cls.templates
- iface_contents = {}
+ iface_contents: Mapping[str, NetInterface] = {}
for iface in network_state.iter_interfaces():
if iface['type'] == "loopback":
continue
@@ -922,7 +919,9 @@ class Renderer(renderer.Renderer):
contents[cpath] = iface_cfg.routes.to_string(proto)
return contents
- def render_network_state(self, network_state, templates=None, target=None):
+ def render_network_state(
+ self, network_state: NetworkState, templates=None, target=None
+ ):
if not templates:
templates = self.templates
file_mode = 0o644
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 8737a76..a698c65 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -3871,6 +3871,106 @@ USERCTL=no
self._compare_files_to_expected(
expected, self._render_and_read(network_config=v2data))
+ def test_from_v2_routes(self):
+ """verify routes (including IPv6) get rendered using v2 config.
+ LP: #1958506
+ """
+ v2_data = {
+ "version": 2,
+ "ethernets": {
+ "eth0": {
+ "addresses": [
+ "10.54.2.19/21",
+ "2a00:1730:fff9:100::52/128",
+ ],
+ "gateway4": "10.54.0.1",
+ "gateway6": "2a00:1730:fff9:100::1",
+ "match": {"macaddress": "52:54:00:3f:fc:f7"},
+ "mtu": 1400,
+ "nameservers": {
+ "addresses": [
+ "10.52.1.1",
+ "10.52.1.71",
+ "2001:4860:4860::8888",
+ "2001:4860:4860::8844",
+ ]
+ },
+ "routes": [
+ {
+ "scope": "link",
+ "to": "10.54.0.1/32",
+ "via": "0.0.0.0",
+ },
+ {
+ "scope": "link",
+ "to": "0.0.0.0/0",
+ "via": "10.54.0.1",
+ },
+ {
+ "scope": "link",
+ "to": "2a00:1730:fff9:100::1/128",
+ "via": "::0",
+ },
+ {
+ "scope": "link",
+ "to": "::0/0",
+ "via": "2a00:1730:fff9:100::1",
+ },
+ ],
+ "set-name": "eth0",
+ }
+ },
+ }
+
+ expected = {
+ "ifcfg-eth0": textwrap.dedent(
+ """\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ BOOTPROTO=none
+ DEFROUTE=yes
+ DEVICE=eth0
+ DNS1=10.52.1.1
+ DNS2=10.52.1.71
+ DNS3=2001:4860:4860::8888
+ GATEWAY=10.54.0.1
+ HWADDR=52:54:00:3f:fc:f7
+ IPADDR=10.54.2.19
+ IPV6ADDR=2a00:1730:fff9:100::52/128
+ IPV6INIT=yes
+ IPV6_AUTOCONF=no
+ IPV6_DEFAULTGW=2a00:1730:fff9:100::1
+ IPV6_FORCE_ACCEPT_RA=no
+ MTU=1400
+ NETMASK=255.255.248.0
+ ONBOOT=yes
+ TYPE=Ethernet
+ USERCTL=no
+ """ # noqa: E501
+ ),
+ "route-eth0": textwrap.dedent(
+ """\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ ADDRESS0=10.54.0.1
+ GATEWAY0=0.0.0.0
+ NETMASK0=255.255.255.255
+ """ # noqa: E501
+ ),
+ "route6-eth0": textwrap.dedent(
+ """\
+ # Created by cloud-init on instance boot automatically, do not edit.
+ #
+ 2a00:1730:fff9:100::1/128 via ::0 dev eth0
+ ::0/64 via 2a00:1730:fff9:100::1 dev eth0
+ """ # noqa: E501
+ ),
+ }
+
+ found = self._render_and_read(network_config=v2_data)
+ self._compare_files_to_expected(expected, found)
+ self._assert_headers(found)
+
@mock.patch(
"cloudinit.net.is_openvswitch_internal_interface",
--
2.33.0