summaryrefslogtreecommitdiffstats
path: root/roles/openshift_register_nodes/library/kubernetes_register_node.py
diff options
context:
space:
mode:
Diffstat (limited to 'roles/openshift_register_nodes/library/kubernetes_register_node.py')
-rw-r--r--roles/openshift_register_nodes/library/kubernetes_register_node.py370
1 files changed, 370 insertions, 0 deletions
diff --git a/roles/openshift_register_nodes/library/kubernetes_register_node.py b/roles/openshift_register_nodes/library/kubernetes_register_node.py
new file mode 100644
index 000000000..409215616
--- /dev/null
+++ b/roles/openshift_register_nodes/library/kubernetes_register_node.py
@@ -0,0 +1,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()