From ddd9f6081e7e467b7ca26812ba1ee6d447e87b1b Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 10 Oct 2019 05:15:24 +0200 Subject: [PATCH] [2.8] docker_container: fix idempotency for network IP addresses (#62959) * 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. (cherry picked from commit 62c0cae29a393859522fcb391562dc1edd73ce53) * Remove IPv6 tests, use intermediate state of tests from PR which don't need ipaddr filter. --- ...ocker_container-ip-address-idempotency.yml | 4 + .../modules/cloud/docker/docker_container.py | 23 ++-- .../docker_container/tasks/tests/network.yml | 126 +++++++++++++++++- 3 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 changelogs/fragments/62928-docker_container-ip-address-idempotency.yml diff --git a/changelogs/fragments/62928-docker_container-ip-address-idempotency.yml b/changelogs/fragments/62928-docker_container-ip-address-idempotency.yml new file mode 100644 index 00000000000..63fead2487c --- /dev/null +++ b/changelogs/fragments/62928-docker_container-ip-address-idempotency.yml @@ -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." diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index b521a4a6852..0db66b13fba 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -1999,7 +1999,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, @@ -2007,18 +2008,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 @@ -2026,10 +2028,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 @@ -2892,7 +2894,8 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): uts=dict(docker_py_version='3.5.0', docker_api_version='1.25'), pids_limit=dict(docker_py_version='1.10.0', docker_api_version='1.23'), # 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() ) diff --git a/test/integration/targets/docker_container/tasks/tests/network.yml b/test/integration/targets/docker_container/tasks/tests/network.yml index f1412eeafe1..6659d5b85c7 100644 --- a/test/integration/targets/docker_container/tasks/tests/network.yml +++ b/test/integration/targets/docker_container/tasks/tests/network.yml @@ -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,26 @@ loop_var: network_name when: docker_py_version is version('1.10.0', '>=') +- set_fact: + subnet_ipv4_base: 192.168.{{ 64 + (192 | random) }} + +- set_fact: + subnet_ipv4: "{{ subnet_ipv4_base }}.0/24" + nname_3_ipv4_2: "{{ subnet_ipv4_base }}.2" + nname_3_ipv4_3: "{{ subnet_ipv4_base }}.3" + nname_3_ipv4_4: "{{ subnet_ipv4_base }}.4" + +- debug: + msg: "Chose random IPv4 subnet {{ subnet_ipv4 }}" + +- name: Create network with fixed IPv4 subnet + docker_network: + name: "{{ nname_3 }}" + ipam_config: + - subnet: "{{ subnet_ipv4 }}" + state: present + when: docker_py_version is version('1.10.0', '>=') + #################################################################### ## network_mode #################################################### #################################################################### @@ -535,6 +556,108 @@ 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 }}" + 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 }}" + 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 }}" + networks_cli_compatible: yes + register: networks_3 + + - 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 }}" + networks_cli_compatible: yes + force_kill: yes + register: networks_6 + + - 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 }}" + 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].IPAddress == "" + - 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].IPAddress == "" + - networks_3 is changed + - networks_3.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_3 + - networks_3.container.NetworkSettings.Networks[nname_3].IPAddress == "" + - networks_5 is changed + - networks_5.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_3 + - networks_5.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_3 + - networks_6 is changed + - networks_6.container.NetworkSettings.Networks[nname_3].IPAMConfig.IPv4Address == nname_3_ipv4_4 + - networks_6.container.NetworkSettings.Networks[nname_3].IPAddress == nname_3_ipv4_4 + - 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].IPAddress == nname_3_ipv4_4 + + when: docker_py_version is version('1.10.0', '>=') + #################################################################### #################################################################### #################################################################### @@ -547,6 +670,7 @@ loop: - "{{ nname_1 }}" - "{{ nname_2 }}" + - "{{ nname_3 }}" loop_control: loop_var: network_name when: docker_py_version is version('1.10.0', '>=')