diff --git a/changelog/46618.fixed.md b/changelog/46618.fixed.md new file mode 100644 index 00000000000..f14ce0099c7 --- /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 f974055ca40..b0d1442dbb8 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 2b7b636965e..3cd5e358987 100644 --- a/tests/pytests/unit/modules/test_debian_ip.py +++ b/tests/pytests/unit/modules/test_debian_ip.py @@ -1123,6 +1123,119 @@ 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 + + +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