1
0
Fork 0
mirror of https://github.com/containers/ansible-podman-collections.git synced 2026-06-29 15:39:22 +00:00

Fix podman_network not idempotent with IPv6 addresses, fix #1041 (#1042)

* Fix podman_network not idempotent with IPv6 addresses, fix #1041

Normalize IPv6 addresses and subnets before comparing in diff methods
using Python's ipaddress module. Different notations of the same IPv6
address (e.g. "::1" vs "0:1") are now correctly recognized as equal,
preventing unnecessary network recreation.

- Guard _normalize_ip/_normalize_subnet with HAS_IP_ADDRESS_MODULE check
- Normalize user-supplied route strings in diffparam_route
- Keep _lease_range_to_str as @staticmethod
- Catch TypeError in _normalize_ip/_normalize_subnet for None inputs
- Use .get("gateway") with filter in multi-gateway join to avoid KeyError
- Fix implicit string concatenation lint warnings
---------

Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
This commit is contained in:
Sergey 2026-06-17 16:33:31 +03:00 committed by GitHub
parent 78ee8ca20e
commit efa337c536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 249 additions and 15 deletions

View file

@ -483,7 +483,9 @@ class PodmanNetworkDiff:
@staticmethod
def _lease_range_to_str(lease_range):
"""Convert lease_range dict to normalized start-end string."""
return f'{lease_range["start_ip"]}-{lease_range["end_ip"]}'
start = PodmanNetworkDiff._normalize_ip(lease_range["start_ip"])
end = PodmanNetworkDiff._normalize_ip(lease_range["end_ip"])
return f"{start}-{end}"
@staticmethod
def _ip_range_to_str(ip_range_cidr):
@ -497,6 +499,26 @@ class PodmanNetworkDiff:
end = net.broadcast_address
return f"{start}-{end}"
@staticmethod
def _normalize_ip(addr):
"""Normalize an IP address string to its compressed canonical form."""
if not HAS_IP_ADDRESS_MODULE:
return addr
try:
return str(ipaddress.ip_address(addr))
except (ValueError, TypeError):
return addr
@staticmethod
def _normalize_subnet(subnet):
"""Normalize a subnet string to its compressed canonical form."""
if not HAS_IP_ADDRESS_MODULE:
return subnet
try:
return str(ipaddress.ip_network(subnet, strict=False))
except (ValueError, TypeError):
return subnet
def diffparam_disable_dns(self):
# For v3 it's impossible to find out DNS settings.
if LooseVersion(self.version) >= LooseVersion("4.0.0"):
@ -534,6 +556,10 @@ class PodmanNetworkDiff:
after = before
if self.params["gateway"] is not None:
after = self.params["gateway"]
if before:
before = self._normalize_ip(before)
if after:
after = self._normalize_ip(after)
return self._diff_update_and_compare("gateway", before, after)
else:
before_subs = self.info.get("subnets")
@ -542,12 +568,17 @@ class PodmanNetworkDiff:
before = None
if before_subs:
if len(before_subs) > 1 and after:
return self._diff_update_and_compare(
"gateway", ",".join([i["gateway"] for i in before_subs]), after
before_gws = ",".join(
[self._normalize_ip(i.get("gateway")) for i in before_subs if i.get("gateway")]
)
return self._diff_update_and_compare("gateway", before_gws, self._normalize_ip(after))
before = [i.get("gateway") for i in before_subs][0]
if not after:
after = before
if before:
before = self._normalize_ip(before)
if after:
after = self._normalize_ip(after)
return self._diff_update_and_compare("gateway", before, after)
def diffparam_internal(self):
@ -564,9 +595,7 @@ class PodmanNetworkDiff:
def diffparam_ip_range(self):
if not HAS_IP_ADDRESS_MODULE:
self.module.warn(
"Python 'ipaddress' module is not available. "
"Skipping ip_range idempotency check.")
self.module.warn("Python 'ipaddress' module is not available. Skipping ip_range idempotency check.")
return False
before = ""
after = self.params["ip_range"] or ""
@ -594,7 +623,9 @@ class PodmanNetworkDiff:
if before_subs:
before_parts = []
for i in before_subs:
parts = [i["subnet"], i.get("gateway") or ""]
subnet = self._normalize_subnet(i["subnet"])
gateway = self._normalize_ip(i["gateway"]) if i.get("gateway") else ""
parts = [subnet, gateway]
lr = i.get("lease_range")
if lr:
parts.append(self._lease_range_to_str(lr))
@ -604,14 +635,17 @@ class PodmanNetworkDiff:
before = ""
after_parts = []
for i in after:
parts = [i["subnet"], i.get("gateway") or ""]
subnet = self._normalize_subnet(i["subnet"])
gateway = self._normalize_ip(i["gateway"]) if i.get("gateway") else ""
parts = [subnet, gateway]
if i.get("ip_range"):
if HAS_IP_ADDRESS_MODULE:
parts.append(self._ip_range_to_str(i["ip_range"]))
else:
self.module.warn(
"Python 'ipaddress' module is not available. "
"Skipping ip_range idempotency check in net_config.")
"Skipping ip_range idempotency check in net_config."
)
after_parts.append(",".join(parts))
after = ":".join(sorted(after_parts))
return self._diff_update_and_compare("net_config", before, after)
@ -619,10 +653,25 @@ class PodmanNetworkDiff:
def diffparam_route(self):
routes = self.info.get("routes", [])
if routes:
before = [",".join([r["destination"], r["gateway"], str(r.get("metric", ""))]).rstrip(",") for r in routes]
before = [
",".join(
[
self._normalize_subnet(r["destination"]),
self._normalize_ip(r["gateway"]),
str(r.get("metric", "")),
]
).rstrip(",")
for r in routes
]
else:
before = []
after = self.params["route"] or []
after = []
for r in self.params["route"] or []:
parts = r.split(",")
if len(parts) >= 2:
parts[0] = self._normalize_subnet(parts[0])
parts[1] = self._normalize_ip(parts[1])
after.append(",".join(parts))
return self._diff_update_and_compare("route", sorted(before), sorted(after))
def diffparam_subnet(self):
@ -635,8 +684,10 @@ class PodmanNetworkDiff:
after = before
if self.params["subnet"] is not None:
after = self.params["subnet"]
if HAS_IP_ADDRESS_MODULE:
after = ipaddress.ip_network(after).compressed
if before:
before = self._normalize_subnet(before)
if after:
after = self._normalize_subnet(after)
return self._diff_update_and_compare("subnet", before, after)
else:
if self.params["ipv6"] is not None:
@ -649,8 +700,13 @@ class PodmanNetworkDiff:
before = self.info.get("subnets")
if before:
if len(before) > 1 and after:
return self._diff_update_and_compare("subnet", ",".join([i["subnet"] for i in before]), after)
before_subs = ",".join([self._normalize_subnet(i["subnet"]) for i in before])
return self._diff_update_and_compare("subnet", before_subs, self._normalize_subnet(after))
before = [i["subnet"] for i in before][0]
if before:
before = self._normalize_subnet(before)
if after:
after = self._normalize_subnet(after)
return self._diff_update_and_compare("subnet", before, after)
def diffparam_macvlan(self):
@ -859,7 +915,7 @@ class PodmanNetworkManager:
}
process_action = states_map[self.state]
process_action()
self.module.fail_json(msg="Unexpected logic error happened, " "please contact maintainers ASAP!")
self.module.fail_json(msg="Unexpected logic error happened, please contact maintainers ASAP!")
def make_present(self):
"""Run actions if desired state is 'started'."""

