summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPiotr Wojciechowski <23406016+WojciechowskiPiotr@users.noreply.github.com>2019-02-04 14:08:46 +0100
committerJohn R Barker <john@johnrbarker.com>2019-02-04 13:08:46 +0000
commitdaf1cfbde09e84d53cbe2548a216fbaec69e62a2 (patch)
tree3be80938e1e43aec14be78b0671e05e2a5dd7557 /lib
parent2e99dea8677e2e8fbd9534f6473e65ecacb72896 (diff)
downloadansible-daf1cfbde09e84d53cbe2548a216fbaec69e62a2.tar.gz
docker_node: Docker Swarm node operations module (#50584)
* * docker_node: New module for operations on Docker Swarm node * Shared code for Docker Swarm modules * * docker_node: Removed the attribute `force` as it is not used for any operation * docker_node_facts: Update module to use client class AnsibleDockerSwarmClient instead of AnsibleDockerClient * docker_node_facts: List of nodes can be provided as input, inspect all registered nodes or manager host itself * docker_node: Update in method name called from AnsibleDockerSwarmClient * docker_node: Additional method to shared module to get formatted output list of registered nodes * docker_node: Additional method to shared module to get formatted output list of registered nodes * docker_node: removed state list (featue moved to docker_swarm_facts) * docker_node: Node labels manipulation (remove, replace, merge) * module_utils/docker_swarm: Updated output for nodes list - adding swarm leader flag * docker_node_facts: update in input and return values, update in documentation section * docker_node: Updated operations on labels, tracking if change is required * docker_node: Updated documentation, parameter 'hostname' is now required docker_node_facts: Updated documentation * * Failing Ansible tasl if not run on swarm manager - code cleanup * * docker_node: Remove the 'action' list from output * * docker_node: variable name change to be align with Python best practice, BOTMETA.yml update * * module_utils/docker_swarm.py: fix for incorrect fail() action * docker_node: documentation and code small updates * * docker_node: revised labels manipulation * docker_node_facts: Reverting to repository version, moving this change to separate PR * * docker_node: Documentation update * * docker_node: Update to node availability and role modification * * docker_node: Update to check_mode handling * * docker_node: Code cleanup * docker_node_facts: Code cleanup * docker_node_facts: Adding back the module with only update to use AnsibleDockerSwarmClient instead of AnsibleDockerClient docker_node: cosmetic code changes BOTMETA: updated on $team_docker * docker_node: BOTMETA update
Diffstat (limited to 'lib')
-rw-r--r--lib/ansible/module_utils/docker_swarm.py218
-rw-r--r--lib/ansible/modules/cloud/docker/docker_node.py295
-rw-r--r--lib/ansible/modules/cloud/docker/docker_node_facts.py5
3 files changed, 516 insertions, 2 deletions
diff --git a/lib/ansible/module_utils/docker_swarm.py b/lib/ansible/module_utils/docker_swarm.py
new file mode 100644
index 0000000000..19e3a379e0
--- /dev/null
+++ b/lib/ansible/module_utils/docker_swarm.py
@@ -0,0 +1,218 @@
+# (c) 2019 Piotr Wojciechowski (@wojciechowskipiotr) <piotr@it-playground.pl>
+# (c) Thierry Bouvet (@tbouvet)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+import json
+
+try:
+ from docker.errors import APIError
+except ImportError:
+ # missing docker-py handled in ansible.module_utils.docker_common
+ pass
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.docker_common import AnsibleDockerClient
+
+
+class AnsibleDockerSwarmClient(AnsibleDockerClient):
+
+ def __init__(self, **kwargs):
+ super(AnsibleDockerSwarmClient, self).__init__(**kwargs)
+
+ def get_swarm_node_id(self):
+ """
+ Get the 'NodeID' of the Swarm node or 'None' if host is not in Swarm. It returns the NodeID
+ of Docker host the module is executed on
+ :return:
+ NodeID of host or 'None' if not part of Swarm
+ """
+
+ try:
+ info = self.info()
+ except APIError as exc:
+ self.fail(msg="Failed to get node information for %s" % to_native(exc))
+
+ if info:
+ json_str = json.dumps(info, ensure_ascii=False)
+ swarm_info = json.loads(json_str)
+ if swarm_info['Swarm']['NodeID']:
+ return swarm_info['Swarm']['NodeID']
+ return None
+
+ def check_if_swarm_node(self, node_id=None):
+ """
+ Checking if host is part of Docker Swarm. If 'node_id' is not provided it reads the Docker host
+ system information looking if specific key in output exists. If 'node_id' is provided then it tries to
+ read node information assuming it is run on Swarm manager. The get_node_inspect() method handles exception if
+ it is not executed on Swarm manager
+
+ :param node_id: Node identifier
+ :return:
+ bool: True if node is part of Swarm, False otherwise
+ """
+
+ if node_id is None:
+ try:
+ info = self.info()
+ except APIError:
+ self.fail(msg="Failed to get host information.")
+
+ if info:
+ json_str = json.dumps(info, ensure_ascii=False)
+ swarm_info = json.loads(json_str)
+ if swarm_info['Swarm']['NodeID']:
+ return True
+ return False
+ else:
+ try:
+ node_info = self.get_node_inspect(node_id=node_id)
+ except APIError:
+ return
+
+ if node_info['ID'] is not None:
+ return True
+ return False
+
+ def check_if_swarm_manager(self):
+ """
+ Checks if node role is set as Manager in Swarm. The node is the docker host on which module action
+ is performed. The inspect_swarm() will fail if node is not a manager
+
+ :return: True if node is Swarm Manager, False otherwise
+ """
+
+ try:
+ self.inspect_swarm()
+ return True
+ except APIError:
+ return False
+
+ def fail_task_if_not_swarm_manager(self):
+ """
+ If host is not a swarm manager then Ansible task on this host should end with 'failed' state
+ """
+ if not self.check_if_swarm_manager():
+ self.fail(msg="This node is not a manager.")
+
+ def check_if_swarm_worker(self):
+ """
+ Checks if node role is set as Worker in Swarm. The node is the docker host on which module action
+ is performed. Will fail if run on host that is not part of Swarm via check_if_swarm_node()
+
+ :return: True if node is Swarm Worker, False otherwise
+ """
+
+ if self.check_if_swarm_node() and not self.check_if_swarm_manager():
+ return True
+ return False
+
+ def check_if_swarm_node_is_down(self, node_id=None):
+ """
+ Checks if node status on Swarm manager is 'down'. If node_id is provided it query manager about
+ node specified in parameter, otherwise it query manager itself. If run on Swarm Worker node or
+ host that is not part of Swarm it will fail the playbook
+
+ :param node_id: node ID or name, if None then method will try to get node_id of host module run on
+ :return:
+ True if node is part of swarm but its state is down, False otherwise
+ """
+
+ if node_id is None:
+ node_id = self.get_swarm_node_id()
+
+ node_info = self.get_node_inspect(node_id=node_id)
+ if node_info['Status']['State'] == 'down':
+ return True
+ return False
+
+ def get_node_inspect(self, node_id=None, skip_missing=False):
+ """
+ Returns Swarm node info as in 'docker node inspect' command about single node
+
+ :param skip_missing: if True then function will return None instead of failing the task
+ :param node_id: node ID or name, if None then method will try to get node_id of host module run on
+ :return:
+ Single node information structure
+ """
+
+ if node_id is None:
+ node_id = self.get_swarm_node_id()
+
+ if node_id is None:
+ self.fail(msg="Failed to get node information.")
+
+ try:
+ node_info = self.inspect_node(node_id=node_id)
+ except APIError as exc:
+ if exc.status_code == 503:
+ self.fail(msg="Cannot inspect node: To inspect node execute module on Swarm Manager")
+ if exc.status_code == 404:
+ if skip_missing is False:
+ self.fail(msg="Error while reading from Swarm manager: %s" % to_native(exc))
+ else:
+ return None
+ except Exception as exc:
+ self.module.fail_json(msg="Error inspecting swarm node: %s" % exc)
+
+ json_str = json.dumps(node_info, ensure_ascii=False)
+ node_info = json.loads(json_str)
+ return node_info
+
+ def get_all_nodes_inspect(self):
+ """
+ Returns Swarm node info as in 'docker node inspect' command about all registered nodes
+
+ :return:
+ Structure with information about all nodes
+ """
+ try:
+ node_info = self.nodes()
+ except APIError as exc:
+ if exc.status_code == 503:
+ self.fail(msg="Cannot inspect node: To inspect node execute module on Swarm Manager")
+ self.fail(msg="Error while reading from Swarm manager: %s" % to_native(exc))
+ except Exception as exc:
+ self.module.fail_json(msg="Error inspecting swarm node: %s" % exc)
+
+ json_str = json.dumps(node_info, ensure_ascii=False)
+ node_info = json.loads(json_str)
+ return node_info
+
+ def get_all_nodes_list(self, output='short'):
+ """
+ Returns list of nodes registered in Swarm
+
+ :param output: Defines format of returned data
+ :return:
+ If 'output' is 'short' then return data is list of nodes hostnames registered in Swarm,
+ if 'output' is 'long' then returns data is list of dict containing the attributes as in
+ output of command 'docker node ls'
+ """
+ nodes_list = []
+
+ nodes_inspect = self.get_all_nodes_inspect()
+ if nodes_inspect is None:
+ return None
+
+ if output == 'short':
+ for node in nodes_inspect:
+ nodes_list.append(node['Description']['Hostname'])
+ elif output == 'long':
+ for node in nodes_inspect:
+ node_property = {}
+
+ node_property.update({'ID': node['ID']})
+ node_property.update({'Hostname': node['Description']['Hostname']})
+ node_property.update({'Status': node['Status']['State']})
+ node_property.update({'Availability': node['Spec']['Availability']})
+ if 'ManagerStatus' in node:
+ if node['ManagerStatus']['Leader'] is True:
+ node_property.update({'Leader': True})
+ node_property.update({'ManagerStatus': node['ManagerStatus']['Reachability']})
+ node_property.update({'EngineVersion': node['Description']['Engine']['EngineVersion']})
+
+ nodes_list.append(node_property)
+ else:
+ return None
+
+ return nodes_list
diff --git a/lib/ansible/modules/cloud/docker/docker_node.py b/lib/ansible/modules/cloud/docker/docker_node.py
new file mode 100644
index 0000000000..ef913df45d
--- /dev/null
+++ b/lib/ansible/modules/cloud/docker/docker_node.py
@@ -0,0 +1,295 @@
+#!/usr/bin/python
+#
+# (c) 2019 Piotr Wojciechowski <piotr@it-playground.pl>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+module: docker_node
+short_description: Manage Docker Swarm node
+version_added: "2.8"
+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.
+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.
+ required: true
+ type: str
+ labels:
+ description:
+ - User-defined key/value metadata that will be assigned as node attribute.
+ - 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.
+ required: false
+ 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.
+ choices:
+ - merge
+ - replace
+ default: 'merge'
+ required: false
+ type: str
+ 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.
+ required: false
+ type: list
+ availability:
+ description: Node availability to assign. If not provided then node availability remains unchanged.
+ choices:
+ - active
+ - pause
+ - drain
+ required: false
+ type: str
+ role:
+ description: Node role to assign. If not provided then node role remains unchanged.
+ choices:
+ - manager
+ - worker
+ required: false
+ type: str
+extends_documentation_fragment:
+ - docker
+requirements:
+ - "python >= 2.6"
+ - "docker-py >= 1.10.0"
+ - "Please note that the L(docker-py,https://pypi.org/project/docker-py/) Python
+ module has been superseded by L(docker,https://pypi.org/project/docker/)
+ (see L(here,https://github.com/docker/docker-py/issues/1310) for details).
+ For Python 2.6, C(docker-py) must be used. Otherwise, it is recommended to
+ install the C(docker) Python module. Note that both modules should I(not)
+ be installed at the same time. Also note that when both modules are installed
+ and one of them is uninstalled, the other might no longer function and a
+ reinstall of it is required."
+ - Docker API >= 1.25
+author:
+ - Piotr Wojciechowski (@WojciechowskiPiotr)
+ - Thierry Bouvet (@tbouvet)
+
+'''
+
+EXAMPLES = '''
+- name: Set node role
+ docker_node:
+ hostname: mynode
+ role: manager
+
+- name: Set node availability
+ docker_node:
+ hostname: mynode
+ availability: drain
+
+- name: Replace node labels with new labels
+ docker_node:
+ hostname: mynode
+ labels:
+ key: value
+ labels_state: replace
+
+- name: Merge node labels and new labels
+ docker_node:
+ hostname: mynode
+ labels:
+ key: value
+
+- name: Remove all labels assigned to node
+ docker_node:
+ hostname: mynode
+ labels_state: replace
+
+- name: Remove selected labels from the node
+ docker_node:
+ hostname: mynode
+ labels_to_remove:
+ - key1
+ - key2
+'''
+
+RETURN = '''
+node_facts:
+ description: Information about node after 'update' operation
+ returned: success
+ type: dict
+
+'''
+
+try:
+ from docker.errors import APIError
+except ImportError:
+ # missing docker-py handled in ansible.module_utils.docker_common
+ pass
+
+from ansible.module_utils.docker_common import (
+ DockerBaseClass,
+)
+
+from ansible.module_utils._text import to_native
+
+from ansible.module_utils.docker_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(msg="This node is not part of a swarm.")
+ return
+
+ if self.client.check_if_swarm_node_is_down():
+ self.client.fail(msg="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(msg="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 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))
+
+ 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(msg="Failed to update node : %s" % to_native(exc))
+ self.results['node_facts'] = self.client.get_node_inspect(node_id=node_info['ID'])
+ self.results['changed'] = changed
+ else:
+ self.results['node_facts'] = 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', choices=['merge', 'replace'], default='merge'),
+ 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='1.10.0',
+ min_docker_api_version='1.25',
+ )
+
+ results = dict(
+ changed=False,
+ )
+
+ SwarmNodeManager(client, results)
+ client.module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/ansible/modules/cloud/docker/docker_node_facts.py b/lib/ansible/modules/cloud/docker/docker_node_facts.py
index df9844cd29..6ac4aee84b 100644
--- a/lib/ansible/modules/cloud/docker/docker_node_facts.py
+++ b/lib/ansible/modules/cloud/docker/docker_node_facts.py
@@ -75,9 +75,10 @@ node_facts:
'''
-from ansible.module_utils.docker_common import AnsibleDockerClient
from ansible.module_utils._text import to_native
+from ansible.module_utils.docker_swarm import AnsibleDockerSwarmClient
+
try:
from docker.errors import APIError, NotFound
except ImportError:
@@ -103,7 +104,7 @@ def main():
name=dict(type='str', required=True),
)
- client = AnsibleDockerClient(
+ client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='1.10.0',