--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian....@packages.debian.org
Usertags: pu
X-Debbugs-Cc: cloud-i...@packages.debian.org
Control: affects -1 + src:cloud-init
[ Reason ]
Cloud-init is a package typically built in to cloud VM images. It provides
functionality to facilitate launch-time customization of VMs based on
user-defined configuration data provided to the VM by various supported
mechanisms. This configuration data may contain network configuration, which
cloud-init translates from its custom representation to supported
configuration for one of several supported backends (e.g. ifupdown,
systemd-networkd, and netplan).
The cloud-init package in bookworm contains a bug that prevents it from
generating valid systemd-networkd configuration from certain valid input.
[ Impact ]
Users are left with a broken IPv6 network configuration when attempting to
configure static dualstack networking.
[ Tests ]
* Unit test coverage in the upstream patch
* Repro case provided in the Debian bug
[ Risks ]
The fix has been in sid/trixie since early this year and is pretty well tested
at this point. The patch cherry-picks cleanly to the bookworm version, and
passes testing, so the risk should be small. Worst case scenario is that the
change introduces a new regression that breaks previously working network
configuration scenarios for cloud users.
[ Checklist ]
[*] *all* changes are documented in the d/changelog
[*] I reviewed all changes and I approve them
[*] attach debdiff against the package in (old)stable
[*] the issue is verified as fixed in unstable
[ Changes ]
Upstream fix is cherry-picked from
https://github.com/canonical/cloud-init/commit/f75be2ebe15b0dc78092fe47b1ef8d506607e9da
diff -Nru cloud-init-22.4.2/debian/changelog cloud-init-22.4.2/debian/changelog
--- cloud-init-22.4.2/debian/changelog 2024-05-28 16:23:38.000000000 -0400
+++ cloud-init-22.4.2/debian/changelog 2024-09-17 11:08:48.000000000 -0400
@@ -1,3 +1,9 @@
+cloud-init (22.4.2-1+deb12u2) bookworm; urgency=medium
+
+ * networkd: Add support for multiple [Route] sections (Closes: #1052535)
+
+ -- Noah Meyerhans <no...@debian.org> Tue, 17 Sep 2024 11:08:48 -0400
+
cloud-init (22.4.2-1+deb12u1) bookworm; urgency=medium
* Add Conflicts/Replaces relationship on cloud-init-22.4.2
diff -Nru
cloud-init-22.4.2/debian/patches/networkd_Add_support_for_multiple_Route_sections.patch
cloud-init-22.4.2/debian/patches/networkd_Add_support_for_multiple_Route_sections.patch
---
cloud-init-22.4.2/debian/patches/networkd_Add_support_for_multiple_Route_sections.patch
1969-12-31 19:00:00.000000000 -0500
+++
cloud-init-22.4.2/debian/patches/networkd_Add_support_for_multiple_Route_sections.patch
2024-09-17 11:08:48.000000000 -0400
@@ -0,0 +1,215 @@
+From f75be2ebe15b0dc78092fe47b1ef8d506607e9da Mon Sep 17 00:00:00 2001
+From: Nigel Kukard <nkuk...@lbsd.net>
+Date: Wed, 7 Dec 2022 23:29:52 +0000
+Subject: [PATCH] networkd: Add support for multiple [Route] sections (#1868)
+
+Networkd supports multiple [Route] sections within the same file.
+Currently all [Route] section tags are squashed into one and if there
+is a default gateway it means defining a device route is not possible
+as the target is set to the default gateway.
+
+This patch adds support for multiple [Route] sections allowing us to
+support device routes. This is done by tracking each route in the route
+list individually and ensuring the key-value pairs are maintained within
+their respective [Route] section. This both maintains backwards
+compatibility with previous behavior and allows the specification of
+routes with no destination IP, causing the destination to be added with
+a device target.
+---
+ cloudinit/net/networkd.py | 51 ++++++++++++++++++++++---
+ tests/unittests/net/test_networkd.py | 57 +++++++++++++++++++++++++++-
+ 2 files changed, 101 insertions(+), 7 deletions(-)
+
+Index: cloud-init/cloudinit/net/networkd.py
+===================================================================
+--- cloud-init.orig/cloudinit/net/networkd.py
++++ cloud-init/cloudinit/net/networkd.py
+@@ -28,7 +28,7 @@ class CfgParser:
+ "DHCPv4": [],
+ "DHCPv6": [],
+ "Address": [],
+- "Route": [],
++ "Route": {},
+ }
+ )
+
+@@ -40,6 +40,22 @@ class CfgParser:
+ self.conf_dict[k] = list(dict.fromkeys(self.conf_dict[k]))
+ self.conf_dict[k].sort()
+
++ def update_route_section(self, sec, rid, key, val):
++ """
++ For each route section we use rid as a key, this allows us to isolate
++ this route from others on subsequent calls.
++ """
++ for k in self.conf_dict.keys():
++ if k == sec:
++ if rid not in self.conf_dict[k]:
++ self.conf_dict[k][rid] = []
++ self.conf_dict[k][rid].append(key + "=" + str(val))
++ # remove duplicates from list
++ self.conf_dict[k][rid] = list(
++ dict.fromkeys(self.conf_dict[k][rid])
++ )
++ self.conf_dict[k][rid].sort()
++
+ def get_final_conf(self):
+ contents = ""
+ for k, v in sorted(self.conf_dict.items()):
+@@ -50,6 +66,12 @@ class CfgParser:
+ contents += "[" + k + "]\n"
+ contents += e + "\n"
+ contents += "\n"
++ elif k == "Route":
++ for n in sorted(v):
++ contents += "[" + k + "]\n"
++ for e in sorted(v[n]):
++ contents += e + "\n"
++ contents += "\n"
+ else:
+ contents += "[" + k + "]\n"
+ for e in sorted(v):
+@@ -112,7 +134,11 @@ class Renderer(renderer.Renderer):
+ if "mtu" in iface and iface["mtu"]:
+ cfg.update_section(sec, "MTUBytes", iface["mtu"])
+
+- def parse_routes(self, conf, cfg: CfgParser):
++ def parse_routes(self, rid, conf, cfg: CfgParser):
++ """
++ Parse a route and use rid as a key in order to isolate the route from
++ others in the route dict.
++ """
+ sec = "Route"
+ route_cfg_map = {
+ "gateway": "Gateway",
+@@ -130,11 +156,12 @@ class Renderer(renderer.Renderer):
+ continue
+ if k == "network":
+ v += prefix
+- cfg.update_section(sec, route_cfg_map[k], v)
++ cfg.update_route_section(sec, rid, route_cfg_map[k], v)
+
+ def parse_subnets(self, iface, cfg: CfgParser):
+ dhcp = "no"
+ sec = "Network"
++ rid = 0
+ for e in iface.get("subnets", []):
+ t = e["type"]
+ if t == "dhcp4" or t == "dhcp":
+@@ -149,7 +176,10 @@ class Renderer(renderer.Renderer):
+ dhcp = "yes"
+ if "routes" in e and e["routes"]:
+ for i in e["routes"]:
+- self.parse_routes(i, cfg)
++ # Use "r" as a dict key prefix for this route to isolate
++ # it from other sources of routes
++ self.parse_routes(f"r{rid}", i, cfg)
++ rid = rid + 1
+ if "address" in e:
+ subnet_cfg_map = {
+ "address": "Address",
+@@ -163,7 +193,12 @@ class Renderer(renderer.Renderer):
+ v += "/" + str(e["prefix"])
+ cfg.update_section("Address", subnet_cfg_map[k], v)
+ elif k == "gateway":
+- cfg.update_section("Route", subnet_cfg_map[k], v)
++ # Use "a" as a dict key prefix for this route to
++ # isolate it from other sources of routes
++ cfg.update_route_section(
++ "Route", f"a{rid}", subnet_cfg_map[k], v
++ )
++ rid = rid + 1
+ elif k == "dns_nameservers" or k == "dns_search":
+ cfg.update_section(sec, subnet_cfg_map[k], "
".join(v))
+
+@@ -280,8 +315,12 @@ class Renderer(renderer.Renderer):
+ dhcp = self.parse_subnets(iface, cfg)
+ self.parse_dns(iface, cfg, ns)
+
++ rid = 0
+ for route in ns.iter_routes():
+- self.parse_routes(route, cfg)
++ # Use "c" as a dict key prefix for this route to isolate it
++ # from other sources of routes
++ self.parse_routes(f"c{rid}", route, cfg)
++ rid = rid + 1
+
+ if ns.version == 2:
+ name: Optional[str] = iface["name"]
+Index: cloud-init/tests/unittests/net/test_networkd.py
+===================================================================
+--- cloud-init.orig/tests/unittests/net/test_networkd.py
++++ cloud-init/tests/unittests/net/test_networkd.py
+@@ -195,10 +195,54 @@ Domains=rgrunbla.github.beta.tailscale.n
+
+ [Route]
+ Gateway=10.0.0.1
++
++[Route]
+ Gateway=2a01:4f8:10a:19d2::2
+
+ """
+
++V2_CONFIG_MULTI_SUBNETS = """
++network:
++ version: 2
++ ethernets:
++ eth0:
++ addresses:
++ - 192.168.1.1/24
++ - fec0::1/64
++ gateway4: 192.168.254.254
++ gateway6: "fec0::ffff"
++ routes:
++ - to: 169.254.1.1/32
++ - to: "fe80::1/128"
++"""
++
++V2_CONFIG_MULTI_SUBNETS_RENDERED = """\
++[Address]
++Address=192.168.1.1/24
++
++[Address]
++Address=fec0::1/64
++
++[Match]
++Name=eth0
++
++[Network]
++DHCP=no
++
++[Route]
++Gateway=192.168.254.254
++
++[Route]
++Gateway=fec0::ffff
++
++[Route]
++Destination=169.254.1.1/32
++
++[Route]
++Destination=fe80::1/128
++
++"""
++
+
+ class TestNetworkdRenderState:
+ def _parse_network_state_from_config(self, config):
+@@ -307,5 +351,16 @@ class TestNetworkdRenderState:
+
+ assert rendered_content["eth0"] == V1_CONFIG_MULTI_SUBNETS_RENDERED
+
++ def test_networkd_render_v2_multi_subnets(self):
++ """
++ Ensure a device with multiple subnets gets correctly rendered.
++
++ Per systemd-networkd docs, [Route] can only contain a single instance
++ of Gateway.
++ """
++ with mock.patch("cloudinit.net.get_interfaces_by_mac"):
++ ns =
self._parse_network_state_from_config(V2_CONFIG_MULTI_SUBNETS)
++ renderer = networkd.Renderer()
++ rendered_content = renderer._render_content(ns)
+
+-# vi: ts=4 expandtab
++ assert rendered_content["eth0"] == V2_CONFIG_MULTI_SUBNETS_RENDERED
diff -Nru cloud-init-22.4.2/debian/patches/series
cloud-init-22.4.2/debian/patches/series
--- cloud-init-22.4.2/debian/patches/series 2024-05-28 16:23:38.000000000
-0400
+++ cloud-init-22.4.2/debian/patches/series 2024-09-17 11:08:48.000000000
-0400
@@ -3,3 +3,4 @@
0009-Drop-all-unused-extended-version-handling.patch
0012-Fix-message-when-a-local-is-missing.patch
0001-config-Support-APT-automated-mirror-selection.patch
+networkd_Add_support_for_multiple_Route_sections.patch
--- End Message ---