summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason DeTiberus <jdetiber@redhat.com>2015-03-18 00:05:51 -0400
committerJason DeTiberus <jdetiber@redhat.com>2015-03-18 15:48:45 -0400
commit7035459d20dd2d278b0a0e6ff96421639f6e0e34 (patch)
tree7bda0eee678972c507c2343984be37d31c821808
parent81628f94bad4b303212bf77752f62c03728e0168 (diff)
downloadopenshift-7035459d20dd2d278b0a0e6ff96421639f6e0e34.tar.gz
openshift-7035459d20dd2d278b0a0e6ff96421639f6e0e34.tar.bz2
openshift-7035459d20dd2d278b0a0e6ff96421639f6e0e34.tar.xz
openshift-7035459d20dd2d278b0a0e6ff96421639f6e0e34.zip
Register node fixes
- Set --hostname flag in node config in openshift_node role - Support some additional node attributes in openshift_node role - podCIDR - labels - annotations - Support both output types for openshift ex config view in openshift_register_node module - Support multiple api versions in openshift_register_node module - Support additional attributes in openshift_register_node module - annotations - labels - pod_cidr - external_ips (v1beta3, will be available after next kube rebase) - internal_ips (v1beta3, will be available after next kube rebase) - hostnames (v1beta3, will be available after next kube rebase) - external_id (v1beta3, will be available after next kube rebase)
-rw-r--r--roles/openshift_node/defaults/main.yml8
-rw-r--r--roles/openshift_node/library/openshift_register_node.py453
-rw-r--r--roles/openshift_node/tasks/main.yml14
3 files changed, 333 insertions, 142 deletions
diff --git a/roles/openshift_node/defaults/main.yml b/roles/openshift_node/defaults/main.yml
index c45524f16..e4d5ebfee 100644
--- a/roles/openshift_node/defaults/main.yml
+++ b/roles/openshift_node/defaults/main.yml
@@ -5,6 +5,8 @@ os_firewall_allow:
- service: OpenShift kubelet
port: 10250/tcp
openshift_node_resources:
- capacity:
- cpu:
- memory:
+ cpu:
+ memory:
+ cidr:
+openshift_node_labels: {}
+openshift_node_annotations: {}
diff --git a/roles/openshift_node/library/openshift_register_node.py b/roles/openshift_node/library/openshift_register_node.py
index 63079e59b..4922585d7 100644
--- a/roles/openshift_node/library/openshift_register_node.py
+++ b/roles/openshift_node/library/openshift_register_node.py
@@ -6,78 +6,315 @@ import os
import multiprocessing
import socket
from subprocess import check_output, Popen
+from decimal import *
DOCUMENTATION = '''
---
-module: openshift_register_node
-short_description: This module registers an openshift-node with an openshift-master
-author: Jason DeTiberus
-requirements: [ openshift-node ]
-notes: Node resources can be specified using either the resources option or the following options: cpu, memory
+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:
- - id for this node (usually the node fqdn)
+ - Identifier for this node (usually the node fqdn).
required: true
- hostIP:
+ api_verison:
+ choices: ['v1beta1', 'v1beta3']
+ default: 'v1beta1'
description:
- - ip address for this node
+ - 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
- cpu:
+ hostnames:
+ default: []
description:
- - number of CPUs for this node
+ - Valid hostnames for this node. Available in the following API
+ versions: v1beta3.
required: false
- default: number of logical CPUs detected
- memory:
+ external_ips:
+ default: []
description:
- - Memory available for this node in bytes
+ - External IP Addresses for this node. Available in the following API
+ versions: v1beta3.
required: false
- default: 80% MemTotal
- resources:
+ internal_ips:
+ default: []
description:
- - A json string representing Node resources
+ - 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. If not provided, then
+ the node will be registered to advertise the number of logical
+ CPUs available. 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. If not provided, then the node
+ will be registered to advertise 80% of MemTotal as available
+ memory. 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 with all options (using cpu and memory options)
+# 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
- apiVersion: v1beta1
cpu: 1
- memory: 1073741824
+ memory: 500000000
-# Node registration with all options (using resources option)
+# 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
- hostIP: 192.168.1.1
- apiVersion: v1beta1
- resources:
- capacity:
- cpu: 1
- memory: 1073741824
+ 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 getLogicalCores():
+ return multiprocessing.cpu_count()
+
+ @staticmethod
+ def getMemoryPct(pct):
+ with open('/proc/meminfo', 'r') as mem:
+ for line in mem:
+ entries = line.split()
+ if str(entries.pop(0)) == 'MemTotal:':
+ mem_total_kb = Decimal(entries.pop(0))
+ mem_capacity_kb = mem_total_kb * Decimal(pct)
+ return str(mem_capacity_kb.to_integral_value() * 1024)
+
+ return ""
+
+ @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 if cpu else Util.getLogicalCores()
+ self.resources['capacity']['memory'] = memory if cpu else Util.getMemoryPct(.75)
+
+ 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 if cpu else Util.getLogicalCores()
+ self.spec['capacity']['memory'] = memory if memory else Util.getMemoryPct(.75)
+
+ 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),
- hostIP = dict(),
- apiVersion = dict(),
- cpu = dict(),
- memory = dict(),
- resources = dict(),
- client_config = dict(),
- client_cluster = dict(default = 'master'),
- client_context = dict(default = 'master'),
- client_user = dict(default = 'admin')
+ 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 = [
- ['resources', 'cpu'],
- ['resources', 'memory']
+ ['host_ip', 'external_ips'],
+ ['host_ip', 'internal_ips'],
+ ['host_ip', 'hostnames'],
],
supports_check_mode=True
)
@@ -93,119 +330,61 @@ def main():
client_opts.append("--kubeconfig=%s" % module.params['client_config'])
try:
- output = check_output(["/usr/bin/openshift", "ex", "config", "view",
- "-o", "json"] + client_opts,
- stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- module.fail_json(msg="Failed to get client configuration",
- command=e.cmd, returncode=e.returncode, output=e.output)
-
- config = json.loads(output)
- if not (bool(config['clusters']) or bool(config['contexts']) or
- bool(config['current-context']) or bool(config['users'])):
- module.fail_json(msg="Client config missing required values",
- output=output)
+ 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 client_context:
- config_context = next((context for context in config['contexts']
- if context['name'] == client_context), None)
- if not config_context:
- module.fail_json(msg="Context %s not found in client config" %
- client_context)
- if not config['current-context'] or config['current-context'] != 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 client_user:
- config_user = next((user for user in config['users']
- if user['name'] == client_user), None)
- if not config_user:
- module.fail_json(msg="User %s not found in client config" %
- client_user)
- if client_user != config_context['context']['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 client_cluster:
- config_cluster = next((cluster for cluster in config['clusters']
- if cluster['name'] == client_cluster), None)
- if not client_cluster:
- module.fail_json(msg="Cluster %s not found in client config" %
- client_cluster)
- if client_cluster != config_context['context']['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)
- node_def = dict(
- id = module.params['name'],
- kind = 'Node',
- apiVersion = 'v1beta1',
- resources = dict(
- capacity = dict()
- )
- )
-
- for key, value in module.params.iteritems():
- if key in ['cpu', 'memory']:
- node_def['resources']['capacity'][key] = value
- elif key == 'name':
- node_def['id'] = value
- elif key != 'client_config':
- if value:
- node_def[key] = value
+ # 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'])
- if not node_def['resources']['capacity']['cpu']:
- node_def['resources']['capacity']['cpu'] = multiprocessing.cpu_count()
-
- if not node_def['resources']['capacity']['memory']:
- with open('/proc/meminfo', 'r') as mem:
- for line in mem:
- entries = line.split()
- if str(entries.pop(0)) == 'MemTotal:':
- mem_total_kb = int(entries.pop(0))
- mem_capacity = int(mem_total_kb * 1024 * .75)
- node_def['resources']['capacity']['memory'] = mem_capacity
- break
-
- try:
- output = check_output(["/usr/bin/osc", "get", "nodes"] + client_opts,
- stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- module.fail_json(msg="Failed to get node list", command=e.cmd,
- returncode=e.returncode, output=e.output)
-
- if re.search(module.params['name'], output, re.MULTILINE):
- module.exit_json(changed=False, node_def=node_def)
+ # 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_def=node_def)
-
- config_def = dict(
- metadata = dict(
- name = "add-node-%s" % module.params['name']
- ),
- kind = 'Config',
- apiVersion = 'v1beta1',
- items = [node_def]
- )
-
- p = Popen(["/usr/bin/osc"] + client_opts + ["create", "node"] + ["-f", "-"],
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE, close_fds=True)
- (out, err) = p.communicate(module.jsonify(config_def))
- ret = p.returncode
-
- if ret != 0:
- if re.search("minion \"%s\" already exists" % module.params['name'],
- err):
- module.exit_json(changed=False,
- msg="node definition already exists", config_def=config_def)
+ 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="Node creation failed.", ret=ret, out=out,
- err=err, config_def=config_def)
+ module.fail_json(msg="Unknown error creating node",
+ node=node.get_node())
- module.exit_json(changed=True, out=out, err=err, ret=ret,
- node_def=config_def)
# import module snippets
from ansible.module_utils.basic import *
-main()
+if __name__ == '__main__':
+ main()
diff --git a/roles/openshift_node/tasks/main.yml b/roles/openshift_node/tasks/main.yml
index 6721c7401..e380ba1fb 100644
--- a/roles/openshift_node/tasks/main.yml
+++ b/roles/openshift_node/tasks/main.yml
@@ -21,7 +21,7 @@
lineinfile:
dest: /etc/sysconfig/openshift-node
regexp: '^OPTIONS='
- line: "OPTIONS=\"--master=https://{{ openshift_master_ips[0] }}:8443 --loglevel={{ openshift_node_debug_level }}\""
+ line: "OPTIONS=\"--master=https://{{ openshift_master_ips[0] }}:8443 --hostname={{ openshift_hostname }} --loglevel={{ openshift_node_debug_level }}\""
notify:
- restart openshift-node
@@ -75,4 +75,14 @@
- name: Register node (if not already registered)
openshift_register_node:
name: "{{ openshift_hostname }}"
- resources: "{{ openshift_node_resources }}"
+ api_version: v1beta1
+ cpu: "{{ openshift_node_resources.cpu }}"
+ memory: "{{ openshift_node_resources.memory }}"
+ pod_cidr: "{{ openshift_node_resources.cidr }}"
+ host_ip: "{{ ansible_default_ipv4.address }}"
+ labels: "{{ openshift_node_labels }}"
+ annotations: "{{ openshift_node_annotations }}"
+ # TODO: support customizing other attributes such as: client_config,
+ # client_cluster, client_context, client_user
+ # TODO: updated for v1beta3 changes after rebase: hostnames, external_ips,
+ # internal_ips, external_id