Vendor Galaxy Roles and Collections
This commit is contained in:
parent
c1e1897cda
commit
2aed20393f
3553 changed files with 387444 additions and 2 deletions
|
|
@ -0,0 +1,886 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2016 Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_network
|
||||
short_description: Manage Docker networks
|
||||
description:
|
||||
- Create/remove Docker networks and connect containers to them.
|
||||
- Performs largely the same function as the C(docker network) CLI subcommand.
|
||||
extends_documentation_fragment:
|
||||
- community.docker._docker.api_documentation
|
||||
- community.docker._attributes
|
||||
- community.docker._attributes.actiongroup_docker
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
idempotent:
|
||||
support: partial
|
||||
details:
|
||||
- If O(force=true) the module is not idempotent.
|
||||
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the network to operate on.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- network_name
|
||||
|
||||
config_from:
|
||||
description:
|
||||
- Specifies the config only network to use the config from.
|
||||
type: str
|
||||
version_added: 3.10.0
|
||||
|
||||
config_only:
|
||||
description:
|
||||
- Sets that this is a config only network.
|
||||
type: bool
|
||||
version_added: 3.10.0
|
||||
|
||||
connected:
|
||||
description:
|
||||
- List of container names or container IDs to connect to a network.
|
||||
- Please note that the module only makes sure that these containers are connected to the network, but does not care
|
||||
about connection options. If you rely on specific IP addresses and so on, use the M(community.docker.docker_container)
|
||||
module to ensure your containers are correctly connected to this network.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
aliases:
|
||||
- containers
|
||||
|
||||
driver:
|
||||
description:
|
||||
- Specify the type of network. Docker provides bridge and overlay drivers, but 3rd party drivers can also be used.
|
||||
type: str
|
||||
default: bridge
|
||||
|
||||
driver_options:
|
||||
description:
|
||||
- Dictionary of network settings. Consult docker docs for valid options and values.
|
||||
type: dict
|
||||
default: {}
|
||||
|
||||
force:
|
||||
description:
|
||||
- With state V(present) will disconnect all containers for existing networks, 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.
|
||||
type: bool
|
||||
default: false
|
||||
|
||||
appends:
|
||||
description:
|
||||
- By default the connected list is canonical, meaning containers not on the list are removed from the network.
|
||||
- Use O(appends) to leave existing containers connected.
|
||||
type: bool
|
||||
default: false
|
||||
aliases:
|
||||
- incremental
|
||||
|
||||
enable_ipv4:
|
||||
description:
|
||||
- Enable IPv4 networking.
|
||||
- This is enabled by default, but can be explicitly disabled.
|
||||
- Requires Docker API 1.47 or newer.
|
||||
type: bool
|
||||
version_added: 4.5.0
|
||||
|
||||
enable_ipv6:
|
||||
description:
|
||||
- Enable IPv6 networking.
|
||||
type: bool
|
||||
|
||||
ingress:
|
||||
description:
|
||||
- Enable Swarm routing-mesh.
|
||||
type: bool
|
||||
version_added: 4.2.0
|
||||
|
||||
ipam_driver:
|
||||
description:
|
||||
- Specify an IPAM driver.
|
||||
type: str
|
||||
|
||||
ipam_driver_options:
|
||||
description:
|
||||
- Dictionary of IPAM driver options.
|
||||
type: dict
|
||||
|
||||
ipam_config:
|
||||
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. Note that O(ipam_config[].iprange) is spelled differently here (we use the notation
|
||||
from the Docker SDK for Python).
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
subnet:
|
||||
description:
|
||||
- IP subset in CIDR notation.
|
||||
type: str
|
||||
iprange:
|
||||
description:
|
||||
- IP address range in CIDR notation.
|
||||
type: str
|
||||
gateway:
|
||||
description:
|
||||
- IP gateway address.
|
||||
type: str
|
||||
aux_addresses:
|
||||
description:
|
||||
- Auxiliary IP addresses used by Network driver, as a mapping from hostname to IP.
|
||||
type: dict
|
||||
|
||||
state:
|
||||
description:
|
||||
- V(absent) deletes the network. If a network has connected containers, these will be detached from the network.
|
||||
- V(present) creates the network, if it does not already exist with the specified parameters, and connects the list
|
||||
of containers provided by the O(connected) parameter. Containers not on the list will be disconnected. An empty list
|
||||
will leave no containers connected to the network. Use the O(appends) option to leave existing containers connected.
|
||||
Use the O(force) options to force re-creation of the network.
|
||||
type: str
|
||||
default: present
|
||||
choices:
|
||||
- absent
|
||||
- present
|
||||
|
||||
internal:
|
||||
description:
|
||||
- Restrict external access to the network.
|
||||
type: bool
|
||||
|
||||
labels:
|
||||
description:
|
||||
- Dictionary of labels.
|
||||
type: dict
|
||||
default: {}
|
||||
|
||||
scope:
|
||||
description:
|
||||
- Specify the network's scope.
|
||||
type: str
|
||||
choices:
|
||||
- local
|
||||
- global
|
||||
- swarm
|
||||
|
||||
attachable:
|
||||
description:
|
||||
- If enabled, and the network is in the global scope, non-service containers on worker nodes will be able to connect
|
||||
to the network.
|
||||
type: bool
|
||||
|
||||
notes:
|
||||
- When network options are changed, the module disconnects all containers from the network, deletes the network, and re-creates
|
||||
the network. It does not try to reconnect containers, except the ones listed in (O(connected), and even for these, it
|
||||
does not consider specific connection options like fixed IP addresses or MAC addresses. If you need more control over
|
||||
how the containers are connected to the network, loop the M(community.docker.docker_container) module to loop over your
|
||||
containers to make sure they are connected properly.
|
||||
- The module does not support Docker Swarm. This means that it will not try to disconnect or reconnect services. If services
|
||||
are connected to the network, deleting the network will fail. When network options are changed, the network has to be
|
||||
deleted and recreated, so this will fail as well.
|
||||
author:
|
||||
- "Ben Keith (@keitwb)"
|
||||
- "Chris Houseknecht (@chouseknecht)"
|
||||
- "Dave Bendit (@DBendit)"
|
||||
|
||||
requirements:
|
||||
- "Docker API >= 1.25"
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
---
|
||||
- name: Create a network
|
||||
community.docker.docker_network:
|
||||
name: network_one
|
||||
|
||||
- name: Remove all but selected list of containers
|
||||
community.docker.docker_network:
|
||||
name: network_one
|
||||
connected:
|
||||
- container_a
|
||||
- container_b
|
||||
- container_c
|
||||
|
||||
- name: Remove a single container
|
||||
community.docker.docker_network:
|
||||
name: network_one
|
||||
connected: "{{ fulllist|difference(['container_a']) }}"
|
||||
|
||||
- name: Add a container to a network, leaving existing containers connected
|
||||
community.docker.docker_network:
|
||||
name: network_one
|
||||
connected:
|
||||
- container_a
|
||||
appends: true
|
||||
|
||||
- name: Create a network with driver options
|
||||
community.docker.docker_network:
|
||||
name: network_two
|
||||
driver_options:
|
||||
com.docker.network.bridge.name: net2
|
||||
|
||||
- name: Create a network with custom IPAM config
|
||||
community.docker.docker_network:
|
||||
name: network_three
|
||||
ipam_config:
|
||||
- subnet: 172.23.27.0/24
|
||||
gateway: 172.23.27.2
|
||||
iprange: 172.23.27.0/26
|
||||
aux_addresses:
|
||||
host1: 172.23.27.3
|
||||
host2: 172.23.27.4
|
||||
|
||||
- name: Create a network with labels
|
||||
community.docker.docker_network:
|
||||
name: network_four
|
||||
labels:
|
||||
key1: value1
|
||||
key2: value2
|
||||
|
||||
- name: Create a network with IPv6 IPAM config
|
||||
community.docker.docker_network:
|
||||
name: network_ipv6_one
|
||||
enable_ipv6: true
|
||||
ipam_config:
|
||||
- subnet: fdd1:ac8c:0557:7ce1::/64
|
||||
|
||||
- name: Create a network with IPv6 and custom IPv4 IPAM config
|
||||
community.docker.docker_network:
|
||||
name: network_ipv6_two
|
||||
enable_ipv6: true
|
||||
ipam_config:
|
||||
- subnet: 172.24.27.0/24
|
||||
- subnet: fdd1:ac8c:0557:7ce2::/64
|
||||
|
||||
- name: Delete a network, disconnecting all containers
|
||||
community.docker.docker_network:
|
||||
name: network_one
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
network:
|
||||
description:
|
||||
- Network inspection results for the affected network.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: {}
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import typing as t
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._util import (
|
||||
DifferenceTracker,
|
||||
DockerBaseClass,
|
||||
clean_dict_booleans_for_docker_api,
|
||||
normalize_ip_address,
|
||||
normalize_ip_network,
|
||||
sanitize_labels,
|
||||
)
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
name: str
|
||||
|
||||
def __init__(self, client: AnsibleDockerClient) -> None:
|
||||
super().__init__()
|
||||
self.client = client
|
||||
|
||||
self.connected: list[str] = []
|
||||
self.config_from: str | None = None
|
||||
self.config_only: bool | None = None
|
||||
self.driver: str = "bridge"
|
||||
self.driver_options: dict[str, t.Any] = {}
|
||||
self.ipam_driver: str | None = None
|
||||
self.ipam_driver_options: dict[str, t.Any] | None = None
|
||||
self.ipam_config: list[dict[str, t.Any]] | None = None
|
||||
self.appends: bool = False
|
||||
self.force: bool = False
|
||||
self.internal: bool | None = None
|
||||
self.labels: dict[str, t.Any] = {}
|
||||
self.debug: bool = False
|
||||
self.enable_ipv4: bool | None = None
|
||||
self.enable_ipv6: bool | None = None
|
||||
self.scope: t.Literal["local", "global", "swarm"] | None = None
|
||||
self.attachable: bool | None = None
|
||||
self.ingress: bool | None = None
|
||||
self.state: t.Literal["present", "absent"] = "present"
|
||||
|
||||
for key, value in client.module.params.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
# config_only sets driver to 'null' (and scope to 'local') so force that here. Otherwise we get
|
||||
# diffs of 'null' --> 'bridge' given that the driver option defaults to 'bridge'.
|
||||
if self.config_only:
|
||||
self.driver = "null" # type: ignore[unreachable]
|
||||
|
||||
|
||||
def container_names_in_network(network: dict[str, t.Any]) -> list[str]:
|
||||
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 validate_cidr(cidr: str) -> t.Literal["ipv4", "ipv6"]:
|
||||
"""Validate CIDR. Return IP version of a CIDR string on success.
|
||||
|
||||
:param cidr: Valid CIDR
|
||||
:type cidr: str
|
||||
:return: ``ipv4`` or ``ipv6``
|
||||
:rtype: str
|
||||
:raises ValueError: If ``cidr`` is not a valid CIDR
|
||||
"""
|
||||
# TODO: Use ipaddress for this instead of rolling your own...
|
||||
if CIDR_IPV4.match(cidr):
|
||||
return "ipv4"
|
||||
if CIDR_IPV6.match(cidr):
|
||||
return "ipv6"
|
||||
raise ValueError(f'"{cidr}" is not a valid CIDR')
|
||||
|
||||
|
||||
def normalize_ipam_config_key(key: str) -> str:
|
||||
"""Normalizes IPAM config keys returned by Docker API to match Ansible keys.
|
||||
|
||||
:param key: Docker API key
|
||||
:type key: str
|
||||
:return Ansible module key
|
||||
:rtype str
|
||||
"""
|
||||
special_cases = {"AuxiliaryAddresses": "aux_addresses"}
|
||||
return special_cases.get(key, key.lower())
|
||||
|
||||
|
||||
def dicts_are_essentially_equal(a: dict[str, t.Any], b: dict[str, t.Any]) -> bool:
|
||||
"""Make sure that a is a subset of b, where None entries of a are ignored."""
|
||||
for k, v in a.items():
|
||||
if v is None:
|
||||
continue
|
||||
if b.get(k) != v:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def normalize_ipam_values(ipam_config: dict[str, t.Any]) -> dict[str, t.Any]:
|
||||
result = {}
|
||||
for key, value in ipam_config.items():
|
||||
if key in ("subnet", "iprange"):
|
||||
value = normalize_ip_network(value)
|
||||
elif key in ("gateway",):
|
||||
value = normalize_ip_address(value)
|
||||
elif key in ("aux_addresses",) and value is not None:
|
||||
value = {k: normalize_ip_address(v) for k, v in value.items()}
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class DockerNetworkManager:
|
||||
def __init__(self, client: AnsibleDockerClient) -> None:
|
||||
self.client = client
|
||||
self.parameters = TaskParameters(client)
|
||||
self.check_mode = self.client.check_mode
|
||||
self.actions: list[str] = []
|
||||
self.results: dict[str, t.Any] = {"changed": False, "actions": self.actions}
|
||||
self.diff = self.client.module._diff
|
||||
self.diff_tracker = DifferenceTracker()
|
||||
self.diff_result: dict[str, t.Any] = {}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
if self.parameters.ipam_config:
|
||||
try:
|
||||
for ipam_config in self.parameters.ipam_config:
|
||||
validate_cidr(ipam_config["subnet"])
|
||||
except ValueError as e:
|
||||
self.client.fail(to_text(e))
|
||||
|
||||
if self.parameters.driver_options:
|
||||
self.parameters.driver_options = clean_dict_booleans_for_docker_api(
|
||||
self.parameters.driver_options
|
||||
)
|
||||
|
||||
state = self.parameters.state
|
||||
if state == "present":
|
||||
self.present()
|
||||
elif state == "absent":
|
||||
self.absent()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
if self.diff:
|
||||
self.diff_result["before"], self.diff_result["after"] = (
|
||||
self.diff_tracker.get_before_after()
|
||||
)
|
||||
self.results["diff"] = self.diff_result
|
||||
|
||||
def get_existing_network(self) -> dict[str, t.Any] | None:
|
||||
return self.client.get_network(name=self.parameters.name)
|
||||
|
||||
def has_different_config(
|
||||
self, net: dict[str, t.Any]
|
||||
) -> tuple[bool, DifferenceTracker]:
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
differences = DifferenceTracker()
|
||||
if (
|
||||
self.parameters.config_only is not None
|
||||
and self.parameters.config_only != net.get("ConfigOnly", False)
|
||||
):
|
||||
differences.add(
|
||||
"config_only",
|
||||
parameter=self.parameters.config_only,
|
||||
active=net.get("ConfigOnly", False),
|
||||
)
|
||||
if (
|
||||
self.parameters.config_from is not None
|
||||
and self.parameters.config_from
|
||||
!= net.get("ConfigFrom", {}).get("Network", "")
|
||||
):
|
||||
differences.add(
|
||||
"config_from",
|
||||
parameter=self.parameters.config_from,
|
||||
active=net.get("ConfigFrom", {}).get("Network", ""),
|
||||
)
|
||||
if self.parameters.driver and self.parameters.driver != net["Driver"]:
|
||||
differences.add(
|
||||
"driver", parameter=self.parameters.driver, active=net["Driver"]
|
||||
)
|
||||
if self.parameters.driver_options:
|
||||
if not net.get("Options"):
|
||||
differences.add(
|
||||
"driver_options",
|
||||
parameter=self.parameters.driver_options,
|
||||
active=net.get("Options"),
|
||||
)
|
||||
else:
|
||||
for key, value in self.parameters.driver_options.items():
|
||||
if key not in net["Options"] or value != net["Options"][key]:
|
||||
differences.add(
|
||||
f"driver_options.{key}",
|
||||
parameter=value,
|
||||
active=net["Options"].get(key),
|
||||
)
|
||||
|
||||
if self.parameters.ipam_driver and (
|
||||
not net.get("IPAM") or net["IPAM"]["Driver"] != self.parameters.ipam_driver
|
||||
):
|
||||
differences.add(
|
||||
"ipam_driver",
|
||||
parameter=self.parameters.ipam_driver,
|
||||
active=net.get("IPAM"),
|
||||
)
|
||||
|
||||
if self.parameters.ipam_driver_options is not None:
|
||||
ipam_driver_options = net["IPAM"].get("Options") or {}
|
||||
if ipam_driver_options != self.parameters.ipam_driver_options:
|
||||
differences.add(
|
||||
"ipam_driver_options",
|
||||
parameter=self.parameters.ipam_driver_options,
|
||||
active=ipam_driver_options,
|
||||
)
|
||||
|
||||
if self.parameters.ipam_config is not None and self.parameters.ipam_config:
|
||||
if not net.get("IPAM") or not net["IPAM"]["Config"]:
|
||||
differences.add(
|
||||
"ipam_config",
|
||||
parameter=self.parameters.ipam_config,
|
||||
active=net.get("IPAM", {}).get("Config"),
|
||||
)
|
||||
else:
|
||||
# Put network's IPAM config into the same format as module's IPAM config
|
||||
net_ipam_configs = []
|
||||
net_ipam_configs_normalized = []
|
||||
for net_ipam_config in net["IPAM"]["Config"]:
|
||||
config = {}
|
||||
for k, v in net_ipam_config.items():
|
||||
config[normalize_ipam_config_key(k)] = v
|
||||
net_ipam_configs.append(config)
|
||||
net_ipam_configs_normalized.append(normalize_ipam_values(config))
|
||||
# Compare lists of dicts as sets of dicts
|
||||
for idx, ipam_config in enumerate(self.parameters.ipam_config):
|
||||
ipam_config_normalized = normalize_ipam_values(ipam_config)
|
||||
net_config = {}
|
||||
net_config_normalized = {}
|
||||
for net_ipam_config, net_ipam_config_normalized in zip(
|
||||
net_ipam_configs, net_ipam_configs_normalized
|
||||
):
|
||||
if dicts_are_essentially_equal(
|
||||
ipam_config_normalized, net_ipam_config_normalized
|
||||
):
|
||||
net_config = net_ipam_config
|
||||
net_config_normalized = net_ipam_config_normalized
|
||||
break
|
||||
for key, value in ipam_config.items():
|
||||
if value is None:
|
||||
# due to recursive argument_spec, all keys are always present
|
||||
# (but have default value None if not specified)
|
||||
continue
|
||||
if ipam_config_normalized[key] != net_config_normalized.get(
|
||||
key
|
||||
):
|
||||
differences.add(
|
||||
f"ipam_config[{idx}].{key}",
|
||||
parameter=value,
|
||||
active=net_config.get(key),
|
||||
)
|
||||
|
||||
if (
|
||||
self.parameters.enable_ipv4 is not None
|
||||
and self.parameters.enable_ipv4 != net.get("EnableIPv4", False)
|
||||
):
|
||||
differences.add(
|
||||
"enable_ipv4",
|
||||
parameter=self.parameters.enable_ipv4,
|
||||
active=net.get("EnableIPv4", False),
|
||||
)
|
||||
if (
|
||||
self.parameters.enable_ipv6 is not None
|
||||
and self.parameters.enable_ipv6 != net.get("EnableIPv6", False)
|
||||
):
|
||||
differences.add(
|
||||
"enable_ipv6",
|
||||
parameter=self.parameters.enable_ipv6,
|
||||
active=net.get("EnableIPv6", False),
|
||||
)
|
||||
|
||||
if (
|
||||
self.parameters.internal is not None
|
||||
and self.parameters.internal != net.get("Internal", False)
|
||||
):
|
||||
differences.add(
|
||||
"internal",
|
||||
parameter=self.parameters.internal,
|
||||
active=net.get("Internal"),
|
||||
)
|
||||
|
||||
if self.parameters.scope is not None and self.parameters.scope != net.get(
|
||||
"Scope"
|
||||
):
|
||||
differences.add(
|
||||
"scope", parameter=self.parameters.scope, active=net.get("Scope")
|
||||
)
|
||||
|
||||
if (
|
||||
self.parameters.attachable is not None
|
||||
and self.parameters.attachable != net.get("Attachable", False)
|
||||
):
|
||||
differences.add(
|
||||
"attachable",
|
||||
parameter=self.parameters.attachable,
|
||||
active=net.get("Attachable"),
|
||||
)
|
||||
if self.parameters.ingress is not None and self.parameters.ingress != net.get(
|
||||
"Ingress", False
|
||||
):
|
||||
differences.add(
|
||||
"ingress", parameter=self.parameters.ingress, active=net.get("Ingress")
|
||||
)
|
||||
if self.parameters.labels:
|
||||
if not net.get("Labels"):
|
||||
differences.add(
|
||||
"labels", parameter=self.parameters.labels, active=net.get("Labels")
|
||||
)
|
||||
else:
|
||||
for key, value in self.parameters.labels.items():
|
||||
if key not in net["Labels"] or value != net["Labels"][key]:
|
||||
differences.add(
|
||||
f"labels.{key}",
|
||||
parameter=value,
|
||||
active=net["Labels"].get(key),
|
||||
)
|
||||
|
||||
return not differences.empty, differences
|
||||
|
||||
def create_network(self) -> None:
|
||||
if not self.existing_network:
|
||||
data: dict[str, t.Any] = {
|
||||
"Name": self.parameters.name,
|
||||
"Driver": self.parameters.driver,
|
||||
"Options": self.parameters.driver_options,
|
||||
"IPAM": None,
|
||||
"CheckDuplicate": None,
|
||||
}
|
||||
|
||||
if self.parameters.config_only is not None:
|
||||
data["ConfigOnly"] = self.parameters.config_only
|
||||
if self.parameters.config_from:
|
||||
data["ConfigFrom"] = {"Network": self.parameters.config_from}
|
||||
if self.parameters.enable_ipv6 is not None:
|
||||
data["EnableIPv6"] = self.parameters.enable_ipv6
|
||||
if self.parameters.enable_ipv4 is not None:
|
||||
data["EnableIPv4"] = self.parameters.enable_ipv4
|
||||
if self.parameters.internal:
|
||||
data["Internal"] = True
|
||||
if self.parameters.scope is not None:
|
||||
data["Scope"] = self.parameters.scope
|
||||
if self.parameters.attachable is not None:
|
||||
data["Attachable"] = self.parameters.attachable
|
||||
if self.parameters.ingress is not None:
|
||||
data["Ingress"] = self.parameters.ingress
|
||||
if self.parameters.labels is not None:
|
||||
data["Labels"] = self.parameters.labels
|
||||
|
||||
ipam_pools = []
|
||||
if self.parameters.ipam_config:
|
||||
for ipam_pool in self.parameters.ipam_config:
|
||||
ipam_pools.append(
|
||||
{
|
||||
"Subnet": ipam_pool["subnet"],
|
||||
"IPRange": ipam_pool["iprange"],
|
||||
"Gateway": ipam_pool["gateway"],
|
||||
"AuxiliaryAddresses": ipam_pool["aux_addresses"],
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
self.parameters.ipam_driver
|
||||
or self.parameters.ipam_driver_options
|
||||
or ipam_pools
|
||||
):
|
||||
# Only add IPAM if a driver was specified or if IPAM parameters were
|
||||
# specified. Leaving this parameter out can significantly speed up
|
||||
# creation; on my machine creation with this option needs ~15 seconds,
|
||||
# and without just a few seconds.
|
||||
data["IPAM"] = {
|
||||
"Driver": self.parameters.ipam_driver,
|
||||
"Config": ipam_pools or [],
|
||||
"Options": self.parameters.ipam_driver_options,
|
||||
}
|
||||
|
||||
if not self.check_mode:
|
||||
resp = self.client.post_json_to_json("/networks/create", data=data)
|
||||
self.client.report_warnings(resp, ["Warning"])
|
||||
self.existing_network = self.client.get_network(network_id=resp["Id"])
|
||||
self.actions.append(
|
||||
f"Created network {self.parameters.name} with driver {self.parameters.driver}"
|
||||
)
|
||||
self.results["changed"] = True
|
||||
|
||||
def remove_network(self) -> None:
|
||||
if self.existing_network:
|
||||
self.disconnect_all_containers()
|
||||
if not self.check_mode:
|
||||
self.client.delete_call("/networks/{0}", self.parameters.name)
|
||||
if self.existing_network.get("Scope", "local") == "swarm":
|
||||
while self.get_existing_network():
|
||||
time.sleep(0.1)
|
||||
self.actions.append(f"Removed network {self.parameters.name}")
|
||||
self.results["changed"] = True
|
||||
|
||||
def is_container_connected(self, container_name: str) -> bool:
|
||||
if not self.existing_network:
|
||||
return False
|
||||
return container_name in container_names_in_network(self.existing_network)
|
||||
|
||||
def is_container_exist(self, container_name: str) -> bool:
|
||||
try:
|
||||
container = self.client.get_container(container_name)
|
||||
return bool(container)
|
||||
|
||||
except DockerException as e:
|
||||
self.client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
self.client.fail(
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
def connect_containers(self) -> None:
|
||||
for name in self.parameters.connected:
|
||||
if not self.is_container_connected(name) and self.is_container_exist(name):
|
||||
if not self.check_mode:
|
||||
data = {
|
||||
"Container": name,
|
||||
"EndpointConfig": None,
|
||||
}
|
||||
self.client.post_json(
|
||||
"/networks/{0}/connect", self.parameters.name, data=data
|
||||
)
|
||||
self.actions.append(f"Connected container {name}")
|
||||
self.results["changed"] = True
|
||||
self.diff_tracker.add(f"connected.{name}", parameter=True, active=False)
|
||||
|
||||
def disconnect_missing(self) -> None:
|
||||
if not self.existing_network:
|
||||
return
|
||||
containers = self.existing_network["Containers"]
|
||||
if not containers:
|
||||
return
|
||||
for c in containers.values():
|
||||
name = c["Name"]
|
||||
if name not in self.parameters.connected:
|
||||
self.disconnect_container(name)
|
||||
|
||||
def disconnect_all_containers(self) -> None:
|
||||
network = self.client.get_network(name=self.parameters.name)
|
||||
if not network:
|
||||
return
|
||||
containers = network["Containers"]
|
||||
if not containers:
|
||||
return
|
||||
for cont in containers.values():
|
||||
self.disconnect_container(cont["Name"])
|
||||
|
||||
def disconnect_container(self, container_name: str) -> None:
|
||||
if not self.check_mode:
|
||||
data = {"Container": container_name, "Force": True}
|
||||
self.client.post_json(
|
||||
"/networks/{0}/disconnect", self.parameters.name, data=data
|
||||
)
|
||||
self.actions.append(f"Disconnected container {container_name}")
|
||||
self.results["changed"] = True
|
||||
self.diff_tracker.add(
|
||||
f"connected.{container_name}", parameter=False, active=True
|
||||
)
|
||||
|
||||
def present(self) -> None:
|
||||
different = False
|
||||
differences = DifferenceTracker()
|
||||
if self.existing_network:
|
||||
different, differences = self.has_different_config(self.existing_network)
|
||||
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=True, active=self.existing_network is not None
|
||||
)
|
||||
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.diff_result["differences"] = differences.get_legacy_docker_diffs()
|
||||
self.diff_tracker.merge(differences)
|
||||
|
||||
if not self.check_mode and not self.parameters.debug:
|
||||
self.results.pop("actions")
|
||||
|
||||
network_facts = self.get_existing_network()
|
||||
self.results["network"] = network_facts
|
||||
|
||||
def absent(self) -> None:
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=False, active=self.existing_network is not None
|
||||
)
|
||||
self.remove_network()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
argument_spec = {
|
||||
"name": {"type": "str", "required": True, "aliases": ["network_name"]},
|
||||
"config_from": {"type": "str"},
|
||||
"config_only": {"type": "bool"},
|
||||
"connected": {
|
||||
"type": "list",
|
||||
"default": [],
|
||||
"elements": "str",
|
||||
"aliases": ["containers"],
|
||||
},
|
||||
"state": {
|
||||
"type": "str",
|
||||
"default": "present",
|
||||
"choices": ["present", "absent"],
|
||||
},
|
||||
"driver": {"type": "str", "default": "bridge"},
|
||||
"driver_options": {"type": "dict", "default": {}},
|
||||
"force": {"type": "bool", "default": False},
|
||||
"appends": {"type": "bool", "default": False, "aliases": ["incremental"]},
|
||||
"ipam_driver": {"type": "str"},
|
||||
"ipam_driver_options": {"type": "dict"},
|
||||
"ipam_config": {
|
||||
"type": "list",
|
||||
"elements": "dict",
|
||||
"options": {
|
||||
"subnet": {"type": "str"},
|
||||
"iprange": {"type": "str"},
|
||||
"gateway": {"type": "str"},
|
||||
"aux_addresses": {"type": "dict"},
|
||||
},
|
||||
},
|
||||
"enable_ipv4": {"type": "bool"},
|
||||
"enable_ipv6": {"type": "bool"},
|
||||
"internal": {"type": "bool"},
|
||||
"labels": {"type": "dict", "default": {}},
|
||||
"debug": {"type": "bool", "default": False},
|
||||
"scope": {"type": "str", "choices": ["local", "global", "swarm"]},
|
||||
"attachable": {"type": "bool"},
|
||||
"ingress": {"type": "bool"},
|
||||
}
|
||||
|
||||
option_minimal_versions = {
|
||||
"config_from": {"docker_api_version": "1.30"},
|
||||
"config_only": {"docker_api_version": "1.30"},
|
||||
"scope": {"docker_api_version": "1.30"},
|
||||
"attachable": {"docker_api_version": "1.26"},
|
||||
"enable_ipv4": {"docker_api_version": "1.47"},
|
||||
}
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
# "The docker server >= 1.10.0"
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
try:
|
||||
cm = DockerNetworkManager(client)
|
||||
client.module.exit_json(**cm.results)
|
||||
except DockerException as e:
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue