From d991360b2daa5f1abe4fcc3b4c22f1fad0bb65c4 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sat, 4 Jul 2026 16:07:56 -0400 Subject: [PATCH 1/2] Accept rh_ip-style ipv6addr/ipv6addrs in debian_ip The Debian ip module only understood ipv6ipaddr/ipv6ipaddrs, while the Red Hat module (rh_ip) uses ipv6addr/ipv6addrs. After the ipv6 prefix is stripped, the remaining addr/addrs had no mapping and failed validation, so no inet6 address stanza was emitted. Add addr->address and addrs->addresses aliases to SALT_ATTR_TO_DEBIAN_ATTR_MAP so both spellings (and bare addr/addrs) resolve to the Debian address settings, letting formulas share one set of names across distributions. Fixes #46618 --- changelog/46618.fixed.md | 1 + salt/modules/debian_ip.py | 10 ++++++ tests/pytests/unit/modules/test_debian_ip.py | 38 ++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 changelog/46618.fixed.md diff --git a/changelog/46618.fixed.md b/changelog/46618.fixed.md new file mode 100644 index 000000000000..f14ce0099c79 --- /dev/null +++ b/changelog/46618.fixed.md @@ -0,0 +1 @@ +Allow the Debian ip module to accept rh_ip-style ipv6addr/ipv6addrs (and bare addr/addrs) as aliases for the address/addresses interface settings. diff --git a/salt/modules/debian_ip.py b/salt/modules/debian_ip.py index f974055ca40f..b0d1442dbb8f 100644 --- a/salt/modules/debian_ip.py +++ b/salt/modules/debian_ip.py @@ -397,6 +397,11 @@ def __space_delimited_list(value): "hwaddr": "hwaddress", # TODO: this limits bootp functionality "ipaddr": "address", "ipaddrs": "addresses", + # Aliases so rh_ip-style names resolve to the Debian attributes. This + # lets ``ipv6addr``/``ipv6addrs`` (stripped to ``addr``/``addrs``) and the + # bare ``addr``/``addrs`` map to the same address stanzas as ``ipaddr``. + "addr": "address", + "addrs": "addresses", } @@ -404,6 +409,7 @@ def __space_delimited_list(value): # TODO DEBIAN_ATTR_TO_SALT_ATTR_MAP["address"] = "address" +DEBIAN_ATTR_TO_SALT_ATTR_MAP["addresses"] = "addresses" DEBIAN_ATTR_TO_SALT_ATTR_MAP["hwaddress"] = "hwaddress" IPV4_VALID_PROTO = ["bootp", "dhcp", "static", "manual", "loopback", "ppp"] @@ -1654,6 +1660,10 @@ def build_interface(iface, iface_type, enabled, **settings): """ Build an interface script for a network interface. + The IPv6 address may be supplied either as ``ipv6ipaddr``/``ipv6ipaddrs`` + or, for consistency with the Red Hat module, as ``ipv6addr``/``ipv6addrs``. + Both spellings map to the same Debian ``address``/``addresses`` stanzas. + CLI Example: .. code-block:: bash diff --git a/tests/pytests/unit/modules/test_debian_ip.py b/tests/pytests/unit/modules/test_debian_ip.py index 2b7b636965ef..b06b4ee3478b 100644 --- a/tests/pytests/unit/modules/test_debian_ip.py +++ b/tests/pytests/unit/modules/test_debian_ip.py @@ -1123,6 +1123,44 @@ def test_build_interface(test_interfaces): ) +def test_build_interface_ipv6addr_alias(): + """ + The rh_ip-style ``ipv6addr``/``ipv6addrs`` names should resolve to the + same Debian ``inet6`` address stanzas as ``ipv6ipaddr``/``ipv6ipaddrs``. + + See https://github.com/saltstack/salt/issues/46618 + """ + common = { + "ipv6proto": "static", + "enable_ipv6": True, + "noifupdown": True, + } + with tempfile.NamedTemporaryFile(mode="r", delete=True) as tfile: + with patch("salt.modules.debian_ip._DEB_NETWORK_FILE", str(tfile.name)): + canonical = debian_ip.build_interface( + iface="eth0", + iface_type="eth", + enabled=True, + interface_file=tfile.name, + ipv6ipaddr="2001:db8:dead:beef::5/64", + ipv6ipaddrs=["2001:db8:dead:beef::7/64"], + **common, + ) + aliased = debian_ip.build_interface( + iface="eth0", + iface_type="eth", + enabled=True, + interface_file=tfile.name, + ipv6addr="2001:db8:dead:beef::5/64", + ipv6addrs=["2001:db8:dead:beef::7/64"], + **common, + ) + + assert " address 2001:db8:dead:beef::5/64\n" in aliased + assert " address 2001:db8:dead:beef::7/64\n" in aliased + assert aliased == canonical + + # 'up' function tests: 1 From 70222cdef9c7250b586be8200b2edd9fc9ddfb44 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 5 Jul 2026 23:20:31 -0400 Subject: [PATCH 2/2] Add direct and inverse regression tests for debian_ip ipv6addr alias The existing PR test already exercises the direct altitude: it calls build_interface() itself with the rh_ip-style ipv6addr/ipv6addrs kwargs, the exact settings names network.managed forwards verbatim from SLS kwargs. This adds the inverse guard against overcorrection from the new bare addr/addrs map entries: on a dual-family interface the aliased IPv6 address must stay confined to the inet6 stanza (the inet stanza renders byte-identical to a build with no IPv6 address), and a MAC-valued bare addr (the legacy shape network.managed remaps to hwaddr) must still be dropped by validation rather than emitted as a bogus address stanza. Both assertions hold with and without the source fix. --- tests/pytests/unit/modules/test_debian_ip.py | 75 ++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/pytests/unit/modules/test_debian_ip.py b/tests/pytests/unit/modules/test_debian_ip.py index b06b4ee3478b..3cd5e3589876 100644 --- a/tests/pytests/unit/modules/test_debian_ip.py +++ b/tests/pytests/unit/modules/test_debian_ip.py @@ -1161,6 +1161,81 @@ def test_build_interface_ipv6addr_alias(): assert aliased == canonical +def test_build_interface_ipv6addr_alias_overcorrection_46618(): + """ + Guard against overcorrection in the issue #46618 fix, which aliased the + rh_ip-style ``addr``/``addrs`` settings names onto the Debian + ``address``/``addresses`` stanzas. + + Two things must NOT start happening because of the alias: + + * on a dual-family interface the aliased ``ipv6addr`` must be confined + to the ``inet6`` stanza; the ``inet`` (IPv4) stanza must render + byte-identical to the same interface built without any IPv6 address + * a MAC-valued bare ``addr`` (the legacy shape that ``network.managed`` + remaps to ``hwaddr`` before calling ``ip.build_interface``) must + still be ignored when passed straight to the module, not rendered as + a bogus ``address`` stanza + + Both assertions hold with and without the source fix applied. + """ + + def inet_stanza(lines): + # Collect only the "iface inet ..." (IPv4) stanza lines. + block = [] + capture = False + for line in lines: + if line.startswith("iface "): + capture = " inet " in line + if capture: + block.append(line) + return block + + common = { + "proto": "static", + "ipaddr": "192.168.4.9", + "netmask": "255.255.255.0", + "ipv6proto": "static", + "enable_ipv6": True, + "noifupdown": True, + } + with tempfile.NamedTemporaryFile(mode="r", delete=True) as tfile: + with patch("salt.modules.debian_ip._DEB_NETWORK_FILE", str(tfile.name)): + baseline = debian_ip.build_interface( + iface="eth9", + iface_type="eth", + enabled=True, + interface_file=tfile.name, + **common, + ) + aliased = debian_ip.build_interface( + iface="eth9", + iface_type="eth", + enabled=True, + interface_file=tfile.name, + ipv6addr="2001:db8:dead:beef::5/64", + **common, + ) + mac_as_addr = debian_ip.build_interface( + iface="eth9", + iface_type="eth", + enabled=True, + interface_file=tfile.name, + proto="manual", + addr="00:11:22:33:44:55", + noifupdown=True, + ) + + # The IPv4 stanza must be untouched by the aliased IPv6 address. + assert inet_stanza(aliased) == inet_stanza(baseline) + assert not any("2001:db8:dead:beef::5/64" in line for line in inet_stanza(aliased)) + + # A MAC in bare ``addr`` fails address validation for both families and + # must be dropped entirely, exactly as before the fix. + assert not any(line.strip().startswith("address ") for line in mac_as_addr) + assert not any("00:11:22:33:44:55" in line for line in mac_as_addr) + + # 'up' function tests: 1