Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
#!/usr/bin/python
#
# Copyright (c) 2020 Matt Clay <mclay@redhat.com>
# Copyright (c) 2020 Felix Fontein <felix@fontein.de>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: current_container_facts
short_description: Return facts about whether the module runs in a Docker container
version_added: 1.1.0
description:
- Return facts about whether the module runs in a Docker container.
- This module attempts a best-effort detection. There might be special cases where
it does not work; if you encounter one, L(please file an issue,
https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&template=bug_report.md).
author:
- Felix Fontein (@felixfontein)
extends_documentation_fragment:
- community.docker.attributes
- community.docker.attributes.facts
- community.docker.attributes.facts_module
'''
EXAMPLES = '''
- name: Get facts on current container
community.docker.current_container_facts:
- name: Print information on current container when running in a container
ansible.builtin.debug:
msg: "Container ID is {{ ansible_module_container_id }}"
when: ansible_module_running_in_container
'''
RETURN = r'''
ansible_facts:
description: Ansible facts returned by the module
type: dict
returned: always
contains:
ansible_module_running_in_container:
description:
- Whether the module was able to detect that it runs in a container or not.
returned: always
type: bool
ansible_module_container_id:
description:
- The detected container ID.
- Contains an empty string if no container was detected.
returned: always
type: str
ansible_module_container_type:
description:
- The detected container environment.
- Contains an empty string if no container was detected, or a non-empty string identifying the container environment.
- C(github_actions) is supported since community.docker 2.4.0.
- C(podman) is supported since community.docker 3.3.0.
returned: always
type: str
choices:
- ''
- docker
- azure_pipelines
- github_actions
- podman
'''
import os
import re
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(dict(), supports_check_mode=True)
cpuset_path = '/proc/self/cpuset'
mountinfo_path = '/proc/self/mountinfo'
container_id = ''
container_type = ''
contents = None
if os.path.exists(cpuset_path):
# File content varies based on the environment:
# No Container: /
# Docker: /docker/c86f3732b5ba3d28bb83b6e14af767ab96abbc52de31313dcb1176a62d91a507
# Azure Pipelines (Docker): /azpl_job/0f2edfed602dd6ec9f2e42c867f4d5ee640ebf4c058e6d3196d4393bb8fd0891
# Podman: /../../../../../..
# While this was true and worked well for a long time, this seems to be no longer accurate
# with newer Docker / Podman versions and/or with cgroupv2. That's why the /proc/self/mountinfo
# detection further down is done when this test is inconclusive.
with open(cpuset_path, 'rb') as f:
contents = f.read().decode('utf-8')
cgroup_path, cgroup_name = os.path.split(contents.strip())
if cgroup_path == '/docker':
container_id = cgroup_name
container_type = 'docker'
if cgroup_path == '/azpl_job':
container_id = cgroup_name
container_type = 'azure_pipelines'
if cgroup_path == '/actions_job':
container_id = cgroup_name
container_type = 'github_actions'
if not container_id and os.path.exists(mountinfo_path):
with open(mountinfo_path, 'rb') as f:
contents = f.read().decode('utf-8')
# As to why this works, see the explanations by Matt Clay in
# https://github.com/ansible/ansible/blob/80d2f8da02052f64396da6b8caaf820eedbf18e2/test/lib/ansible_test/_internal/docker_util.py#L571-L610
for line in contents.splitlines():
parts = line.split()
if len(parts) >= 5 and parts[4] == '/etc/hostname':
m = re.match('.*/([a-f0-9]{64})/hostname$', parts[3])
if m:
container_id = m.group(1)
container_type = 'docker'
m = re.match('.*/([a-f0-9]{64})/userdata/hostname$', parts[3])
if m:
container_id = m.group(1)
container_type = 'podman'
module.exit_json(ansible_facts=dict(
ansible_module_running_in_container=container_id != '',
ansible_module_container_id=container_id,
ansible_module_container_type=container_type,
))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,434 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_config
short_description: Manage docker configs.
description:
- Create and remove Docker configs in a Swarm environment. Similar to C(docker config create) and C(docker config rm).
- Adds to the metadata of new configs 'ansible_key', an encrypted hash representation of the data, which is then used
in future runs to test if a config has changed. If 'ansible_key' is not present, then a config will not be updated
unless the I(force) option is set.
- Updates to configs are performed by removing the config and creating it again.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_2_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
data:
description:
- The value of the config.
- Mutually exclusive with I(data_src). One of I(data) and I(data_src) is required if I(state=present).
type: str
data_is_b64:
description:
- If set to C(true), the data is assumed to be Base64 encoded and will be
decoded before being used.
- To use binary I(data), it is better to keep it Base64 encoded and let it
be decoded by this option.
type: bool
default: false
data_src:
description:
- The file on the target from which to read the config.
- Mutually exclusive with I(data). One of I(data) and I(data_src) is required if I(state=present).
type: path
version_added: 1.10.0
labels:
description:
- "A map of key:value meta data, where both the I(key) and I(value) are expected to be a string."
- If new meta data is provided, or existing meta data is modified, the config will be updated by removing it and creating it again.
type: dict
force:
description:
- Use with state C(present) to always remove and recreate an existing config.
- If C(true), an existing config will be replaced, even if it has not been changed.
type: bool
default: false
rolling_versions:
description:
- If set to C(true), configs are created with an increasing version number appended to their name.
- Adds a label containing the version number to the managed configs with the name C(ansible_version).
type: bool
default: false
version_added: 2.2.0
versions_to_keep:
description:
- When using I(rolling_versions), the number of old versions of the config to keep.
- Extraneous old configs are deleted after the new one is created.
- Set to C(-1) to keep everything or to C(0) or C(1) to keep only the current one.
type: int
default: 5
version_added: 2.2.0
name:
description:
- The name of the config.
type: str
required: true
state:
description:
- Set to C(present), if the config should exist, and C(absent), if it should not.
type: str
default: present
choices:
- absent
- present
template_driver:
description:
- Set to C(golang) to use a Go template in I(data) or a Go template file in I(data_src).
type: str
choices:
- golang
version_added: 2.5.0
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.6.0"
- "Docker API >= 1.30"
author:
- Chris Houseknecht (@chouseknecht)
- John Hu (@ushuz)
'''
EXAMPLES = '''
- name: Create config foo (from a file on the control machine)
community.docker.docker_config:
name: foo
# If the file is JSON or binary, Ansible might modify it (because
# it is first decoded and later re-encoded). Base64-encoding the
# file directly after reading it prevents this to happen.
data: "{{ lookup('file', '/path/to/config/file') | b64encode }}"
data_is_b64: true
state: present
- name: Create config foo (from a file on the target machine)
community.docker.docker_config:
name: foo
data_src: /path/to/config/file
state: present
- name: Change the config data
community.docker.docker_config:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
state: present
- name: Add a new label
community.docker.docker_config:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
# Adding a new label will cause a remove/create of the config
two: '2'
state: present
- name: No change
community.docker.docker_config:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
# Even though 'two' is missing, there is no change to the existing config
state: present
- name: Update an existing label
community.docker.docker_config:
name: foo
data: Goodnight everyone!
labels:
bar: monkey # Changing a label will cause a remove/create of the config
one: '1'
state: present
- name: Force the (re-)creation of the config
community.docker.docker_config:
name: foo
data: Goodnight everyone!
force: true
state: present
- name: Remove config foo
community.docker.docker_config:
name: foo
state: absent
'''
RETURN = '''
config_id:
description:
- The ID assigned by Docker to the config object.
returned: success and I(state) is C(present)
type: str
sample: 'hzehrmyjigmcp2gb6nlhmjqcv'
config_name:
description:
- The name of the created config object.
returned: success and I(state) is C(present)
type: str
sample: 'awesome_config'
version_added: 2.2.0
'''
import base64
import hashlib
import traceback
try:
from docker.errors import DockerException, APIError
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
compare_generic,
)
from ansible.module_utils.common.text.converters import to_native, to_bytes
class ConfigManager(DockerBaseClass):
def __init__(self, client, results):
super(ConfigManager, self).__init__()
self.client = client
self.results = results
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters.get('name')
self.state = parameters.get('state')
self.data = parameters.get('data')
if self.data is not None:
if parameters.get('data_is_b64'):
self.data = base64.b64decode(self.data)
else:
self.data = to_bytes(self.data)
data_src = parameters.get('data_src')
if data_src is not None:
try:
with open(data_src, 'rb') as f:
self.data = f.read()
except Exception as exc:
self.client.fail('Error while reading {src}: {error}'.format(src=data_src, error=to_native(exc)))
self.labels = parameters.get('labels')
self.force = parameters.get('force')
self.rolling_versions = parameters.get('rolling_versions')
self.versions_to_keep = parameters.get('versions_to_keep')
self.template_driver = parameters.get('template_driver')
if self.rolling_versions:
self.version = 0
self.data_key = None
self.configs = []
def __call__(self):
self.get_config()
if self.state == 'present':
self.data_key = hashlib.sha224(self.data).hexdigest()
self.present()
self.remove_old_versions()
elif self.state == 'absent':
self.absent()
def get_version(self, config):
try:
return int(config.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
except ValueError:
return 0
def remove_old_versions(self):
if not self.rolling_versions or self.versions_to_keep < 0:
return
if not self.check_mode:
while len(self.configs) > max(self.versions_to_keep, 1):
self.remove_config(self.configs.pop(0))
def get_config(self):
''' Find an existing config. '''
try:
configs = self.client.configs(filters={'name': self.name})
except APIError as exc:
self.client.fail("Error accessing config %s: %s" % (self.name, to_native(exc)))
if self.rolling_versions:
self.configs = [
config
for config in configs
if config['Spec']['Name'].startswith('{name}_v'.format(name=self.name))
]
self.configs.sort(key=self.get_version)
else:
self.configs = [
config for config in configs if config['Spec']['Name'] == self.name
]
def create_config(self):
''' Create a new config '''
config_id = None
# We can't see the data after creation, so adding a label we can use for idempotency check
labels = {
'ansible_key': self.data_key
}
if self.rolling_versions:
self.version += 1
labels['ansible_version'] = str(self.version)
self.name = '{name}_v{version}'.format(name=self.name, version=self.version)
if self.labels:
labels.update(self.labels)
try:
if not self.check_mode:
# only use templating argument when self.template_driver is defined
kwargs = {}
if self.template_driver:
kwargs['templating'] = {
'name': self.template_driver
}
config_id = self.client.create_config(self.name, self.data, labels=labels, **kwargs)
self.configs += self.client.configs(filters={'id': config_id})
except APIError as exc:
self.client.fail("Error creating config: %s" % to_native(exc))
if isinstance(config_id, dict):
config_id = config_id['ID']
return config_id
def remove_config(self, config):
try:
if not self.check_mode:
self.client.remove_config(config['ID'])
except APIError as exc:
self.client.fail("Error removing config %s: %s" % (config['Spec']['Name'], to_native(exc)))
def present(self):
''' Handles state == 'present', creating or updating the config '''
if self.configs:
config = self.configs[-1]
self.results['config_id'] = config['ID']
self.results['config_name'] = config['Spec']['Name']
data_changed = False
template_driver_changed = False
attrs = config.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key:
data_changed = True
else:
if not self.force:
self.client.module.warn("'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'true'")
# template_driver has changed if it was set in the previous config
# and now it differs, or if it wasn't set but now it is.
if attrs.get('Templating', {}).get('Name'):
if attrs['Templating']['Name'] != self.template_driver:
template_driver_changed = True
elif self.template_driver:
template_driver_changed = True
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
if self.rolling_versions:
self.version = self.get_version(config)
if data_changed or template_driver_changed or labels_changed or self.force:
# if something changed or force, delete and re-create the config
if not self.rolling_versions:
self.absent()
config_id = self.create_config()
self.results['changed'] = True
self.results['config_id'] = config_id
self.results['config_name'] = self.name
else:
self.results['changed'] = True
self.results['config_id'] = self.create_config()
self.results['config_name'] = self.name
def absent(self):
''' Handles state == 'absent', removing the config '''
if self.configs:
for config in self.configs:
self.remove_config(config)
self.results['changed'] = True
def main():
argument_spec = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
data=dict(type='str'),
data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'),
labels=dict(type='dict'),
force=dict(type='bool', default=False),
rolling_versions=dict(type='bool', default=False),
versions_to_keep=dict(type='int', default=5),
template_driver=dict(type='str', choices=['golang']),
)
required_if = [
('state', 'present', ['data', 'data_src'], True),
]
mutually_exclusive = [
('data', 'data_src'),
]
option_minimal_versions = dict(
template_driver=dict(docker_py_version='5.0.3', docker_api_version='1.37'),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
min_docker_version='2.6.0',
min_docker_api_version='1.30',
option_minimal_versions=option_minimal_versions,
)
try:
results = dict(
changed=False,
)
ConfigManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,871 @@
#!/usr/bin/python
#
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_container_copy_into
short_description: Copy a file into a Docker container
version_added: 3.4.0
description:
- Copy a file into a Docker container.
- Similar to C(docker cp).
- To copy files in a non-running container, you must provide the I(owner_id) and I(group_id) options.
This is also necessary if the container does not contain a C(/bin/sh) shell with an C(id) tool.
attributes:
check_mode:
support: full
diff_mode:
support: full
details:
- Additional data will need to be transferred to compute diffs.
- The module uses R(the MAX_FILE_SIZE_FOR_DIFF ansible-core configuration,MAX_FILE_SIZE_FOR_DIFF)
to determine for how large files diffs should be computed.
options:
container:
description:
- The name of the container to copy files to.
type: str
required: true
path:
description:
- Path to a file on the managed node.
- Mutually exclusive with I(content). One of I(content) and I(path) is required.
type: path
content:
description:
- The file's content.
- If you plan to provide binary data, provide it pre-encoded to base64, and set I(content_is_b64=true).
- Mutually exclusive with I(path). One of I(content) and I(path) is required.
type: str
content_is_b64:
description:
- If set to C(true), the content in I(content) is assumed to be Base64 encoded and
will be decoded before being used.
- To use binary I(content), it is better to keep it Base64 encoded and let it
be decoded by this option. Otherwise you risk the data to be interpreted as
UTF-8 and corrupted.
type: bool
default: false
container_path:
description:
- Path to a file inside the Docker container.
- Must be an absolute path.
type: str
required: true
follow:
description:
- This flag indicates that filesystem links in the Docker container, if they exist, should be followed.
type: bool
default: false
local_follow:
description:
- This flag indicates that filesystem links in the source tree (where the module is executed), if they exist, should be followed.
type: bool
default: true
owner_id:
description:
- The owner ID to use when writing the file to disk.
- If provided, I(group_id) must also be provided.
- If not provided, the module will try to determine the user and group ID for the current user in the container.
This will only work if C(/bin/sh) is present in the container and the C(id) binary or shell builtin is available.
Also the container must be running.
type: int
group_id:
description:
- The group ID to use when writing the file to disk.
- If provided, I(owner_id) must also be provided.
- If not provided, the module will try to determine the user and group ID for the current user in the container.
This will only work if C(/bin/sh) is present in the container and the C(id) binary or shell builtin is available.
Also the container must be running.
type: int
mode:
description:
- The file mode to use when writing the file to disk.
- Will use the file's mode from the source system if this option is not provided.
type: int
force:
description:
- If set to C(true), force writing the file (without performing any idempotency checks).
- If set to C(false), only write the file if it does not exist on the target. If a filesystem object exists at
the destination, the module will not do any change.
- If this option is not specified, the module will be idempotent. To verify idempotency, it will try to get information
on the filesystem object in the container, and if everything seems to match will download the file from the container
to compare it to the file to upload.
type: bool
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
author:
- "Felix Fontein (@felixfontein)"
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Copy a file into the container
community.docker.docker_container_copy_into:
container: mydata
path: /home/user/data.txt
container_path: /data/input.txt
- name: Copy a file into the container with owner, group, and mode set
community.docker.docker_container_copy_into:
container: mydata
path: /home/user/bin/runme.o
container_path: /bin/runme
owner: 0 # root
group: 0 # root
mode: 0o755 # readable and executable by all users, writable by root
'''
RETURN = '''
container_path:
description:
- The actual path in the container.
- Can only be different from I(container_path) when I(follow=true).
type: str
returned: success
'''
import base64
import io
import os
import random
import stat
import traceback
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.copy import (
DockerFileCopyError,
DockerFileNotFound,
DockerUnexpectedError,
determine_user_group,
fetch_file_ex,
put_file,
put_file_content,
stat_file,
)
from ansible_collections.community.docker.plugins.module_utils._scramble import generate_insecure_key, scramble
def are_fileobjs_equal(f1, f2):
'''Given two (buffered) file objects, compare their contents.'''
blocksize = 65536
b1buf = b''
b2buf = b''
while True:
if f1 and len(b1buf) < blocksize:
f1b = f1.read(blocksize)
if not f1b:
# f1 is EOF, so stop reading from it
f1 = None
b1buf += f1b
if f2 and len(b2buf) < blocksize:
f2b = f2.read(blocksize)
if not f2b:
# f2 is EOF, so stop reading from it
f2 = None
b2buf += f2b
if not b1buf or not b2buf:
# At least one of f1 and f2 is EOF and all its data has
# been processed. If both are EOF and their data has been
# processed, the files are equal, otherwise not.
return not b1buf and not b2buf
# Compare the next chunk of data, and remove it from the buffers
buflen = min(len(b1buf), len(b2buf))
if b1buf[:buflen] != b2buf[:buflen]:
return False
b1buf = b1buf[buflen:]
b2buf = b2buf[buflen:]
def are_fileobjs_equal_read_first(f1, f2):
'''Given two (buffered) file objects, compare their contents.
Returns a tuple (is_equal, content_of_f1), where the first element indicates
whether the two file objects have the same content, and the second element is
the content of the first file object.'''
blocksize = 65536
b1buf = b''
b2buf = b''
is_equal = True
content = []
while True:
if f1 and len(b1buf) < blocksize:
f1b = f1.read(blocksize)
if not f1b:
# f1 is EOF, so stop reading from it
f1 = None
b1buf += f1b
if f2 and len(b2buf) < blocksize:
f2b = f2.read(blocksize)
if not f2b:
# f2 is EOF, so stop reading from it
f2 = None
b2buf += f2b
if not b1buf or not b2buf:
# At least one of f1 and f2 is EOF and all its data has
# been processed. If both are EOF and their data has been
# processed, the files are equal, otherwise not.
is_equal = not b1buf and not b2buf
break
# Compare the next chunk of data, and remove it from the buffers
buflen = min(len(b1buf), len(b2buf))
if b1buf[:buflen] != b2buf[:buflen]:
is_equal = False
break
content.append(b1buf[:buflen])
b1buf = b1buf[buflen:]
b2buf = b2buf[buflen:]
content.append(b1buf)
if f1:
content.append(f1.read())
return is_equal, b''.join(content)
def is_container_file_not_regular_file(container_stat):
for bit in (
# https://pkg.go.dev/io/fs#FileMode
32 - 1, # ModeDir
32 - 4, # ModeTemporary
32 - 5, # ModeSymlink
32 - 6, # ModeDevice
32 - 7, # ModeNamedPipe
32 - 8, # ModeSocket
32 - 11, # ModeCharDevice
32 - 13, # ModeIrregular
):
if container_stat['mode'] & (1 << bit) != 0:
return True
return False
def get_container_file_mode(container_stat):
mode = container_stat['mode'] & 0xFFF
if container_stat['mode'] & (1 << (32 - 9)) != 0: # ModeSetuid
mode |= stat.S_ISUID # set UID bit
if container_stat['mode'] & (1 << (32 - 10)) != 0: # ModeSetgid
mode |= stat.S_ISGID # set GID bit
if container_stat['mode'] & (1 << (32 - 12)) != 0: # ModeSticky
mode |= stat.S_ISVTX # sticky bit
return mode
def add_other_diff(diff, in_path, member):
if diff is None:
return
diff['before_header'] = in_path
if member.isdir():
diff['before'] = '(directory)'
elif member.issym() or member.islnk():
diff['before'] = member.linkname
elif member.ischr():
diff['before'] = '(character device)'
elif member.isblk():
diff['before'] = '(block device)'
elif member.isfifo():
diff['before'] = '(fifo)'
elif member.isdev():
diff['before'] = '(device)'
elif member.isfile():
raise DockerUnexpectedError('should not be a regular file')
else:
diff['before'] = '(unknown filesystem object)'
def retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat=None, link_target=None):
if diff is None:
return
if regular_stat is not None:
# First handle all filesystem object types that are not regular files
if regular_stat['mode'] & (1 << (32 - 1)) != 0:
diff['before_header'] = container_path
diff['before'] = '(directory)'
return
elif regular_stat['mode'] & (1 << (32 - 4)) != 0:
diff['before_header'] = container_path
diff['before'] = '(temporary file)'
return
elif regular_stat['mode'] & (1 << (32 - 5)) != 0:
diff['before_header'] = container_path
diff['before'] = link_target
return
elif regular_stat['mode'] & (1 << (32 - 6)) != 0:
diff['before_header'] = container_path
diff['before'] = '(device)'
return
elif regular_stat['mode'] & (1 << (32 - 7)) != 0:
diff['before_header'] = container_path
diff['before'] = '(named pipe)'
return
elif regular_stat['mode'] & (1 << (32 - 8)) != 0:
diff['before_header'] = container_path
diff['before'] = '(socket)'
return
elif regular_stat['mode'] & (1 << (32 - 11)) != 0:
diff['before_header'] = container_path
diff['before'] = '(character device)'
return
elif regular_stat['mode'] & (1 << (32 - 13)) != 0:
diff['before_header'] = container_path
diff['before'] = '(unknown filesystem object)'
return
# Check whether file is too large
if regular_stat['size'] > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
return
# We need to get hold of the content
def process_none(in_path):
diff['before'] = ''
def process_regular(in_path, tar, member):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
def process_symlink(in_path, member):
diff['before_header'] = in_path
diff['before'] = member.linkname
def process_other(in_path, member):
add_other_diff(diff, in_path, member)
fetch_file_ex(
client,
container,
in_path=container_path,
process_none=process_none,
process_regular=process_regular,
process_symlink=process_symlink,
process_other=process_other,
follow_links=follow_links,
)
def is_binary(content):
if b'\x00' in content:
return True
# TODO: better detection
# (ansible-core also just checks for 0x00, and even just sticks to the first 8k, so this isn't too bad...)
return False
def are_fileobjs_equal_with_diff_of_first(f1, f2, size, diff, max_file_size_for_diff, container_path):
if diff is None:
return are_fileobjs_equal(f1, f2)
if size > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
return are_fileobjs_equal(f1, f2)
is_equal, content = are_fileobjs_equal_read_first(f1, f2)
if is_binary(content):
diff['dst_binary'] = 1
else:
diff['before_header'] = container_path
diff['before'] = to_text(content)
return is_equal
def add_diff_dst_from_regular_member(diff, max_file_size_for_diff, container_path, tar, member):
if diff is None:
return
if member.size > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
return
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
content = tar_f.read()
if is_binary(content):
diff['dst_binary'] = 1
else:
diff['before_header'] = container_path
diff['before'] = to_text(content)
def copy_dst_to_src(diff):
if diff is None:
return
for f, t in [
('dst_size', 'src_size'),
('dst_binary', 'src_binary'),
('before_header', 'after_header'),
('before', 'after'),
]:
if f in diff:
diff[t] = diff[f]
elif t in diff:
diff.pop(t)
def is_file_idempotent(client, container, managed_path, container_path, follow_links, local_follow_links, owner_id, group_id, mode,
force=False, diff=None, max_file_size_for_diff=1):
# Retrieve information of local file
try:
file_stat = os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
except OSError as exc:
if exc.errno == 2:
raise DockerFileNotFound('Cannot find local file {managed_path}'.format(managed_path=managed_path))
raise
if mode is None:
mode = stat.S_IMODE(file_stat.st_mode)
if not stat.S_ISLNK(file_stat.st_mode) and not stat.S_ISREG(file_stat.st_mode):
raise DockerFileCopyError('Local path {managed_path} is not a symbolic link or file')
if diff is not None:
if file_stat.st_size > max_file_size_for_diff > 0:
diff['src_larger'] = max_file_size_for_diff
elif stat.S_ISLNK(file_stat.st_mode):
diff['after_header'] = managed_path
diff['after'] = os.readlink(managed_path)
else:
with open(managed_path, 'rb') as f:
content = f.read()
if is_binary(content):
diff['src_binary'] = 1
else:
diff['after_header'] = managed_path
diff['after'] = to_text(content)
# When forcing and we're not following links in the container, go!
if force and not follow_links:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
return container_path, mode, False
# Resolve symlinks in the container (if requested), and get information on container's file
real_container_path, regular_stat, link_target = stat_file(
client,
container,
in_path=container_path,
follow_links=follow_links,
)
# Follow links in the Docker container?
if follow_links:
container_path = real_container_path
# If the file wasn't found, continue
if regular_stat is None:
if diff is not None:
diff['before_header'] = container_path
diff['before'] = ''
return container_path, mode, False
# When forcing, go!
if force:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
# If force is set to False, and the destination exists, assume there's nothing to do
if force is False:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
copy_dst_to_src(diff)
return container_path, mode, True
# Basic idempotency checks
if stat.S_ISLNK(file_stat.st_mode):
if link_target is None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
local_link_target = os.readlink(managed_path)
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, local_link_target == link_target
if link_target is not None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if is_container_file_not_regular_file(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if file_stat.st_size != regular_stat['size']:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if mode != get_container_file_mode(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
# Fetch file from container
def process_none(in_path):
return container_path, mode, False
def process_regular(in_path, tar, member):
# Check things like user/group ID and mode
if any([
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
not stat.S_ISREG(file_stat.st_mode),
member.size != file_stat.st_size,
]):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
return container_path, mode, False
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
with open(managed_path, 'rb') as local_f:
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path)
return container_path, mode, is_equal
def process_symlink(in_path, member):
if diff is not None:
diff['before_header'] = in_path
diff['before'] = member.linkname
# Check things like user/group ID and mode
if member.mode & 0xFFF != mode:
return container_path, mode, False
if member.uid != owner_id:
return container_path, mode, False
if member.gid != group_id:
return container_path, mode, False
if not stat.S_ISLNK(file_stat.st_mode):
return container_path, mode, False
local_link_target = os.readlink(managed_path)
return container_path, mode, member.linkname == local_link_target
def process_other(in_path, member):
add_other_diff(diff, in_path, member)
return container_path, mode, False
return fetch_file_ex(
client,
container,
in_path=container_path,
process_none=process_none,
process_regular=process_regular,
process_symlink=process_symlink,
process_other=process_other,
follow_links=follow_links,
)
def copy_file_into_container(client, container, managed_path, container_path, follow_links, local_follow_links,
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
if diff:
diff = {}
else:
diff = None
container_path, mode, idempotent = is_file_idempotent(
client,
container,
managed_path,
container_path,
follow_links,
local_follow_links,
owner_id,
group_id,
mode,
force=force,
diff=diff,
max_file_size_for_diff=max_file_size_for_diff,
)
changed = not idempotent
if changed and not client.module.check_mode:
put_file(
client,
container,
in_path=managed_path,
out_path=container_path,
user_id=owner_id,
group_id=group_id,
mode=mode,
follow_links=local_follow_links,
)
result = dict(
container_path=container_path,
changed=changed,
)
if diff:
result['diff'] = diff
client.module.exit_json(**result)
def is_content_idempotent(client, container, content, container_path, follow_links, owner_id, group_id, mode,
force=False, diff=None, max_file_size_for_diff=1):
if diff is not None:
if len(content) > max_file_size_for_diff > 0:
diff['src_larger'] = max_file_size_for_diff
elif is_binary(content):
diff['src_binary'] = 1
else:
diff['after_header'] = 'dynamically generated'
diff['after'] = to_text(content)
# When forcing and we're not following links in the container, go!
if force and not follow_links:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
return container_path, mode, False
# Resolve symlinks in the container (if requested), and get information on container's file
real_container_path, regular_stat, link_target = stat_file(
client,
container,
in_path=container_path,
follow_links=follow_links,
)
# Follow links in the Docker container?
if follow_links:
container_path = real_container_path
# If the file wasn't found, continue
if regular_stat is None:
if diff is not None:
diff['before_header'] = container_path
diff['before'] = ''
return container_path, mode, False
# When forcing, go!
if force:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
# If force is set to False, and the destination exists, assume there's nothing to do
if force is False:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
copy_dst_to_src(diff)
return container_path, mode, True
# Basic idempotency checks
if link_target is not None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if is_container_file_not_regular_file(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if len(content) != regular_stat['size']:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
if mode != get_container_file_mode(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
return container_path, mode, False
# Fetch file from container
def process_none(in_path):
if diff is not None:
diff['before'] = ''
return container_path, mode, False
def process_regular(in_path, tar, member):
# Check things like user/group ID and mode
if any([
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
member.size != len(content),
]):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
return container_path, mode, False
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, io.BytesIO(content), member.size, diff, max_file_size_for_diff, in_path)
return container_path, mode, is_equal
def process_symlink(in_path, member):
if diff is not None:
diff['before_header'] = in_path
diff['before'] = member.linkname
return container_path, mode, False
def process_other(in_path, member):
add_other_diff(diff, in_path, member)
return container_path, mode, False
return fetch_file_ex(
client,
container,
in_path=container_path,
process_none=process_none,
process_regular=process_regular,
process_symlink=process_symlink,
process_other=process_other,
follow_links=follow_links,
)
def copy_content_into_container(client, container, content, container_path, follow_links,
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
if diff:
diff = {}
else:
diff = None
container_path, mode, idempotent = is_content_idempotent(
client,
container,
content,
container_path,
follow_links,
owner_id,
group_id,
mode,
force=force,
diff=diff,
max_file_size_for_diff=max_file_size_for_diff,
)
changed = not idempotent
if changed and not client.module.check_mode:
put_file_content(
client,
container,
content=content,
out_path=container_path,
user_id=owner_id,
group_id=group_id,
mode=mode,
)
result = dict(
container_path=container_path,
changed=changed,
)
if diff:
# Since the content is no_log, make sure that the before/after strings look sufficiently different
key = generate_insecure_key()
diff['scrambled_diff'] = base64.b64encode(key)
for k in ('before', 'after'):
if k in diff:
diff[k] = scramble(diff[k], key)
result['diff'] = diff
client.module.exit_json(**result)
def main():
argument_spec = dict(
container=dict(type='str', required=True),
path=dict(type='path'),
container_path=dict(type='str', required=True),
follow=dict(type='bool', default=False),
local_follow=dict(type='bool', default=True),
owner_id=dict(type='int'),
group_id=dict(type='int'),
mode=dict(type='int'),
force=dict(type='bool'),
content=dict(type='str', no_log=True),
content_is_b64=dict(type='bool', default=False),
# Undocumented parameters for use by the action plugin
_max_file_size_for_diff=dict(type='int'),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
min_docker_api_version='1.20',
supports_check_mode=True,
mutually_exclusive=[('path', 'content')],
required_together=[('owner_id', 'group_id')],
required_by={
'content': ['mode'],
},
)
container = client.module.params['container']
managed_path = client.module.params['path']
container_path = client.module.params['container_path']
follow = client.module.params['follow']
local_follow = client.module.params['local_follow']
owner_id = client.module.params['owner_id']
group_id = client.module.params['group_id']
mode = client.module.params['mode']
force = client.module.params['force']
content = client.module.params['content']
max_file_size_for_diff = client.module.params['_max_file_size_for_diff'] or 1
if content is not None:
if client.module.params['content_is_b64']:
try:
content = base64.b64decode(content)
except Exception as e: # depending on Python version and error, multiple different exceptions can be raised
client.fail('Cannot Base64 decode the content option: {0}'.format(e))
else:
content = to_bytes(content)
if not container_path.startswith(os.path.sep):
container_path = os.path.join(os.path.sep, container_path)
container_path = os.path.normpath(container_path)
try:
if owner_id is None or group_id is None:
owner_id, group_id = determine_user_group(client, container)
if content is not None:
copy_content_into_container(
client,
container,
content,
container_path,
follow_links=follow,
owner_id=owner_id,
group_id=group_id,
mode=mode,
force=force,
diff=client.module._diff,
max_file_size_for_diff=max_file_size_for_diff,
)
elif managed_path is not None:
copy_file_into_container(
client,
container,
managed_path,
container_path,
follow_links=follow,
local_follow_links=local_follow,
owner_id=owner_id,
group_id=group_id,
mode=mode,
force=force,
diff=client.module._diff,
max_file_size_for_diff=max_file_size_for_diff,
)
else:
# Can happen if a user explicitly passes `content: null` or `path: null`...
client.fail('One of path and content must be supplied')
except NotFound as exc:
client.fail('Could not find container "{1}" or resource in it ({0})'.format(exc, container))
except APIError as exc:
client.fail('An unexpected Docker error occurred for container "{1}": {0}'.format(exc, container), exception=traceback.format_exc())
except DockerException as exc:
client.fail('An unexpected Docker error occurred for container "{1}": {0}'.format(exc, container), exception=traceback.format_exc())
except RequestException as exc:
client.fail(
'An unexpected requests error occurred for container "{1}" when trying to talk to the Docker daemon: {0}'.format(exc, container),
exception=traceback.format_exc())
except DockerUnexpectedError as exc:
client.fail('Unexpected error: {exc}'.format(exc=to_native(exc)), exception=traceback.format_exc())
except DockerFileCopyError as exc:
client.fail(to_native(exc))
except OSError as exc:
client.fail('Unexpected error: {exc}'.format(exc=to_native(exc)), exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,310 @@
#!/usr/bin/python
#
# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_container_exec
short_description: Execute command in a docker container
version_added: 1.5.0
description:
- Executes a command in a Docker container.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
container:
type: str
required: true
description:
- The name of the container to execute the command in.
argv:
type: list
elements: str
description:
- The command to execute.
- Since this is a list of arguments, no quoting is needed.
- Exactly one of I(argv) and I(command) must be specified.
command:
type: str
description:
- The command to execute.
- Exactly one of I(argv) and I(command) must be specified.
chdir:
type: str
description:
- The directory to run the command in.
detach:
description:
- Whether to run the command synchronously (I(detach=false), default) or asynchronously (I(detach=true)).
- If set to C(true), I(stdin) cannot be provided, and the return values C(stdout), C(stderr) and
C(rc) are not returned.
type: bool
default: false
version_added: 2.1.0
user:
type: str
description:
- If specified, the user to execute this command with.
stdin:
type: str
description:
- Set the stdin of the command directly to the specified value.
- Can only be used if I(detach=false).
stdin_add_newline:
type: bool
default: true
description:
- If set to C(true), appends a newline to I(stdin).
strip_empty_ends:
type: bool
default: true
description:
- Strip empty lines from the end of stdout/stderr in result.
tty:
type: bool
default: false
description:
- Whether to allocate a TTY.
env:
description:
- Dictionary of environment variables with their respective values to be passed to the command ran inside the container.
- Values which might be parsed as numbers, booleans or other types by the YAML parser must be quoted (for example C("true")) in order to avoid data loss.
- Please note that if you are passing values in with Jinja2 templates, like C("{{ value }}"), you need to add C(| string) to prevent Ansible to
convert strings such as C("true") back to booleans. The correct way is to use C("{{ value | string }}").
type: dict
version_added: 2.1.0
notes:
- Does not support C(check_mode).
author:
- "Felix Fontein (@felixfontein)"
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Run a simple command (command)
community.docker.docker_container_exec:
container: foo
command: /bin/bash -c "ls -lah"
chdir: /root
register: result
- name: Print stdout
debug:
var: result.stdout
- name: Run a simple command (argv)
community.docker.docker_container_exec:
container: foo
argv:
- /bin/bash
- "-c"
- "ls -lah > /dev/stderr"
chdir: /root
register: result
- name: Print stderr lines
debug:
var: result.stderr_lines
'''
RETURN = '''
stdout:
type: str
returned: success and I(detach=false)
description:
- The standard output of the container command.
stderr:
type: str
returned: success and I(detach=false)
description:
- The standard error output of the container command.
rc:
type: int
returned: success and I(detach=false)
sample: 0
description:
- The exit code of the command.
exec_id:
type: str
returned: success and I(detach=true)
sample: 249d9e3075655baf705ed8f40488c5e9434049cf3431976f1bfdb73741c574c5
description:
- The execution ID of the command.
version_added: 2.1.0
'''
import shlex
import traceback
from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
from ansible.module_utils.compat import selectors
from ansible.module_utils.six import string_types
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.socket_helper import (
shutdown_writing,
write_to_socket,
)
from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
DockerSocketHandlerModule,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import format_environment
def main():
argument_spec = dict(
container=dict(type='str', required=True),
argv=dict(type='list', elements='str'),
command=dict(type='str'),
chdir=dict(type='str'),
detach=dict(type='bool', default=False),
user=dict(type='str'),
stdin=dict(type='str'),
stdin_add_newline=dict(type='bool', default=True),
strip_empty_ends=dict(type='bool', default=True),
tty=dict(type='bool', default=False),
env=dict(type='dict'),
)
option_minimal_versions = dict(
chdir=dict(docker_api_version='1.35'),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
option_minimal_versions=option_minimal_versions,
mutually_exclusive=[('argv', 'command')],
required_one_of=[('argv', 'command')],
)
container = client.module.params['container']
argv = client.module.params['argv']
command = client.module.params['command']
chdir = client.module.params['chdir']
detach = client.module.params['detach']
user = client.module.params['user']
stdin = client.module.params['stdin']
strip_empty_ends = client.module.params['strip_empty_ends']
tty = client.module.params['tty']
env = client.module.params['env']
if env is not None:
for name, value in list(env.items()):
if not isinstance(value, string_types):
client.module.fail_json(
msg="Non-string value found for env option. Ambiguous env options must be "
"wrapped in quotes to avoid them being interpreted. Key: %s" % (name, ))
env[name] = to_text(value, errors='surrogate_or_strict')
if command is not None:
argv = shlex.split(command)
if detach and stdin is not None:
client.module.fail_json(msg='If detach=true, stdin cannot be provided.')
if stdin is not None and client.module.params['stdin_add_newline']:
stdin += '\n'
try:
data = {
'Container': container,
'User': user or '',
'Privileged': False,
'Tty': False,
'AttachStdin': bool(stdin),
'AttachStdout': True,
'AttachStderr': True,
'Cmd': argv,
'Env': format_environment(env) if env is not None else None,
}
if chdir is not None:
data['WorkingDir'] = chdir
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
exec_id = exec_data['Id']
data = {
'Tty': tty,
'Detach': detach,
}
if detach:
client.post_json_to_text('/exec/{0}/start', exec_id, data=data)
client.module.exit_json(changed=True, exec_id=exec_id)
else:
if stdin and not detach:
exec_socket = client.post_json_to_stream_socket('/exec/{0}/start', exec_id, data=data)
try:
with DockerSocketHandlerModule(exec_socket, client.module, selectors) as exec_socket_handler:
if stdin:
exec_socket_handler.write(to_bytes(stdin))
stdout, stderr = exec_socket_handler.consume()
finally:
exec_socket.close()
else:
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, data=data, stream=False, tty=tty, demux=True)
result = client.get_json('/exec/{0}/json', exec_id)
stdout = to_text(stdout or b'')
stderr = to_text(stderr or b'')
if strip_empty_ends:
stdout = stdout.rstrip('\r\n')
stderr = stderr.rstrip('\r\n')
client.module.exit_json(
changed=True,
stdout=stdout,
stderr=stderr,
rc=result.get('ExitCode') or 0,
)
except NotFound:
client.fail('Could not find container "{0}"'.format(container))
except APIError as e:
if e.response is not None and e.response.status_code == 409:
client.fail('The container "{0}" has been paused ({1})'.format(container, to_native(e)))
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,144 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_container_info
short_description: Retrieves facts about docker container
description:
- Retrieves facts about a docker container.
- Essentially returns the output of C(docker inspect <name>), similar to what M(community.docker.docker_container)
returns for a non-absent container.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- The name of the container to inspect.
- When identifying an existing container name may be a name or a long or short container ID.
type: str
required: true
author:
- "Felix Fontein (@felixfontein)"
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get infos on container
community.docker.docker_container_info:
name: mydata
register: result
- name: Does container exist?
ansible.builtin.debug:
msg: "The container {{ 'exists' if result.exists else 'does not exist' }}"
- name: Print information about container
ansible.builtin.debug:
var: result.container
when: result.exists
'''
RETURN = '''
exists:
description:
- Returns whether the container exists.
type: bool
returned: always
sample: true
container:
description:
- Facts representing the current state of the container. Matches the docker inspection output.
- Will be C(none) if container does not exist.
returned: always
type: dict
sample: '{
"AppArmorProfile": "",
"Args": [],
"Config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/usr/bin/supervisord"
],
"Domainname": "",
"Entrypoint": null,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"ExposedPorts": {
"443/tcp": {},
"80/tcp": {}
},
"Hostname": "8e47bf643eb9",
"Image": "lnmp_nginx:v1",
"Labels": {},
"OnBuild": null,
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": {
"/tmp/lnmp/nginx-sites/logs/": {}
},
...
}'
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
def main():
argument_spec = dict(
name=dict(type='str', required=True),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
)
try:
container = client.get_container(client.module.params['name'])
client.module.exit_json(
changed=False,
exists=(True if container else False),
container=container,
)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,383 @@
#!/usr/bin/python
#
# Copyright (c) 2019 Piotr Wojciechowski <piotr@it-playground.pl>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_host_info
short_description: Retrieves facts about docker host and lists of objects of the services.
description:
- Retrieves facts about a docker host.
- Essentially returns the output of C(docker system info).
- The module also allows to list object names for containers, images, networks and volumes.
It also allows to query information on disk usage.
- The output differs depending on API version of the docker daemon.
- If the docker daemon cannot be contacted or does not meet the API version requirements,
the module will fail.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
details:
- This action does not modify state.
diff_mode:
support: N/A
details:
- This action does not modify state.
options:
containers:
description:
- Whether to list containers.
type: bool
default: false
containers_all:
description:
- By default, only running containers are returned.
- This corresponds to the C(--all) option to C(docker container list).
type: bool
default: false
version_added: 3.4.0
containers_filters:
description:
- A dictionary of filter values used for selecting containers to list.
- "For example, C(until: 24h)."
- C(label) is a special case of filter which can be a string C(<key>) matching when a label is present, a string
C(<key>=<value>) matching when a label has a particular value, or a list of strings C(<key>)/C(<key>=<value).
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/container_prune/#filtering)
for more information on possible filters.
type: dict
images:
description:
- Whether to list images.
type: bool
default: false
images_filters:
description:
- A dictionary of filter values used for selecting images to list.
- "For example, C(dangling: true)."
- C(label) is a special case of filter which can be a string C(<key>) matching when a label is present, a string
C(<key>=<value>) matching when a label has a particular value, or a list of strings C(<key>)/C(<key>=<value).
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/image_prune/#filtering)
for more information on possible filters.
type: dict
networks:
description:
- Whether to list networks.
type: bool
default: false
networks_filters:
description:
- A dictionary of filter values used for selecting networks to list.
- C(label) is a special case of filter which can be a string C(<key>) matching when a label is present, a string
C(<key>=<value>) matching when a label has a particular value, or a list of strings C(<key>)/C(<key>=<value).
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/network_prune/#filtering)
for more information on possible filters.
type: dict
volumes:
description:
- Whether to list volumes.
type: bool
default: false
volumes_filters:
description:
- A dictionary of filter values used for selecting volumes to list.
- C(label) is a special case of filter which can be a string C(<key>) matching when a label is present, a string
C(<key>=<value>) matching when a label has a particular value, or a list of strings C(<key>)/C(<key>=<value).
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/volume_prune/#filtering)
for more information on possible filters.
type: dict
disk_usage:
description:
- Summary information on used disk space by all Docker layers.
- The output is a sum of images, volumes, containers and build cache.
type: bool
default: false
verbose_output:
description:
- When set to C(true) and I(networks), I(volumes), I(images), I(containers) or I(disk_usage) is set to C(true)
then output will contain verbose information about objects matching the full output of API method.
For details see the documentation of your version of Docker API at U(https://docs.docker.com/engine/api/).
- The verbose output in this module contains only subset of information returned by I(_info) module
for each type of the objects.
type: bool
default: false
author:
- Piotr Wojciechowski (@WojciechowskiPiotr)
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get info on docker host
community.docker.docker_host_info:
register: result
- name: Get info on docker host and list images
community.docker.docker_host_info:
images: true
register: result
- name: Get info on docker host and list images matching the filter
community.docker.docker_host_info:
images: true
images_filters:
label: "mylabel"
register: result
- name: Get info on docker host and verbose list images
community.docker.docker_host_info:
images: true
verbose_output: true
register: result
- name: Get info on docker host and used disk space
community.docker.docker_host_info:
disk_usage: true
register: result
- name: Get info on docker host and list containers matching the filter
community.docker.docker_host_info:
containers: true
containers_filters:
label:
- key1=value1
- key2=value2
register: result
- ansible.builtin.debug:
var: result.host_info
'''
RETURN = '''
can_talk_to_docker:
description:
- Will be C(true) if the module can talk to the docker daemon.
returned: both on success and on error
type: bool
host_info:
description:
- Facts representing the basic state of the docker host. Matches the C(docker system info) output.
returned: always
type: dict
volumes:
description:
- List of dict objects containing the basic information about each volume.
Keys matches the C(docker volume ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(volumes) is C(true)
type: list
elements: dict
networks:
description:
- List of dict objects containing the basic information about each network.
Keys matches the C(docker network ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(networks) is C(true)
type: list
elements: dict
containers:
description:
- List of dict objects containing the basic information about each container.
Keys matches the C(docker container ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(containers) is C(true)
type: list
elements: dict
images:
description:
- List of dict objects containing the basic information about each image.
Keys matches the C(docker image ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(images) is C(true)
type: list
elements: dict
disk_usage:
description:
- Information on summary disk usage by images, containers and volumes on docker host
unless I(verbose_output=true). See description for I(verbose_output).
returned: When I(disk_usage) is C(true)
type: dict
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
clean_dict_booleans_for_docker_api,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, APIError
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
class DockerHostManager(DockerBaseClass):
def __init__(self, client, results):
super(DockerHostManager, self).__init__()
self.client = client
self.results = results
self.verbose_output = self.client.module.params['verbose_output']
listed_objects = ['volumes', 'networks', 'containers', 'images']
self.results['host_info'] = self.get_docker_host_info()
if self.client.module.params['disk_usage']:
self.results['disk_usage'] = self.get_docker_disk_usage_facts()
for docker_object in listed_objects:
if self.client.module.params[docker_object]:
returned_name = docker_object
filter_name = docker_object + "_filters"
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name), True)
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
def get_docker_host_info(self):
try:
return self.client.info()
except APIError as exc:
self.client.fail("Error inspecting docker host: %s" % to_native(exc))
def get_docker_disk_usage_facts(self):
try:
if self.verbose_output:
return self.client.df()
else:
return dict(LayersSize=self.client.df()['LayersSize'])
except APIError as exc:
self.client.fail("Error inspecting docker host: %s" % to_native(exc))
def get_docker_items_list(self, docker_object=None, filters=None, verbose=False):
items = None
items_list = []
header_containers = ['Id', 'Image', 'Command', 'Created', 'Status', 'Ports', 'Names']
header_volumes = ['Driver', 'Name']
header_images = ['Id', 'RepoTags', 'Created', 'Size']
header_networks = ['Id', 'Driver', 'Name', 'Scope']
filter_arg = dict()
if filters:
filter_arg['filters'] = filters
try:
if docker_object == 'containers':
params = {
'limit': -1,
'all': 1 if self.client.module.params['containers_all'] else 0,
'size': 0,
'trunc_cmd': 0,
'filters': convert_filters(filters) if filters else None,
}
items = self.client.get_json("/containers/json", params=params)
elif docker_object == 'networks':
params = {
'filters': convert_filters(filters or {})
}
items = self.client.get_json("/networks", params=params)
elif docker_object == 'images':
params = {
'only_ids': 0,
'all': 0,
'filters': convert_filters(filters) if filters else None,
}
items = self.client.get_json("/images/json", params=params)
elif docker_object == 'volumes':
params = {
'filters': convert_filters(filters) if filters else None,
}
items = self.client.get_json('/volumes', params=params)
items = items['Volumes']
except APIError as exc:
self.client.fail("Error inspecting docker host for object '%s': %s" % (docker_object, to_native(exc)))
if self.verbose_output:
return items
for item in items:
item_record = dict()
if docker_object == 'containers':
for key in header_containers:
item_record[key] = item.get(key)
elif docker_object == 'networks':
for key in header_networks:
item_record[key] = item.get(key)
elif docker_object == 'images':
for key in header_images:
item_record[key] = item.get(key)
elif docker_object == 'volumes':
for key in header_volumes:
item_record[key] = item.get(key)
items_list.append(item_record)
return items_list
def main():
argument_spec = dict(
containers=dict(type='bool', default=False),
containers_all=dict(type='bool', default=False),
containers_filters=dict(type='dict'),
images=dict(type='bool', default=False),
images_filters=dict(type='dict'),
networks=dict(type='bool', default=False),
networks_filters=dict(type='dict'),
volumes=dict(type='bool', default=False),
volumes_filters=dict(type='dict'),
disk_usage=dict(type='bool', default=False),
verbose_output=dict(type='bool', default=False),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
fail_results=dict(
can_talk_to_docker=False,
),
)
client.fail_results['can_talk_to_docker'] = True
try:
results = dict(
changed=False,
)
DockerHostManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,273 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_image_info
short_description: Inspect docker images
description:
- Provide one or more image names, and the module will inspect each, returning an array of inspection results.
- If an image does not exist locally, it will not appear in the results. If you want to check whether an image exists
locally, you can call the module with the image name, then check whether the result list is empty (image does not
exist) or has one element (the image exists locally).
- The module will not attempt to pull images from registries. Use M(community.docker.docker_image) with I(source) set to C(pull)
to ensure an image is pulled.
notes:
- This module was called C(docker_image_facts) before Ansible 2.8. The usage did not change.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- An image name or a list of image names. Name format will be C(name[:tag]) or C(repository/name[:tag]),
where C(tag) is optional. If a tag is not provided, C(latest) will be used. Instead of image names, also
image IDs can be used.
- If no name is provided, a list of all images will be returned.
type: list
elements: str
requirements:
- "Docker API >= 1.25"
author:
- Chris Houseknecht (@chouseknecht)
'''
EXAMPLES = '''
- name: Inspect a single image
community.docker.docker_image_info:
name: pacur/centos-7
- name: Inspect multiple images
community.docker.docker_image_info:
name:
- pacur/centos-7
- sinatra
register: result
- name: Make sure that both images pacur/centos-7 and sinatra exist locally
ansible.builtin.assert:
that:
- result.images | length == 2
'''
RETURN = '''
images:
description:
- Inspection results for the selected images.
- The list only contains inspection results of images existing locally.
returned: always
type: list
elements: dict
sample: [
{
"Architecture": "amd64",
"Author": "",
"Comment": "",
"Config": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/etc/docker/registry/config.yml"
],
"Domainname": "",
"Entrypoint": [
"/bin/registry"
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"ExposedPorts": {
"5000/tcp": {}
},
"Hostname": "e5c68db50333",
"Image": "c72dce2618dc8f7b794d2b2c2b1e64e0205ead5befc294f8111da23bd6a2c799",
"Labels": {},
"OnBuild": [],
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": {
"/var/lib/registry": {}
},
"WorkingDir": ""
},
"Container": "e83a452b8fb89d78a25a6739457050131ca5c863629a47639530d9ad2008d610",
"ContainerConfig": {
"AttachStderr": false,
"AttachStdin": false,
"AttachStdout": false,
"Cmd": [
"/bin/sh",
"-c",
'#(nop) CMD ["/etc/docker/registry/config.yml"]'
],
"Domainname": "",
"Entrypoint": [
"/bin/registry"
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"ExposedPorts": {
"5000/tcp": {}
},
"Hostname": "e5c68db50333",
"Image": "c72dce2618dc8f7b794d2b2c2b1e64e0205ead5befc294f8111da23bd6a2c799",
"Labels": {},
"OnBuild": [],
"OpenStdin": false,
"StdinOnce": false,
"Tty": false,
"User": "",
"Volumes": {
"/var/lib/registry": {}
},
"WorkingDir": ""
},
"Created": "2016-03-08T21:08:15.399680378Z",
"DockerVersion": "1.9.1",
"GraphDriver": {
"Data": null,
"Name": "aufs"
},
"Id": "53773d8552f07b730f3e19979e32499519807d67b344141d965463a950a66e08",
"Name": "registry:2",
"Os": "linux",
"Parent": "f0b1f729f784b755e7bf9c8c2e65d8a0a35a533769c2588f02895f6781ac0805",
"RepoDigests": [],
"RepoTags": [
"registry:2"
],
"Size": 0,
"VirtualSize": 165808884
}
]
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import parse_repository_tag
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
super(ImageManager, self).__init__()
self.client = client
self.results = results
self.name = self.client.module.params.get('name')
self.log("Gathering facts for images: %s" % (str(self.name)))
if self.name:
self.results['images'] = self.get_facts()
else:
self.results['images'] = self.get_all_images()
def fail(self, msg):
self.client.fail(msg)
def get_facts(self):
'''
Lookup and inspect each image name found in the names parameter.
:returns array of image dictionaries
'''
results = []
names = self.name
if not isinstance(names, list):
names = [names]
for name in names:
if is_image_name_id(name):
self.log('Fetching image %s (ID)' % (name))
image = self.client.find_image_by_id(name, accept_missing_image=True)
else:
repository, tag = parse_repository_tag(name)
if not tag:
tag = 'latest'
self.log('Fetching image %s:%s' % (repository, tag))
image = self.client.find_image(name=repository, tag=tag)
if image:
results.append(image)
return results
def get_all_images(self):
results = []
params = {
'only_ids': 0,
'all': 0,
}
images = self.client.get_json("/images/json", params=params)
for image in images:
try:
inspection = self.client.get_json('/images/{0}/json', image['Id'])
except NotFound:
inspection = None
except Exception as exc:
self.fail("Error inspecting image %s - %s" % (image['Id'], to_native(exc)))
results.append(inspection)
return results
def main():
argument_spec = dict(
name=dict(type='list', elements='str'),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
)
try:
results = dict(
changed=False,
images=[]
)
ImageManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,194 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_image_load
short_description: Load docker image(s) from archives
version_added: 1.3.0
description:
- Load one or multiple Docker images from a C(.tar) archive, and return information on
the loaded image(s).
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
path:
description:
- The path to the C(.tar) archive to load Docker image(s) from.
type: path
required: true
notes:
- Does not support C(check_mode).
requirements:
- "Docker API >= 1.25"
author:
- Felix Fontein (@felixfontein)
'''
EXAMPLES = '''
- name: Load all image(s) from the given tar file
community.docker.docker_image_load:
path: /path/to/images.tar
register: result
- name: Print the loaded image names
ansible.builtin.debug:
msg: "Loaded the following images: {{ result.image_names | join(', ') }}"
'''
RETURN = '''
image_names:
description: List of image names and IDs loaded from the archive.
returned: success
type: list
elements: str
sample:
- 'hello-world:latest'
- 'sha256:e004c2cc521c95383aebb1fb5893719aa7a8eae2e7a71f316a4410784edb00a9'
images:
description: Image inspection results for the loaded images.
returned: success
type: list
elements: dict
sample: []
'''
import errno
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
super(ImageManager, self).__init__()
self.client = client
self.results = results
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.path = parameters['path']
self.load_images()
@staticmethod
def _extract_output_line(line, output):
'''
Extract text line from stream output and, if found, adds it to output.
'''
if 'stream' in line or 'status' in line:
# Make sure we have a string (assuming that line['stream'] and
# line['status'] are either not defined, falsish, or a string)
text_line = line.get('stream') or line.get('status') or ''
output.extend(text_line.splitlines())
def load_images(self):
'''
Load images from a .tar archive
'''
# Load image(s) from file
load_output = []
try:
self.log("Opening image {0}".format(self.path))
with open(self.path, 'rb') as image_tar:
self.log("Loading images from {0}".format(self.path))
res = self.client._post(self.client._url("/images/load"), data=image_tar, stream=True)
for line in self.client._stream_helper(res, decode=True):
self.log(line, pretty_print=True)
self._extract_output_line(line, load_output)
except EnvironmentError as exc:
if exc.errno == errno.ENOENT:
self.client.fail("Error opening archive {0} - {1}".format(self.path, to_native(exc)))
self.client.fail("Error loading archive {0} - {1}".format(self.path, to_native(exc)), stdout='\n'.join(load_output))
except Exception as exc:
self.client.fail("Error loading archive {0} - {1}".format(self.path, to_native(exc)), stdout='\n'.join(load_output))
# Collect loaded images
loaded_images = []
for line in load_output:
if line.startswith('Loaded image:'):
loaded_images.append(line[len('Loaded image:'):].strip())
if line.startswith('Loaded image ID:'):
loaded_images.append(line[len('Loaded image ID:'):].strip())
if not loaded_images:
self.client.fail("Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output))
images = []
for image_name in loaded_images:
if is_image_name_id(image_name):
images.append(self.client.find_image_by_id(image_name))
elif ':' in image_name:
image_name, tag = image_name.rsplit(':', 1)
images.append(self.client.find_image(image_name, tag))
else:
self.client.module.warn('Image name "{0}" is neither ID nor has a tag'.format(image_name))
self.results['image_names'] = loaded_images
self.results['images'] = images
self.results['changed'] = True
self.results['stdout'] = '\n'.join(load_output)
def main():
client = AnsibleDockerClient(
argument_spec=dict(
path=dict(type='path', required=True),
),
supports_check_mode=False,
)
try:
results = dict(
image_names=[],
images=[],
)
ImageManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,451 @@
#!/usr/bin/python
#
# Copyright (c) 2016 Olaf Kilian <olaf.kilian@symanex.com>
# Chris Houseknecht, <house@redhat.com>
# James Tanner, <jtanner@redhat.com>
#
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_login
short_description: Log into a Docker registry.
description:
- Provides functionality similar to the C(docker login) command.
- Authenticate with a docker registry and add the credentials to your local Docker config file respectively the
credentials store associated to the registry. Adding the credentials to the config files resp. the credential
store allows future connections to the registry using tools such as Ansible's Docker modules, the Docker CLI
and Docker SDK for Python without needing to provide credentials.
- Running in check mode will perform the authentication without updating the config file.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
registry_url:
description:
- The registry URL.
type: str
default: "https://index.docker.io/v1/"
aliases:
- registry
- url
username:
description:
- The username for the registry account.
- Required when I(state) is C(present).
type: str
password:
description:
- The plaintext password for the registry account.
- Required when I(state) is C(present).
type: str
reauthorize:
description:
- Refresh existing authentication found in the configuration file.
type: bool
default: false
aliases:
- reauth
config_path:
description:
- Custom path to the Docker CLI configuration file.
type: path
default: ~/.docker/config.json
aliases:
- dockercfg_path
state:
description:
- This controls the current state of the user. C(present) will login in a user, C(absent) will log them out.
- To logout you only need the registry server, which defaults to DockerHub.
- Before 2.1 you could ONLY log in.
- Docker does not support 'logout' with a custom config file.
type: str
default: 'present'
choices: ['present', 'absent']
requirements:
- "Docker API >= 1.25"
author:
- Olaf Kilian (@olsaki) <olaf.kilian@symanex.com>
- Chris Houseknecht (@chouseknecht)
'''
EXAMPLES = '''
- name: Log into DockerHub
community.docker.docker_login:
username: docker
password: rekcod
- name: Log into private registry and force re-authorization
community.docker.docker_login:
registry_url: your.private.registry.io
username: yourself
password: secrets3
reauthorize: true
- name: Log into DockerHub using a custom config file
community.docker.docker_login:
username: docker
password: rekcod
config_path: /tmp/.mydockercfg
- name: Log out of DockerHub
community.docker.docker_login:
state: absent
'''
RETURN = '''
login_results:
description: Results from the login.
returned: when I(state=present)
type: dict
sample: {
"serveraddress": "localhost:5000",
"username": "testuser"
}
'''
import base64
import json
import os
import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_DOCKER_REGISTRY,
DockerBaseClass,
)
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.auth import decode_auth
from ansible_collections.community.docker.plugins.module_utils._api.credentials.errors import CredentialsNotFound
from ansible_collections.community.docker.plugins.module_utils._api.credentials.store import Store
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class DockerFileStore(object):
'''
A custom credential store class that implements only the functionality we need to
update the docker config file when no credential helpers is provided.
'''
program = "<legacy config>"
def __init__(self, config_path):
self._config_path = config_path
# Make sure we have a minimal config if none is available.
self._config = dict(
auths=dict()
)
try:
# Attempt to read the existing config.
with open(self._config_path, "r") as f:
config = json.load(f)
except (ValueError, IOError):
# No config found or an invalid config found so we'll ignore it.
config = dict()
# Update our internal config with what ever was loaded.
self._config.update(config)
@property
def config_path(self):
'''
Return the config path configured in this DockerFileStore instance.
'''
return self._config_path
def get(self, server):
'''
Retrieve credentials for `server` if there are any in the config file.
Otherwise raise a `StoreError`
'''
server_creds = self._config['auths'].get(server)
if not server_creds:
raise CredentialsNotFound('No matching credentials')
(username, password) = decode_auth(server_creds['auth'])
return dict(
Username=username,
Secret=password
)
def _write(self):
'''
Write config back out to disk.
'''
# Make sure directory exists
dir = os.path.dirname(self._config_path)
if not os.path.exists(dir):
os.makedirs(dir)
# Write config; make sure it has permissions 0x600
content = json.dumps(self._config, indent=4, sort_keys=True).encode('utf-8')
f = os.open(self._config_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.write(f, content)
finally:
os.close(f)
def store(self, server, username, password):
'''
Add a credentials for `server` to the current configuration.
'''
b64auth = base64.b64encode(
to_bytes(username) + b':' + to_bytes(password)
)
auth = to_text(b64auth)
# build up the auth structure
if 'auths' not in self._config:
self._config['auths'] = dict()
self._config['auths'][server] = dict(
auth=auth
)
self._write()
def erase(self, server):
'''
Remove credentials for the given server from the configuration.
'''
if 'auths' in self._config and server in self._config['auths']:
self._config['auths'].pop(server)
self._write()
class LoginManager(DockerBaseClass):
def __init__(self, client, results):
super(LoginManager, self).__init__()
self.client = client
self.results = results
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.registry_url = parameters.get('registry_url')
self.username = parameters.get('username')
self.password = parameters.get('password')
self.reauthorize = parameters.get('reauthorize')
self.config_path = parameters.get('config_path')
self.state = parameters.get('state')
def run(self):
'''
Do the actuall work of this task here. This allows instantiation for partial
testing.
'''
if self.state == 'present':
self.login()
else:
self.logout()
def fail(self, msg):
self.client.fail(msg)
def _login(self, reauth):
if self.config_path and os.path.exists(self.config_path):
self.client._auth_configs = auth.load_config(
self.config_path, credstore_env=self.client.credstore_env
)
elif not self.client._auth_configs or self.client._auth_configs.is_empty:
self.client._auth_configs = auth.load_config(
credstore_env=self.client.credstore_env
)
authcfg = self.client._auth_configs.resolve_authconfig(self.registry_url)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == self.username \
and not reauth:
return authcfg
req_data = {
'username': self.username,
'password': self.password,
'email': None,
'serveraddress': self.registry_url,
}
response = self.client._post_json(self.client._url('/auth'), data=req_data)
if response.status_code == 200:
self.client._auth_configs.add_auth(self.registry_url or auth.INDEX_NAME, req_data)
return self.client._result(response, json=True)
def login(self):
'''
Log into the registry with provided username/password. On success update the config
file with the new authorization.
:return: None
'''
self.results['actions'].append("Logged into %s" % (self.registry_url))
self.log("Log into %s with username %s" % (self.registry_url, self.username))
try:
response = self._login(self.reauthorize)
except Exception as exc:
self.fail("Logging into %s for user %s failed - %s" % (self.registry_url, self.username, to_native(exc)))
# If user is already logged in, then response contains password for user
if 'password' in response:
# This returns correct password if user is logged in and wrong password is given.
# So if it returns another password as we passed, and the user didn't request to
# reauthorize, still do it.
if not self.reauthorize and response['password'] != self.password:
try:
response = self._login(True)
except Exception as exc:
self.fail("Logging into %s for user %s failed - %s" % (self.registry_url, self.username, to_native(exc)))
response.pop('password', None)
self.results['login_result'] = response
self.update_credentials()
def logout(self):
'''
Log out of the registry. On success update the config file.
:return: None
'''
# Get the configuration store.
store = self.get_credential_store_instance(self.registry_url, self.config_path)
try:
store.get(self.registry_url)
except CredentialsNotFound:
# get raises an exception on not found.
self.log("Credentials for %s not present, doing nothing." % (self.registry_url))
self.results['changed'] = False
return
if not self.check_mode:
store.erase(self.registry_url)
self.results['changed'] = True
def update_credentials(self):
'''
If the authorization is not stored attempt to store authorization values via
the appropriate credential helper or to the config file.
:return: None
'''
# Check to see if credentials already exist.
store = self.get_credential_store_instance(self.registry_url, self.config_path)
try:
current = store.get(self.registry_url)
except CredentialsNotFound:
# get raises an exception on not found.
current = dict(
Username='',
Secret=''
)
if current['Username'] != self.username or current['Secret'] != self.password or self.reauthorize:
if not self.check_mode:
store.store(self.registry_url, self.username, self.password)
self.log("Writing credentials to configured helper %s for %s" % (store.program, self.registry_url))
self.results['actions'].append("Wrote credentials to configured helper %s for %s" % (
store.program, self.registry_url))
self.results['changed'] = True
def get_credential_store_instance(self, registry, dockercfg_path):
'''
Return an instance of docker.credentials.Store used by the given registry.
:return: A Store or None
:rtype: Union[docker.credentials.Store, NoneType]
'''
credstore_env = self.client.credstore_env
config = auth.load_config(config_path=dockercfg_path)
store_name = auth.get_credential_store(config, registry)
# Make sure that there is a credential helper before trying to instantiate a
# Store object.
if store_name:
self.log("Found credential store %s" % store_name)
return Store(store_name, environment=credstore_env)
return DockerFileStore(dockercfg_path)
def main():
argument_spec = dict(
registry_url=dict(type='str', default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']),
username=dict(type='str'),
password=dict(type='str', no_log=True),
reauthorize=dict(type='bool', default=False, aliases=['reauth']),
state=dict(type='str', default='present', choices=['present', 'absent']),
config_path=dict(type='path', default='~/.docker/config.json', aliases=['dockercfg_path']),
)
required_if = [
('state', 'present', ['username', 'password']),
]
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
)
try:
results = dict(
changed=False,
actions=[],
login_result={}
)
manager = LoginManager(client, results)
manager.run()
if 'actions' in results:
del results['actions']
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,679 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
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
options:
name:
description:
- Name of the network to operate on.
type: str
required: true
aliases:
- network_name
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 etc., 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 C(absent) forces disconnecting all containers from the
network prior to deleting the network. With state C(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.
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 I(appends) to leave existing containers connected.
type: bool
default: false
aliases:
- incremental
enable_ipv6:
description:
- Enable IPv6 networking.
type: bool
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 I(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:
- C(absent) deletes the network. If a network has connected containers, it
cannot be deleted. Use the I(force) option to disconnect all containers
and delete the network.
- C(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
I(appends) option to leave existing containers connected. Use the I(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 (I(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 = '''
- 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
force: true
'''
RETURN = '''
network:
description:
- Network inspection results for the affected network.
returned: success
type: dict
sample: {}
'''
import re
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
clean_dict_booleans_for_docker_api,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class TaskParameters(DockerBaseClass):
def __init__(self, client):
super(TaskParameters, self).__init__()
self.client = client
self.name = None
self.connected = None
self.driver = None
self.driver_options = None
self.ipam_driver = None
self.ipam_driver_options = None
self.ipam_config = None
self.appends = None
self.force = None
self.internal = None
self.labels = None
self.debug = None
self.enable_ipv6 = None
self.scope = None
self.attachable = 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()] 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):
"""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
"""
if CIDR_IPV4.match(cidr):
return 'ipv4'
elif CIDR_IPV6.match(cidr):
return 'ipv6'
raise ValueError('"{0}" is not a valid CIDR'.format(cidr))
def normalize_ipam_config_key(key):
"""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, b):
"""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
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.diff_tracker = DifferenceTracker()
self.diff_result = dict()
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_native(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):
return self.client.get_network(name=self.parameters.name)
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)
'''
differences = DifferenceTracker()
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 not (key in net['Options']) or value != net['Options'][key]:
differences.add('driver_options.%s' % key,
parameter=value,
active=net['Options'].get(key))
if self.parameters.ipam_driver:
if 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 = []
for net_ipam_config in net['IPAM']['Config']:
config = dict()
for k, v in net_ipam_config.items():
config[normalize_ipam_config_key(k)] = v
net_ipam_configs.append(config)
# Compare lists of dicts as sets of dicts
for idx, ipam_config in enumerate(self.parameters.ipam_config):
net_config = dict()
for net_ipam_config in net_ipam_configs:
if dicts_are_essentially_equal(ipam_config, net_ipam_config):
net_config = net_ipam_config
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 value != net_config.get(key):
differences.add('ipam_config[%s].%s' % (idx, key),
parameter=value,
active=net_config.get(key))
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.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 not (key in net['Labels']) or value != net['Labels'][key]:
differences.add('labels.%s' % key,
parameter=value,
active=net['Labels'].get(key))
return not differences.empty, differences
def create_network(self):
if not self.existing_network:
data = {
'Name': self.parameters.name,
'Driver': self.parameters.driver,
'Options': self.parameters.driver_options,
'IPAM': None,
'CheckDuplicate': None,
}
if self.parameters.enable_ipv6:
data['EnableIPv6'] = True
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.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.results['actions'].append("Created network %s with driver %s" % (self.parameters.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.delete_call('/networks/{0}', self.parameters.name)
self.results['actions'].append("Removed network %s" % (self.parameters.name,))
self.results['changed'] = True
def is_container_connected(self, container_name):
if not self.existing_network:
return False
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:
data = {
"Container": name,
"EndpointConfig": None,
}
self.client.post_json('/networks/{0}/connect', self.parameters.name, data=data)
self.results['actions'].append("Connected container %s" % (name,))
self.results['changed'] = True
self.diff_tracker.add('connected.{0}'.format(name), parameter=True, active=False)
def disconnect_missing(self):
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):
containers = self.client.get_network(name=self.parameters.name)['Containers']
if not containers:
return
for cont in containers.values():
self.disconnect_container(cont['Name'])
def disconnect_container(self, container_name):
if not self.check_mode:
data = {"Container": container_name}
self.client.post_json('/networks/{0}/disconnect', self.parameters.name, data=data)
self.results['actions'].append("Disconnected container %s" % (container_name,))
self.results['changed'] = True
self.diff_tracker.add('connected.{0}'.format(container_name),
parameter=False,
active=True)
def present(self):
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):
self.diff_tracker.add('exists', parameter=False, active=self.existing_network is not None)
self.remove_network()
def main():
argument_spec = dict(
name=dict(type='str', required=True, aliases=['network_name']),
connected=dict(type='list', default=[], elements='str', aliases=['containers']),
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'),
ipam_driver_options=dict(type='dict'),
ipam_config=dict(type='list', elements='dict', options=dict(
subnet=dict(type='str'),
iprange=dict(type='str'),
gateway=dict(type='str'),
aux_addresses=dict(type='dict'),
)),
enable_ipv6=dict(type='bool'),
internal=dict(type='bool'),
labels=dict(type='dict', default={}),
debug=dict(type='bool', default=False),
scope=dict(type='str', choices=['local', 'global', 'swarm']),
attachable=dict(type='bool'),
)
option_minimal_versions = dict(
scope=dict(docker_api_version='1.30'),
attachable=dict(docker_api_version='1.26'),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
# "The docker server >= 1.10.0"
option_minimal_versions=option_minimal_versions,
)
try:
cm = DockerNetworkManager(client)
client.module.exit_json(**cm.results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,140 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_network_info
short_description: Retrieves facts about docker network
description:
- Retrieves facts about a docker network.
- Essentially returns the output of C(docker network inspect <name>), similar to what M(community.docker.docker_network)
returns for a non-absent network.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- The name of the network to inspect.
- When identifying an existing network name may be a name or a long or short network ID.
type: str
required: true
author:
- "Dave Bendit (@DBendit)"
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get infos on network
community.docker.docker_network_info:
name: mydata
register: result
- name: Does network exist?
ansible.builtin.debug:
msg: "The network {{ 'exists' if result.exists else 'does not exist' }}"
- name: Print information about network
ansible.builtin.debug:
var: result.network
when: result.exists
'''
RETURN = '''
exists:
description:
- Returns whether the network exists.
type: bool
returned: always
sample: true
network:
description:
- Facts representing the current state of the network. Matches the docker inspection output.
- Will be C(none) if network does not exist.
returned: always
type: dict
sample: {
"Attachable": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Created": "2018-12-07T01:47:51.250835114-06:00",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Config": [
{
"Gateway": "192.168.96.1",
"Subnet": "192.168.96.0/20"
}
],
"Driver": "default",
"Options": null
},
"Id": "0856968545f22026c41c2c7c3d448319d3b4a6a03a40b148b3ac4031696d1c0a",
"Ingress": false,
"Internal": false,
"Labels": {},
"Name": "ansible-test-f2700bba",
"Options": {},
"Scope": "local"
}
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
def main():
argument_spec = dict(
name=dict(type='str', required=True),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
)
try:
network = client.get_network(client.module.params['name'])
client.module.exit_json(
changed=False,
exists=(True if network else False),
network=network,
)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,306 @@
#!/usr/bin/python
#
# Copyright (c) 2019 Piotr Wojciechowski <piotr@it-playground.pl>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_node
short_description: Manage Docker Swarm node
description:
- Manages the Docker nodes via Swarm Manager.
- This module allows to change the node's role, its availability, and to modify, add or remove node labels.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
hostname:
description:
- The hostname or ID of node as registered in Swarm.
- If more than one node is registered using the same hostname the ID must be used,
otherwise module will fail.
type: str
required: true
labels:
description:
- User-defined key/value metadata that will be assigned as node attribute.
- Label operations in this module apply to the docker swarm node specified by I(hostname).
Use M(community.docker.docker_swarm) module to add/modify/remove swarm cluster labels.
- The actual state of labels assigned to the node when module completes its work depends on
I(labels_state) and I(labels_to_remove) parameters values. See description below.
type: dict
labels_state:
description:
- It defines the operation on the labels assigned to node and labels specified in I(labels) option.
- Set to C(merge) to combine labels provided in I(labels) with those already assigned to the node.
If no labels are assigned then it will add listed labels. For labels that are already assigned
to the node, it will update their values. The labels not specified in I(labels) will remain unchanged.
If I(labels) is empty then no changes will be made.
- Set to C(replace) to replace all assigned labels with provided ones. If I(labels) is empty then
all labels assigned to the node will be removed.
type: str
default: 'merge'
choices:
- merge
- replace
labels_to_remove:
description:
- List of labels that will be removed from the node configuration. The list has to contain only label
names, not their values.
- If the label provided on the list is not assigned to the node, the entry is ignored.
- If the label is both on the I(labels_to_remove) and I(labels), then value provided in I(labels) remains
assigned to the node.
- If I(labels_state) is C(replace) and I(labels) is not provided or empty then all labels assigned to
node are removed and I(labels_to_remove) is ignored.
type: list
elements: str
availability:
description: Node availability to assign. If not provided then node availability remains unchanged.
choices:
- active
- pause
- drain
type: str
role:
description: Node role to assign. If not provided then node role remains unchanged.
choices:
- manager
- worker
type: str
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.4.0"
- Docker API >= 1.25
author:
- Piotr Wojciechowski (@WojciechowskiPiotr)
- Thierry Bouvet (@tbouvet)
'''
EXAMPLES = '''
- name: Set node role
community.docker.docker_node:
hostname: mynode
role: manager
- name: Set node availability
community.docker.docker_node:
hostname: mynode
availability: drain
- name: Replace node labels with new labels
community.docker.docker_node:
hostname: mynode
labels:
key: value
labels_state: replace
- name: Merge node labels and new labels
community.docker.docker_node:
hostname: mynode
labels:
key: value
- name: Remove all labels assigned to node
community.docker.docker_node:
hostname: mynode
labels_state: replace
- name: Remove selected labels from the node
community.docker.docker_node:
hostname: mynode
labels_to_remove:
- key1
- key2
'''
RETURN = '''
node:
description: Information about node after 'update' operation
returned: success
type: dict
'''
import traceback
try:
from docker.errors import DockerException, APIError
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.common import (
DockerBaseClass,
RequestException,
)
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
class TaskParameters(DockerBaseClass):
def __init__(self, client):
super(TaskParameters, self).__init__()
# Spec
self.name = None
self.labels = None
self.labels_state = None
self.labels_to_remove = None
# Node
self.availability = None
self.role = None
for key, value in client.module.params.items():
setattr(self, key, value)
class SwarmNodeManager(DockerBaseClass):
def __init__(self, client, results):
super(SwarmNodeManager, self).__init__()
self.client = client
self.results = results
self.check_mode = self.client.check_mode
self.client.fail_task_if_not_swarm_manager()
self.parameters = TaskParameters(client)
self.node_update()
def node_update(self):
if not (self.client.check_if_swarm_node(node_id=self.parameters.hostname)):
self.client.fail("This node is not part of a swarm.")
return
if self.client.check_if_swarm_node_is_down():
self.client.fail("Can not update the node. The node is down.")
try:
node_info = self.client.inspect_node(node_id=self.parameters.hostname)
except APIError as exc:
self.client.fail("Failed to get node information for %s" % to_native(exc))
changed = False
node_spec = dict(
Availability=self.parameters.availability,
Role=self.parameters.role,
Labels=self.parameters.labels,
)
if self.parameters.role is None:
node_spec['Role'] = node_info['Spec']['Role']
else:
if not node_info['Spec']['Role'] == self.parameters.role:
node_spec['Role'] = self.parameters.role
changed = True
if self.parameters.availability is None:
node_spec['Availability'] = node_info['Spec']['Availability']
else:
if not node_info['Spec']['Availability'] == self.parameters.availability:
node_info['Spec']['Availability'] = self.parameters.availability
changed = True
if self.parameters.labels_state == 'replace':
if self.parameters.labels is None:
node_spec['Labels'] = {}
if node_info['Spec']['Labels']:
changed = True
else:
if (node_info['Spec']['Labels'] or {}) != self.parameters.labels:
node_spec['Labels'] = self.parameters.labels
changed = True
elif self.parameters.labels_state == 'merge':
node_spec['Labels'] = dict(node_info['Spec']['Labels'] or {})
if self.parameters.labels is not None:
for key, value in self.parameters.labels.items():
if node_spec['Labels'].get(key) != value:
node_spec['Labels'][key] = value
changed = True
if self.parameters.labels_to_remove is not None:
for key in self.parameters.labels_to_remove:
if self.parameters.labels is not None:
if not self.parameters.labels.get(key):
if node_spec['Labels'].get(key):
node_spec['Labels'].pop(key)
changed = True
else:
self.client.module.warn(
"Label '%s' listed both in 'labels' and 'labels_to_remove'. "
"Keeping the assigned label value."
% to_native(key))
else:
if node_spec['Labels'].get(key):
node_spec['Labels'].pop(key)
changed = True
if changed is True:
if not self.check_mode:
try:
self.client.update_node(node_id=node_info['ID'], version=node_info['Version']['Index'],
node_spec=node_spec)
except APIError as exc:
self.client.fail("Failed to update node : %s" % to_native(exc))
self.results['node'] = self.client.get_node_inspect(node_id=node_info['ID'])
self.results['changed'] = changed
else:
self.results['node'] = node_info
self.results['changed'] = changed
def main():
argument_spec = dict(
hostname=dict(type='str', required=True),
labels=dict(type='dict'),
labels_state=dict(type='str', default='merge', choices=['merge', 'replace']),
labels_to_remove=dict(type='list', elements='str'),
availability=dict(type='str', choices=['active', 'pause', 'drain']),
role=dict(type='str', choices=['worker', 'manager']),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.4.0',
)
try:
results = dict(
changed=False,
)
SwarmNodeManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,162 @@
#!/usr/bin/python
#
# Copyright (c) 2019 Piotr Wojciechowski <piotr@it-playground.pl>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_node_info
short_description: Retrieves facts about docker swarm node from Swarm Manager
description:
- Retrieves facts about a docker node.
- Essentially returns the output of C(docker node inspect <name>).
- Must be executed on a host running as Swarm Manager, otherwise the module will fail.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- The name of the node to inspect.
- The list of nodes names to inspect.
- If empty then return information of all nodes in Swarm cluster.
- When identifying the node use either the hostname of the node (as registered in Swarm) or node ID.
- If I(self) is C(true) then this parameter is ignored.
type: list
elements: str
self:
description:
- If C(true), queries the node (that is, the docker daemon) the module communicates with.
- If C(true) then I(name) is ignored.
- If C(false) then query depends on I(name) presence and value.
type: bool
default: false
author:
- Piotr Wojciechowski (@WojciechowskiPiotr)
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.4.0"
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get info on all nodes
community.docker.docker_node_info:
register: result
- name: Get info on node
community.docker.docker_node_info:
name: mynode
register: result
- name: Get info on list of nodes
community.docker.docker_node_info:
name:
- mynode1
- mynode2
register: result
- name: Get info on host if it is Swarm Manager
community.docker.docker_node_info:
self: true
register: result
'''
RETURN = '''
nodes:
description:
- Facts representing the current state of the nodes. Matches the C(docker node inspect) output.
- Can contain multiple entries if more than one node provided in I(name), or I(name) is not provided.
- If I(name) contains a list of nodes, the output will provide information on all nodes registered
at the swarm, including nodes that left the swarm but have not been removed from the cluster on swarm
managers and nodes that are unreachable.
returned: always
type: list
elements: dict
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common import (
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
try:
from docker.errors import DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
def get_node_facts(client):
results = []
if client.module.params['self'] is True:
self_node_id = client.get_swarm_node_id()
node_info = client.get_node_inspect(node_id=self_node_id)
results.append(node_info)
return results
if client.module.params['name'] is None:
node_info = client.get_all_nodes_inspect()
return node_info
nodes = client.module.params['name']
if not isinstance(nodes, list):
nodes = [nodes]
for next_node_name in nodes:
next_node_info = client.get_node_inspect(node_id=next_node_name, skip_missing=True)
if next_node_info:
results.append(next_node_info)
return results
def main():
argument_spec = dict(
name=dict(type='list', elements='str'),
self=dict(type='bool', default=False),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.4.0',
)
client.fail_task_if_not_swarm_manager()
try:
nodes = get_node_facts(client)
client.module.exit_json(
changed=False,
nodes=nodes,
)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,392 @@
#!/usr/bin/python
# coding: utf-8
#
# Copyright (c) 2021 Red Hat | Ansible Sakar Mehra<@sakarmehra100@gmail.com | @sakar97>
# Copyright (c) 2019, Vladimir Porshkevich (@porshkevich) <neosonic@mail.ru>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: docker_plugin
short_description: Manage Docker plugins
version_added: 1.3.0
description:
- This module allows to install, delete, enable and disable Docker plugins.
- Performs largely the same function as the C(docker plugin) 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
options:
plugin_name:
description:
- Name of the plugin to operate on.
required: true
type: str
state:
description:
- C(absent) remove the plugin.
- C(present) install the plugin, if it does not already exist.
- C(enable) enable the plugin.
- C(disable) disable the plugin.
default: present
choices:
- absent
- present
- enable
- disable
type: str
alias:
description:
- Local name for plugin.
type: str
version_added: 1.8.0
plugin_options:
description:
- Dictionary of plugin settings.
type: dict
default: {}
force_remove:
description:
- Remove even if the plugin is enabled.
default: False
type: bool
enable_timeout:
description:
- Timeout in seconds.
type: int
default: 0
author:
- Sakar Mehra (@sakar97)
- Vladimir Porshkevich (@porshkevich)
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Install a plugin
community.docker.docker_plugin:
plugin_name: plugin_one
state: present
- name: Remove a plugin
community.docker.docker_plugin:
plugin_name: plugin_one
state: absent
- name: Enable the plugin
community.docker.docker_plugin:
plugin_name: plugin_one
state: enable
- name: Disable the plugin
community.docker.docker_plugin:
plugin_name: plugin_one
state: disable
- name: Install a plugin with options
community.docker.docker_plugin:
plugin_name: weaveworks/net-plugin:latest_release
plugin_options:
IPALLOC_RANGE: "10.32.0.0/12"
WEAVE_PASSWORD: "PASSWORD"
'''
RETURN = '''
plugin:
description:
- Plugin inspection results for the affected plugin.
returned: success
type: dict
sample: {}
actions:
description:
- List of actions performed during task execution.
returned: when I(state!=absent)
type: list
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
)
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
class TaskParameters(DockerBaseClass):
def __init__(self, client):
super(TaskParameters, self).__init__()
self.client = client
self.plugin_name = None
self.alias = None
self.plugin_options = None
self.debug = None
self.force_remove = None
self.enable_timeout = None
for key, value in client.module.params.items():
setattr(self, key, value)
def prepare_options(options):
return ['%s=%s' % (k, v if v is not None else "") for k, v in options.items()] if options else []
def parse_options(options_list):
return dict(x.split('=', 1) for x in options_list) if options_list else {}
class DockerPluginManager(object):
def __init__(self, client):
self.client = client
self.parameters = TaskParameters(client)
self.preferred_name = self.parameters.alias or self.parameters.plugin_name
self.check_mode = self.client.check_mode
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = dict()
self.actions = []
self.changed = False
self.existing_plugin = self.get_existing_plugin()
state = self.parameters.state
if state == 'present':
self.present()
elif state == 'absent':
self.absent()
elif state == 'enable':
self.enable()
elif state == 'disable':
self.disable()
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.diff = self.diff_result
def get_existing_plugin(self):
try:
return self.client.get_json('/plugins/{0}/json', self.preferred_name)
except NotFound:
return None
except APIError as e:
self.client.fail(to_native(e))
def has_different_config(self):
"""
Return the list of differences between the current parameters and the existing plugin.
:return: list of options that differ
"""
differences = DifferenceTracker()
if self.parameters.plugin_options:
settings = self.existing_plugin.get('Settings')
if not settings:
differences.add('plugin_options', parameters=self.parameters.plugin_options, active=settings)
else:
existing_options = parse_options(settings.get('Env'))
for key, value in self.parameters.plugin_options.items():
if ((not existing_options.get(key) and value) or
not value or
value != existing_options[key]):
differences.add('plugin_options.%s' % key,
parameter=value,
active=existing_options.get(key))
return differences
def install_plugin(self):
if not self.existing_plugin:
if not self.check_mode:
try:
# Get privileges
headers = {}
registry, repo_name = auth.resolve_repository_name(self.parameters.plugin_name)
header = auth.get_config_header(self.client, registry)
if header:
headers['X-Registry-Auth'] = header
privileges = self.client.get_json('/plugins/privileges', params={'remote': self.parameters.plugin_name}, headers=headers)
# Pull plugin
params = {
'remote': self.parameters.plugin_name,
}
if self.parameters.alias:
params['name'] = self.parameters.alias
response = self.client._post_json(self.client._url('/plugins/pull'), params=params, headers=headers, data=privileges, stream=True)
self.client._raise_for_status(response)
for data in self.client._stream_helper(response, decode=True):
pass
# Inspect and configure plugin
self.existing_plugin = self.client.get_json('/plugins/{0}/json', self.preferred_name)
if self.parameters.plugin_options:
data = prepare_options(self.parameters.plugin_options)
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Installed plugin %s" % self.preferred_name)
self.changed = True
def remove_plugin(self):
force = self.parameters.force_remove
if self.existing_plugin:
if not self.check_mode:
try:
self.client.delete_call('/plugins/{0}', self.preferred_name, params={'force': force})
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Removed plugin %s" % self.preferred_name)
self.changed = True
def update_plugin(self):
if self.existing_plugin:
differences = self.has_different_config()
if not differences.empty:
if not self.check_mode:
try:
data = prepare_options(self.parameters.plugin_options)
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Updated plugin %s settings" % self.preferred_name)
self.changed = True
else:
self.client.fail("Cannot update the plugin: Plugin does not exist")
def present(self):
differences = DifferenceTracker()
if self.existing_plugin:
differences = self.has_different_config()
self.diff_tracker.add('exists', parameter=True, active=self.existing_plugin is not None)
if self.existing_plugin:
self.update_plugin()
else:
self.install_plugin()
if self.diff or self.check_mode or self.parameters.debug:
self.diff_tracker.merge(differences)
if not self.check_mode and not self.parameters.debug:
self.actions = None
def absent(self):
self.remove_plugin()
def enable(self):
timeout = self.parameters.enable_timeout
if self.existing_plugin:
if not self.existing_plugin.get('Enabled'):
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Enabled plugin %s" % self.preferred_name)
self.changed = True
else:
self.install_plugin()
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Enabled plugin %s" % self.preferred_name)
self.changed = True
def disable(self):
if self.existing_plugin:
if self.existing_plugin.get('Enabled'):
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/disable', self.preferred_name)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append("Disable plugin %s" % self.preferred_name)
self.changed = True
else:
self.client.fail("Plugin not found: Plugin does not exist.")
@property
def result(self):
plugin_data = {}
if self.parameters.state != 'absent':
try:
plugin_data = self.client.get_json('/plugins/{0}/json', self.preferred_name)
except NotFound:
# This can happen in check mode
pass
result = {
'actions': self.actions,
'changed': self.changed,
'diff': self.diff,
'plugin': plugin_data,
}
return dict((k, v) for k, v in result.items() if v is not None)
def main():
argument_spec = dict(
alias=dict(type='str'),
plugin_name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent', 'enable', 'disable']),
plugin_options=dict(type='dict', default={}),
debug=dict(type='bool', default=False),
force_remove=dict(type='bool', default=False),
enable_timeout=dict(type='int', default=0),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
)
try:
cm = DockerPluginManager(client)
client.module.exit_json(**cm.result)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,260 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_prune
short_description: Allows to prune various docker objects
description:
- Allows to run C(docker container prune), C(docker image prune), C(docker network prune)
and C(docker volume prune) via the Docker API.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
containers:
description:
- Whether to prune containers.
type: bool
default: false
containers_filters:
description:
- A dictionary of filter values used for selecting containers to delete.
- "For example, C(until: 24h)."
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/container_prune/#filtering)
for more information on possible filters.
type: dict
images:
description:
- Whether to prune images.
type: bool
default: false
images_filters:
description:
- A dictionary of filter values used for selecting images to delete.
- "For example, C(dangling: true)."
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/image_prune/#filtering)
for more information on possible filters.
type: dict
networks:
description:
- Whether to prune networks.
type: bool
default: false
networks_filters:
description:
- A dictionary of filter values used for selecting networks to delete.
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/network_prune/#filtering)
for more information on possible filters.
type: dict
volumes:
description:
- Whether to prune volumes.
type: bool
default: false
volumes_filters:
description:
- A dictionary of filter values used for selecting volumes to delete.
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/volume_prune/#filtering)
for more information on possible filters.
type: dict
builder_cache:
description:
- Whether to prune the builder cache.
type: bool
default: false
author:
- "Felix Fontein (@felixfontein)"
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Prune containers older than 24h
community.docker.docker_prune:
containers: true
containers_filters:
# only consider containers created more than 24 hours ago
until: 24h
- name: Prune everything
community.docker.docker_prune:
containers: true
images: true
networks: true
volumes: true
builder_cache: true
- name: Prune everything (including non-dangling images)
community.docker.docker_prune:
containers: true
images: true
images_filters:
dangling: false
networks: true
volumes: true
builder_cache: true
'''
RETURN = '''
# containers
containers:
description:
- List of IDs of deleted containers.
returned: I(containers) is C(true)
type: list
elements: str
sample: []
containers_space_reclaimed:
description:
- Amount of reclaimed disk space from container pruning in bytes.
returned: I(containers) is C(true)
type: int
sample: 0
# images
images:
description:
- List of IDs of deleted images.
returned: I(images) is C(true)
type: list
elements: str
sample: []
images_space_reclaimed:
description:
- Amount of reclaimed disk space from image pruning in bytes.
returned: I(images) is C(true)
type: int
sample: 0
# networks
networks:
description:
- List of IDs of deleted networks.
returned: I(networks) is C(true)
type: list
elements: str
sample: []
# volumes
volumes:
description:
- List of IDs of deleted volumes.
returned: I(volumes) is C(true)
type: list
elements: str
sample: []
volumes_space_reclaimed:
description:
- Amount of reclaimed disk space from volumes pruning in bytes.
returned: I(volumes) is C(true)
type: int
sample: 0
# builder_cache
builder_cache_space_reclaimed:
description:
- Amount of reclaimed disk space from builder cache pruning in bytes.
returned: I(builder_cache) is C(true)
type: int
sample: 0
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import clean_dict_booleans_for_docker_api
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
def main():
argument_spec = dict(
containers=dict(type='bool', default=False),
containers_filters=dict(type='dict'),
images=dict(type='bool', default=False),
images_filters=dict(type='dict'),
networks=dict(type='bool', default=False),
networks_filters=dict(type='dict'),
volumes=dict(type='bool', default=False),
volumes_filters=dict(type='dict'),
builder_cache=dict(type='bool', default=False),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
# supports_check_mode=True,
)
try:
result = dict()
if client.module.params['containers']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('containers_filters'))
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/containers/prune', params=params)
result['containers'] = res.get('ContainersDeleted') or []
result['containers_space_reclaimed'] = res['SpaceReclaimed']
if client.module.params['images']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('images_filters'))
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/images/prune', params=params)
result['images'] = res.get('ImagesDeleted') or []
result['images_space_reclaimed'] = res['SpaceReclaimed']
if client.module.params['networks']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('networks_filters'))
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/networks/prune', params=params)
result['networks'] = res.get('NetworksDeleted') or []
if client.module.params['volumes']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('volumes_filters'))
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/volumes/prune', params=params)
result['volumes'] = res.get('VolumesDeleted') or []
result['volumes_space_reclaimed'] = res['SpaceReclaimed']
if client.module.params['builder_cache']:
res = client.post_to_json('/build/prune')
result['builder_cache_space_reclaimed'] = res['SpaceReclaimed']
client.module.exit_json(**result)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,406 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_secret
short_description: Manage docker secrets.
description:
- Create and remove Docker secrets in a Swarm environment. Similar to C(docker secret create) and C(docker secret rm).
- Adds to the metadata of new secrets C(ansible_key), an encrypted hash representation of the data, which is then used
in future runs to test if a secret has changed. If C(ansible_key) is not present, then a secret will not be updated
unless the I(force) option is set.
- Updates to secrets are performed by removing the secret and creating it again.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_2_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
data:
description:
- The value of the secret.
- Mutually exclusive with I(data_src). One of I(data) and I(data_src) is required if I(state=present).
type: str
data_is_b64:
description:
- If set to C(true), the data is assumed to be Base64 encoded and will be
decoded before being used.
- To use binary I(data), it is better to keep it Base64 encoded and let it
be decoded by this option.
type: bool
default: false
data_src:
description:
- The file on the target from which to read the secret.
- Mutually exclusive with I(data). One of I(data) and I(data_src) is required if I(state=present).
type: path
version_added: 1.10.0
labels:
description:
- "A map of key:value meta data, where both key and value are expected to be strings."
- If new meta data is provided, or existing meta data is modified, the secret will be updated by removing it and creating it again.
type: dict
force:
description:
- Use with state C(present) to always remove and recreate an existing secret.
- If C(true), an existing secret will be replaced, even if it has not changed.
type: bool
default: false
rolling_versions:
description:
- If set to C(true), secrets are created with an increasing version number appended to their name.
- Adds a label containing the version number to the managed secrets with the name C(ansible_version).
type: bool
default: false
version_added: 2.2.0
versions_to_keep:
description:
- When using I(rolling_versions), the number of old versions of the secret to keep.
- Extraneous old secrets are deleted after the new one is created.
- Set to C(-1) to keep everything or to C(0) or C(1) to keep only the current one.
type: int
default: 5
version_added: 2.2.0
name:
description:
- The name of the secret.
type: str
required: true
state:
description:
- Set to C(present), if the secret should exist, and C(absent), if it should not.
type: str
default: present
choices:
- absent
- present
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.1.0"
- "Docker API >= 1.25"
author:
- Chris Houseknecht (@chouseknecht)
'''
EXAMPLES = '''
- name: Create secret foo (from a file on the control machine)
community.docker.docker_secret:
name: foo
# If the file is JSON or binary, Ansible might modify it (because
# it is first decoded and later re-encoded). Base64-encoding the
# file directly after reading it prevents this to happen.
data: "{{ lookup('file', '/path/to/secret/file') | b64encode }}"
data_is_b64: true
state: present
- name: Create secret foo (from a file on the target machine)
community.docker.docker_secret:
name: foo
data_src: /path/to/secret/file
state: present
- name: Change the secret data
community.docker.docker_secret:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
state: present
- name: Add a new label
community.docker.docker_secret:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
# Adding a new label will cause a remove/create of the secret
two: '2'
state: present
- name: No change
community.docker.docker_secret:
name: foo
data: Goodnight everyone!
labels:
bar: baz
one: '1'
# Even though 'two' is missing, there is no change to the existing secret
state: present
- name: Update an existing label
community.docker.docker_secret:
name: foo
data: Goodnight everyone!
labels:
bar: monkey # Changing a label will cause a remove/create of the secret
one: '1'
state: present
- name: Force the removal/creation of the secret
community.docker.docker_secret:
name: foo
data: Goodnight everyone!
force: true
state: present
- name: Remove secret foo
community.docker.docker_secret:
name: foo
state: absent
'''
RETURN = '''
secret_id:
description:
- The ID assigned by Docker to the secret object.
returned: success and I(state) is C(present)
type: str
sample: 'hzehrmyjigmcp2gb6nlhmjqcv'
secret_name:
description:
- The name of the created secret object.
returned: success and I(state) is C(present)
type: str
sample: 'awesome_secret'
version_added: 2.2.0
'''
import base64
import hashlib
import traceback
try:
from docker.errors import DockerException, APIError
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
compare_generic,
)
from ansible.module_utils.common.text.converters import to_native, to_bytes
class SecretManager(DockerBaseClass):
def __init__(self, client, results):
super(SecretManager, self).__init__()
self.client = client
self.results = results
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters.get('name')
self.state = parameters.get('state')
self.data = parameters.get('data')
if self.data is not None:
if parameters.get('data_is_b64'):
self.data = base64.b64decode(self.data)
else:
self.data = to_bytes(self.data)
data_src = parameters.get('data_src')
if data_src is not None:
try:
with open(data_src, 'rb') as f:
self.data = f.read()
except Exception as exc:
self.client.fail('Error while reading {src}: {error}'.format(src=data_src, error=to_native(exc)))
self.labels = parameters.get('labels')
self.force = parameters.get('force')
self.rolling_versions = parameters.get('rolling_versions')
self.versions_to_keep = parameters.get('versions_to_keep')
if self.rolling_versions:
self.version = 0
self.data_key = None
self.secrets = []
def __call__(self):
self.get_secret()
if self.state == 'present':
self.data_key = hashlib.sha224(self.data).hexdigest()
self.present()
self.remove_old_versions()
elif self.state == 'absent':
self.absent()
def get_version(self, secret):
try:
return int(secret.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
except ValueError:
return 0
def remove_old_versions(self):
if not self.rolling_versions or self.versions_to_keep < 0:
return
if not self.check_mode:
while len(self.secrets) > max(self.versions_to_keep, 1):
self.remove_secret(self.secrets.pop(0))
def get_secret(self):
''' Find an existing secret. '''
try:
secrets = self.client.secrets(filters={'name': self.name})
except APIError as exc:
self.client.fail("Error accessing secret %s: %s" % (self.name, to_native(exc)))
if self.rolling_versions:
self.secrets = [
secret
for secret in secrets
if secret['Spec']['Name'].startswith('{name}_v'.format(name=self.name))
]
self.secrets.sort(key=self.get_version)
else:
self.secrets = [
secret for secret in secrets if secret['Spec']['Name'] == self.name
]
def create_secret(self):
''' Create a new secret '''
secret_id = None
# We can't see the data after creation, so adding a label we can use for idempotency check
labels = {
'ansible_key': self.data_key
}
if self.rolling_versions:
self.version += 1
labels['ansible_version'] = str(self.version)
self.name = '{name}_v{version}'.format(name=self.name, version=self.version)
if self.labels:
labels.update(self.labels)
try:
if not self.check_mode:
secret_id = self.client.create_secret(self.name, self.data, labels=labels)
self.secrets += self.client.secrets(filters={'id': secret_id})
except APIError as exc:
self.client.fail("Error creating secret: %s" % to_native(exc))
if isinstance(secret_id, dict):
secret_id = secret_id['ID']
return secret_id
def remove_secret(self, secret):
try:
if not self.check_mode:
self.client.remove_secret(secret['ID'])
except APIError as exc:
self.client.fail("Error removing secret %s: %s" % (secret['Spec']['Name'], to_native(exc)))
def present(self):
''' Handles state == 'present', creating or updating the secret '''
if self.secrets:
secret = self.secrets[-1]
self.results['secret_id'] = secret['ID']
self.results['secret_name'] = secret['Spec']['Name']
data_changed = False
attrs = secret.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key:
data_changed = True
else:
if not self.force:
self.client.module.warn("'ansible_key' label not found. Secret will not be changed unless the force parameter is set to 'true'")
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
if self.rolling_versions:
self.version = self.get_version(secret)
if data_changed or labels_changed or self.force:
# if something changed or force, delete and re-create the secret
if not self.rolling_versions:
self.absent()
secret_id = self.create_secret()
self.results['changed'] = True
self.results['secret_id'] = secret_id
self.results['secret_name'] = self.name
else:
self.results['changed'] = True
self.results['secret_id'] = self.create_secret()
self.results['secret_name'] = self.name
def absent(self):
''' Handles state == 'absent', removing the secret '''
if self.secrets:
for secret in self.secrets:
self.remove_secret(secret)
self.results['changed'] = True
def main():
argument_spec = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
data=dict(type='str', no_log=True),
data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'),
labels=dict(type='dict'),
force=dict(type='bool', default=False),
rolling_versions=dict(type='bool', default=False),
versions_to_keep=dict(type='int', default=5),
)
required_if = [
('state', 'present', ['data', 'data_src'], True),
]
mutually_exclusive = [
('data', 'data_src'),
]
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
min_docker_version='2.1.0',
)
try:
results = dict(
changed=False,
secret_id='',
secret_name=''
)
SecretManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,309 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Dario Zanzico (git@dariozanzico.com)
# 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 (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_stack
author: "Dario Zanzico (@dariko)"
short_description: docker stack module
description:
- Manage docker stacks using the C(docker stack) command
on the target node (see examples).
extends_documentation_fragment:
- community.docker.attributes
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
name:
description:
- Stack name
type: str
required: true
state:
description:
- Service state.
type: str
default: "present"
choices:
- present
- absent
compose:
description:
- List of compose definitions. Any element may be a string
referring to the path of the compose file on the target host
or the YAML contents of a compose file nested as dictionary.
type: list
elements: raw
default: []
prune:
description:
- If true will add the C(--prune) option to the C(docker stack deploy) command.
This will have docker remove the services not present in the
current stack definition.
type: bool
default: false
with_registry_auth:
description:
- If true will add the C(--with-registry-auth) option to the C(docker stack deploy) command.
This will have docker send registry authentication details to Swarm agents.
type: bool
default: false
resolve_image:
description:
- If set will add the C(--resolve-image) option to the C(docker stack deploy) command.
This will have docker query the registry to resolve image digest and
supported platforms. If not set, docker use "always" by default.
type: str
choices: ["always", "changed", "never"]
absent_retries:
description:
- If C(>0) and I(state) is C(absent) the module will retry up to
I(absent_retries) times to delete the stack until all the
resources have been effectively deleted.
If the last try still reports the stack as not completely
removed the module will fail.
type: int
default: 0
absent_retries_interval:
description:
- Interval in seconds between consecutive I(absent_retries).
type: int
default: 1
requirements:
- jsondiff
- pyyaml
'''
RETURN = '''
stack_spec_diff:
description: |
dictionary containing the differences between the 'Spec' field
of the stack services before and after applying the new stack
definition.
sample: >
"stack_spec_diff":
{'test_stack_test_service': {u'TaskTemplate': {u'ContainerSpec': {delete: [u'Env']}}}}
returned: on change
type: dict
'''
EXAMPLES = '''
- name: Deploy stack from a compose file
community.docker.docker_stack:
state: present
name: mystack
compose:
- /opt/docker-compose.yml
- name: Deploy stack from base compose file and override the web service
community.docker.docker_stack:
state: present
name: mystack
compose:
- /opt/docker-compose.yml
- version: '3'
services:
web:
image: nginx:latest
environment:
ENVVAR: envvar
- name: Remove stack
community.docker.docker_stack:
name: mystack
state: absent
'''
import json
import tempfile
from ansible.module_utils.six import string_types
from time import sleep
try:
from jsondiff import diff as json_diff
HAS_JSONDIFF = True
except ImportError:
HAS_JSONDIFF = False
try:
from yaml import dump as yaml_dump
HAS_YAML = True
except ImportError:
HAS_YAML = False
from ansible.module_utils.basic import AnsibleModule, os
def docker_stack_services(module, stack_name):
docker_bin = module.get_bin_path('docker', required=True)
rc, out, err = module.run_command([docker_bin,
"stack",
"services",
stack_name,
"--format",
"{{.Name}}"])
if err == "Nothing found in stack: %s\n" % stack_name:
return []
return out.strip().split('\n')
def docker_service_inspect(module, service_name):
docker_bin = module.get_bin_path('docker', required=True)
rc, out, err = module.run_command([docker_bin,
"service",
"inspect",
service_name])
if rc != 0:
return None
else:
ret = json.loads(out)[0]['Spec']
return ret
def docker_stack_deploy(module, stack_name, compose_files):
docker_bin = module.get_bin_path('docker', required=True)
command = [docker_bin, "stack", "deploy"]
if module.params["prune"]:
command += ["--prune"]
if module.params["with_registry_auth"]:
command += ["--with-registry-auth"]
if module.params["resolve_image"]:
command += ["--resolve-image",
module.params["resolve_image"]]
for compose_file in compose_files:
command += ["--compose-file",
compose_file]
command += [stack_name]
return module.run_command(command)
def docker_stack_inspect(module, stack_name):
ret = {}
for service_name in docker_stack_services(module, stack_name):
ret[service_name] = docker_service_inspect(module, service_name)
return ret
def docker_stack_rm(module, stack_name, retries, interval):
docker_bin = module.get_bin_path('docker', required=True)
command = [docker_bin, "stack", "rm", stack_name]
rc, out, err = module.run_command(command)
while err != "Nothing found in stack: %s\n" % stack_name and retries > 0:
sleep(interval)
retries = retries - 1
rc, out, err = module.run_command(command)
return rc, out, err
def main():
module = AnsibleModule(
argument_spec={
'name': dict(type='str', required=True),
'compose': dict(type='list', elements='raw', default=[]),
'prune': dict(type='bool', default=False),
'with_registry_auth': dict(type='bool', default=False),
'resolve_image': dict(type='str', choices=['always', 'changed', 'never']),
'state': dict(type='str', default='present', choices=['present', 'absent']),
'absent_retries': dict(type='int', default=0),
'absent_retries_interval': dict(type='int', default=1)
},
supports_check_mode=False
)
if not HAS_JSONDIFF:
return module.fail_json(msg="jsondiff is not installed, try 'pip install jsondiff'")
if not HAS_YAML:
return module.fail_json(msg="yaml is not installed, try 'pip install pyyaml'")
state = module.params['state']
compose = module.params['compose']
name = module.params['name']
absent_retries = module.params['absent_retries']
absent_retries_interval = module.params['absent_retries_interval']
if state == 'present':
if not compose:
module.fail_json(msg=("compose parameter must be a list "
"containing at least one element"))
compose_files = []
for i, compose_def in enumerate(compose):
if isinstance(compose_def, dict):
compose_file_fd, compose_file = tempfile.mkstemp()
module.add_cleanup_file(compose_file)
with os.fdopen(compose_file_fd, 'w') as stack_file:
compose_files.append(compose_file)
stack_file.write(yaml_dump(compose_def))
elif isinstance(compose_def, string_types):
compose_files.append(compose_def)
else:
module.fail_json(msg="compose element '%s' must be a string or a dictionary" % compose_def)
before_stack_services = docker_stack_inspect(module, name)
rc, out, err = docker_stack_deploy(module, name, compose_files)
after_stack_services = docker_stack_inspect(module, name)
if rc != 0:
module.fail_json(msg="docker stack up deploy command failed",
rc=rc,
stdout=out, stderr=err)
before_after_differences = json_diff(before_stack_services,
after_stack_services)
for k in before_after_differences.keys():
if isinstance(before_after_differences[k], dict):
before_after_differences[k].pop('UpdatedAt', None)
before_after_differences[k].pop('Version', None)
if not list(before_after_differences[k].keys()):
before_after_differences.pop(k)
if not before_after_differences:
module.exit_json(
changed=False,
rc=rc,
stdout=out,
stderr=err)
else:
module.exit_json(
changed=True,
rc=rc,
stdout=out,
stderr=err,
stack_spec_diff=json_diff(before_stack_services,
after_stack_services,
dump=True))
else:
if docker_stack_services(module, name):
rc, out, err = docker_stack_rm(module, name, absent_retries, absent_retries_interval)
if rc != 0:
module.fail_json(msg="'docker stack down' command failed",
rc=rc,
stdout=out, stderr=err)
else:
module.exit_json(changed=True,
msg=out, rc=rc,
stdout=out, stderr=err)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,88 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2020 Jose Angel Munoz (@imjoseangel)
# 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 (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_stack_info
author: "Jose Angel Munoz (@imjoseangel)"
short_description: Return information on a docker stack
description:
- Retrieve information on docker stacks using the C(docker stack) command
on the target node (see examples).
extends_documentation_fragment:
- community.docker.attributes
- community.docker.attributes.info_module
'''
RETURN = '''
results:
description: |
List of dictionaries containing the list of stacks or tasks associated
to a stack name.
sample:
- {"name":"grafana","namespace":"default","orchestrator":"Kubernetes","services":"2"}
returned: always
type: list
elements: dict
'''
EXAMPLES = '''
- name: Shows stack info
community.docker.docker_stack_info:
register: result
- name: Show results
ansible.builtin.debug:
var: result.results
'''
import json
from ansible.module_utils.basic import AnsibleModule
def docker_stack_list(module):
docker_bin = module.get_bin_path('docker', required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ls", "--format={{json .}}"])
return rc, out.strip(), err.strip()
def main():
module = AnsibleModule(
argument_spec={
},
supports_check_mode=True
)
rc, out, err = docker_stack_list(module)
if rc != 0:
module.fail_json(msg="Error running docker stack. {0}".format(err),
rc=rc, stdout=out, stderr=err)
else:
if out:
ret = list(
json.loads(outitem)
for outitem in out.splitlines())
else:
ret = []
module.exit_json(changed=False,
rc=rc,
stdout=out,
stderr=err,
results=ret)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,98 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2020 Jose Angel Munoz (@imjoseangel)
# 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 (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_stack_task_info
author: "Jose Angel Munoz (@imjoseangel)"
short_description: Return information of the tasks on a docker stack
description:
- Retrieve information on docker stacks tasks using the C(docker stack) command
on the target node (see examples).
extends_documentation_fragment:
- community.docker.attributes
- community.docker.attributes.info_module
options:
name:
description:
- Stack name.
type: str
required: true
'''
RETURN = '''
results:
description: |
List of dictionaries containing the list of tasks associated
to a stack name.
sample: >
[{"CurrentState":"Running","DesiredState":"Running","Error":"","ID":"7wqv6m02ugkw","Image":"busybox","Name":"test_stack.1","Node":"swarm","Ports":""}]
returned: always
type: list
elements: dict
'''
EXAMPLES = '''
- name: Shows stack info
community.docker.docker_stack_task_info:
name: test_stack
register: result
- name: Show results
ansible.builtin.debug:
var: result.results
'''
import json
from ansible.module_utils.basic import AnsibleModule
def docker_stack_task(module, stack_name):
docker_bin = module.get_bin_path('docker', required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"])
return rc, out.strip(), err.strip()
def main():
module = AnsibleModule(
argument_spec={
'name': dict(type='str', required=True)
},
supports_check_mode=True
)
name = module.params['name']
rc, out, err = docker_stack_task(module, name)
if rc != 0:
module.fail_json(msg="Error running docker stack. {0}".format(err),
rc=rc, stdout=out, stderr=err)
else:
if out:
ret = list(
json.loads(outitem)
for outitem in out.splitlines())
else:
ret = []
module.exit_json(changed=False,
rc=rc,
stdout=out,
stderr=err,
results=ret)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,725 @@
#!/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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_swarm
short_description: Manage Swarm cluster
description:
- Create a new Swarm cluster.
- Add/Remove nodes or managers to an existing cluster.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
advertise_addr:
description:
- Externally reachable address advertised to other nodes.
- This can either be an address/port combination
in the form C(192.168.1.1:4567), or an interface followed by a
port number, like C(eth0:4567).
- If the port number is omitted,
the port number from the listen address is used.
- If I(advertise_addr) is not specified, it will be automatically
detected when possible.
- Only used when swarm is initialised or joined. Because of this it's not
considered for idempotency checking.
type: str
default_addr_pool:
description:
- Default address pool in CIDR format.
- Only used when swarm is initialised. Because of this it's not considered
for idempotency checking.
- Requires API version >= 1.39.
type: list
elements: str
subnet_size:
description:
- Default address pool subnet mask length.
- Only used when swarm is initialised. Because of this it's not considered
for idempotency checking.
- Requires API version >= 1.39.
type: int
listen_addr:
description:
- Listen address used for inter-manager communication.
- This can either be an address/port combination in the form
C(192.168.1.1:4567), or an interface followed by a port number,
like C(eth0:4567).
- If the port number is omitted, the default swarm listening port
is used.
- Only used when swarm is initialised or joined. Because of this it's not
considered for idempotency checking.
type: str
default: 0.0.0.0:2377
force:
description:
- Use with state C(present) to force creating a new Swarm, even if already part of one.
- Use with state C(absent) to Leave the swarm even if this node is a manager.
type: bool
default: false
state:
description:
- Set to C(present), to create/update a new cluster.
- Set to C(join), to join an existing cluster.
- Set to C(absent), to leave an existing cluster.
- Set to C(remove), to remove an absent node from the cluster.
Note that removing requires Docker SDK for Python >= 2.4.0.
type: str
default: present
choices:
- present
- join
- absent
- remove
node_id:
description:
- Swarm id of the node to remove.
- Used with I(state=remove).
type: str
join_token:
description:
- Swarm token used to join a swarm cluster.
- Used with I(state=join).
- If this value is specified, the corresponding value in the return values will be censored by Ansible.
This is a side-effect of this value not being logged.
type: str
remote_addrs:
description:
- Remote address of one or more manager nodes of an existing Swarm to connect to.
- Used with I(state=join).
type: list
elements: str
task_history_retention_limit:
description:
- Maximum number of tasks history stored.
- Docker default value is C(5).
type: int
snapshot_interval:
description:
- Number of logs entries between snapshot.
- Docker default value is C(10000).
type: int
keep_old_snapshots:
description:
- Number of snapshots to keep beyond the current snapshot.
- Docker default value is C(0).
type: int
log_entries_for_slow_followers:
description:
- Number of log entries to keep around to sync up slow followers after a snapshot is created.
type: int
heartbeat_tick:
description:
- Amount of ticks (in seconds) between each heartbeat.
- Docker default value is C(1s).
type: int
election_tick:
description:
- Amount of ticks (in seconds) needed without a leader to trigger a new election.
- Docker default value is C(10s).
type: int
dispatcher_heartbeat_period:
description:
- The delay for an agent to send a heartbeat to the dispatcher.
- Docker default value is C(5s).
type: int
node_cert_expiry:
description:
- Automatic expiry for nodes certificates.
- Docker default value is C(3months).
type: int
name:
description:
- The name of the swarm.
type: str
labels:
description:
- User-defined key/value metadata.
- Label operations in this module apply to the docker swarm cluster.
Use M(community.docker.docker_node) module to add/modify/remove swarm node labels.
- Requires API version >= 1.32.
type: dict
signing_ca_cert:
description:
- The desired signing CA certificate for all swarm node TLS leaf certificates, in PEM format.
- This must not be a path to a certificate, but the contents of the certificate.
- Requires API version >= 1.30.
type: str
signing_ca_key:
description:
- The desired signing CA key for all swarm node TLS leaf certificates, in PEM format.
- This must not be a path to a key, but the contents of the key.
- Requires API version >= 1.30.
type: str
ca_force_rotate:
description:
- An integer whose purpose is to force swarm to generate a new signing CA certificate and key,
if none have been specified.
- Docker default value is C(0).
- Requires API version >= 1.30.
type: int
autolock_managers:
description:
- If set, generate a key and use it to lock data stored on the managers.
- Docker default value is C(false).
- M(community.docker.docker_swarm_info) can be used to retrieve the unlock key.
type: bool
rotate_worker_token:
description: Rotate the worker join token.
type: bool
default: false
rotate_manager_token:
description: Rotate the manager join token.
type: bool
default: false
data_path_addr:
description:
- Address or interface to use for data path traffic.
- This can either be an address in the form C(192.168.1.1), or an interface,
like C(eth0).
- Only used when swarm is initialised or joined. Because of this it is not
considered for idempotency checking.
type: str
version_added: 2.5.0
data_path_port:
description:
- Port to use for data path traffic.
- This needs to be a port number like C(9789).
- Only used when swarm is initialised. Because of this it is not
considered for idempotency checking.
type: int
version_added: 3.1.0
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.10.0"
- Docker API >= 1.25
author:
- Thierry Bouvet (@tbouvet)
- Piotr Wojciechowski (@WojciechowskiPiotr)
'''
EXAMPLES = '''
- name: Init a new swarm with default parameters
community.docker.docker_swarm:
state: present
- name: Update swarm configuration
community.docker.docker_swarm:
state: present
election_tick: 5
- name: Add nodes
community.docker.docker_swarm:
state: join
advertise_addr: 192.168.1.2
join_token: SWMTKN-1--xxxxx
remote_addrs: [ '192.168.1.1:2377' ]
- name: Leave swarm for a node
community.docker.docker_swarm:
state: absent
- name: Remove a swarm manager
community.docker.docker_swarm:
state: absent
force: true
- name: Remove node from swarm
community.docker.docker_swarm:
state: remove
node_id: mynode
- name: Init a new swarm with different data path interface
community.docker.docker_swarm:
state: present
advertise_addr: eth0
data_path_addr: ens10
- name: Init a new swarm with a different data path port
community.docker.docker_swarm:
state: present
data_path_port: 9789
'''
RETURN = '''
swarm_facts:
description: Informations about swarm.
returned: success
type: dict
contains:
JoinTokens:
description: Tokens to connect to the Swarm.
returned: success
type: dict
contains:
Worker:
description:
- Token to join the cluster as a new *worker* node.
- "B(Note:) if this value has been specified as I(join_token), the value here will not
be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). If you pass I(join_token),
make sure your playbook/role does not depend on this return value!"
returned: success
type: str
example: SWMTKN-1--xxxxx
Manager:
description:
- Token to join the cluster as a new *manager* node.
- "B(Note:) if this value has been specified as I(join_token), the value here will not
be the token, but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER). If you pass I(join_token),
make sure your playbook/role does not depend on this return value!"
returned: success
type: str
example: SWMTKN-1--xxxxx
UnlockKey:
description: The swarm unlock-key if I(autolock_managers) is C(true).
returned: on success if I(autolock_managers) is C(true)
and swarm is initialised, or if I(autolock_managers) has changed.
type: str
example: SWMKEY-1-xxx
actions:
description: Provides the actions done on the swarm.
returned: when action failed.
type: list
elements: str
example: ['This cluster is already a swarm cluster']
'''
import json
import traceback
try:
from docker.errors import DockerException, APIError
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.common import (
DockerBaseClass,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DifferenceTracker,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible.module_utils.common.text.converters import to_native
class TaskParameters(DockerBaseClass):
def __init__(self):
super(TaskParameters, self).__init__()
self.advertise_addr = None
self.listen_addr = None
self.remote_addrs = None
self.join_token = None
self.data_path_addr = None
self.data_path_port = None
# Spec
self.snapshot_interval = None
self.task_history_retention_limit = None
self.keep_old_snapshots = None
self.log_entries_for_slow_followers = None
self.heartbeat_tick = None
self.election_tick = None
self.dispatcher_heartbeat_period = None
self.node_cert_expiry = None
self.name = None
self.labels = None
self.log_driver = None
self.signing_ca_cert = None
self.signing_ca_key = None
self.ca_force_rotate = None
self.autolock_managers = None
self.rotate_worker_token = None
self.rotate_manager_token = None
self.default_addr_pool = None
self.subnet_size = None
@staticmethod
def from_ansible_params(client):
result = TaskParameters()
for key, value in client.module.params.items():
if key in result.__dict__:
setattr(result, key, value)
result.update_parameters(client)
return result
def update_from_swarm_info(self, swarm_info):
spec = swarm_info['Spec']
ca_config = spec.get('CAConfig') or dict()
if self.node_cert_expiry is None:
self.node_cert_expiry = ca_config.get('NodeCertExpiry')
if self.ca_force_rotate is None:
self.ca_force_rotate = ca_config.get('ForceRotate')
dispatcher = spec.get('Dispatcher') or dict()
if self.dispatcher_heartbeat_period is None:
self.dispatcher_heartbeat_period = dispatcher.get('HeartbeatPeriod')
raft = spec.get('Raft') or dict()
if self.snapshot_interval is None:
self.snapshot_interval = raft.get('SnapshotInterval')
if self.keep_old_snapshots is None:
self.keep_old_snapshots = raft.get('KeepOldSnapshots')
if self.heartbeat_tick is None:
self.heartbeat_tick = raft.get('HeartbeatTick')
if self.log_entries_for_slow_followers is None:
self.log_entries_for_slow_followers = raft.get('LogEntriesForSlowFollowers')
if self.election_tick is None:
self.election_tick = raft.get('ElectionTick')
orchestration = spec.get('Orchestration') or dict()
if self.task_history_retention_limit is None:
self.task_history_retention_limit = orchestration.get('TaskHistoryRetentionLimit')
encryption_config = spec.get('EncryptionConfig') or dict()
if self.autolock_managers is None:
self.autolock_managers = encryption_config.get('AutoLockManagers')
if self.name is None:
self.name = spec['Name']
if self.labels is None:
self.labels = spec.get('Labels') or {}
if 'LogDriver' in spec['TaskDefaults']:
self.log_driver = spec['TaskDefaults']['LogDriver']
def update_parameters(self, client):
assign = dict(
snapshot_interval='snapshot_interval',
task_history_retention_limit='task_history_retention_limit',
keep_old_snapshots='keep_old_snapshots',
log_entries_for_slow_followers='log_entries_for_slow_followers',
heartbeat_tick='heartbeat_tick',
election_tick='election_tick',
dispatcher_heartbeat_period='dispatcher_heartbeat_period',
node_cert_expiry='node_cert_expiry',
name='name',
labels='labels',
signing_ca_cert='signing_ca_cert',
signing_ca_key='signing_ca_key',
ca_force_rotate='ca_force_rotate',
autolock_managers='autolock_managers',
log_driver='log_driver',
)
params = dict()
for dest, source in assign.items():
if not client.option_minimal_versions[source]['supported']:
continue
value = getattr(self, source)
if value is not None:
params[dest] = value
self.spec = client.create_swarm_spec(**params)
def compare_to_active(self, other, client, differences):
for k in self.__dict__:
if k in ('advertise_addr', 'listen_addr', 'remote_addrs', 'join_token',
'rotate_worker_token', 'rotate_manager_token', 'spec',
'default_addr_pool', 'subnet_size', 'data_path_addr',
'data_path_port'):
continue
if not client.option_minimal_versions[k]['supported']:
continue
value = getattr(self, k)
if value is None:
continue
other_value = getattr(other, k)
if value != other_value:
differences.add(k, parameter=value, active=other_value)
if self.rotate_worker_token:
differences.add('rotate_worker_token', parameter=True, active=False)
if self.rotate_manager_token:
differences.add('rotate_manager_token', parameter=True, active=False)
return differences
class SwarmManager(DockerBaseClass):
def __init__(self, client, results):
super(SwarmManager, self).__init__()
self.client = client
self.results = results
self.check_mode = self.client.check_mode
self.swarm_info = {}
self.state = client.module.params['state']
self.force = client.module.params['force']
self.node_id = client.module.params['node_id']
self.differences = DifferenceTracker()
self.parameters = TaskParameters.from_ansible_params(client)
self.created = False
def __call__(self):
choice_map = {
"present": self.init_swarm,
"join": self.join,
"absent": self.leave,
"remove": self.remove,
}
choice_map.get(self.state)()
if self.client.module._diff or self.parameters.debug:
diff = dict()
diff['before'], diff['after'] = self.differences.get_before_after()
self.results['diff'] = diff
def inspect_swarm(self):
try:
data = self.client.inspect_swarm()
json_str = json.dumps(data, ensure_ascii=False)
self.swarm_info = json.loads(json_str)
self.results['changed'] = False
self.results['swarm_facts'] = self.swarm_info
unlock_key = self.get_unlock_key()
self.swarm_info.update(unlock_key)
except APIError:
return
def get_unlock_key(self):
default = {'UnlockKey': None}
if not self.has_swarm_lock_changed():
return default
try:
return self.client.get_unlock_key() or default
except APIError:
return default
def has_swarm_lock_changed(self):
return self.parameters.autolock_managers and (
self.created or self.differences.has_difference_for('autolock_managers')
)
def init_swarm(self):
if not self.force and self.client.check_if_swarm_manager():
self.__update_swarm()
return
if not self.check_mode:
init_arguments = {
'advertise_addr': self.parameters.advertise_addr,
'listen_addr': self.parameters.listen_addr,
'data_path_addr': self.parameters.data_path_addr,
'force_new_cluster': self.force,
'swarm_spec': self.parameters.spec,
}
if self.parameters.default_addr_pool is not None:
init_arguments['default_addr_pool'] = self.parameters.default_addr_pool
if self.parameters.subnet_size is not None:
init_arguments['subnet_size'] = self.parameters.subnet_size
if self.parameters.data_path_port is not None:
init_arguments['data_path_port'] = self.parameters.data_path_port
try:
self.client.init_swarm(**init_arguments)
except APIError as exc:
self.client.fail("Can not create a new Swarm Cluster: %s" % to_native(exc))
if not self.client.check_if_swarm_manager():
if not self.check_mode:
self.client.fail("Swarm not created or other error!")
self.created = True
self.inspect_swarm()
self.results['actions'].append("New Swarm cluster created: %s" % (self.swarm_info.get('ID')))
self.differences.add('state', parameter='present', active='absent')
self.results['changed'] = True
self.results['swarm_facts'] = {
'JoinTokens': self.swarm_info.get('JoinTokens'),
'UnlockKey': self.swarm_info.get('UnlockKey')
}
def __update_swarm(self):
try:
self.inspect_swarm()
version = self.swarm_info['Version']['Index']
self.parameters.update_from_swarm_info(self.swarm_info)
old_parameters = TaskParameters()
old_parameters.update_from_swarm_info(self.swarm_info)
self.parameters.compare_to_active(old_parameters, self.client, self.differences)
if self.differences.empty:
self.results['actions'].append("No modification")
self.results['changed'] = False
return
update_parameters = TaskParameters.from_ansible_params(self.client)
update_parameters.update_parameters(self.client)
if not self.check_mode:
self.client.update_swarm(
version=version, swarm_spec=update_parameters.spec,
rotate_worker_token=self.parameters.rotate_worker_token,
rotate_manager_token=self.parameters.rotate_manager_token)
except APIError as exc:
self.client.fail("Can not update a Swarm Cluster: %s" % to_native(exc))
return
self.inspect_swarm()
self.results['actions'].append("Swarm cluster updated")
self.results['changed'] = True
def join(self):
if self.client.check_if_swarm_node():
self.results['actions'].append("This node is already part of a swarm.")
return
if not self.check_mode:
try:
self.client.join_swarm(
remote_addrs=self.parameters.remote_addrs, join_token=self.parameters.join_token,
listen_addr=self.parameters.listen_addr, advertise_addr=self.parameters.advertise_addr,
data_path_addr=self.parameters.data_path_addr)
except APIError as exc:
self.client.fail("Can not join the Swarm Cluster: %s" % to_native(exc))
self.results['actions'].append("New node is added to swarm cluster")
self.differences.add('joined', parameter=True, active=False)
self.results['changed'] = True
def leave(self):
if not self.client.check_if_swarm_node():
self.results['actions'].append("This node is not part of a swarm.")
return
if not self.check_mode:
try:
self.client.leave_swarm(force=self.force)
except APIError as exc:
self.client.fail("This node can not leave the Swarm Cluster: %s" % to_native(exc))
self.results['actions'].append("Node has left the swarm cluster")
self.differences.add('joined', parameter='absent', active='present')
self.results['changed'] = True
def remove(self):
if not self.client.check_if_swarm_manager():
self.client.fail("This node is not a manager.")
try:
status_down = self.client.check_if_swarm_node_is_down(node_id=self.node_id, repeat_check=5)
except APIError:
return
if not status_down:
self.client.fail("Can not remove the node. The status node is ready and not down.")
if not self.check_mode:
try:
self.client.remove_node(node_id=self.node_id, force=self.force)
except APIError as exc:
self.client.fail("Can not remove the node from the Swarm Cluster: %s" % to_native(exc))
self.results['actions'].append("Node is removed from swarm cluster.")
self.differences.add('joined', parameter=False, active=True)
self.results['changed'] = True
def _detect_remove_operation(client):
return client.module.params['state'] == 'remove'
def main():
argument_spec = dict(
advertise_addr=dict(type='str'),
data_path_addr=dict(type='str'),
data_path_port=dict(type='int'),
state=dict(type='str', default='present', choices=['present', 'join', 'absent', 'remove']),
force=dict(type='bool', default=False),
listen_addr=dict(type='str', default='0.0.0.0:2377'),
remote_addrs=dict(type='list', elements='str'),
join_token=dict(type='str', no_log=True),
snapshot_interval=dict(type='int'),
task_history_retention_limit=dict(type='int'),
keep_old_snapshots=dict(type='int'),
log_entries_for_slow_followers=dict(type='int'),
heartbeat_tick=dict(type='int'),
election_tick=dict(type='int'),
dispatcher_heartbeat_period=dict(type='int'),
node_cert_expiry=dict(type='int'),
name=dict(type='str'),
labels=dict(type='dict'),
signing_ca_cert=dict(type='str'),
signing_ca_key=dict(type='str', no_log=True),
ca_force_rotate=dict(type='int'),
autolock_managers=dict(type='bool'),
node_id=dict(type='str'),
rotate_worker_token=dict(type='bool', default=False),
rotate_manager_token=dict(type='bool', default=False),
default_addr_pool=dict(type='list', elements='str'),
subnet_size=dict(type='int'),
)
required_if = [
('state', 'join', ['remote_addrs', 'join_token']),
('state', 'remove', ['node_id'])
]
option_minimal_versions = dict(
labels=dict(docker_py_version='2.6.0', docker_api_version='1.32'),
signing_ca_cert=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
signing_ca_key=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
ca_force_rotate=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
autolock_managers=dict(docker_py_version='2.6.0'),
log_driver=dict(docker_py_version='2.6.0'),
remove_operation=dict(
docker_py_version='2.4.0',
detect_usage=_detect_remove_operation,
usage_msg='remove swarm nodes'
),
default_addr_pool=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
subnet_size=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
data_path_addr=dict(docker_py_version='4.0.0', docker_api_version='1.30'),
data_path_port=dict(docker_py_version='6.0.0', docker_api_version='1.40'),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
min_docker_version='1.10.0',
option_minimal_versions=option_minimal_versions,
)
try:
results = dict(
changed=False,
result='',
actions=[]
)
SwarmManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,388 @@
#!/usr/bin/python
#
# Copyright (c) 2019 Piotr Wojciechowski <piotr@it-playground.pl>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_swarm_info
short_description: Retrieves facts about Docker Swarm cluster.
description:
- Retrieves facts about a Docker Swarm.
- Returns lists of swarm objects names for the services - nodes, services, tasks.
- The output differs depending on API version available on docker host.
- Must be run on Swarm Manager node; otherwise module fails with error message.
It does return boolean flags in on both error and success which indicate whether
the docker daemon can be communicated with, whether it is in Swarm mode, and
whether it is a Swarm Manager node.
author:
- Piotr Wojciechowski (@WojciechowskiPiotr)
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
nodes:
description:
- Whether to list swarm nodes.
type: bool
default: false
nodes_filters:
description:
- A dictionary of filter values used for selecting nodes to list.
- "For example, C(name: mynode)."
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/node_ls/#filtering)
for more information on possible filters.
type: dict
services:
description:
- Whether to list swarm services.
type: bool
default: false
services_filters:
description:
- A dictionary of filter values used for selecting services to list.
- "For example, C(name: myservice)."
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/service_ls/#filtering)
for more information on possible filters.
type: dict
tasks:
description:
- Whether to list containers.
type: bool
default: false
tasks_filters:
description:
- A dictionary of filter values used for selecting tasks to list.
- "For example, C(node: mynode-1)."
- See L(the docker documentation,https://docs.docker.com/engine/reference/commandline/service_ps/#filtering)
for more information on possible filters.
type: dict
unlock_key:
description:
- Whether to retrieve the swarm unlock key.
type: bool
default: false
verbose_output:
description:
- When set to C(true) and I(nodes), I(services) or I(tasks) is set to C(true), then the module output will
contain verbose information about objects matching the full output of API method.
- For details see the documentation of your version of Docker API at U(https://docs.docker.com/engine/api/).
- The verbose output in this module contains only subset of information returned by I(_info) module
for each type of the objects.
type: bool
default: false
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.10.0"
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get info on Docker Swarm
community.docker.docker_swarm_info:
ignore_errors: true
register: result
- name: Inform about basic flags
ansible.builtin.debug:
msg: |
Was able to talk to docker daemon: {{ result.can_talk_to_docker }}
Docker in Swarm mode: {{ result.docker_swarm_active }}
This is a Manager node: {{ result.docker_swarm_manager }}
- block:
- name: Get info on Docker Swarm and list of registered nodes
community.docker.docker_swarm_info:
nodes: true
register: result
- name: Get info on Docker Swarm and extended list of registered nodes
community.docker.docker_swarm_info:
nodes: true
verbose_output: true
register: result
- name: Get info on Docker Swarm and filtered list of registered nodes
community.docker.docker_swarm_info:
nodes: true
nodes_filters:
name: mynode
register: result
- ansible.builtin.debug:
var: result.swarm_facts
- name: Get the swarm unlock key
community.docker.docker_swarm_info:
unlock_key: true
register: result
- ansible.builtin.debug:
var: result.swarm_unlock_key
'''
RETURN = '''
can_talk_to_docker:
description:
- Will be C(true) if the module can talk to the docker daemon.
returned: both on success and on error
type: bool
docker_swarm_active:
description:
- Will be C(true) if the module can talk to the docker daemon,
and the docker daemon is in Swarm mode.
returned: both on success and on error
type: bool
docker_swarm_manager:
description:
- Will be C(true) if the module can talk to the docker daemon,
the docker daemon is in Swarm mode, and the current node is
a manager node.
- Only if this one is C(true), the module will not fail.
returned: both on success and on error
type: bool
swarm_facts:
description:
- Facts representing the basic state of the docker Swarm cluster.
- Contains tokens to connect to the Swarm
returned: always
type: dict
swarm_unlock_key:
description:
- Contains the key needed to unlock the swarm.
returned: When I(unlock_key) is C(true).
type: str
nodes:
description:
- List of dict objects containing the basic information about each volume.
Keys matches the C(docker node ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(nodes) is C(true)
type: list
elements: dict
services:
description:
- List of dict objects containing the basic information about each volume.
Keys matches the C(docker service ls) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(services) is C(true)
type: list
elements: dict
tasks:
description:
- List of dict objects containing the basic information about each volume.
Keys matches the C(docker service ps) output unless I(verbose_output=true).
See description for I(verbose_output).
returned: When I(tasks) is C(true)
type: list
elements: dict
'''
import traceback
try:
from docker.errors import DockerException, APIError
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker_common
pass
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible_collections.community.docker.plugins.module_utils.common import RequestException
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
clean_dict_booleans_for_docker_api,
)
class DockerSwarmManager(DockerBaseClass):
def __init__(self, client, results):
super(DockerSwarmManager, self).__init__()
self.client = client
self.results = results
self.verbose_output = self.client.module.params['verbose_output']
listed_objects = ['tasks', 'services', 'nodes']
self.client.fail_task_if_not_swarm_manager()
self.results['swarm_facts'] = self.get_docker_swarm_facts()
for docker_object in listed_objects:
if self.client.module.params[docker_object]:
returned_name = docker_object
filter_name = docker_object + "_filters"
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name))
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
if self.client.module.params['unlock_key']:
self.results['swarm_unlock_key'] = self.get_docker_swarm_unlock_key()
def get_docker_swarm_facts(self):
try:
return self.client.inspect_swarm()
except APIError as exc:
self.client.fail("Error inspecting docker swarm: %s" % to_native(exc))
def get_docker_items_list(self, docker_object=None, filters=None):
items = None
items_list = []
try:
if docker_object == 'nodes':
items = self.client.nodes(filters=filters)
elif docker_object == 'tasks':
items = self.client.tasks(filters=filters)
elif docker_object == 'services':
items = self.client.services(filters=filters)
except APIError as exc:
self.client.fail("Error inspecting docker swarm for object '%s': %s" %
(docker_object, to_native(exc)))
if self.verbose_output:
return items
for item in items:
item_record = dict()
if docker_object == 'nodes':
item_record = self.get_essential_facts_nodes(item)
elif docker_object == 'tasks':
item_record = self.get_essential_facts_tasks(item)
elif docker_object == 'services':
item_record = self.get_essential_facts_services(item)
if item_record['Mode'] == 'Global':
item_record['Replicas'] = len(items)
items_list.append(item_record)
return items_list
@staticmethod
def get_essential_facts_nodes(item):
object_essentials = dict()
object_essentials['ID'] = item.get('ID')
object_essentials['Hostname'] = item['Description']['Hostname']
object_essentials['Status'] = item['Status']['State']
object_essentials['Availability'] = item['Spec']['Availability']
if 'ManagerStatus' in item:
object_essentials['ManagerStatus'] = item['ManagerStatus']['Reachability']
if 'Leader' in item['ManagerStatus'] and item['ManagerStatus']['Leader'] is True:
object_essentials['ManagerStatus'] = "Leader"
else:
object_essentials['ManagerStatus'] = None
object_essentials['EngineVersion'] = item['Description']['Engine']['EngineVersion']
return object_essentials
def get_essential_facts_tasks(self, item):
object_essentials = dict()
object_essentials['ID'] = item['ID']
# Returning container ID to not trigger another connection to host
# Container ID is sufficient to get extended info in other tasks
object_essentials['ContainerID'] = item['Status']['ContainerStatus']['ContainerID']
object_essentials['Image'] = item['Spec']['ContainerSpec']['Image']
object_essentials['Node'] = self.client.get_node_name_by_id(item['NodeID'])
object_essentials['DesiredState'] = item['DesiredState']
object_essentials['CurrentState'] = item['Status']['State']
if 'Err' in item['Status']:
object_essentials['Error'] = item['Status']['Err']
else:
object_essentials['Error'] = None
return object_essentials
@staticmethod
def get_essential_facts_services(item):
object_essentials = dict()
object_essentials['ID'] = item['ID']
object_essentials['Name'] = item['Spec']['Name']
if 'Replicated' in item['Spec']['Mode']:
object_essentials['Mode'] = "Replicated"
object_essentials['Replicas'] = item['Spec']['Mode']['Replicated']['Replicas']
elif 'Global' in item['Spec']['Mode']:
object_essentials['Mode'] = "Global"
# Number of replicas have to be updated in calling method or may be left as None
object_essentials['Replicas'] = None
object_essentials['Image'] = item['Spec']['TaskTemplate']['ContainerSpec']['Image']
if 'Ports' in item['Spec']['EndpointSpec']:
object_essentials['Ports'] = item['Spec']['EndpointSpec']['Ports']
else:
object_essentials['Ports'] = []
return object_essentials
def get_docker_swarm_unlock_key(self):
unlock_key = self.client.get_unlock_key() or {}
return unlock_key.get('UnlockKey') or None
def main():
argument_spec = dict(
nodes=dict(type='bool', default=False),
nodes_filters=dict(type='dict'),
tasks=dict(type='bool', default=False),
tasks_filters=dict(type='dict'),
services=dict(type='bool', default=False),
services_filters=dict(type='dict'),
unlock_key=dict(type='bool', default=False),
verbose_output=dict(type='bool', default=False),
)
option_minimal_versions = dict(
unlock_key=dict(docker_py_version='2.7.0'),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='1.10.0',
option_minimal_versions=option_minimal_versions,
fail_results=dict(
can_talk_to_docker=False,
docker_swarm_active=False,
docker_swarm_manager=False,
),
)
client.fail_results['can_talk_to_docker'] = True
client.fail_results['docker_swarm_active'] = client.check_if_swarm_node()
client.fail_results['docker_swarm_manager'] = client.check_if_swarm_manager()
try:
results = dict(
changed=False,
)
DockerSwarmManager(client, results)
results.update(client.fail_results)
client.module.exit_json(**results)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,121 @@
#!/usr/bin/python
#
# Copyright (c) 2019 Hannes Ljungberg <hannes.ljungberg@gmail.com>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: docker_swarm_service_info
short_description: Retrieves information about docker services from a Swarm Manager
description:
- Retrieves information about a docker service.
- Essentially returns the output of C(docker service inspect <name>).
- Must be executed on a host running as Swarm Manager, otherwise the module will fail.
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- The name of the service to inspect.
type: str
required: true
author:
- Hannes Ljungberg (@hannseman)
requirements:
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.0.0"
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get info from a service
community.docker.docker_swarm_service_info:
name: myservice
register: result
'''
RETURN = '''
exists:
description:
- Returns whether the service exists.
type: bool
returned: always
sample: true
service:
description:
- A dictionary representing the current state of the service. Matches the C(docker service inspect) output.
- Will be C(none) if service does not exist.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
try:
from docker.errors import DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.common import (
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
def get_service_info(client):
service = client.module.params['name']
return client.get_service_inspect(
service_id=service,
skip_missing=True
)
def main():
argument_spec = dict(
name=dict(type='str', required=True),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.0.0',
)
client.fail_task_if_not_swarm_manager()
try:
service = get_service_info(client)
client.module.exit_json(
changed=False,
service=service,
exists=bool(service)
)
except DockerException as e:
client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,313 @@
#!/usr/bin/python
# coding: utf-8
#
# Copyright 2017 Red Hat | Ansible, Alex Grönholm <alex.gronholm@nextday.fi>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: docker_volume
short_description: Manage Docker volumes
description:
- Create/remove Docker volumes.
- Performs largely the same function as the C(docker volume) 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
options:
volume_name:
description:
- Name of the volume to operate on.
type: str
required: true
aliases:
- name
driver:
description:
- Specify the type of volume. Docker provides the C(local) driver, but 3rd party drivers can also be used.
type: str
default: local
driver_options:
description:
- "Dictionary of volume settings. Consult docker docs for valid options and values:
U(https://docs.docker.com/engine/reference/commandline/volume_create/#driver-specific-options)."
type: dict
default: {}
labels:
description:
- Dictionary of label key/values to set for the volume
type: dict
recreate:
description:
- Controls when a volume will be recreated when I(state) is C(present). Please
note that recreating an existing volume will cause B(any data in the existing volume
to be lost!) The volume will be deleted and a new volume with the same name will be
created.
- The value C(always) forces the volume to be always recreated.
- The value C(never) makes sure the volume will not be recreated.
- The value C(options-changed) makes sure the volume will be recreated if the volume
already exist and the driver, driver options or labels differ.
type: str
default: never
choices:
- always
- never
- options-changed
state:
description:
- C(absent) deletes the volume.
- C(present) creates the volume, if it does not already exist.
type: str
default: present
choices:
- absent
- present
author:
- Alex Grönholm (@agronholm)
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Create a volume
community.docker.docker_volume:
name: volume_one
- name: Remove a volume
community.docker.docker_volume:
name: volume_one
state: absent
- name: Create a volume with options
community.docker.docker_volume:
name: volume_two
driver_options:
type: btrfs
device: /dev/sda2
'''
RETURN = '''
volume:
description:
- Volume inspection results for the affected volume.
returned: success
type: dict
sample: {}
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import iteritems, text_type
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
NotFound,
)
class TaskParameters(DockerBaseClass):
def __init__(self, client):
super(TaskParameters, self).__init__()
self.client = client
self.volume_name = None
self.driver = None
self.driver_options = None
self.labels = None
self.recreate = None
self.debug = None
for key, value in iteritems(client.module.params):
setattr(self, key, value)
class DockerVolumeManager(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.diff_tracker = DifferenceTracker()
self.diff_result = dict()
self.existing_volume = self.get_existing_volume()
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_volume(self):
try:
volumes = self.client.get_json('/volumes')
except APIError as e:
self.client.fail(to_native(e))
if volumes[u'Volumes'] is None:
return None
for volume in volumes[u'Volumes']:
if volume['Name'] == self.parameters.volume_name:
return volume
return None
def has_different_config(self):
"""
Return the list of differences between the current parameters and the existing volume.
:return: list of options that differ
"""
differences = DifferenceTracker()
if self.parameters.driver and self.parameters.driver != self.existing_volume['Driver']:
differences.add('driver', parameter=self.parameters.driver, active=self.existing_volume['Driver'])
if self.parameters.driver_options:
if not self.existing_volume.get('Options'):
differences.add('driver_options',
parameter=self.parameters.driver_options,
active=self.existing_volume.get('Options'))
else:
for key, value in iteritems(self.parameters.driver_options):
if (not self.existing_volume['Options'].get(key) or
value != self.existing_volume['Options'][key]):
differences.add('driver_options.%s' % key,
parameter=value,
active=self.existing_volume['Options'].get(key))
if self.parameters.labels:
existing_labels = self.existing_volume.get('Labels', {})
for label in self.parameters.labels:
if existing_labels.get(label) != self.parameters.labels.get(label):
differences.add('labels.%s' % label,
parameter=self.parameters.labels.get(label),
active=existing_labels.get(label))
return differences
def create_volume(self):
if not self.existing_volume:
if not self.check_mode:
try:
data = {
'Name': self.parameters.volume_name,
'Driver': self.parameters.driver,
'DriverOpts': self.parameters.driver_options,
}
if self.parameters.labels is not None:
data['Labels'] = self.parameters.labels
resp = self.client.post_json_to_json('/volumes/create', data=data)
self.existing_volume = self.client.get_json('/volumes/{0}', resp['Name'])
except APIError as e:
self.client.fail(to_native(e))
self.results['actions'].append("Created volume %s with driver %s" % (self.parameters.volume_name, self.parameters.driver))
self.results['changed'] = True
def remove_volume(self):
if self.existing_volume:
if not self.check_mode:
try:
self.client.delete_call('/volumes/{0}', self.parameters.volume_name)
except APIError as e:
self.client.fail(to_native(e))
self.results['actions'].append("Removed volume %s" % self.parameters.volume_name)
self.results['changed'] = True
def present(self):
differences = DifferenceTracker()
if self.existing_volume:
differences = self.has_different_config()
self.diff_tracker.add('exists', parameter=True, active=self.existing_volume is not None)
if (not differences.empty and self.parameters.recreate == 'options-changed') or self.parameters.recreate == 'always':
self.remove_volume()
self.existing_volume = None
self.create_volume()
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')
volume_facts = self.get_existing_volume()
self.results['volume'] = volume_facts
def absent(self):
self.diff_tracker.add('exists', parameter=False, active=self.existing_volume is not None)
self.remove_volume()
def main():
argument_spec = dict(
volume_name=dict(type='str', required=True, aliases=['name']),
state=dict(type='str', default='present', choices=['present', 'absent']),
driver=dict(type='str', default='local'),
driver_options=dict(type='dict', default={}),
labels=dict(type='dict'),
recreate=dict(type='str', default='never', choices=['always', 'never', 'options-changed']),
debug=dict(type='bool', default=False)
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
# "The docker server >= 1.9.0"
)
try:
cm = DockerVolumeManager(client)
client.module.exit_json(**cm.results)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,127 @@
#!/usr/bin/python
# coding: utf-8
#
# Copyright 2017 Red Hat | Ansible, Alex Grönholm <alex.gronholm@nextday.fi>
# 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 absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: docker_volume_info
short_description: Retrieve facts about Docker volumes
description:
- Performs largely the same function as the C(docker volume inspect) CLI subcommand.
extends_documentation_fragment:
- community.docker.docker.api_documentation
- community.docker.attributes
- community.docker.attributes.actiongroup_docker
- community.docker.attributes.info_module
options:
name:
description:
- Name of the volume to inspect.
type: str
required: true
aliases:
- volume_name
author:
- Felix Fontein (@felixfontein)
requirements:
- "Docker API >= 1.25"
'''
EXAMPLES = '''
- name: Get infos on volume
community.docker.docker_volume_info:
name: mydata
register: result
- name: Does volume exist?
ansible.builtin.debug:
msg: "The volume {{ 'exists' if result.exists else 'does not exist' }}"
- name: Print information about volume
ansible.builtin.debug:
var: result.volume
when: result.exists
'''
RETURN = '''
exists:
description:
- Returns whether the volume exists.
type: bool
returned: always
sample: true
volume:
description:
- Volume inspection results for the affected volume.
- Will be C(none) if volume does not exist.
returned: success
type: dict
sample: '{
"CreatedAt": "2018-12-09T17:43:44+01:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/ansible-test-bd3f6172/_data",
"Name": "ansible-test-bd3f6172",
"Options": {},
"Scope": "local"
}'
'''
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
def get_existing_volume(client, volume_name):
try:
return client.get_json('/volumes/{0}', volume_name)
except NotFound as dummy:
return None
except Exception as exc:
client.fail("Error inspecting volume: %s" % to_native(exc))
def main():
argument_spec = dict(
name=dict(type='str', required=True, aliases=['volume_name']),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
)
try:
volume = get_existing_volume(client, client.module.params['name'])
client.module.exit_json(
changed=False,
exists=(True if volume else False),
volume=volume,
)
except DockerException as e:
client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
except RequestException as e:
client.fail(
'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)),
exception=traceback.format_exc())
if __name__ == '__main__':
main()