mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-15 19:42:06 +00:00
[stable-4] docker_network: fix IP subnet and address idempotency (#1203)
* Fix IP subnet and address idempotency. (#1201)
(cherry picked from commit 3da2799e03)
* Add warning about missing normalization if ipaddress is not there on Python 2.
* Fix mistake.
This commit is contained in:
parent
99a81449c5
commit
8f50319434
8
changelogs/fragments/1201-docker_network.yml
Normal file
8
changelogs/fragments/1201-docker_network.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
bugfixes:
|
||||||
|
- "docker_network - fix idempotency for IPv6 addresses and networks with Docker 29.0.0 (https://github.com/ansible-collections/community.docker/pull/1201)."
|
||||||
|
known_issues:
|
||||||
|
- "docker_network - when specifying IPv6 addresses or networks, Docker since version 29 no longer returns the orignal address/network used
|
||||||
|
when creating a network, but normalizes them. The module will try to normalize IP addresses for comparison,
|
||||||
|
but it uses the ``ipaddress`` module from the Python 3 standard library for that. When using the module with Python 2,
|
||||||
|
please install the `ipaddress backport for Python 2.x <https://pypi.org/project/ipaddress/>`__
|
||||||
|
(https://github.com/ansible-collections/community.docker/pull/1203)."
|
||||||
@ -447,3 +447,21 @@ def normalize_ip_address(ip_address):
|
|||||||
# If we don't have ipaddress, simply give up...
|
# If we don't have ipaddress, simply give up...
|
||||||
# This mainly affects Python 2.7.
|
# This mainly affects Python 2.7.
|
||||||
return ip_address
|
return ip_address
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_ip_network(network):
|
||||||
|
"""
|
||||||
|
Given a network in CIDR notation as a string, normalize it so that it can be
|
||||||
|
used to compare networks as strings.
|
||||||
|
"""
|
||||||
|
if network is None:
|
||||||
|
return None
|
||||||
|
if HAS_IPADDRESS:
|
||||||
|
try:
|
||||||
|
return ipaddress.ip_network(network).compressed
|
||||||
|
except ValueError:
|
||||||
|
# Fallback for invalid networks: simply return the input
|
||||||
|
return network
|
||||||
|
# If we don't have ipaddress, simply give up...
|
||||||
|
# This mainly affects Python 2.7.
|
||||||
|
return network
|
||||||
|
|||||||
@ -193,6 +193,10 @@ notes:
|
|||||||
- The module does not support Docker Swarm. This means that it will not try to disconnect or reconnect services. If services
|
- The module does not support Docker Swarm. This means that it will not try to disconnect or reconnect services. If services
|
||||||
are connected to the network, deleting the network will fail. When network options are changed, the network has to be
|
are connected to the network, deleting the network will fail. When network options are changed, the network has to be
|
||||||
deleted and recreated, so this will fail as well.
|
deleted and recreated, so this will fail as well.
|
||||||
|
- When specifying IPv6 addresses for networks, Docker since version 29 no longer returns the orignal address used
|
||||||
|
when creating a network, but normalizes them. The module will try to normalize IP addresses for comparison,
|
||||||
|
but it uses the C(ipaddress) module from the Python 3 standard library for that. When using the module with Python 2,
|
||||||
|
please install the L(ipaddress backport for Python 2.x, https://pypi.org/project/ipaddress/).
|
||||||
author:
|
author:
|
||||||
- "Ben Keith (@keitwb)"
|
- "Ben Keith (@keitwb)"
|
||||||
- "Chris Houseknecht (@chouseknecht)"
|
- "Chris Houseknecht (@chouseknecht)"
|
||||||
@ -296,6 +300,8 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
|||||||
DockerBaseClass,
|
DockerBaseClass,
|
||||||
DifferenceTracker,
|
DifferenceTracker,
|
||||||
clean_dict_booleans_for_docker_api,
|
clean_dict_booleans_for_docker_api,
|
||||||
|
normalize_ip_address,
|
||||||
|
normalize_ip_network,
|
||||||
sanitize_labels,
|
sanitize_labels,
|
||||||
)
|
)
|
||||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||||
@ -352,6 +358,7 @@ def validate_cidr(cidr):
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
:raises ValueError: If ``cidr`` is not a valid CIDR
|
:raises ValueError: If ``cidr`` is not a valid CIDR
|
||||||
"""
|
"""
|
||||||
|
# TODO: Use ipaddress for this instead of rolling your own...
|
||||||
if CIDR_IPV4.match(cidr):
|
if CIDR_IPV4.match(cidr):
|
||||||
return 'ipv4'
|
return 'ipv4'
|
||||||
elif CIDR_IPV6.match(cidr):
|
elif CIDR_IPV6.match(cidr):
|
||||||
@ -383,6 +390,19 @@ def dicts_are_essentially_equal(a, b):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_ipam_values(ipam_config):
|
||||||
|
result = {}
|
||||||
|
for key, value in ipam_config.items():
|
||||||
|
if key in ("subnet", "iprange"):
|
||||||
|
value = normalize_ip_network(value)
|
||||||
|
elif key in ("gateway",):
|
||||||
|
value = normalize_ip_address(value)
|
||||||
|
elif key in ("aux_addresses",) and value is not None:
|
||||||
|
value = {k: normalize_ip_address(v) for k, v in value.items()}
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class DockerNetworkManager(object):
|
class DockerNetworkManager(object):
|
||||||
|
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
@ -480,24 +500,35 @@ class DockerNetworkManager(object):
|
|||||||
else:
|
else:
|
||||||
# Put network's IPAM config into the same format as module's IPAM config
|
# Put network's IPAM config into the same format as module's IPAM config
|
||||||
net_ipam_configs = []
|
net_ipam_configs = []
|
||||||
|
net_ipam_configs_normalized = []
|
||||||
for net_ipam_config in net['IPAM']['Config']:
|
for net_ipam_config in net['IPAM']['Config']:
|
||||||
config = dict()
|
config = dict()
|
||||||
for k, v in net_ipam_config.items():
|
for k, v in net_ipam_config.items():
|
||||||
config[normalize_ipam_config_key(k)] = v
|
config[normalize_ipam_config_key(k)] = v
|
||||||
net_ipam_configs.append(config)
|
net_ipam_configs.append(config)
|
||||||
|
net_ipam_configs_normalized.append(normalize_ipam_values(config))
|
||||||
# Compare lists of dicts as sets of dicts
|
# Compare lists of dicts as sets of dicts
|
||||||
for idx, ipam_config in enumerate(self.parameters.ipam_config):
|
for idx, ipam_config in enumerate(self.parameters.ipam_config):
|
||||||
net_config = dict()
|
ipam_config_normalized = normalize_ipam_values(ipam_config)
|
||||||
for net_ipam_config in net_ipam_configs:
|
net_config = {}
|
||||||
if dicts_are_essentially_equal(ipam_config, net_ipam_config):
|
net_config_normalized = {}
|
||||||
|
for net_ipam_config, net_ipam_config_normalized in zip(
|
||||||
|
net_ipam_configs, net_ipam_configs_normalized
|
||||||
|
):
|
||||||
|
if dicts_are_essentially_equal(
|
||||||
|
ipam_config_normalized, net_ipam_config_normalized
|
||||||
|
):
|
||||||
net_config = net_ipam_config
|
net_config = net_ipam_config
|
||||||
|
net_config_normalized = net_ipam_config_normalized
|
||||||
break
|
break
|
||||||
for key, value in ipam_config.items():
|
for key, value in ipam_config.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
# due to recursive argument_spec, all keys are always present
|
# due to recursive argument_spec, all keys are always present
|
||||||
# (but have default value None if not specified)
|
# (but have default value None if not specified)
|
||||||
continue
|
continue
|
||||||
if value != net_config.get(key):
|
if ipam_config_normalized[key] != net_config_normalized.get(
|
||||||
|
key
|
||||||
|
):
|
||||||
differences.add('ipam_config[%s].%s' % (idx, key),
|
differences.add('ipam_config[%s].%s' % (idx, key),
|
||||||
parameter=value,
|
parameter=value,
|
||||||
active=net_config.get(key))
|
active=net_config.get(key))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user