[docker_network] add ipv6 support (#47492)

* [docker_network] add ipv6 support

* docker_network: review ipam_options

* docker_network: fix requirements

* docker_network: fix deprecation notice

* docker_network: add minimum docker version change

* docker_network: remove trailing whitespace

* docker_network: revert rename of network_four #discussion_r228707101

* docker_network: refactor IPAM config comparison #discussion_r228707255, #discussion_r228707280

* docker_network: correct spelling of IPv4 and IPv6 #discussion_r228707114, #discussion_r228707138

* docker_network: manually remove networks #discussion_r228709051

* docker_network: refactor enable_ipv6 condition #discussion_r228707317

* docker_network: add mutually_exclusive #discussion_r228707185

* docker_network: fix iprange #discussion_r228709072

* docker_network: add auxiliary addresses in examples and tests

* docker_network: link to docker docs #discussion_r228707018

* docker_network: remove list default #discussion_r228707060, #discussion_r228709091

* docker_network: introduce params syntax for create_network() #discussion_r228709031

* docker_network: beautify code

* docker_network: resolve change requests

* docker_network: add yaml header

* docker_networking: fix get_ip_version

* docker_network: extend CIDR test

* docker_network: use backported unittest2 for python 2.6

* docker_network: migrate unittest to pytest
This commit is contained in:
Stephan Müller 2018-11-01 23:59:16 +01:00 committed by Will Thames
parent 80ca779aa7
commit 00bab2d24d
6 changed files with 386 additions and 44 deletions

View file

@ -1,4 +1,3 @@
---
minor_changes:
- "docker_network - ``internal`` is now used to set the ``Internal`` property of the docker network during creation."
- "docker_network - Minimum docker-py version increased from ``1.8.0`` to ``1.9.0``."

View file

@ -0,0 +1,5 @@
---
minor_changes:
- "docker_network - Add support for IPv6 networks."
deprecated_features:
- "docker_network - Deprecate ``ipam_options`` in favour of ``ipam_config``."

View file

@ -0,0 +1,4 @@
---
minor_changes:
- "docker_network - Minimum docker-py version increased from ``1.8.0`` to ``1.10.0``."
- "docker_network - Minimum docker server version increased from ``1.9.0`` to ``1.10.0``."

View file

@ -62,6 +62,14 @@ options:
aliases:
- incremental
enable_ipv6:
version_added: 2.8
description:
- Enable IPv6 networking.
type: bool
default: null
required: false
ipam_driver:
description:
- Specify an IPAM driver.
@ -69,6 +77,18 @@ options:
ipam_options:
description:
- Dictionary of IPAM options.
- Deprecated in 2.8, will be removed in 2.12. Use parameter ``ipam_config`` instead. In Docker 1.10.0, IPAM
options were introduced (see L(here,https://github.com/moby/moby/pull/17316)). This module parameter addresses
the IPAM config not the newly introduced IPAM options.
ipam_config:
version_added: 2.8
description:
- List of IPAM config blocks. Consult
L(Docker docs,https://docs.docker.com/compose/compose-file/compose-file-v2/#ipam) for valid options and values.
type: list
default: null
required: false
state:
description:
@ -104,7 +124,7 @@ author:
requirements:
- "python >= 2.6"
- "docker-py >= 1.9.0"
- "docker-py >= 1.10.0"
- "Please note that the L(docker-py,https://pypi.org/project/docker-py/) Python
module has been superseded by L(docker,https://pypi.org/project/docker/)
(see L(here,https://github.com/docker/docker-py/issues/1310) for details).
@ -113,7 +133,7 @@ requirements:
be installed at the same time. Also note that when both modules are installed
and one of them is uninstalled, the other might no longer function and a
reinstall of it is required."
- "The docker server >= 1.9.0"
- "The docker server >= 1.10.0"
'''
EXAMPLES = '''
@ -141,15 +161,37 @@ EXAMPLES = '''
- container_a
appends: yes
- name: Create a network with options
- name: Create a network with driver options
docker_network:
name: network_two
driver_options:
com.docker.network.bridge.name: net2
ipam_options:
subnet: '172.3.26.0/16'
gateway: 172.3.26.1
iprange: '192.168.1.0/24'
- name: Create a network with custom IPAM config
docker_network:
name: network_three
ipam_config:
- subnet: 172.3.27.0/24
gateway: 172.3.27.2
iprange: 172.3.27.0/26
aux_addresses:
host1: 172.3.27.3
host2: 172.3.27.4
- name: Create a network with IPv6 IPAM config
docker_network:
name: network_ipv6_one
enable_ipv6: yes
ipam_config:
- subnet: fdd1:ac8c:0557:7ce1::/64
- name: Create a network with IPv6 and custom IPv4 IPAM config
docker_network:
name: network_ipv6_two
enable_ipv6: yes
ipam_config:
- subnet: 172.4.27.0/24
- subnet: fdd1:ac8c:0557:7ce2::/64
- name: Delete a network, disconnecting all containers
docker_network:
@ -166,6 +208,8 @@ facts:
sample: {}
'''
import re
from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, HAS_DOCKER_PY_2, HAS_DOCKER_PY_3
try:
@ -189,10 +233,12 @@ class TaskParameters(DockerBaseClass):
self.driver_options = None
self.ipam_driver = None
self.ipam_options = None
self.ipam_config = None
self.appends = None
self.force = None
self.internal = None
self.debug = None
self.enable_ipv6 = None
for key, value in client.module.params.items():
setattr(self, key, value)
@ -202,6 +248,26 @@ def container_names_in_network(network):
return [c['Name'] for c in network['Containers'].values()] if network['Containers'] else []
CIDR_IPV4 = re.compile(r'^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$')
CIDR_IPV6 = re.compile(r'^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$')
def get_ip_version(cidr):
"""Gets the IP version of a CIDR string
:param cidr: Valid CIDR
:type cidr: str
:return: ``ipv4`` or ``ipv6``
:rtype: str
:raises ValueError: If ``cidr`` is not a valid CIDR
"""
if CIDR_IPV4.match(cidr):
return 'ipv4'
elif CIDR_IPV6.match(cidr):
return 'ipv6'
raise ValueError('"{0}" is not a valid CIDR'.format(cidr))
class DockerNetworkManager(object):
def __init__(self, client):
@ -219,6 +285,9 @@ class DockerNetworkManager(object):
if not self.parameters.connected and self.existing_network:
self.parameters.connected = container_names_in_network(self.existing_network)
if self.parameters.ipam_options:
self.parameters.ipam_config = [self.parameters.ipam_options]
state = self.parameters.state
if state == 'present':
self.present()
@ -253,29 +322,41 @@ class DockerNetworkManager(object):
if not (key in net['Options']) or value != net['Options'][key]:
different = True
differences.append('driver_options.%s' % key)
if self.parameters.ipam_driver:
if not net.get('IPAM') or net['IPAM']['Driver'] != self.parameters.ipam_driver:
different = True
differences.append('ipam_driver')
if self.parameters.ipam_options:
if not net.get('IPAM') or not net['IPAM'].get('Config'):
if self.parameters.ipam_config is not None and self.parameters.ipam_config:
if not net.get('IPAM') or not net['IPAM']['Config']:
different = True
differences.append('ipam_options')
differences.append('ipam_config')
else:
for key, value in self.parameters.ipam_options.items():
camelkey = None
for net_key in net['IPAM']['Config'][0]:
if key == net_key.lower():
camelkey = net_key
break
if not camelkey:
# key not found
different = True
differences.append('ipam_options.%s' % key)
elif net['IPAM']['Config'][0].get(camelkey) != value:
# key has different value
different = True
differences.append('ipam_options.%s' % key)
for idx, ipam_config in enumerate(self.parameters.ipam_config):
net_config = dict()
try:
ip_version = get_ip_version(ipam_config['subnet'])
for net_ipam_config in net['IPAM']['Config']:
if ip_version == get_ip_version(net_ipam_config['Subnet']):
net_config = net_ipam_config
except ValueError as e:
self.client.fail(str(e))
for key, value in ipam_config.items():
camelkey = None
for net_key in net_config:
if key == net_key.lower():
camelkey = net_key
break
if not camelkey or net_config.get(camelkey) != value:
different = True
differences.append('ipam_config[%s].%s' % (idx, key))
if self.parameters.enable_ipv6 is not None and self.parameters.enable_ipv6 != net.get('EnableIPv6', False):
different = True
differences.append('enable_ipv6')
if self.parameters.internal is not None:
if self.parameters.internal:
if not net.get('Internal'):
@ -289,26 +370,33 @@ class DockerNetworkManager(object):
def create_network(self):
if not self.existing_network:
params = dict(
driver=self.parameters.driver,
options=self.parameters.driver_options,
)
ipam_pools = []
if self.parameters.ipam_options:
if HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3:
ipam_pools.append(IPAMPool(**self.parameters.ipam_options))
else:
ipam_pools.append(utils.create_ipam_pool(**self.parameters.ipam_options))
if self.parameters.ipam_config:
for ipam_pool in self.parameters.ipam_config:
if HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3:
ipam_pools.append(IPAMPool(**ipam_pool))
else:
ipam_pools.append(utils.create_ipam_pool(**ipam_pool))
if HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3:
ipam_config = IPAMConfig(driver=self.parameters.ipam_driver,
pool_configs=ipam_pools)
params['ipam'] = IPAMConfig(driver=self.parameters.ipam_driver,
pool_configs=ipam_pools)
else:
ipam_config = utils.create_ipam_config(driver=self.parameters.ipam_driver,
pool_configs=ipam_pools)
params['ipam'] = utils.create_ipam_config(driver=self.parameters.ipam_driver,
pool_configs=ipam_pools)
if self.parameters.enable_ipv6 is not None:
params['enable_ipv6'] = self.parameters.enable_ipv6
if self.parameters.internal is not None:
params['internal'] = self.parameters.internal
if not self.check_mode:
resp = self.client.create_network(self.parameters.network_name,
driver=self.parameters.driver,
options=self.parameters.driver_options,
ipam=ipam_config,
internal=self.parameters.internal)
resp = self.client.create_network(self.parameters.network_name, **params)
self.existing_network = self.client.inspect_network(resp['Id'])
self.results['actions'].append("Created network %s with driver %s" % (self.parameters.network_name, self.parameters.driver))
@ -393,17 +481,24 @@ def main():
driver_options=dict(type='dict', default={}),
force=dict(type='bool', default=False),
appends=dict(type='bool', default=False, aliases=['incremental']),
ipam_driver=dict(type='str', default=None),
ipam_options=dict(type='dict', default={}),
internal=dict(type='bool', default=None),
ipam_driver=dict(type='str'),
ipam_options=dict(type='dict', default={}, removed_in_version='2.12'),
ipam_config=dict(type='list', elements='dict'),
enable_ipv6=dict(type='bool'),
internal=dict(type='bool'),
debug=dict(type='bool', default=False)
)
mutually_exclusive = [
('ipam_config', 'ipam_options')
]
client = AnsibleDockerClient(
argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
min_docker_version='1.9.0'
# "The docker server >= 1.9.0"
min_docker_version='1.10.0'
# "The docker server >= 1.10.0"
)
cm = DockerNetworkManager(client)

View file

@ -0,0 +1,212 @@
---
- name: Registering network names
set_fact:
nname_ipam_0: "{{ name_prefix ~ '-network-ipam-0' }}"
nname_ipam_1: "{{ name_prefix ~ '-network-ipam-1' }}"
nname_ipam_2: "{{ name_prefix ~ '-network-ipam-2' }}"
nname_ipam_3: "{{ name_prefix ~ '-network-ipam-3' }}"
- name: Registering network names
set_fact:
dnetworks: "{{ dnetworks }} + [nname_ipam_0, nname_ipam_1, nname_ipam_2, nname_ipam_3]"
#################### network-ipam-0 deprecated ####################
- name: Create network with ipam_config and deprecated ipam_options
docker_network:
name: "{{ nname_ipam_0 }}"
ipam_options:
subnet: 172.3.29.0/24
ipam_config:
- subnet: 172.3.29.0/24
register: network
ignore_errors: yes
- assert:
that:
- network is failed
- "network.msg == 'parameters are mutually exclusive: ipam_config, ipam_options'"
- name: Create network with deprecated custom IPAM config
docker_network:
name: "{{ nname_ipam_0 }}"
ipam_options:
subnet: 172.3.29.0/24
register: network
- assert:
that:
- network is changed
- name: Change subnet of network with deprecated custom IPAM config
docker_network:
name: "{{ nname_ipam_0 }}"
ipam_options:
subnet: 172.3.30.0/24
register: network
diff: yes
- assert:
that:
- network is changed
- network['diff'] | length == 1
- network['diff'][0] == "ipam_config[0].subnet"
- name: Cleanup network with ipam_config and deprecated ipam_options
docker_network:
name: "{{ nname_ipam_0 }}"
state: absent
#################### network-ipam-1 ####################
- name: Create network with custom IPAM config
docker_network:
name: "{{ nname_ipam_1 }}"
ipam_config:
- subnet: 172.3.27.0/24
gateway: 172.3.27.2
iprange: 172.3.27.0/26
aux_addresses:
host1: 172.3.27.3
host2: 172.3.27.4
register: network
- assert:
that:
- network is changed
- name: Change subnet, gateway, iprange and auxiliary addresses of network with custom IPAM config
docker_network:
name: "{{ nname_ipam_1 }}"
ipam_config:
- subnet: 172.3.28.0/24
gateway: 172.3.28.2
iprange: 172.3.28.0/26
aux_addresses:
host1: 172.3.28.3
register: network
diff: yes
- assert:
that:
- network is changed
- network['diff'] | length == 4
- '"ipam_config[0].subnet" in network["diff"]'
- '"ipam_config[0].gateway" in network["diff"]'
- '"ipam_config[0].iprange" in network["diff"]'
- '"ipam_config[0].aux_addresses" in network["diff"]'
- name: Remove gateway and iprange of network with custom IPAM config
docker_network:
name: "{{ nname_ipam_1 }}"
ipam_config:
- subnet: 172.3.28.0/24
register: network
- assert:
that:
- network is not changed
- name: Cleanup network with custom IPAM config
docker_network:
name: "{{ nname_ipam_1 }}"
state: absent
#################### network-ipam-2 ####################
- name: Create network with IPv6 IPAM config
docker_network:
name: "{{ nname_ipam_2 }}"
enable_ipv6: yes
ipam_config:
- subnet: fdd1:ac8c:0557:7ce0::/64
register: network
- assert:
that:
- network is changed
- name: Change subnet of network with IPv6 IPAM config
docker_network:
name: "{{ nname_ipam_2 }}"
enable_ipv6: yes
ipam_config:
- subnet: fdd1:ac8c:0557:7ce1::/64
register: network
diff: yes
- assert:
that:
- network is changed
- network['diff'] | length == 1
- network['diff'][0] == "ipam_config[0].subnet"
- name: Change subnet of network with IPv6 IPAM config
docker_network:
name: "{{ nname_ipam_2 }}"
enable_ipv6: yes
ipam_config:
- subnet: "fdd1:ac8c:0557:7ce1::"
register: network
ignore_errors: yes
- assert:
that:
- network is failed
- "network.msg == '\"fdd1:ac8c:0557:7ce1::\" is not a valid CIDR'"
- name: Cleanup network with IPv6 IPAM config
docker_network:
name: "{{ nname_ipam_2 }}"
state: absent
#################### network-ipam-3 ####################
- name: Create network with IPv6 and custom IPv4 IPAM config
docker_network:
name: "{{ nname_ipam_3 }}"
enable_ipv6: yes
ipam_config:
- subnet: 172.4.27.0/24
- subnet: fdd1:ac8c:0557:7ce2::/64
register: network
- assert:
that:
- network is changed
- name: Change subnet order of network with IPv6 and custom IPv4 IPAM config
docker_network:
name: "{{ nname_ipam_3 }}"
enable_ipv6: yes
ipam_config:
- subnet: fdd1:ac8c:0557:7ce2::/64
- subnet: 172.4.27.0/24
register: network
- assert:
that:
- network is not changed
- name: Remove IPv6 from network with custom IPv4 and IPv6 IPAM config
docker_network:
name: "{{ nname_ipam_3 }}"
enable_ipv6: no
ipam_config:
- subnet: 172.4.27.0/24
register: network
diff: yes
- assert:
that:
- network is changed
- network['diff'] | length == 1
- network['diff'][0] == "enable_ipv6"
- name: Cleanup network with IPv6 and custom IPv4 IPAM config
docker_network:
name: "{{ nname_ipam_3 }}"
state: absent

View file

@ -0,0 +1,27 @@
"""Unit tests for docker_network."""
import pytest
from ansible.modules.cloud.docker.docker_network import get_ip_version
@pytest.mark.parametrize("cidr,expected", [
('192.168.0.1/16', 'ipv4'),
('192.168.0.1/24', 'ipv4'),
('192.168.0.1/32', 'ipv4'),
('fdd1:ac8c:0557:7ce2::/64', 'ipv6'),
('fdd1:ac8c:0557:7ce2::/128', 'ipv6'),
])
def test_get_ip_version_positives(cidr, expected):
assert get_ip_version(cidr) == expected
@pytest.mark.parametrize("cidr", [
'192.168.0.1',
'192.168.0.1/34',
'192.168.0.1/asd',
'fdd1:ac8c:0557:7ce2::',
])
def test_get_ip_version_negatives(cidr):
with pytest.raises(ValueError) as e:
get_ip_version(cidr)
assert '"{0}" is not a valid CIDR'.format(cidr) == str(e.value)