diff options
Diffstat (limited to 'roles/openshift_register_nodes')
-rw-r--r-- | roles/openshift_register_nodes/README.md | 34 | ||||
-rw-r--r-- | roles/openshift_register_nodes/defaults/main.yml | 5 | ||||
-rwxr-xr-x | roles/openshift_register_nodes/library/kubernetes_register_node.py | 371 | ||||
-rw-r--r-- | roles/openshift_register_nodes/meta/main.yml | 17 | ||||
-rw-r--r-- | roles/openshift_register_nodes/tasks/main.yml | 67 |
5 files changed, 494 insertions, 0 deletions
diff --git a/roles/openshift_register_nodes/README.md b/roles/openshift_register_nodes/README.md new file mode 100644 index 000000000..b96faa044 --- /dev/null +++ b/roles/openshift_register_nodes/README.md @@ -0,0 +1,34 @@ +OpenShift Register Nodes +======================== + +TODO + +Requirements +------------ + +TODO + +Role Variables +-------------- + +TODO + +Dependencies +------------ + +TODO + +Example Playbook +---------------- + +TODO + +License +------- + +Apache License Version 2.0 + +Author Information +------------------ + +Jason DeTiberus (jdetiber@redhat.com) diff --git a/roles/openshift_register_nodes/defaults/main.yml b/roles/openshift_register_nodes/defaults/main.yml new file mode 100644 index 000000000..3501e8922 --- /dev/null +++ b/roles/openshift_register_nodes/defaults/main.yml @@ -0,0 +1,5 @@ +--- +openshift_kube_api_version: v1beta1 +openshift_cert_dir: openshift.local.certificates +openshift_cert_dir_parent: /var/lib/openshift +openshift_cert_dir_abs: "{{ openshift_cert_dir_parent ~ '/' ~ openshift_cert_dir }}" 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 100755 index 000000000..8ebeb087a --- /dev/null +++ b/roles/openshift_register_nodes/library/kubernetes_register_node.py @@ -0,0 +1,371 @@ +#!/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, + externalID = externalID + ) + 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() diff --git a/roles/openshift_register_nodes/meta/main.yml b/roles/openshift_register_nodes/meta/main.yml new file mode 100644 index 000000000..e40a152c1 --- /dev/null +++ b/roles/openshift_register_nodes/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + author: Jason DeTiberus + description: + company: Red Hat, Inc. + license: Apache License, Version 2.0 + min_ansible_version: 1.8 + platforms: + - name: EL + versions: + - 7 + categories: + - cloud + - system +dependencies: +- { role: openshift_facts } + diff --git a/roles/openshift_register_nodes/tasks/main.yml b/roles/openshift_register_nodes/tasks/main.yml new file mode 100644 index 000000000..7319b88b1 --- /dev/null +++ b/roles/openshift_register_nodes/tasks/main.yml @@ -0,0 +1,67 @@ +--- +# TODO: support new create-config command to generate node certs and config +# TODO: recreate master/node configs if settings that affect the configs +# change (hostname, public_hostname, ip, public_ip, etc) + +# TODO: create a failed_when condition +- name: Create node server certificates + command: > + /usr/bin/openshift admin create-server-cert + --overwrite=false + --cert={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/server.crt + --key={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/server.key + --hostnames={{ [item.openshift.common.hostname, + item.openshift.common.public_hostname]|unique|join(",") }} + args: + chdir: "{{ openshift_cert_dir_parent }}" + creates: "{{ openshift_cert_dir_abs }}/node-{{ item.openshift.common.hostname }}/server.crt" + with_items: openshift_nodes + register: server_cert_result + +# TODO: create a failed_when condition +- name: Create node client certificates + command: > + /usr/bin/openshift admin create-node-cert + --overwrite=false + --cert={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/cert.crt + --key={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/key.key + --node-name={{ item.openshift.common.hostname }} + args: + chdir: "{{ openshift_cert_dir_parent }}" + creates: "{{ openshift_cert_dir_abs }}/node-{{ item.openshift.common.hostname }}/cert.crt" + with_items: openshift_nodes + register: node_cert_result + +# TODO: create a failed_when condition +- name: Create kubeconfigs for nodes + command: > + /usr/bin/openshift admin create-kubeconfig + --client-certificate={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/cert.crt + --client-key={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/key.key + --kubeconfig={{ openshift_cert_dir }}/node-{{ item.openshift.common.hostname }}/.kubeconfig + --master={{ openshift.master.api_url }} + --public-master={{ openshift.master.public_api_url }} + args: + chdir: "{{ openshift_cert_dir_parent }}" + creates: "{{ openshift_cert_dir_abs }}/node-{{ item.openshift.common.hostname }}/.kubeconfig" + with_items: openshift_nodes + register: kubeconfig_result + +- name: Register unregistered nodes + kubernetes_register_node: + client_user: openshift-client + name: "{{ item.openshift.common.hostname }}" + api_version: "{{ openshift_kube_api_version }}" + cpu: "{{ item.openshift.node.resources_cpu | default(None) }}" + memory: "{{ item.openshift.node.resources_memory | default(None) }}" + pod_cidr: "{{ item.openshift.node.pod_cidr | default(None) }}" + host_ip: "{{ item.openshift.common.ip }}" + labels: "{{ item.openshift.node.labels | default({}) }}" + annotations: "{{ item.openshift.node.annotations | default({}) }}" + external_id: "{{ item.openshift.node.external_id }}" + # TODO: support customizing other attributes such as: client_config, + # client_cluster, client_context, client_user + # TODO: update for v1beta3 changes after rebase: hostnames, external_ips, + # internal_ips, external_id + with_items: openshift_nodes + register: register_result |