docker_container: fix idempotency for network IP addresses (#62928)

* Specifying IP addresses needs API version 1.22 or newer.

* Simplify code.

* Use IPAMConfig.IPv*Address instead of IPAddress and GlobalIPv6Address.

* Add changelog.

* Fix syntax errors.

* Add integration test.

* Don't rely on netaddr.

* Normalize IPv6 addresses before comparison.

* Install netaddr, and use it.
This commit is contained in:
Felix Fontein 2019-09-30 10:47:02 +02:00 committed by GitHub
parent a79f7e575a
commit 62c0cae29a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 11 deletions

View file

@ -0,0 +1,4 @@
bugfixes:
- "docker_container - fix idempotency for IP addresses for networks. The old implementation checked the effective
IP addresses assigned by the Docker daemon, and not the specified ones. This causes idempotency issues for
containers which are not running, since they have no effective IP addresses assigned."

View file

@ -2197,7 +2197,8 @@ class Container(DockerBaseClass):
connected_networks = self.container['NetworkSettings']['Networks']
for network in self.parameters.networks:
if connected_networks.get(network['name'], None) is None:
network_info = connected_networks.get(network['name'])
if network_info is None:
different = True
differences.append(dict(
parameter=network,
@ -2205,18 +2206,19 @@ class Container(DockerBaseClass):
))
else:
diff = False
if network.get('ipv4_address') and network['ipv4_address'] != connected_networks[network['name']].get('IPAddress'):
network_info_ipam = network_info.get('IPAMConfig', {})
if network.get('ipv4_address') and network['ipv4_address'] != network_info_ipam.get('IPv4Address'):
diff = True
if network.get('ipv6_address') and network['ipv6_address'] != connected_networks[network['name']].get('GlobalIPv6Address'):
if network.get('ipv6_address') and network['ipv6_address'] != network_info_ipam.get('IPv6Address'):
diff = True
if network.get('aliases'):
if not compare_generic(network['aliases'], connected_networks[network['name']].get('Aliases'), 'allow_more_present', 'set'):
if not compare_generic(network['aliases'], network_info.get('Aliases'), 'allow_more_present', 'set'):
diff = True
if network.get('links'):
expected_links = []
for link, alias in network['links']:
expected_links.append("%s:%s" % (link, alias))
if not compare_generic(expected_links, connected_networks[network['name']].get('Links'), 'allow_more_present', 'set'):
if not compare_generic(expected_links, network_info.get('Links'), 'allow_more_present', 'set'):
diff = True
if diff:
different = True
@ -2224,10 +2226,10 @@ class Container(DockerBaseClass):
parameter=network,
container=dict(
name=network['name'],
ipv4_address=connected_networks[network['name']].get('IPAddress'),
ipv6_address=connected_networks[network['name']].get('GlobalIPv6Address'),
aliases=connected_networks[network['name']].get('Aliases'),
links=connected_networks[network['name']].get('Links')
ipv4_address=network_info_ipam.get('IPv4Address'),
ipv6_address=network_info_ipam.get('IPv6Address'),
aliases=network_info.get('Aliases'),
links=network_info.get('Links')
)
))
return different, differences
@ -3092,7 +3094,8 @@ class AnsibleDockerClientContainer(AnsibleDockerClient):
pids_limit=dict(docker_py_version='1.10.0', docker_api_version='1.23'),
mounts=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
# specials
ipvX_address_supported=dict(docker_py_version='1.9.0', detect_usage=detect_ipvX_address_usage,
ipvX_address_supported=dict(docker_py_version='1.9.0', docker_api_version='1.22',
detect_usage=detect_ipvX_address_usage,
usage_msg='ipv4_address or ipv6_address in networks'),
stop_timeout=dict(), # see _get_additional_minimal_versions()
)

View file

@ -9,6 +9,11 @@
- debug:
msg: "Using container name prefix {{ cname_prefix }}"
# Install netaddr
- name: Install netaddr for ipaddr filter
pip:
name: netaddr
# Run the tests
- block:
- include_tasks: run-test.yml

View file

