diff --git a/lib/ansible/modules/cloud/docker/docker_network.py b/lib/ansible/modules/cloud/docker/docker_network.py new file mode 100644 index 00000000000..4019a4f3df8 --- /dev/null +++ b/lib/ansible/modules/cloud/docker/docker_network.py @@ -0,0 +1,385 @@ +#!/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 . + +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. + default: null + 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()