summaryrefslogtreecommitdiffstats
path: root/collections-debian-merged/ansible_collections/community/docker/plugins/modules/docker_node.py
blob: d73b2d7056ca5dc42692e56426cc397678f9030c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
#!/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

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.
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: yes
    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
extends_documentation_fragment:
- community.docker.docker
- community.docker.docker.docker_py_1_documentation

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._text 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',
        min_docker_api_version='1.25',
    )

    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(e), exception=traceback.format_exc())
    except RequestException as e:
        client.fail('An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e), exception=traceback.format_exc())


if __name__ == '__main__':
    main()