View file

@ -832,6 +832,9 @@
- name: Test ip_range diff
include_tasks: test_ip_range_diff.yml
- name: Test IPv6 normalization idempotency (issue 1041)
include_tasks: test_issue_1041.yml
always:
- name: Cleanup

View file

@ -0,0 +1,175 @@
- name: Test IPv6 address normalization idempotency (issue 1041)
block:
- name: Make sure network doesn't exist
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
state: absent
# Test 1: net_config with compressed IPv6 gateway (the reported bug)
- name: Create network with IPv6 gateway using compressed notation
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
net_config:
- subnet: "fcb8:b02:5b0e:bc43:aa32:8d2c::/96"
gateway: "fcb8:b02:5b0e:bc43:aa32:8d2c::1"
- subnet: "10.89.55.0/24"
gateway: "10.89.55.1"
register: ipv6_gw_create
- name: Check network was created
assert:
that:
- ipv6_gw_create is changed
- name: Re-run with same compressed IPv6 gateway - idempotency
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
net_config:
- subnet: "fcb8:b02:5b0e:bc43:aa32:8d2c::/96"
gateway: "fcb8:b02:5b0e:bc43:aa32:8d2c::1"
- subnet: "10.89.55.0/24"
gateway: "10.89.55.1"
register: ipv6_gw_same
diff: true
- name: Check idempotency with compressed IPv6 gateway
assert:
that:
- ipv6_gw_same is not changed
- name: Re-run with expanded IPv6 gateway - must also be idempotent
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
net_config:
- subnet: "fcb8:b02:5b0e:bc43:aa32:8d2c::/96"
gateway: "fcb8:b02:5b0e:bc43:aa32:8d2c:0:1"
- subnet: "10.89.55.0/24"
gateway: "10.89.55.1"
register: ipv6_gw_expanded
diff: true
- name: Check idempotency with expanded IPv6 gateway
assert:
that:
- ipv6_gw_expanded is not changed
- name: Re-run with fully expanded IPv6 gateway and subnet
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
net_config:
- subnet: "fcb8:0b02:5b0e:bc43:aa32:8d2c:0000:0000/96"
gateway: "fcb8:0b02:5b0e:bc43:aa32:8d2c:0000:0001"
- subnet: "10.89.55.0/24"
gateway: "10.89.55.1"
register: ipv6_gw_full
diff: true
- name: Check idempotency with fully expanded IPv6
assert:
that:
- ipv6_gw_full is not changed
# Test 2: standalone subnet/gateway params with IPv6
- name: Delete network
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
state: absent
- name: Create IPv6 network with standalone subnet/gateway
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
subnet: "fd00:dead:beef::/48"
gateway: "fd00:dead:beef::1"
register: ipv6_standalone_create
- name: Check network was created
assert:
that:
- ipv6_standalone_create is changed
- name: Re-run with same compressed notation - idempotency
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
subnet: "fd00:dead:beef::/48"
gateway: "fd00:dead:beef::1"
register: ipv6_standalone_same
diff: true
- name: Check standalone IPv6 idempotency
assert:
that:
- ipv6_standalone_same is not changed
- name: Re-run with expanded notation - must be idempotent
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
subnet: "fd00:dead:beef:0000::/48"
gateway: "fd00:dead:beef:0000:0000:0000:0000:0001"
register: ipv6_standalone_expanded
diff: true
- name: Check standalone IPv6 idempotency with expanded form
assert:
that:
- ipv6_standalone_expanded is not changed
# Test 3: dual-stack net_config with mixed IPv4 and IPv6
- name: Delete network
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
state: absent
- name: Create dual-stack network
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
ipv6: true
net_config:
- subnet: "172.31.55.0/24"
gateway: "172.31.55.1"
- subnet: "fd99:abcd:ef01::/64"
gateway: "fd99:abcd:ef01::1"
register: dual_create
- name: Check dual-stack was created
assert:
that:
- dual_create is changed
- name: Re-run dual-stack with expanded IPv6 - idempotency
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
ipv6: true
net_config:
- subnet: "172.31.55.0/24"
gateway: "172.31.55.1"
- subnet: "fd99:abcd:ef01:0000::/64"
gateway: "fd99:abcd:ef01:0:0:0:0:1"
register: dual_expanded
diff: true
- name: Check dual-stack idempotency with expanded IPv6
assert:
that:
- dual_expanded is not changed
always:
- name: Cleanup network
containers.podman.podman_network:
executable: "{{ test_executable | default('podman') }}"
name: "{{ network_name }}"
state: absent
ignore_errors: true