384 lines
13 KiB
Python
384 lines
13 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Copyright 2016 Red Hat | Ansible
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
DOCUMENTATION = '''
|
|
module: docker_network
|
|
version_added: "2.2"
|
|
short_description: Manage Docker networks
|
|
description:
|
|
- Create/remove Docker networks and connect containers to them.
|
|
- Performs largely the same function as the "docker network" CLI subcommand.
|
|
options:
|
|
name:
|
|
description:
|
|
- Name of the network to operate on.
|
|
required: true
|
|
aliases:
|
|
- network_name
|
|
|
|
connected:
|
|
description:
|
|
- List of container names or container IDs to connect to a network.
|
|
default: null
|
|
|
|
driver:
|
|
description:
|
|
- Specify the type of network. Docker provides bridge and overlay drivers, but 3rd party drivers can also be used.
|
|
default: bridge
|
|
|
|
driver_options:
|
|
description:
|
|
- Dictionary of network settings. Consult docker docs for valid options and values.
|
|
default: null
|
|
|
|
force:
|
|
description:
|
|
- >
|
|
With state 'absent' forces disconnecting all containers from the
|
|
network prior to deleting the network. With state 'present' will
|
|
disconnect all containers, delete the network and re-create the
|
|
network. This option is required if you have changed the ipam or
|
|
driver options and want an existing network to be updated to use the
|
|
new options.
|
|
default: false
|
|
|
|
appends:
|
|
description:
|
|
- By default the connected list is canonical, meaning containers not on the list are removed from the network.
|
|
Use 'appends' to leave existing containers connected.
|
|
default: false
|
|
aliases:
|
|
- incremental
|
|
|
|
ipam_driver:
|
|
description:
|
|
- Specifiy an IPAM driver.
|
|
default: null
|
|
|
|
ipam_options:
|
|
description:
|
|
- Dictionary of IPAM options.
|
|
default: null
|
|
|
|
state:
|
|
description:
|
|
- >
|
|
"absent" deletes the network. If a network has connected containers, it
|
|
cannot be deleted. Use the force option to disconnect all containers
|
|
and delete the network.
|
|
- >
|
|
"present" creates the network, if it does not already exist with the
|
|
specified parameters, and connects the list of containers provided via
|
|
the connected parameter. Containers not on the list will be
|
|
disconnected. An empty list will leave no containers connected to the
|
|
network. Use the appends option to leave existing containers
|
|
connected. Use the force options to force re-creation of the network.
|
|
default: present
|
|
choices:
|
|
- absent
|
|
- present
|
|
|
|
extends_documentation_fragment:
|
|
- docker
|
|
|
|
authors:
|
|
- "Ben Keith (@keitwb)"
|
|
- "Chris Houseknecht (@chouseknecht)"
|
|
|
|
requirements:
|
|
- "python >= 2.6"
|
|
- "docker-py >= 1.7.0"
|
|
- "The docker server >= 1.9.0"
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
- name: Create a network
|
|
docker_network:
|
|
name: network_one
|
|
|
|
- name: Remove all but selected list of containers
|
|
docker_network:
|
|
name: network_one
|
|
connected:
|
|
- container_a
|
|
- container_b
|
|
- container_c
|
|
|
|
- name: Remove a single container
|
|
docker_network:
|
|
name: network_one
|
|
connected: "{{ fulllist|difference(['container_a']) }}"
|
|
|
|
- name: Add a container to a network, leaving existing containers connected
|
|
docker_network:
|
|
name: network_one
|
|
connected:
|
|
- container_a
|
|
appends: yes
|
|
|
|
- name: Create a network with 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: Delete a network, disconnecting all containers
|
|
docker_network:
|
|
name: network_one
|
|
state: absent
|
|
force: yes
|
|
'''
|
|
|
|
RETURN = '''
|
|
facts:
|
|
description: Network inspection results for the affected network.
|
|
returned: success
|
|
type: complex
|
|
sample: {}
|
|
'''
|
|
|
|
|
|
from ansible.module_utils.docker_common import *
|
|
|
|
try:
|
|
from docker import utils
|
|
from docker.utils.types import Ulimit
|
|
except:
|
|
# missing docker-py handled in ansible.module_utils.docker
|
|
pass
|
|
|
|
|
|
class TaskParameters(DockerBaseClass):
|
|
def __init__(self, client):
|
|
super(TaskParameters, self).__init__()
|
|
self.client = client
|
|
|
|
self.network_name = None
|
|
self.connected = None
|
|
self.driver = None
|
|
self.driver_options = None
|
|
self.ipam_driver = None
|
|
self.ipam_options = None
|
|
self.appends = None
|
|
self.force = None
|
|
self.debug = None
|
|
|
|
for key, value in client.module.params.items():
|
|
setattr(self, key, value)
|
|
|
|
|
|
def container_names_in_network(network):
|
|
return [c['Name'] for c in network['Containers'].values()]
|
|
|
|
|
|
class DockerNetworkManager(object):
|
|
|
|
def __init__(self, client):
|
|
self.client = client
|
|
self.parameters = TaskParameters(client)
|
|
self.check_mode = self.client.check_mode
|
|
self.results = {
|
|
u'changed': False,
|
|
u'actions': []
|
|
}
|
|
self.diff = self.client.module._diff
|
|
|
|
self.existing_network = self.get_existing_network()
|
|
|
|
if not self.parameters.connected and self.existing_network:
|
|
self.parameters.connected = container_names_in_network(self.existing_network)
|
|
|
|
state = self.parameters.state
|
|
if state == 'present':
|
|
self.present()
|
|
elif state == 'absent':
|
|
self.absent()
|
|
|
|
def get_existing_network(self):
|
|
networks = self.client.networks()
|
|
network = None
|
|
for n in networks:
|
|
if n['Name'] == self.parameters.network_name:
|
|
self.results[u'actions'].append('Found network %s' % self.parameters.network_name)
|
|
network = n
|
|
return network
|
|
|
|
def has_different_config(self, net):
|
|
'''
|
|
Evaluates an existing network and returns a tuple containing a boolean
|
|
indicating if the configuration is different and a list of differences.
|
|
|
|
:param net: the inspection output for an existing network
|
|
:return: (bool, list)
|
|
'''
|
|
different = False
|
|
differences = []
|
|
if self.parameters.driver and self.parameters.driver != net['Driver']:
|
|
different = True
|
|
differences.append('driver')
|
|
if self.parameters.driver_options:
|
|
if not net.get('Options'):
|
|
different = True
|
|
differences.append('driver_options')
|
|
else:
|
|
for key, value in self.parameters.driver_options.iteritems():
|
|
if not net['Options'].get(key) 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'):
|
|
different = True
|
|
differences.append('ipam_options')
|
|
else:
|
|
for key, value in self.parameters.ipam_options.iteritems():
|
|
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)
|
|
return different, differences
|
|
|
|
def create_network(self):
|
|
if not self.existing_network:
|
|
ipam_pools = []
|
|
if self.parameters.ipam_options:
|
|
ipam_pools.append(utils.create_ipam_pool(**self.parameters.ipam_options))
|
|
|
|
ipam_config = utils.create_ipam_config(driver=self.parameters.ipam_driver,
|
|
pool_configs=ipam_pools)
|
|
|
|
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)
|
|
|
|
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))
|
|
self.results['changed'] = True
|
|
|
|
def remove_network(self):
|
|
if self.existing_network:
|
|
self.disconnect_all_containers()
|
|
if not self.check_mode:
|
|
self.client.remove_network(self.parameters.network_name)
|
|
self.results['actions'].append("Removed network %s" % (self.parameters.network_name,))
|
|
self.results['changed'] = True
|
|
|
|
def is_container_connected(self, container_name):
|
|
return container_name in container_names_in_network(self.existing_network)
|
|
|
|
def connect_containers(self):
|
|
for name in self.parameters.connected:
|
|
if not self.is_container_connected(name):
|
|
if not self.check_mode:
|
|
self.client.connect_container_to_network(name, self.parameters.network_name)
|
|
self.results['actions'].append("Connected container %s" % (name,))
|
|
self.results['changed'] = True
|
|
|
|
def disconnect_missing(self):
|
|
for c in self.existing_network['Containers'].values():
|
|
name = c['Name']
|
|
if name not in self.parameters.connected:
|
|
self.disconnect_container(name)
|
|
|
|
def disconnect_all_containers(self):
|
|
containers = self.client.inspect_network(self.parameters.network_name)['Containers']
|
|
for cont in containers.values():
|
|
self.disconnect_container(cont['Name'])
|
|
|
|
def disconnect_container(self, container_name):
|
|
if not self.check_mode:
|
|
self.client.disconnect_container_from_network(container_name, self.parameters.network_name)
|
|
self.results['actions'].append("Disconnected container %s" % (container_name,))
|
|
self.results['changed'] = True
|
|
|
|
def present(self):
|
|
different = False
|
|
differences = []
|
|
if self.existing_network:
|
|
different, differences = self.has_different_config(self.existing_network)
|
|
|
|
if self.parameters.force or different:
|
|
self.remove_network()
|
|
self.existing_network = None
|
|
|
|
self.create_network()
|
|
self.connect_containers()
|
|
if not self.parameters.appends:
|
|
self.disconnect_missing()
|
|
|
|
if self.diff or self.check_mode or self.parameters.debug:
|
|
self.results['diff'] = differences
|
|
|
|
if not self.check_mode and not self.parameters.debug:
|
|
self.results.pop('actions')
|
|
|
|
self.results['facts'] = self.get_existing_network()
|
|
|
|
def absent(self):
|
|
self.remove_network()
|
|
|
|
|
|
def main():
|
|
argument_spec = dict(
|
|
network_name = dict(type='str', required=True, aliases=['name']),
|
|
connected = dict(type='list', default=[]),
|
|
state = dict(type='str', default='present', choices=['present', 'absent']),
|
|
driver = dict(type='str', default='bridge'),
|
|
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={}),
|
|
debug = dict(type='bool', default=False)
|
|
)
|
|
|
|
required_if = []
|
|
|
|
client = AnsibleDockerClient(
|
|
argument_spec=argument_spec,
|
|
required_if=required_if,
|
|
supports_check_mode=True
|
|
)
|
|
|
|
cm = DockerNetworkManager(client)
|
|
client.module.exit_json(**cm.results)
|
|
|
|
# import module snippets
|
|
from ansible.module_utils.basic import *
|
|
|
|
if __name__ == '__main__':
|
|
main()
|