292 lines
12 KiB
Diff
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
|
|
|
|
|