summaryrefslogtreecommitdiffstats
path: root/roles/openshift_register_nodes/library/kubernetes_register_node.py
blob: 409215616cbbf29ae7c3d0b6b39e5cf705d52d5a (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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: expandtab:tabstop=4:shiftwidth=4

import os
import multiprocessing
import socket
from subprocess import check_output, Popen
from decimal import *

DOCUMENTATION = '''
---
module: kubernetes_register_node
short_description: Registers a kubernetes node with a master
description:
    - Registers a kubernetes node with a master
options:
    name:
        default: null
        description:
            - Identifier for this node (usually the node fqdn).
        required: true
    api_verison:
        choices: ['v1beta1', 'v1beta3']
        default: 'v1beta1'
        description:
            - Kubernetes API version to use
        required: true
    host_ip:
        default: null
        description:
            - IP Address to associate with the node when registering.
              Available in the following API versions: v1beta1.
        required: false
    hostnames:
        default: []
        description:
            - Valid hostnames for this node. Available in the following API
              versions: v1beta3.
        required: false
    external_ips:
        default: []
        description:
            - External IP Addresses for this node. Available in the following API
              versions: v1beta3.
        required: false
    internal_ips:
        default: []
        description:
            - Internal IP Addresses for this node. Available in the following API
              versions: v1beta3.
        required: false
    cpu:
        default: null
        description:
            - Number of CPUs to allocate for this node. When using the v1beta1
              API, you must specify the CPU count as a floating point number
              with no more than 3 decimal places. API version v1beta3 and newer
              accepts arbitrary float values.
        required: false
    memory:
        default: null
        description:
            - Memory available for this node. When using the v1beta1 API, you
              must specify the memory size in bytes. API version v1beta3 and
              newer accepts binary SI and decimal SI values.
        required: false
'''
EXAMPLES = '''
# Minimal node registration
- openshift_register_node: name=ose3.node.example.com

# Node registration using the v1beta1 API and assigning 1 CPU core and 10 GB of
# Memory
- openshift_register_node:
    name: ose3.node.example.com
    api_version: v1beta1
    hostIP: 192.168.1.1
    cpu: 1
    memory: 500000000

# Node registration using the v1beta3 API, setting an alternate hostname,
# internalIP, externalIP and assigning 3.5 CPU cores and 1 TiB of Memory
- openshift_register_node:
    name: ose3.node.example.com
    api_version: v1beta3
    external_ips: ['192.168.1.5']
    internal_ips: ['10.0.0.5']
    hostnames: ['ose2.node.internal.local']
    cpu: 3.5
    memory: 1Ti
'''


class ClientConfigException(Exception):
    pass

class ClientConfig:
    def __init__(self, client_opts, module):
        _, output, error = module.run_command(["/usr/bin/openshift", "ex",
                                               "config", "view", "-o",
                                               "json"] + client_opts,
                                              check_rc = True)
        self.config = json.loads(output)

        if not (bool(self.config['clusters']) or
                bool(self.config['contexts']) or
                bool(self.config['current-context']) or
                bool(self.config['users'])):
            raise ClientConfigException(msg="Client config missing required " \
                                            "values",
                                        output=output)

    def current_context(self):
        return self.config['current-context']

    def section_has_value(self, section_name, value):
        section = self.config[section_name]
        if isinstance(section, dict):
            return value in section
        else:
            val = next((item for item in section
                      if item['name'] == value), None)
            return val is not None

    def has_context(self, context):
        return self.section_has_value('contexts', context)

    def has_user(self, user):
        return self.section_has_value('users', user)

    def has_cluster(self, cluster):
        return self.section_has_value('clusters', cluster)

    def get_value_for_context(self, context, attribute):
        contexts = self.config['contexts']
        if isinstance(contexts, dict):
            return contexts[context][attribute]
        else:
            return next((c['context'][attribute] for c in contexts
                      if c['name'] == context), None)

    def get_user_for_context(self, context):
        return self.get_value_for_context(context, 'user')

    def get_cluster_for_context(self, context):
        return self.get_value_for_context(context, 'cluster')

class Util:
    @staticmethod
    def remove_empty_elements(mapping):
        if isinstance(mapping, dict):
            m = mapping.copy()
            for key, val in mapping.iteritems():
                if not val:
                    del m[key]
            return m
        else:
            return mapping

class NodeResources:
    def __init__(self, version, cpu=None, memory=None):
        if version == 'v1beta1':
            self.resources = dict(capacity=dict())
            self.resources['capacity']['cpu'] = cpu
            self.resources['capacity']['memory'] = memory

    def get_resources(self):
        return Util.remove_empty_elements(self.resources)

class NodeSpec:
    def __init__(self, version, cpu=None, memory=None, cidr=None, externalID=None):
        if version == 'v1beta3':
            self.spec = dict(podCIDR=cidr, externalID=externalID,
                             capacity=dict())
            self.spec['capacity']['cpu'] = cpu
            self.spec['capacity']['memory'] = memory

    def get_spec(self):
        return Util.remove_empty_elements(self.spec)

class NodeStatus:
    def addAddresses(self, addressType, addresses):
        addressList = []
        for address in addresses:
            addressList.append(dict(type=addressType, address=address))
        return addressList

    def __init__(self, version, externalIPs = [], internalIPs = [],
                 hostnames = []):
        if version == 'v1beta3':
            self.status = dict(addresses = addAddresses('ExternalIP',
                                                        externalIPs) +
                                           addAddresses('InternalIP',
                                                        internalIPs) +
                                           addAddresses('Hostname',
                                                        hostnames))

    def get_status(self):
        return Util.remove_empty_elements(self.status)

class Node:
    def __init__(self, module, client_opts, version='v1beta1', name=None,
                 hostIP = None, hostnames=[], externalIPs=[], internalIPs=[],
                 cpu=None, memory=None, labels=dict(), annotations=dict(),
                 podCIDR=None, externalID=None):
        self.module = module
        self.client_opts = client_opts
        if version == 'v1beta1':
            self.node = dict(id = name,
                             kind = 'Node',
                             apiVersion = version,
                             hostIP = hostIP,
                             resources = NodeResources(version, cpu, memory),
                             cidr = podCIDR,
                             labels = labels,
                             annotations = annotations
                        )
        elif version == 'v1beta3':
            metadata = dict(name = name,
                            labels = labels,
                            annotations = annotations
                        )
            self.node = dict(kind = 'Node',
                             apiVersion = version,
                             metadata = metadata,
                             spec = NodeSpec(version, cpu, memory, podCIDR,
                                             externalID),
                             status = NodeStatus(version, externalIPs,
                                                 internalIPs, hostnames),
                        )

    def get_name(self):
        if self.node['apiVersion'] == 'v1beta1':
            return self.node['id']
        elif self.node['apiVersion'] == 'v1beta3':
            return self.node['name']

    def get_node(self):
        node = self.node.copy()
        if self.node['apiVersion'] == 'v1beta1':
            node['resources'] = self.node['resources'].get_resources()
        elif self.node['apiVersion'] == 'v1beta3':
            node['spec'] = self.node['spec'].get_spec()
            node['status'] = self.node['status'].get_status()
        return Util.remove_empty_elements(node)

    def exists(self):
        _, output, error = self.module.run_command(["/usr/bin/osc", "get",
                                                    "nodes"] +  self.client_opts,
                                                   check_rc = True)
        if re.search(self.module.params['name'], output, re.MULTILINE):
            return True
        return False

    def create(self):
        cmd = ['/usr/bin/osc'] + self.client_opts + ['create', 'node', '-f', '-']
        rc, output, error = self.module.run_command(cmd,
                                               data=self.module.jsonify(self.get_node()))
        if rc != 0:
            if re.search("minion \"%s\" already exists" % self.get_name(),
                         error):
                self.module.exit_json(changed=False,
                                 msg="node definition already exists",
                                 node=self.get_node())
            else:
                self.module.fail_json(msg="Node creation failed.", rc=rc,
                                 output=output, error=error,
                                 node=self.get_node())
        else:
            return True

def main():
    module = AnsibleModule(
        argument_spec      = dict(
            name           = dict(required = True, type = 'str'),
            host_ip        = dict(type = 'str'),
            hostnames      = dict(type = 'list', default = []),
            external_ips   = dict(type = 'list', default = []),
            internal_ips   = dict(type = 'list', default = []),
            api_version    = dict(type = 'str', default = 'v1beta1', # TODO: after kube rebase, we can default to v1beta3
                                  choices = ['v1beta1', 'v1beta3']),
            cpu            = dict(type = 'str'),
            memory         = dict(type = 'str'),
            labels         = dict(type = 'dict', default = {}), # TODO: needs documented
            annotations    = dict(type = 'dict', default = {}), # TODO: needs documented
            pod_cidr       = dict(type = 'str'), # TODO: needs documented
            external_id    = dict(type = 'str'), # TODO: needs documented
            client_config  = dict(type = 'str'), # TODO: needs documented
            client_cluster = dict(type = 'str', default = 'master'), # TODO: needs documented
            client_context = dict(type = 'str', default = 'master'), # TODO: needs documented
            client_user    = dict(type = 'str', default = 'admin') # TODO: needs documented
        ),
        mutually_exclusive = [
            ['host_ip', 'external_ips'],
            ['host_ip', 'internal_ips'],
            ['host_ip', 'hostnames'],
        ],
        supports_check_mode=True
    )

    user_has_client_config = os.path.exists(os.path.expanduser('~/.kube/.kubeconfig'))
    if not (user_has_client_config or module.params['client_config']):
        module.fail_json(msg="Could not locate client configuration, "
                         "client_config must be specified if "
                         "~/.kube/.kubeconfig is not present")

    client_opts = []
    if module.params['client_config']:
        client_opts.append("--kubeconfig=%s" % module.params['client_config'])

    try:
        config = ClientConfig(client_opts, module)
    except ClientConfigException as e:
        module.fail_json(msg="Failed to get client configuration", exception=e)

    client_context = module.params['client_context']
    if config.has_context(client_context):
        if client_context != config.current_context():
            client_opts.append("--context=%s" % client_context)
    else:
        module.fail_json(msg="Context %s not found in client config" %
                         client_context)

    client_user = module.params['client_user']
    if config.has_user(client_user):
        if client_user != config.get_user_for_context(client_context):
            client_opts.append("--user=%s" % client_user)
    else:
        module.fail_json(msg="User %s not found in client config" %
                         client_user)

    client_cluster = module.params['client_cluster']
    if config.has_cluster(client_cluster):
        if client_cluster != config.get_cluster_for_context(client_cluster):
            client_opts.append("--cluster=%s" % client_cluster)
    else:
        module.fail_json(msg="Cluster %s not found in client config" %
                         client_cluster)

    # TODO: provide sane defaults for some (like hostname, externalIP,
    # internalIP, etc)
    node = Node(module, client_opts, module.params['api_version'],
                module.params['name'], module.params['host_ip'],
                module.params['hostnames'], module.params['external_ips'],
                module.params['internal_ips'], module.params['cpu'],
                module.params['memory'], module.params['labels'],
                module.params['annotations'], module.params['pod_cidr'],
                module.params['external_id'])

    # TODO: attempt to support changing node settings where possible and/or
    # modifying node resources
    if node.exists():
        module.exit_json(changed=False, node=node.get_node())
    elif module.check_mode:
        module.exit_json(changed=True, node=node.get_node())
    else:
        if node.create():
            module.exit_json(changed=True,
                             msg="Node created successfully",
                             node=node.get_node())
        else:
            module.fail_json(msg="Unknown error creating node",
                             node=node.get_node())


# import module snippets
from ansible.module_utils.basic import *
if __name__ == '__main__':
    main()