[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

@ -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)