@ -5,10 +5,11 @@
cname_h1: "{{ cname_prefix ~ '-network-h1' }}"
nname_1: "{{ cname_prefix ~ '-network-1' }}"
nname_2: "{{ cname_prefix ~ '-network-2' }}"
nname_3: "{{ cname_prefix ~ '-network-3' }}"
- name: Registering container name
set_fact:
cnames: "{{ cnames + [cname, cname_h1] }}"
dnetworks: "{{ dnetworks + [nname_1, nname_2] }}"
dnetworks: "{{ dnetworks + [nname_1, nname_2, nname_3] }}"
- name: Create networks
docker_network:
@ -21,6 +22,32 @@
loop_var: network_name
when: docker_py_version is version('1.10.0', '>=')
- set_fact:
subnet_ipv4: "192.168.{{ 64 + (192 | random) }}.0/24"
subnet_ipv6: "fdb6:feea:{{ '%0.4x:%0.4x' | format(65536 | random, 65536 | random) }}::/64"
- set_fact:
# If netaddr would be installed on the controller, one could do:
nname_3_ipv4_2: "{{ subnet_ipv4 | next_nth_usable(2) }}"
nname_3_ipv4_3: "{{ subnet_ipv4 | next_nth_usable(3) }}"
nname_3_ipv4_4: "{{ subnet_ipv4 | next_nth_usable(4) }}"
nname_3_ipv6_2: "{{ subnet_ipv6 | next_nth_usable(2) }}"
nname_3_ipv6_3: "{{ subnet_ipv6 | next_nth_usable(3) }}"
nname_3_ipv6_4: "{{ subnet_ipv6 | next_nth_usable(4) }}"
- debug:
msg: "Chose random IPv4 subnet {{ subnet_ipv4 }} and random IPv6 subnet {{ subnet_ipv6 }}"
- name: Create network with fixed IPv4 and IPv6 subnets
docker_network:
name: "{{ nname_3 }}"
enable_ipv6: yes
ipam_config:
- subnet: "{{ subnet_ipv4 }}"
- subnet: "{{ subnet_ipv6 }}"
state: present
when: docker_py_version is version('1.10.0', '>=')
####################################################################
## network_mode ####################################################
####################################################################
@ -535,6 +562,162 @@
when: docker_py_version is version('1.10.0', '>=')
####################################################################
## networks with IP address ########################################
####################################################################
- block:
- name: create container (stopped) with one network and fixed IP
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: stopped
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_2 }}"
ipv6_address: "{{ nname_3_ipv6_2 }}"
networks_cli_compatible: yes
register: networks_1
- name: create container (stopped) with one network and fixed IP (idempotent)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: stopped
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_2 }}"
ipv6_address: "{{ nname_3_ipv6_2 }}"
networks_cli_compatible: yes
register: networks_2
- name: create container (stopped) with one network and fixed IP (different IPv4)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: stopped
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_3 }}"
ipv6_address: "{{ nname_3_ipv6_2 }}"
networks_cli_compatible: yes
register: networks_3
- name: create container (stopped) with one network and fixed IP (different IPv6)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: stopped
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_3 }}"
ipv6_address: "{{ nname_3_ipv6_3 }}"
networks_cli_compatible: yes
register: networks_4
- name: create container (started) with one network and fixed IP
docker_container:
name: "{{ cname }}"
state: started
register: networks_5
- name: create container (started) with one network and fixed IP (different IPv4)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_4 }}"
ipv6_address: "{{ nname_3_ipv6_3 }}"
networks_cli_compatible: yes
force_kill: yes
register: networks_6
- name: create container (started) with one network and fixed IP (different IPv6)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_4 }}"
ipv6_address: "{{ nname_3_ipv6_4 }}"
networks_cli_compatible: yes
force_kill: yes
register: networks_7
- name: create container (started) with one network and fixed IP (idempotent)
docker_container:
image: alpine:3.8
command: '/bin/sh -c "sleep 10m"'
name: "{{ cname }}"
state: started
networks:
- name: "{{ nname_3 }}"
ipv4_address: "{{ nname_3_ipv4_4 }}"
ipv6_address: "{{ nname_3_ipv6_4 }}"
networks_cli_compatible: yes
register: networks_8
- name: cleanup
docker_container:
name: "{{ cname }}"
state: absent
force_kill: yes
diff: no
- assert:
that:
- networks_1 is changed
- networks_1.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_2
- networks_1.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_2 | ipaddr
- networks_1.container.NetworkSettings.Networks[nname_3].IPAddress == ""
- networks_1.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address == ""
- networks_2 is not changed
- networks_2.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_2
- networks_2.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_2 | ipaddr
- networks_2.container.NetworkSettings.Networks[nname_3].IPAddress == ""
- networks_2.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address == ""
- networks_3 is changed
- networks_3.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_3
- networks_3.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_2 | ipaddr
- networks_3.container.NetworkSettings.Networks[nname_3].IPAddress == ""
- networks_3.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address == ""
- networks_4 is changed
- networks_4.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_3
- networks_4.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_3 | ipaddr
- networks_4.container.NetworkSettings.Networks[nname_3].IPAddress == ""
- networks_4.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address == ""
- networks_5 is changed
- networks_5.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_3
- networks_5.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_3 | ipaddr
- networks_5.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_3
- networks_5.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address | ipaddr == nname_3_ipv6_3 | ipaddr
- networks_6 is changed
- networks_6.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_4
- networks_6.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_3 | ipaddr
- networks_6.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_4
- networks_6.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address | ipaddr == nname_3_ipv6_3 | ipaddr
- networks_7 is changed
- networks_7.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_4
- networks_7.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_4 | ipaddr
- networks_7.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_4
- networks_7.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address | ipaddr == nname_3_ipv6_4 | ipaddr
- networks_8 is not changed
- networks_8.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_4
- networks_8.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv6Address | ipaddr == nname_3_ipv6_4 | ipaddr
- networks_8.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_4
- networks_8.container.NetworkSettings.Networks[nname_3].GlobalIPv6Address | ipaddr == nname_3_ipv6_4 | ipaddr
when: docker_py_version is version('1.10.0', '>=')
####################################################################
####################################################################
####################################################################
@ -547,6 +730,7 @@
loop:
- "{{ nname_1 }}"
- "{{ nname_2 }}"
- "{{ nname_3 }}"
loop_control:
loop_var: network_name
when: docker_py_version is version('1.10.0', '>=')