diff options
Diffstat (limited to 'roles')
44 files changed, 7725 insertions, 32 deletions
diff --git a/roles/lib_openshift/library/oc_adm_registry.py b/roles/lib_openshift/library/oc_adm_registry.py new file mode 100644 index 000000000..32a4f8637 --- /dev/null +++ b/roles/lib_openshift/library/oc_adm_registry.py @@ -0,0 +1,2522 @@ +#!/usr/bin/env python +# pylint: disable=missing-docstring +# flake8: noqa: T001 +# ___ ___ _ _ ___ ___ _ _____ ___ ___ +# / __| __| \| | __| _ \ /_\_ _| __| \ +# | (_ | _|| .` | _|| / / _ \| | | _|| |) | +# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____ +# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _| +# | |) | (_) | | .` | (_) || | | _|| |) | | | | +# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_| +# +# Copyright 2016 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*- +''' + OpenShiftCLI class that wraps the oc commands in a subprocess +''' +# pylint: disable=too-many-lines + +from __future__ import print_function +import atexit +import copy +import json +import os +import re +import shutil +import subprocess +import tempfile +# pylint: disable=import-error +try: + import ruamel.yaml as yaml +except ImportError: + import yaml + +from ansible.module_utils.basic import AnsibleModule + +# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: doc/registry -*- -*- -*- + +DOCUMENTATION = ''' +--- +module: oc_adm_registry +short_description: Module to manage openshift registry +description: + - Manage openshift registry programmatically. +options: + state: + description: + - The desired action when managing openshift registry + - present - update or create the registry + - absent - tear down the registry service and deploymentconfig + - list - returns the current representiation of a registry + required: false + default: False + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + name: + description: + - The name of the registry + required: false + default: None + aliases: [] + namespace: + description: + - The selector when filtering on node labels + required: false + default: None + aliases: [] + images: + description: + - The image to base this registry on - ${component} will be replaced with --type + required: 'openshift3/ose-${component}:${version}' + default: None + aliases: [] + latest_images: + description: + - If true, attempt to use the latest image for the registry instead of the latest release. + required: false + default: False + aliases: [] + labels: + description: + - A set of labels to uniquely identify the registry and its components. + required: false + default: None + aliases: [] + enforce_quota: + description: + - If set, the registry will refuse to write blobs if they exceed quota limits + required: False + default: False + aliases: [] + mount_host: + description: + - If set, the registry volume will be created as a host-mount at this path. + required: False + default: False + aliases: [] + ports: + description: + - A comma delimited list of ports or port pairs to expose on the registry pod. The default is set for 5000. + required: False + default: [5000] + aliases: [] + replicas: + description: + - The replication factor of the registry; commonly 2 when high availability is desired. + required: False + default: 1 + aliases: [] + selector: + description: + - Selector used to filter nodes on deployment. Used to run registries on a specific set of nodes. + required: False + default: None + aliases: [] + service_account: + description: + - Name of the service account to use to run the registry pod. + required: False + default: 'registry' + aliases: [] + tls_certificate: + description: + - An optional path to a PEM encoded certificate (which may contain the private key) for serving over TLS + required: false + default: None + aliases: [] + tls_key: + description: + - An optional path to a PEM encoded private key for serving over TLS + required: false + default: None + aliases: [] + volume_mounts: + description: + - The volume mounts for the registry. + required: false + default: None + aliases: [] + daemonset: + description: + - Use a daemonset instead of a deployment config. + required: false + default: False + aliases: [] + edits: + description: + - A list of modifications to make on the deploymentconfig + required: false + default: None + aliases: [] + env_vars: + description: + - A dictionary of modifications to make on the deploymentconfig. e.g. FOO: BAR + required: false + default: None + aliases: [] + force: + description: + - Force a registry update. + required: false + default: False + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: create a secure registry + oc_adm_registry: + name: docker-registry + service_account: registry + replicas: 2 + namespace: default + selector: type=infra + images: "registry.ops.openshift.com/openshift3/ose-${component}:${version}" + env_vars: + REGISTRY_CONFIGURATION_PATH: /etc/registryconfig/config.yml + REGISTRY_HTTP_TLS_CERTIFICATE: /etc/secrets/registry.crt + REGISTRY_HTTP_TLS_KEY: /etc/secrets/registry.key + REGISTRY_HTTP_SECRET: supersecret + volume_mounts: + - path: /etc/secrets + name: dockercerts + type: secret + secret_name: registry-secret + - path: /etc/registryconfig + name: dockersecrets + type: secret + secret_name: docker-registry-config + edits: + - key: spec.template.spec.containers[0].livenessProbe.httpGet.scheme + value: HTTPS + action: put + - key: spec.template.spec.containers[0].readinessProbe.httpGet.scheme + value: HTTPS + action: put + - key: spec.strategy.rollingParams + value: + intervalSeconds: 1 + maxSurge: 50% + maxUnavailable: 50% + timeoutSeconds: 600 + updatePeriodSeconds: 1 + action: put + - key: spec.template.spec.containers[0].resources.limits.memory + value: 2G + action: update + - key: spec.template.spec.containers[0].resources.requests.memory + value: 1G + action: update + + register: registryout + +''' + +# -*- -*- -*- End included fragment: doc/registry -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- +# noqa: E301,E302 + + +class YeditException(Exception): + ''' Exception class for Yedit ''' + pass + + +# pylint: disable=too-many-public-methods +class Yedit(object): + ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z%s/_-]+)" + com_sep = set(['.', '#', '|', ':']) + + # pylint: disable=too-many-arguments + def __init__(self, + filename=None, + content=None, + content_type='yaml', + separator='.', + backup=False): + self.content = content + self._separator = separator + self.filename = filename + self.__yaml_dict = content + self.content_type = content_type + self.backup = backup + self.load(content_type=self.content_type) + if self.__yaml_dict is None: + self.__yaml_dict = {} + + @property + def separator(self): + ''' getter method for yaml_dict ''' + return self._separator + + @separator.setter + def separator(self): + ''' getter method for yaml_dict ''' + return self._separator + + @property + def yaml_dict(self): + ''' getter method for yaml_dict ''' + return self.__yaml_dict + + @yaml_dict.setter + def yaml_dict(self, value): + ''' setter method for yaml_dict ''' + self.__yaml_dict = value + + @staticmethod + def parse_key(key, sep='.'): + '''parse the key allowing the appropriate separator''' + common_separators = list(Yedit.com_sep - set([sep])) + return re.findall(Yedit.re_key % ''.join(common_separators), key) + + @staticmethod + def valid_key(key, sep='.'): + '''validate the incoming key''' + common_separators = list(Yedit.com_sep - set([sep])) + if not re.match(Yedit.re_valid_key % ''.join(common_separators), key): + return False + + return True + + @staticmethod + def remove_entry(data, key, sep='.'): + ''' remove data at location key ''' + if key == '' and isinstance(data, dict): + data.clear() + return True + elif key == '' and isinstance(data, list): + del data[:] + return True + + if not (key and Yedit.valid_key(key, sep)) and \ + isinstance(data, (list, dict)): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501 + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True + + @staticmethod + def add_entry(data, key, item=None, sep='.'): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a#b + return c + ''' + if key == '': + pass + elif (not (key and Yedit.valid_key(key, sep)) and + isinstance(data, (list, dict))): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501 + data = data[dict_key] + continue + + elif data and not isinstance(data, dict): + return None + + data[dict_key] = {} + data = data[dict_key] + + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + if key == '': + data = item + + # process last index for add + # expected list entry + elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501 + data[int(key_indexes[-1][0])] = item + + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return data + + @staticmethod + def get_entry(data, key, sep='.'): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a.b + return c + ''' + if key == '': + pass + elif (not (key and Yedit.valid_key(key, sep)) and + isinstance(data, (list, dict))): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + return data + + @staticmethod + def _write(filename, contents): + ''' Actually write the file contents to disk. This helps with mocking. ''' + + tmp_filename = filename + '.yedit' + + with open(tmp_filename, 'w') as yfd: + yfd.write(contents) + + os.rename(tmp_filename, filename) + + def write(self): + ''' write to file ''' + if not self.filename: + raise YeditException('Please specify a filename.') + + if self.backup and self.file_exists(): + shutil.copy(self.filename, self.filename + '.orig') + + if hasattr(yaml, 'RoundTripDumper'): + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + self.yaml_dict.fa.set_block_style() + + # pylint: disable=no-member + Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper)) + else: + Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False)) + + return (True, self.yaml_dict) + + def read(self): + ''' read from file ''' + # check if it exists + if self.filename is None or not self.file_exists(): + return None + + contents = None + with open(self.filename) as yfd: + contents = yfd.read() + + return contents + + def file_exists(self): + ''' return whether file exists ''' + if os.path.exists(self.filename): + return True + + return False + + def load(self, content_type='yaml'): + ''' return yaml file ''' + contents = self.read() + + if not contents and not self.content: + return None + + if self.content: + if isinstance(self.content, dict): + self.yaml_dict = self.content + return self.yaml_dict + elif isinstance(self.content, str): + contents = self.content + + # check if it is yaml + try: + if content_type == 'yaml' and contents: + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripLoader'): + self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader) + else: + self.yaml_dict = yaml.safe_load(contents) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + self.yaml_dict.fa.set_block_style() + + elif content_type == 'json' and contents: + self.yaml_dict = json.loads(contents) + except yaml.YAMLError as err: + # Error loading yaml or json + raise YeditException('Problem with loading yaml file. %s' % err) + + return self.yaml_dict + + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key, self.separator) + except KeyError: + entry = None + + return entry + + def pop(self, path, key_or_item): + ''' remove a key, value pair from a dict or an item for a list''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + return (False, self.yaml_dict) + + if isinstance(entry, dict): + # pylint: disable=no-member,maybe-no-member + if key_or_item in entry: + entry.pop(key_or_item) + return (True, self.yaml_dict) + return (False, self.yaml_dict) + + elif isinstance(entry, list): + # pylint: disable=no-member,maybe-no-member + ind = None + try: + ind = entry.index(key_or_item) + except ValueError: + return (False, self.yaml_dict) + + entry.pop(ind) + return (True, self.yaml_dict) + + return (False, self.yaml_dict) + + def delete(self, path): + ''' remove path from a dict''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + return (False, self.yaml_dict) + + result = Yedit.remove_entry(self.yaml_dict, path, self.separator) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) + + def exists(self, path, value): + ''' check if value exists at path''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if isinstance(entry, list): + if value in entry: + return True + return False + + elif isinstance(entry, dict): + if isinstance(value, dict): + rval = False + for key, val in value.items(): + if entry[key] != val: + rval = False + break + else: + rval = True + return rval + + return value in entry + + return entry == value + + def append(self, path, value): + '''append value to a list''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + self.put(path, []) + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + if not isinstance(entry, list): + return (False, self.yaml_dict) + + # pylint: disable=no-member,maybe-no-member + entry.append(value) + return (True, self.yaml_dict) + + # pylint: disable=too-many-arguments + def update(self, path, value, index=None, curr_value=None): + ''' put path, value into a dict ''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if isinstance(entry, dict): + # pylint: disable=no-member,maybe-no-member + if not isinstance(value, dict): + raise YeditException('Cannot replace key, value entry in ' + + 'dict with non-dict type. value=[%s] [%s]' % (value, type(value))) # noqa: E501 + + entry.update(value) + return (True, self.yaml_dict) + + elif isinstance(entry, list): + # pylint: disable=no-member,maybe-no-member + ind = None + if curr_value: + try: + ind = entry.index(curr_value) + except ValueError: + return (False, self.yaml_dict) + + elif index is not None: + ind = index + + if ind is not None and entry[ind] != value: + entry[ind] = value + return (True, self.yaml_dict) + + # see if it exists in the list + try: + ind = entry.index(value) + except ValueError: + # doesn't exist, append it + entry.append(value) + return (True, self.yaml_dict) + + # already exists, return + if ind is not None: + return (False, self.yaml_dict) + return (False, self.yaml_dict) + + def put(self, path, value): + ''' put path, value into a dict ''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry == value: + return (False, self.yaml_dict) + + # deepcopy didn't work + if hasattr(yaml, 'round_trip_dump'): + # pylint: disable=no-member + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + tmp_copy.fa.set_block_style() + + else: + tmp_copy = copy.deepcopy(self.yaml_dict) + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if not result: + return (False, self.yaml_dict) + + self.yaml_dict = tmp_copy + + return (True, self.yaml_dict) + + def create(self, path, value): + ''' create a yaml file ''' + if not self.file_exists(): + # deepcopy didn't work + if hasattr(yaml, 'round_trip_dump'): + # pylint: disable=no-member + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, default_flow_style=False), # noqa: E501 + yaml.RoundTripLoader) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + tmp_copy.fa.set_block_style() + else: + tmp_copy = copy.deepcopy(self.yaml_dict) + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result: + self.yaml_dict = tmp_copy + return (True, self.yaml_dict) + + return (False, self.yaml_dict) + + @staticmethod + def get_curr_value(invalue, val_type): + '''return the current value''' + if invalue is None: + return None + + curr_value = invalue + if val_type == 'yaml': + curr_value = yaml.load(invalue) + elif val_type == 'json': + curr_value = json.loads(invalue) + + return curr_value + + @staticmethod + def parse_value(inc_value, vtype=''): + '''determine value type passed''' + true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE', + 'on', 'On', 'ON', ] + false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE', + 'off', 'Off', 'OFF'] + + # It came in as a string but you didn't specify value_type as string + # we will convert to bool if it matches any of the above cases + if isinstance(inc_value, str) and 'bool' in vtype: + if inc_value not in true_bools and inc_value not in false_bools: + raise YeditException('Not a boolean type. str=[%s] vtype=[%s]' + % (inc_value, vtype)) + elif isinstance(inc_value, bool) and 'str' in vtype: + inc_value = str(inc_value) + + # If vtype is not str then go ahead and attempt to yaml load it. + if isinstance(inc_value, str) and 'str' not in vtype: + try: + inc_value = yaml.load(inc_value) + except Exception: + raise YeditException('Could not determine type of incoming ' + + 'value. value=[%s] vtype=[%s]' + % (type(inc_value), vtype)) + + return inc_value + + # pylint: disable=too-many-return-statements,too-many-branches + @staticmethod + def run_ansible(module): + '''perform the idempotent crud operations''' + yamlfile = Yedit(filename=module.params['src'], + backup=module.params['backup'], + separator=module.params['separator']) + + if module.params['src']: + rval = yamlfile.load() + + if yamlfile.yaml_dict is None and \ + module.params['state'] != 'present': + return {'failed': True, + 'msg': 'Error opening file [%s]. Verify that the ' + + 'file exists, that it is has correct' + + ' permissions, and is valid yaml.'} + + if module.params['state'] == 'list': + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + yamlfile.yaml_dict = content + + if module.params['key']: + rval = yamlfile.get(module.params['key']) or {} + + return {'changed': False, 'result': rval, 'state': "list"} + + elif module.params['state'] == 'absent': + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + yamlfile.yaml_dict = content + + if module.params['update']: + rval = yamlfile.pop(module.params['key'], + module.params['value']) + else: + rval = yamlfile.delete(module.params['key']) + + if rval[0] and module.params['src']: + yamlfile.write() + + return {'changed': rval[0], 'result': rval[1], 'state': "absent"} + + elif module.params['state'] == 'present': + # check if content is different than what is in the file + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + + # We had no edits to make and the contents are the same + if yamlfile.yaml_dict == content and \ + module.params['value'] is None: + return {'changed': False, + 'result': yamlfile.yaml_dict, + 'state': "present"} + + yamlfile.yaml_dict = content + + # we were passed a value; parse it + if module.params['value']: + value = Yedit.parse_value(module.params['value'], + module.params['value_type']) + key = module.params['key'] + if module.params['update']: + # pylint: disable=line-too-long + curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501 + module.params['curr_value_format']) # noqa: E501 + + rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501 + + elif module.params['append']: + rval = yamlfile.append(key, value) + else: + rval = yamlfile.put(key, value) + + if rval[0] and module.params['src']: + yamlfile.write() + + return {'changed': rval[0], + 'result': rval[1], 'state': "present"} + + # no edits to make + if module.params['src']: + # pylint: disable=redefined-variable-type + rval = yamlfile.write() + return {'changed': rval[0], + 'result': rval[1], + 'state': "present"} + + return {'failed': True, 'msg': 'Unkown state passed'} + +# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*- +# pylint: disable=too-many-lines +# noqa: E301,E302,E303,T001 + + +class OpenShiftCLIError(Exception): + '''Exception class for openshiftcli''' + pass + + +# pylint: disable=too-few-public-methods +class OpenShiftCLI(object): + ''' Class to wrap the command line tools ''' + def __init__(self, + namespace, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False, + all_namespaces=False): + ''' Constructor for OpenshiftCLI ''' + self.namespace = namespace + self.verbose = verbose + self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig) + self.all_namespaces = all_namespaces + + # Pylint allows only 5 arguments to be passed. + # pylint: disable=too-many-arguments + def _replace_content(self, resource, rname, content, force=False, sep='.'): + ''' replace the current object with the content ''' + res = self._get(resource, rname) + if not res['results']: + return res + + fname = Utils.create_tmpfile(rname + '-') + + yed = Yedit(fname, res['results'][0], separator=sep) + changes = [] + for key, value in content.items(): + changes.append(yed.put(key, value)) + + if any([change[0] for change in changes]): + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self._replace(fname, force) + + return {'returncode': 0, 'updated': False} + + def _replace(self, fname, force=False): + '''replace the current object with oc replace''' + cmd = ['replace', '-f', fname] + if force: + cmd.append('--force') + return self.openshift_cmd(cmd) + + def _create_from_content(self, rname, content): + '''create a temporary file and then call oc create on it''' + fname = Utils.create_tmpfile(rname + '-') + yed = Yedit(fname, content=content) + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self._create(fname) + + def _create(self, fname): + '''call oc create on a filename''' + return self.openshift_cmd(['create', '-f', fname]) + + def _delete(self, resource, rname, selector=None): + '''call oc delete on a resource''' + cmd = ['delete', resource, rname] + if selector: + cmd.append('--selector=%s' % selector) + + return self.openshift_cmd(cmd) + + def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501 + '''process a template + + template_name: the name of the template to process + create: whether to send to oc create after processing + params: the parameters for the template + template_data: the incoming template's data; instead of a file + ''' + cmd = ['process'] + if template_data: + cmd.extend(['-f', '-']) + else: + cmd.append(template_name) + if params: + param_str = ["%s=%s" % (key, value) for key, value in params.items()] + cmd.append('-v') + cmd.extend(param_str) + + results = self.openshift_cmd(cmd, output=True, input_data=template_data) + + if results['returncode'] != 0 or not create: + return results + + fname = Utils.create_tmpfile(template_name + '-') + yed = Yedit(fname, results['results']) + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self.openshift_cmd(['create', '-f', fname]) + + def _get(self, resource, rname=None, selector=None): + '''return a resource by name ''' + cmd = ['get', resource] + if selector: + cmd.append('--selector=%s' % selector) + elif rname: + cmd.append(rname) + + cmd.extend(['-o', 'json']) + + rval = self.openshift_cmd(cmd, output=True) + + # Ensure results are retuned in an array + if 'items' in rval: + rval['results'] = rval['items'] + elif not isinstance(rval['results'], list): + rval['results'] = [rval['results']] + + return rval + + def _schedulable(self, node=None, selector=None, schedulable=True): + ''' perform oadm manage-node scheduable ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + cmd.append('--schedulable=%s' % schedulable) + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501 + + def _list_pods(self, node=None, selector=None, pod_selector=None): + ''' perform oadm list pods + + node: the node in which to list pods + selector: the label selector filter if provided + pod_selector: the pod selector filter if provided + ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + if pod_selector: + cmd.append('--pod-selector=%s' % pod_selector) + + cmd.extend(['--list-pods', '-o', 'json']) + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') + + # pylint: disable=too-many-arguments + def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False): + ''' perform oadm manage-node evacuate ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + if dry_run: + cmd.append('--dry-run') + + if pod_selector: + cmd.append('--pod-selector=%s' % pod_selector) + + if grace_period: + cmd.append('--grace-period=%s' % int(grace_period)) + + if force: + cmd.append('--force') + + cmd.append('--evacuate') + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') + + def _version(self): + ''' return the openshift version''' + return self.openshift_cmd(['version'], output=True, output_type='raw') + + def _import_image(self, url=None, name=None, tag=None): + ''' perform image import ''' + cmd = ['import-image'] + + image = '{0}'.format(name) + if tag: + image += ':{0}'.format(tag) + + cmd.append(image) + + if url: + cmd.append('--from={0}/{1}'.format(url, image)) + + cmd.append('-n{0}'.format(self.namespace)) + + cmd.append('--confirm') + return self.openshift_cmd(cmd) + + def _run(self, cmds, input_data): + ''' Actually executes the command. This makes mocking easier. ''' + curr_env = os.environ.copy() + curr_env.update({'KUBECONFIG': self.kubeconfig}) + proc = subprocess.Popen(cmds, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=curr_env) + + stdout, stderr = proc.communicate(input_data) + + return proc.returncode, stdout.decode(), stderr.decode() + + # pylint: disable=too-many-arguments,too-many-branches + def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None): + '''Base command for oc ''' + cmds = [] + if oadm: + cmds = ['oadm'] + else: + cmds = ['oc'] + + if self.all_namespaces: + cmds.extend(['--all-namespaces']) + elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 + cmds.extend(['-n', self.namespace]) + + cmds.extend(cmd) + + rval = {} + results = '' + err = None + + if self.verbose: + print(' '.join(cmds)) + + returncode, stdout, stderr = self._run(cmds, input_data) + + rval = {"returncode": returncode, + "results": results, + "cmd": ' '.join(cmds)} + + if returncode == 0: + if output: + if output_type == 'json': + try: + rval['results'] = json.loads(stdout) + except ValueError as err: + if "No JSON object could be decoded" in err.args: + err = err.args + elif output_type == 'raw': + rval['results'] = stdout + + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds}) + + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}}) + + return rval + + +class Utils(object): + ''' utilities for openshiftcli modules ''' + + @staticmethod + def _write(filename, contents): + ''' Actually write the file contents to disk. This helps with mocking. ''' + + with open(filename, 'w') as sfd: + sfd.write(contents) + + @staticmethod + def create_tmp_file_from_contents(rname, data, ftype='yaml'): + ''' create a file in tmp with name and contents''' + + tmp = Utils.create_tmpfile(prefix=rname) + + if ftype == 'yaml': + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripDumper'): + Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper)) + else: + Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False)) + + elif ftype == 'json': + Utils._write(tmp, json.dumps(data)) + else: + Utils._write(tmp, data) + + # Register cleanup when module is done + atexit.register(Utils.cleanup, [tmp]) + return tmp + + @staticmethod + def create_tmpfile_copy(inc_file): + '''create a temporary copy of a file''' + tmpfile = Utils.create_tmpfile('lib_openshift-') + Utils._write(tmpfile, open(inc_file).read()) + + # Cleanup the tmpfile + atexit.register(Utils.cleanup, [tmpfile]) + + return tmpfile + + @staticmethod + def create_tmpfile(prefix='tmp'): + ''' Generates and returns a temporary file name ''' + + with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp: + return tmp.name + + @staticmethod + def create_tmp_files_from_contents(content, content_type=None): + '''Turn an array of dict: filename, content into a files array''' + if not isinstance(content, list): + content = [content] + files = [] + for item in content: + path = Utils.create_tmp_file_from_contents(item['path'] + '-', + item['data'], + ftype=content_type) + files.append({'name': os.path.basename(item['path']), + 'path': path}) + return files + + @staticmethod + def cleanup(files): + '''Clean up on exit ''' + for sfile in files: + if os.path.exists(sfile): + if os.path.isdir(sfile): + shutil.rmtree(sfile) + elif os.path.isfile(sfile): + os.remove(sfile) + + @staticmethod + def exists(results, _name): + ''' Check to see if the results include the name ''' + if not results: + return False + + if Utils.find_result(results, _name): + return True + + return False + + @staticmethod + def find_result(results, _name): + ''' Find the specified result by name''' + rval = None + for result in results: + if 'metadata' in result and result['metadata']['name'] == _name: + rval = result + break + + return rval + + @staticmethod + def get_resource_file(sfile, sfile_type='yaml'): + ''' return the service file ''' + contents = None + with open(sfile) as sfd: + contents = sfd.read() + + if sfile_type == 'yaml': + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripLoader'): + contents = yaml.load(contents, yaml.RoundTripLoader) + else: + contents = yaml.safe_load(contents) + elif sfile_type == 'json': + contents = json.loads(contents) + + return contents + + @staticmethod + def filter_versions(stdout): + ''' filter the oc version output ''' + + version_dict = {} + version_search = ['oc', 'openshift', 'kubernetes'] + + for line in stdout.strip().split('\n'): + for term in version_search: + if not line: + continue + if line.startswith(term): + version_dict[term] = line.split()[-1] + + # horrible hack to get openshift version in Openshift 3.2 + # By default "oc version in 3.2 does not return an "openshift" version + if "openshift" not in version_dict: + version_dict["openshift"] = version_dict["oc"] + + return version_dict + + @staticmethod + def add_custom_versions(versions): + ''' create custom versions strings ''' + + versions_dict = {} + + for tech, version in versions.items(): + # clean up "-" from version + if "-" in version: + version = version.split("-")[0] + + if version.startswith('v'): + versions_dict[tech + '_numeric'] = version[1:].split('+')[0] + # "v3.3.0.33" is what we have, we want "3.3" + versions_dict[tech + '_short'] = version[1:4] + + return versions_dict + + @staticmethod + def openshift_installed(): + ''' check if openshift is installed ''' + import yum + + yum_base = yum.YumBase() + if yum_base.rpmdb.searchNevra(name='atomic-openshift'): + return True + + return False + + # Disabling too-many-branches. This is a yaml dictionary comparison function + # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements + @staticmethod + def check_def_equal(user_def, result_def, skip_keys=None, debug=False): + ''' Given a user defined definition, compare it with the results given back by our query. ''' + + # Currently these values are autogenerated and we do not need to check them + skip = ['metadata', 'status'] + if skip_keys: + skip.extend(skip_keys) + + for key, value in result_def.items(): + if key in skip: + continue + + # Both are lists + if isinstance(value, list): + if key not in user_def: + if debug: + print('User data does not have key [%s]' % key) + print('User data: %s' % user_def) + return False + + if not isinstance(user_def[key], list): + if debug: + print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key])) + return False + + if len(user_def[key]) != len(value): + if debug: + print("List lengths are not equal.") + print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value))) + print("user_def: %s" % user_def[key]) + print("value: %s" % value) + return False + + for values in zip(user_def[key], value): + if isinstance(values[0], dict) and isinstance(values[1], dict): + if debug: + print('sending list - list') + print(type(values[0])) + print(type(values[1])) + result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug) + if not result: + print('list compare returned false') + return False + + elif value != user_def[key]: + if debug: + print('value should be identical') + print(value) + print(user_def[key]) + return False + + # recurse on a dictionary + elif isinstance(value, dict): + if key not in user_def: + if debug: + print("user_def does not have key [%s]" % key) + return False + if not isinstance(user_def[key], dict): + if debug: + print("dict returned false: not instance of dict") + return False + + # before passing ensure keys match + api_values = set(value.keys()) - set(skip) + user_values = set(user_def[key].keys()) - set(skip) + if api_values != user_values: + if debug: + print("keys are not equal in dict") + print(api_values) + print(user_values) + return False + + result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug) + if not result: + if debug: + print("dict returned false") + print(result) + return False + + # Verify each key, value pair is the same + else: + if key not in user_def or value != user_def[key]: + if debug: + print("value not equal; user_def does not have key") + print(key) + print(value) + if key in user_def: + print(user_def[key]) + return False + + if debug: + print('returning true') + return True + + +class OpenShiftCLIConfig(object): + '''Generic Config''' + def __init__(self, rname, namespace, kubeconfig, options): + self.kubeconfig = kubeconfig + self.name = rname + self.namespace = namespace + self._options = options + + @property + def config_options(self): + ''' return config options ''' + return self._options + + def to_option_list(self): + '''return all options as a string''' + return self.stringify() + + def stringify(self): + ''' return the options hash as cli params in a string ''' + rval = [] + for key, data in self.config_options.items(): + if data['include'] \ + and (data['value'] or isinstance(data['value'], int)): + rval.append('--%s=%s' % (key.replace('_', '-'), data['value'])) + + return rval + + +# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*- + + +# pylint: disable=too-many-public-methods +class DeploymentConfig(Yedit): + ''' Class to model an openshift DeploymentConfig''' + default_deployment_config = ''' +apiVersion: v1 +kind: DeploymentConfig +metadata: + name: default_dc + namespace: default +spec: + replicas: 0 + selector: + default_dc: default_dc + strategy: + resources: {} + rollingParams: + intervalSeconds: 1 + maxSurge: 0 + maxUnavailable: 25% + timeoutSeconds: 600 + updatePercent: -25 + updatePeriodSeconds: 1 + type: Rolling + template: + metadata: + spec: + containers: + - env: + - name: default + value: default + image: default + imagePullPolicy: IfNotPresent + name: default_dc + ports: + - containerPort: 8000 + hostPort: 8000 + protocol: TCP + name: default_port + resources: {} + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + hostNetwork: true + nodeSelector: + type: compute + restartPolicy: Always + securityContext: {} + serviceAccount: default + serviceAccountName: default + terminationGracePeriodSeconds: 30 + triggers: + - type: ConfigChange +''' + + replicas_path = "spec.replicas" + env_path = "spec.template.spec.containers[0].env" + volumes_path = "spec.template.spec.volumes" + container_path = "spec.template.spec.containers" + volume_mounts_path = "spec.template.spec.containers[0].volumeMounts" + + def __init__(self, content=None): + ''' Constructor for deploymentconfig ''' + if not content: + content = DeploymentConfig.default_deployment_config + + super(DeploymentConfig, self).__init__(content=content) + + # pylint: disable=no-member + def add_env_value(self, key, value): + ''' add key, value pair to env array ''' + rval = False + env = self.get_env_vars() + if env: + env.append({'name': key, 'value': value}) + rval = True + else: + result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value}) + rval = result[0] + + return rval + + def exists_env_value(self, key, value): + ''' return whether a key, value pair exists ''' + results = self.get_env_vars() + if not results: + return False + + for result in results: + if result['name'] == key and result['value'] == value: + return True + + return False + + def exists_env_key(self, key): + ''' return whether a key, value pair exists ''' + results = self.get_env_vars() + if not results: + return False + + for result in results: + if result['name'] == key: + return True + + return False + + def get_env_vars(self): + '''return a environment variables ''' + return self.get(DeploymentConfig.env_path) or [] + + def delete_env_var(self, keys): + '''delete a list of keys ''' + if not isinstance(keys, list): + keys = [keys] + + env_vars_array = self.get_env_vars() + modified = False + idx = None + for key in keys: + for env_idx, env_var in enumerate(env_vars_array): + if env_var['name'] == key: + idx = env_idx + break + + if idx: + modified = True + del env_vars_array[idx] + + if modified: + return True + + return False + + def update_env_var(self, key, value): + '''place an env in the env var list''' + + env_vars_array = self.get_env_vars() + idx = None + for env_idx, env_var in enumerate(env_vars_array): + if env_var['name'] == key: + idx = env_idx + break + + if idx: + env_vars_array[idx]['value'] = value + else: + self.add_env_value(key, value) + + return True + + def exists_volume_mount(self, volume_mount): + ''' return whether a volume mount exists ''' + exist_volume_mounts = self.get_volume_mounts() + + if not exist_volume_mounts: + return False + + volume_mount_found = False + for exist_volume_mount in exist_volume_mounts: + if exist_volume_mount['name'] == volume_mount['name']: + volume_mount_found = True + break + + return volume_mount_found + + def exists_volume(self, volume): + ''' return whether a volume exists ''' + exist_volumes = self.get_volumes() + + volume_found = False + for exist_volume in exist_volumes: + if exist_volume['name'] == volume['name']: + volume_found = True + break + + return volume_found + + def find_volume_by_name(self, volume, mounts=False): + ''' return the index of a volume ''' + volumes = [] + if mounts: + volumes = self.get_volume_mounts() + else: + volumes = self.get_volumes() + for exist_volume in volumes: + if exist_volume['name'] == volume['name']: + return exist_volume + + return None + + def get_replicas(self): + ''' return replicas setting ''' + return self.get(DeploymentConfig.replicas_path) + + def get_volume_mounts(self): + '''return volume mount information ''' + return self.get_volumes(mounts=True) + + def get_volumes(self, mounts=False): + '''return volume mount information ''' + if mounts: + return self.get(DeploymentConfig.volume_mounts_path) or [] + + return self.get(DeploymentConfig.volumes_path) or [] + + def delete_volume_by_name(self, volume): + '''delete a volume ''' + modified = False + exist_volume_mounts = self.get_volume_mounts() + exist_volumes = self.get_volumes() + del_idx = None + for idx, exist_volume in enumerate(exist_volumes): + if 'name' in exist_volume and exist_volume['name'] == volume['name']: + del_idx = idx + break + + if del_idx != None: + del exist_volumes[del_idx] + modified = True + + del_idx = None + for idx, exist_volume_mount in enumerate(exist_volume_mounts): + if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']: + del_idx = idx + break + + if del_idx != None: + del exist_volume_mounts[idx] + modified = True + + return modified + + def add_volume_mount(self, volume_mount): + ''' add a volume or volume mount to the proper location ''' + exist_volume_mounts = self.get_volume_mounts() + + if not exist_volume_mounts and volume_mount: + self.put(DeploymentConfig.volume_mounts_path, [volume_mount]) + else: + exist_volume_mounts.append(volume_mount) + + def add_volume(self, volume): + ''' add a volume or volume mount to the proper location ''' + exist_volumes = self.get_volumes() + if not volume: + return + + if not exist_volumes: + self.put(DeploymentConfig.volumes_path, [volume]) + else: + exist_volumes.append(volume) + + def update_replicas(self, replicas): + ''' update replicas value ''' + self.put(DeploymentConfig.replicas_path, replicas) + + def update_volume(self, volume): + '''place an env in the env var list''' + exist_volumes = self.get_volumes() + + if not volume: + return False + + # update the volume + update_idx = None + for idx, exist_vol in enumerate(exist_volumes): + if exist_vol['name'] == volume['name']: + update_idx = idx + break + + if update_idx != None: + exist_volumes[update_idx] = volume + else: + self.add_volume(volume) + + return True + + def update_volume_mount(self, volume_mount): + '''place an env in the env var list''' + modified = False + + exist_volume_mounts = self.get_volume_mounts() + + if not volume_mount: + return False + + # update the volume mount + for exist_vol_mount in exist_volume_mounts: + if exist_vol_mount['name'] == volume_mount['name']: + if 'mountPath' in exist_vol_mount and \ + str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']): + exist_vol_mount['mountPath'] = volume_mount['mountPath'] + modified = True + break + + if not modified: + self.add_volume_mount(volume_mount) + modified = True + + return modified + + def needs_update_volume(self, volume, volume_mount): + ''' verify a volume update is needed ''' + exist_volume = self.find_volume_by_name(volume) + exist_volume_mount = self.find_volume_by_name(volume, mounts=True) + results = [] + results.append(exist_volume['name'] == volume['name']) + + if 'secret' in volume: + results.append('secret' in exist_volume) + results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName']) + results.append(exist_volume_mount['name'] == volume_mount['name']) + results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath']) + + elif 'emptyDir' in volume: + results.append(exist_volume_mount['name'] == volume['name']) + results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath']) + + elif 'persistentVolumeClaim' in volume: + pvc = 'persistentVolumeClaim' + results.append(pvc in exist_volume) + if results[-1]: + results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName']) + + if 'claimSize' in volume[pvc]: + results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize']) + + elif 'hostpath' in volume: + results.append('hostPath' in exist_volume) + results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath']) + + return not all(results) + + def needs_update_replicas(self, replicas): + ''' verify whether a replica update is needed ''' + current_reps = self.get(DeploymentConfig.replicas_path) + return not current_reps == replicas + +# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*- + +# pylint: disable=too-many-instance-attributes +class SecretConfig(object): + ''' Handle secret options ''' + # pylint: disable=too-many-arguments + def __init__(self, + sname, + namespace, + kubeconfig, + secrets=None): + ''' constructor for handling secret options ''' + self.kubeconfig = kubeconfig + self.name = sname + self.namespace = namespace + self.secrets = secrets + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' assign the correct properties for a secret dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'Secret' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + self.data['data'] = {} + if self.secrets: + for key, value in self.secrets.items(): + self.data['data'][key] = value + +# pylint: disable=too-many-instance-attributes +class Secret(Yedit): + ''' Class to wrap the oc command line tools ''' + secret_path = "data" + kind = 'secret' + + def __init__(self, content): + '''secret constructor''' + super(Secret, self).__init__(content=content) + self._secrets = None + + @property + def secrets(self): + '''secret property getter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + @secrets.setter + def secrets(self): + '''secret property setter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + def get_secrets(self): + ''' returns all of the defined secrets ''' + return self.get(Secret.secret_path) or {} + + def add_secret(self, key, value): + ''' add a secret ''' + if self.secrets: + self.secrets[key] = value + else: + self.put(Secret.secret_path, {key: value}) + + return True + + def delete_secret(self, key): + ''' delete secret''' + try: + del self.secrets[key] + except KeyError as _: + return False + + return True + + def find_secret(self, key): + ''' find secret''' + rval = None + try: + rval = self.secrets[key] + except KeyError as _: + return None + + return {'key': key, 'value': rval} + + def update_secret(self, key, value): + ''' update a secret''' + # pylint: disable=no-member + if key in self.secrets: + self.secrets[key] = value + else: + self.add_secret(key, value) + + return True + +# -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*- + + +# pylint: disable=too-many-instance-attributes +class ServiceConfig(object): + ''' Handle service options ''' + # pylint: disable=too-many-arguments + def __init__(self, + sname, + namespace, + ports, + selector=None, + labels=None, + cluster_ip=None, + portal_ip=None, + session_affinity=None, + service_type=None): + ''' constructor for handling service options ''' + self.name = sname + self.namespace = namespace + self.ports = ports + self.selector = selector + self.labels = labels + self.cluster_ip = cluster_ip + self.portal_ip = portal_ip + self.session_affinity = session_affinity + self.service_type = service_type + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' instantiates a service dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'Service' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + if self.labels: + for lab, lab_value in self.labels.items(): + self.data['metadata'][lab] = lab_value + self.data['spec'] = {} + + if self.ports: + self.data['spec']['ports'] = self.ports + else: + self.data['spec']['ports'] = [] + + if self.selector: + self.data['spec']['selector'] = self.selector + + self.data['spec']['sessionAffinity'] = self.session_affinity or 'None' + + if self.cluster_ip: + self.data['spec']['clusterIP'] = self.cluster_ip + + if self.portal_ip: + self.data['spec']['portalIP'] = self.portal_ip + + if self.service_type: + self.data['spec']['type'] = self.service_type + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class Service(Yedit): + ''' Class to model the oc service object ''' + port_path = "spec.ports" + portal_ip = "spec.portalIP" + cluster_ip = "spec.clusterIP" + kind = 'Service' + + def __init__(self, content): + '''Service constructor''' + super(Service, self).__init__(content=content) + + def get_ports(self): + ''' get a list of ports ''' + return self.get(Service.port_path) or [] + + def add_ports(self, inc_ports): + ''' add a port object to the ports list ''' + if not isinstance(inc_ports, list): + inc_ports = [inc_ports] + + ports = self.get_ports() + if not ports: + self.put(Service.port_path, inc_ports) + else: + ports.extend(inc_ports) + + return True + + def find_ports(self, inc_port): + ''' find a specific port ''' + for port in self.get_ports(): + if port['port'] == inc_port['port']: + return port + + return None + + def delete_ports(self, inc_ports): + ''' remove a port from a service ''' + if not isinstance(inc_ports, list): + inc_ports = [inc_ports] + + ports = self.get(Service.port_path) or [] + + if not ports: + return True + + removed = False + for inc_port in inc_ports: + port = self.find_ports(inc_port) + if port: + ports.remove(port) + removed = True + + return removed + + def add_cluster_ip(self, sip): + '''add cluster ip''' + self.put(Service.cluster_ip, sip) + + def add_portal_ip(self, pip): + '''add cluster ip''' + self.put(Service.portal_ip, pip) + +# -*- -*- -*- End included fragment: lib/service.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/volume.py -*- -*- -*- + +class Volume(object): + ''' Class to model an openshift volume object''' + volume_mounts_path = {"pod": "spec.containers[0].volumeMounts", + "dc": "spec.template.spec.containers[0].volumeMounts", + "rc": "spec.template.spec.containers[0].volumeMounts", + } + volumes_path = {"pod": "spec.volumes", + "dc": "spec.template.spec.volumes", + "rc": "spec.template.spec.volumes", + } + + @staticmethod + def create_volume_structure(volume_info): + ''' return a properly structured volume ''' + volume_mount = None + volume = {'name': volume_info['name']} + if volume_info['type'] == 'secret': + volume['secret'] = {} + volume[volume_info['type']] = {'secretName': volume_info['secret_name']} + volume_mount = {'mountPath': volume_info['path'], + 'name': volume_info['name']} + elif volume_info['type'] == 'emptydir': + volume['emptyDir'] = {} + volume_mount = {'mountPath': volume_info['path'], + 'name': volume_info['name']} + elif volume_info['type'] == 'pvc': + volume['persistentVolumeClaim'] = {} + volume['persistentVolumeClaim']['claimName'] = volume_info['claimName'] + volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize'] + elif volume_info['type'] == 'hostpath': + volume['hostPath'] = {} + volume['hostPath']['path'] = volume_info['path'] + + return (volume, volume_mount) + +# -*- -*- -*- End included fragment: lib/volume.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: class/oc_version.py -*- -*- -*- + + +# pylint: disable=too-many-instance-attributes +class OCVersion(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + # pylint allows 5 + # pylint: disable=too-many-arguments + def __init__(self, + config, + debug): + ''' Constructor for OCVersion ''' + super(OCVersion, self).__init__(None, config) + self.debug = debug + + def get(self): + '''get and return version information ''' + + results = {} + + version_results = self._version() + + if version_results['returncode'] == 0: + filtered_vers = Utils.filter_versions(version_results['results']) + custom_vers = Utils.add_custom_versions(filtered_vers) + + results['returncode'] = version_results['returncode'] + results.update(filtered_vers) + results.update(custom_vers) + + return results + + raise OpenShiftCLIError('Problem detecting openshift version.') + + @staticmethod + def run_ansible(params): + '''run the idempotent ansible code''' + oc_version = OCVersion(params['kubeconfig'], params['debug']) + + if params['state'] == 'list': + + #pylint: disable=protected-access + result = oc_version.get() + return {'state': params['state'], + 'results': result, + 'changed': False} + +# -*- -*- -*- End included fragment: class/oc_version.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: class/oc_adm_registry.py -*- -*- -*- + +class RegistryException(Exception): + ''' Registry Exception Class ''' + pass + + +class RegistryConfig(OpenShiftCLIConfig): + ''' RegistryConfig is a DTO for the registry. ''' + def __init__(self, rname, namespace, kubeconfig, registry_options): + super(RegistryConfig, self).__init__(rname, namespace, kubeconfig, registry_options) + + +class Registry(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + + volume_mount_path = 'spec.template.spec.containers[0].volumeMounts' + volume_path = 'spec.template.spec.volumes' + env_path = 'spec.template.spec.containers[0].env' + + def __init__(self, + registry_config, + verbose=False): + ''' Constructor for Registry + + a registry consists of 3 or more parts + - dc/docker-registry + - svc/docker-registry + + Parameters: + :registry_config: + :verbose: + ''' + super(Registry, self).__init__(registry_config.namespace, registry_config.kubeconfig, verbose) + self.version = OCVersion(registry_config.kubeconfig, verbose) + self.svc_ip = None + self.portal_ip = None + self.config = registry_config + self.verbose = verbose + self.registry_parts = [{'kind': 'dc', 'name': self.config.name}, + {'kind': 'svc', 'name': self.config.name}, + ] + + self.__prepared_registry = None + self.volume_mounts = [] + self.volumes = [] + if self.config.config_options['volume_mounts']['value']: + for volume in self.config.config_options['volume_mounts']['value']: + volume_info = {'secret_name': volume.get('secret_name', None), + 'name': volume.get('name', None), + 'type': volume.get('type', None), + 'path': volume.get('path', None), + 'claimName': volume.get('claim_name', None), + 'claimSize': volume.get('claim_size', None), + } + + vol, vol_mount = Volume.create_volume_structure(volume_info) + self.volumes.append(vol) + self.volume_mounts.append(vol_mount) + + self.dconfig = None + self.svc = None + + @property + def deploymentconfig(self): + ''' deploymentconfig property ''' + return self.dconfig + + @deploymentconfig.setter + def deploymentconfig(self, config): + ''' setter for deploymentconfig property ''' + self.dconfig = config + + @property + def service(self): + ''' service property ''' + return self.svc + + @service.setter + def service(self, config): + ''' setter for service property ''' + self.svc = config + + @property + def prepared_registry(self): + ''' prepared_registry property ''' + if not self.__prepared_registry: + results = self.prepare_registry() + if not results: + raise RegistryException('Could not perform registry preparation.') + self.__prepared_registry = results + + return self.__prepared_registry + + @prepared_registry.setter + def prepared_registry(self, data): + ''' setter method for prepared_registry attribute ''' + self.__prepared_registry = data + + def get(self): + ''' return the self.registry_parts ''' + self.deploymentconfig = None + self.service = None + + rval = 0 + for part in self.registry_parts: + result = self._get(part['kind'], rname=part['name']) + if result['returncode'] == 0 and part['kind'] == 'dc': + self.deploymentconfig = DeploymentConfig(result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'svc': + self.service = Yedit(content=result['results'][0]) + + if result['returncode'] != 0: + rval = result['returncode'] + + + return {'returncode': rval, 'deploymentconfig': self.deploymentconfig, 'service': self.service} + + def exists(self): + '''does the object exist?''' + self.get() + if self.deploymentconfig or self.service: + return True + + return False + + def delete(self, complete=True): + '''return all pods ''' + parts = [] + for part in self.registry_parts: + if not complete and part['kind'] == 'svc': + continue + parts.append(self._delete(part['kind'], part['name'])) + + # Clean up returned results + rval = 0 + for part in parts: + # pylint: disable=invalid-sequence-index + if 'returncode' in part and part['returncode'] != 0: + rval = part['returncode'] + + return {'returncode': rval, 'results': parts} + + def prepare_registry(self): + ''' prepare a registry for instantiation ''' + options = self.config.to_option_list() + + cmd = ['registry', '-n', self.config.namespace] + cmd.extend(options) + cmd.extend(['--dry-run=True', '-o', 'json']) + + results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json') + # probably need to parse this + # pylint thinks results is a string + # pylint: disable=no-member + if results['returncode'] != 0 and results['results'].has_key('items'): + return results + + service = None + deploymentconfig = None + # pylint: disable=invalid-sequence-index + for res in results['results']['items']: + if res['kind'] == 'DeploymentConfig': + deploymentconfig = DeploymentConfig(res) + elif res['kind'] == 'Service': + service = Service(res) + + # Verify we got a service and a deploymentconfig + if not service or not deploymentconfig: + return results + + # results will need to get parsed here and modifications added + deploymentconfig = DeploymentConfig(self.add_modifications(deploymentconfig)) + + # modify service ip + if self.svc_ip: + service.put('spec.clusterIP', self.svc_ip) + if self.portal_ip: + service.put('spec.portalIP', self.portal_ip) + + # need to create the service and the deploymentconfig + service_file = Utils.create_tmp_file_from_contents('service', service.yaml_dict) + deployment_file = Utils.create_tmp_file_from_contents('deploymentconfig', deploymentconfig.yaml_dict) + + return {"service": service, + "service_file": service_file, + "service_update": False, + "deployment": deploymentconfig, + "deployment_file": deployment_file, + "deployment_update": False} + + def create(self): + '''Create a registry''' + results = [] + for config_file in ['deployment_file', 'service_file']: + results.append(self._create(self.prepared_registry[config_file])) + + # Clean up returned results + rval = 0 + for result in results: + # pylint: disable=invalid-sequence-index + if 'returncode' in result and result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def update(self): + '''run update for the registry. This performs a delete and then create ''' + # Store the current service IP + if self.service: + svcip = self.service.get('spec.clusterIP') + if svcip: + self.svc_ip = svcip + portip = self.service.get('spec.portalIP') + if portip: + self.portal_ip = portip + + results = [] + if self.prepared_registry['deployment_update']: + results.append(self._replace(self.prepared_registry['deployment_file'])) + if self.prepared_registry['service_update']: + results.append(self._replace(self.prepared_registry['service_file'])) + + # Clean up returned results + rval = 0 + for result in results: + if result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def add_modifications(self, deploymentconfig): + ''' update a deployment config with changes ''' + # Currently we know that our deployment of a registry requires a few extra modifications + # Modification 1 + # we need specific environment variables to be set + for key, value in self.config.config_options['env_vars']['value'].items(): + if not deploymentconfig.exists_env_key(key): + deploymentconfig.add_env_value(key, value) + else: + deploymentconfig.update_env_var(key, value) + + # Modification 2 + # we need specific volume variables to be set + for volume in self.volumes: + deploymentconfig.update_volume(volume) + + for vol_mount in self.volume_mounts: + deploymentconfig.update_volume_mount(vol_mount) + + # Modification 3 + # Edits + edit_results = [] + for edit in self.config.config_options['edits'].get('value', []): + if edit['action'] == 'put': + edit_results.append(deploymentconfig.put(edit['key'], + edit['value'])) + if edit['action'] == 'update': + edit_results.append(deploymentconfig.update(edit['key'], + edit['value'], + edit.get('index', None), + edit.get('curr_value', None))) + if edit['action'] == 'append': + edit_results.append(deploymentconfig.append(edit['key'], + edit['value'])) + + if edit_results and not any([res[0] for res in edit_results]): + return None + + return deploymentconfig.yaml_dict + + def needs_update(self): + ''' check to see if we need to update ''' + if not self.service or not self.deploymentconfig: + return True + + exclude_list = ['clusterIP', 'portalIP', 'type', 'protocol'] + if not Utils.check_def_equal(self.prepared_registry['service'].yaml_dict, + self.service.yaml_dict, + exclude_list, + debug=self.verbose): + self.prepared_registry['service_update'] = True + + exclude_list = ['dnsPolicy', + 'terminationGracePeriodSeconds', + 'restartPolicy', 'timeoutSeconds', + 'livenessProbe', 'readinessProbe', + 'terminationMessagePath', + 'securityContext', + 'imagePullPolicy', + 'protocol', # ports.portocol: TCP + 'type', # strategy: {'type': 'rolling'} + 'defaultMode', # added on secrets + 'activeDeadlineSeconds', # added in 1.5 for timeouts + ] + + if not Utils.check_def_equal(self.prepared_registry['deployment'].yaml_dict, + self.deploymentconfig.yaml_dict, + exclude_list, + debug=self.verbose): + self.prepared_registry['deployment_update'] = True + + return self.prepared_registry['deployment_update'] or self.prepared_registry['service_update'] or False + + # In the future, we would like to break out each ansible state into a function. + # pylint: disable=too-many-branches,too-many-return-statements + @staticmethod + def run_ansible(params, check_mode): + '''run idempotent ansible code''' + + rconfig = RegistryConfig(params['name'], + params['namespace'], + params['kubeconfig'], + {'images': {'value': params['images'], 'include': True}, + 'latest_images': {'value': params['latest_images'], 'include': True}, + 'labels': {'value': params['labels'], 'include': True}, + 'ports': {'value': ','.join(params['ports']), 'include': True}, + 'replicas': {'value': params['replicas'], 'include': True}, + 'selector': {'value': params['selector'], 'include': True}, + 'service_account': {'value': params['service_account'], 'include': True}, + 'mount_host': {'value': params['mount_host'], 'include': True}, + 'env_vars': {'value': params['env_vars'], 'include': False}, + 'volume_mounts': {'value': params['volume_mounts'], 'include': False}, + 'edits': {'value': params['edits'], 'include': False}, + 'enforce_quota': {'value': params['enforce_quota'], 'include': True}, + 'daemonset': {'value': params['daemonset'], 'include': True}, + 'tls_key': {'value': params['tls_key'], 'include': True}, + 'tls_certificate': {'value': params['tls_certificate'], 'include': True}, + }) + + + ocregistry = Registry(rconfig, params['debug']) + + api_rval = ocregistry.get() + + state = params['state'] + ######## + # get + ######## + if state == 'list': + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': False, 'results': api_rval, 'state': state} + + ######## + # Delete + ######## + if state == 'absent': + if not ocregistry.exists(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'} + + # Unsure as to why this is angry with the return type. + # pylint: disable=redefined-variable-type + api_rval = ocregistry.delete() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + if state == 'present': + ######## + # Create + ######## + if not ocregistry.exists(): + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} + + api_rval = ocregistry.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + ######## + # Update + ######## + if not params['force'] and not ocregistry.needs_update(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'} + + api_rval = ocregistry.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + return {'failed': True, 'msg': 'Unknown state passed. %s' % state} + +# -*- -*- -*- End included fragment: class/oc_adm_registry.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ansible/oc_adm_registry.py -*- -*- -*- + +def main(): + ''' + ansible oc module for registry + ''' + + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', type='str', + choices=['present', 'absent']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default=None, required=True, type='str'), + + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + images=dict(default=None, type='str'), + latest_images=dict(default=False, type='bool'), + labels=dict(default=None, type='list'), + ports=dict(default=['5000'], type='list'), + replicas=dict(default=1, type='int'), + selector=dict(default=None, type='str'), + service_account=dict(default='registry', type='str'), + mount_host=dict(default=None, type='str'), + volume_mounts=dict(default=None, type='list'), + env_vars=dict(default=None, type='dict'), + edits=dict(default=None, type='list'), + enforce_quota=dict(default=False, type='bool'), + force=dict(default=False, type='bool'), + daemonset=dict(default=False, type='bool'), + tls_key=dict(default=None, type='str'), + tls_certificate=dict(default=None, type='str'), + ), + + supports_check_mode=True, + ) + + results = Registry.run_ansible(module.params, module.check_mode) + if 'failed' in results: + module.fail_json(**results) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() + +# -*- -*- -*- End included fragment: ansible/oc_adm_registry.py -*- -*- -*- diff --git a/roles/lib_openshift/library/oc_adm_router.py b/roles/lib_openshift/library/oc_adm_router.py new file mode 100644 index 000000000..e6d0f795e --- /dev/null +++ b/roles/lib_openshift/library/oc_adm_router.py @@ -0,0 +1,2955 @@ +#!/usr/bin/env python +# pylint: disable=missing-docstring +# flake8: noqa: T001 +# ___ ___ _ _ ___ ___ _ _____ ___ ___ +# / __| __| \| | __| _ \ /_\_ _| __| \ +# | (_ | _|| .` | _|| / / _ \| | | _|| |) | +# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____ +# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _| +# | |) | (_) | | .` | (_) || | | _|| |) | | | | +# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_| +# +# Copyright 2016 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*- +''' + OpenShiftCLI class that wraps the oc commands in a subprocess +''' +# pylint: disable=too-many-lines + +from __future__ import print_function +import atexit +import copy +import json +import os +import re +import shutil +import subprocess +import tempfile +# pylint: disable=import-error +try: + import ruamel.yaml as yaml +except ImportError: + import yaml + +from ansible.module_utils.basic import AnsibleModule + +# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: doc/router -*- -*- -*- + +DOCUMENTATION = ''' +--- +module: oc_adm_router +short_description: Module to manage openshift router +description: + - Manage openshift router programmatically. +options: + state: + description: + - Whether to create or delete the router + - present - create the router + - absent - remove the router + - list - return the current representation of a router + required: false + default: present + choices: + - present + - absent + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + name: + description: + - The name of the router + required: false + default: router + aliases: [] + namespace: + description: + - The namespace where to manage the router. + required: false + default: default + aliases: [] + images: + description: + - The image to base this router on - ${component} will be replaced with --type + required: 'openshift3/ose-${component}:${version}' + default: None + aliases: [] + latest_images: + description: + - If true, attempt to use the latest image for the registry instead of the latest release. + required: false + default: False + aliases: [] + labels: + description: + - A set of labels to uniquely identify the registry and its components. + required: false + default: None + aliases: [] + ports: + description: + - A list of strings in the 'port:port' format + required: False + default: + - 80:80 + - 443:443 + aliases: [] + replicas: + description: + - The replication factor of the registry; commonly 2 when high availability is desired. + required: False + default: 1 + aliases: [] + selector: + description: + - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes. + required: False + default: None + aliases: [] + service_account: + description: + - Name of the service account to use to run the router pod. + required: False + default: router + aliases: [] + router_type: + description: + - The router image to use - if you specify --images this flag may be ignored. + required: false + default: haproxy-router + aliases: [] + external_host: + description: + - If the underlying router implementation connects with an external host, this is the external host's hostname. + required: false + default: None + aliases: [] + external_host_vserver: + description: + - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections. + required: false + default: None + aliases: [] + external_host_insecure: + description: + - If the underlying router implementation connects with an external host + - over a secure connection, this causes the router to skip strict certificate verification with the external host. + required: false + default: False + aliases: [] + external_host_partition_path: + description: + - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition. + required: false + default: None + aliases: [] + external_host_username: + description: + - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host. + required: false + default: None + aliases: [] + external_host_password: + description: + - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host. + required: false + default: None + aliases: [] + external_host_private_key: + description: + - If the underlying router implementation requires an SSH private key, this is the path to the private key file. + required: false + default: None + aliases: [] + expose_metrics: + description: + - This is a hint to run an extra container in the pod to expose metrics - the image + - will either be set depending on the router implementation or provided with --metrics-image. + required: false + default: False + aliases: [] + metrics_image: + description: + - If expose_metrics is specified this is the image to use to run a sidecar container + - in the pod exposing metrics. If not set and --expose-metrics is true the image will + - depend on router implementation. + required: false + default: None + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: +- There are some exceptions to note when doing the idempotency in this module. +- The strategy is to use the oc adm router command to generate a default +- configuration when creating or updating a router. Often times there +- differences from the generated template and what is in memory in openshift. +- We make exceptions to not check these specific values when comparing objects. +- Here are a list of exceptions: +- - DeploymentConfig: + - dnsPolicy + - terminationGracePeriodSeconds + - restartPolicy + - timeoutSeconds + - livenessProbe + - readinessProbe + - terminationMessagePath + - hostPort + - defaultMode + - Service: + - portalIP + - clusterIP + - sessionAffinity + - type + - ServiceAccount: + - secrets + - imagePullSecrets +''' + +EXAMPLES = ''' +- name: create routers + oc_adm_router: + name: router + service_account: router + replicas: 2 + namespace: default + selector: type=infra + cert_file: /etc/origin/master/named_certificates/router.crt + key_file: /etc/origin/master/named_certificates/router.key + cacert_file: /etc/origin/master/named_certificates/router.ca + edits: + - key: spec.strategy.rollingParams + value: + intervalSeconds: 1 + maxSurge: 50% + maxUnavailable: 50% + timeoutSeconds: 600 + updatePeriodSeconds: 1 + action: put + - key: spec.template.spec.containers[0].resources.limits.memory + value: 2G + action: put + - key: spec.template.spec.containers[0].resources.requests.memory + value: 1G + action: put + - key: spec.template.spec.containers[0].env + value: + name: EXTENDED_VALIDATION + value: 'false' + action: update + register: router_out + run_once: True +''' + +# -*- -*- -*- End included fragment: doc/router -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- +# noqa: E301,E302 + + +class YeditException(Exception): + ''' Exception class for Yedit ''' + pass + + +# pylint: disable=too-many-public-methods +class Yedit(object): + ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z%s/_-]+)" + com_sep = set(['.', '#', '|', ':']) + + # pylint: disable=too-many-arguments + def __init__(self, + filename=None, + content=None, + content_type='yaml', + separator='.', + backup=False): + self.content = content + self._separator = separator + self.filename = filename + self.__yaml_dict = content + self.content_type = content_type + self.backup = backup + self.load(content_type=self.content_type) + if self.__yaml_dict is None: + self.__yaml_dict = {} + + @property + def separator(self): + ''' getter method for yaml_dict ''' + return self._separator + + @separator.setter + def separator(self): + ''' getter method for yaml_dict ''' + return self._separator + + @property + def yaml_dict(self): + ''' getter method for yaml_dict ''' + return self.__yaml_dict + + @yaml_dict.setter + def yaml_dict(self, value): + ''' setter method for yaml_dict ''' + self.__yaml_dict = value + + @staticmethod + def parse_key(key, sep='.'): + '''parse the key allowing the appropriate separator''' + common_separators = list(Yedit.com_sep - set([sep])) + return re.findall(Yedit.re_key % ''.join(common_separators), key) + + @staticmethod + def valid_key(key, sep='.'): + '''validate the incoming key''' + common_separators = list(Yedit.com_sep - set([sep])) + if not re.match(Yedit.re_valid_key % ''.join(common_separators), key): + return False + + return True + + @staticmethod + def remove_entry(data, key, sep='.'): + ''' remove data at location key ''' + if key == '' and isinstance(data, dict): + data.clear() + return True + elif key == '' and isinstance(data, list): + del data[:] + return True + + if not (key and Yedit.valid_key(key, sep)) and \ + isinstance(data, (list, dict)): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + # process last index for remove + # expected list entry + if key_indexes[-1][0]: + if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501 + del data[int(key_indexes[-1][0])] + return True + + # expected dict entry + elif key_indexes[-1][1]: + if isinstance(data, dict): + del data[key_indexes[-1][1]] + return True + + @staticmethod + def add_entry(data, key, item=None, sep='.'): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a#b + return c + ''' + if key == '': + pass + elif (not (key and Yedit.valid_key(key, sep)) and + isinstance(data, (list, dict))): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes[:-1]: + if dict_key: + if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501 + data = data[dict_key] + continue + + elif data and not isinstance(data, dict): + return None + + data[dict_key] = {} + data = data[dict_key] + + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + if key == '': + data = item + + # process last index for add + # expected list entry + elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501 + data[int(key_indexes[-1][0])] = item + + # expected dict entry + elif key_indexes[-1][1] and isinstance(data, dict): + data[key_indexes[-1][1]] = item + + return data + + @staticmethod + def get_entry(data, key, sep='.'): + ''' Get an item from a dictionary with key notation a.b.c + d = {'a': {'b': 'c'}}} + key = a.b + return c + ''' + if key == '': + pass + elif (not (key and Yedit.valid_key(key, sep)) and + isinstance(data, (list, dict))): + return None + + key_indexes = Yedit.parse_key(key, sep) + for arr_ind, dict_key in key_indexes: + if dict_key and isinstance(data, dict): + data = data.get(dict_key, None) + elif (arr_ind and isinstance(data, list) and + int(arr_ind) <= len(data) - 1): + data = data[int(arr_ind)] + else: + return None + + return data + + @staticmethod + def _write(filename, contents): + ''' Actually write the file contents to disk. This helps with mocking. ''' + + tmp_filename = filename + '.yedit' + + with open(tmp_filename, 'w') as yfd: + yfd.write(contents) + + os.rename(tmp_filename, filename) + + def write(self): + ''' write to file ''' + if not self.filename: + raise YeditException('Please specify a filename.') + + if self.backup and self.file_exists(): + shutil.copy(self.filename, self.filename + '.orig') + + if hasattr(yaml, 'RoundTripDumper'): + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + self.yaml_dict.fa.set_block_style() + + # pylint: disable=no-member + Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper)) + else: + Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False)) + + return (True, self.yaml_dict) + + def read(self): + ''' read from file ''' + # check if it exists + if self.filename is None or not self.file_exists(): + return None + + contents = None + with open(self.filename) as yfd: + contents = yfd.read() + + return contents + + def file_exists(self): + ''' return whether file exists ''' + if os.path.exists(self.filename): + return True + + return False + + def load(self, content_type='yaml'): + ''' return yaml file ''' + contents = self.read() + + if not contents and not self.content: + return None + + if self.content: + if isinstance(self.content, dict): + self.yaml_dict = self.content + return self.yaml_dict + elif isinstance(self.content, str): + contents = self.content + + # check if it is yaml + try: + if content_type == 'yaml' and contents: + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripLoader'): + self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader) + else: + self.yaml_dict = yaml.safe_load(contents) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + self.yaml_dict.fa.set_block_style() + + elif content_type == 'json' and contents: + self.yaml_dict = json.loads(contents) + except yaml.YAMLError as err: + # Error loading yaml or json + raise YeditException('Problem with loading yaml file. %s' % err) + + return self.yaml_dict + + def get(self, key): + ''' get a specified key''' + try: + entry = Yedit.get_entry(self.yaml_dict, key, self.separator) + except KeyError: + entry = None + + return entry + + def pop(self, path, key_or_item): + ''' remove a key, value pair from a dict or an item for a list''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + return (False, self.yaml_dict) + + if isinstance(entry, dict): + # pylint: disable=no-member,maybe-no-member + if key_or_item in entry: + entry.pop(key_or_item) + return (True, self.yaml_dict) + return (False, self.yaml_dict) + + elif isinstance(entry, list): + # pylint: disable=no-member,maybe-no-member + ind = None + try: + ind = entry.index(key_or_item) + except ValueError: + return (False, self.yaml_dict) + + entry.pop(ind) + return (True, self.yaml_dict) + + return (False, self.yaml_dict) + + def delete(self, path): + ''' remove path from a dict''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + return (False, self.yaml_dict) + + result = Yedit.remove_entry(self.yaml_dict, path, self.separator) + if not result: + return (False, self.yaml_dict) + + return (True, self.yaml_dict) + + def exists(self, path, value): + ''' check if value exists at path''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if isinstance(entry, list): + if value in entry: + return True + return False + + elif isinstance(entry, dict): + if isinstance(value, dict): + rval = False + for key, val in value.items(): + if entry[key] != val: + rval = False + break + else: + rval = True + return rval + + return value in entry + + return entry == value + + def append(self, path, value): + '''append value to a list''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry is None: + self.put(path, []) + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + if not isinstance(entry, list): + return (False, self.yaml_dict) + + # pylint: disable=no-member,maybe-no-member + entry.append(value) + return (True, self.yaml_dict) + + # pylint: disable=too-many-arguments + def update(self, path, value, index=None, curr_value=None): + ''' put path, value into a dict ''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if isinstance(entry, dict): + # pylint: disable=no-member,maybe-no-member + if not isinstance(value, dict): + raise YeditException('Cannot replace key, value entry in ' + + 'dict with non-dict type. value=[%s] [%s]' % (value, type(value))) # noqa: E501 + + entry.update(value) + return (True, self.yaml_dict) + + elif isinstance(entry, list): + # pylint: disable=no-member,maybe-no-member + ind = None + if curr_value: + try: + ind = entry.index(curr_value) + except ValueError: + return (False, self.yaml_dict) + + elif index is not None: + ind = index + + if ind is not None and entry[ind] != value: + entry[ind] = value + return (True, self.yaml_dict) + + # see if it exists in the list + try: + ind = entry.index(value) + except ValueError: + # doesn't exist, append it + entry.append(value) + return (True, self.yaml_dict) + + # already exists, return + if ind is not None: + return (False, self.yaml_dict) + return (False, self.yaml_dict) + + def put(self, path, value): + ''' put path, value into a dict ''' + try: + entry = Yedit.get_entry(self.yaml_dict, path, self.separator) + except KeyError: + entry = None + + if entry == value: + return (False, self.yaml_dict) + + # deepcopy didn't work + if hasattr(yaml, 'round_trip_dump'): + # pylint: disable=no-member + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + tmp_copy.fa.set_block_style() + + else: + tmp_copy = copy.deepcopy(self.yaml_dict) + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if not result: + return (False, self.yaml_dict) + + self.yaml_dict = tmp_copy + + return (True, self.yaml_dict) + + def create(self, path, value): + ''' create a yaml file ''' + if not self.file_exists(): + # deepcopy didn't work + if hasattr(yaml, 'round_trip_dump'): + # pylint: disable=no-member + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, default_flow_style=False), # noqa: E501 + yaml.RoundTripLoader) + + # pylint: disable=no-member + if hasattr(self.yaml_dict, 'fa'): + tmp_copy.fa.set_block_style() + else: + tmp_copy = copy.deepcopy(self.yaml_dict) + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result: + self.yaml_dict = tmp_copy + return (True, self.yaml_dict) + + return (False, self.yaml_dict) + + @staticmethod + def get_curr_value(invalue, val_type): + '''return the current value''' + if invalue is None: + return None + + curr_value = invalue + if val_type == 'yaml': + curr_value = yaml.load(invalue) + elif val_type == 'json': + curr_value = json.loads(invalue) + + return curr_value + + @staticmethod + def parse_value(inc_value, vtype=''): + '''determine value type passed''' + true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE', + 'on', 'On', 'ON', ] + false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE', + 'off', 'Off', 'OFF'] + + # It came in as a string but you didn't specify value_type as string + # we will convert to bool if it matches any of the above cases + if isinstance(inc_value, str) and 'bool' in vtype: + if inc_value not in true_bools and inc_value not in false_bools: + raise YeditException('Not a boolean type. str=[%s] vtype=[%s]' + % (inc_value, vtype)) + elif isinstance(inc_value, bool) and 'str' in vtype: + inc_value = str(inc_value) + + # If vtype is not str then go ahead and attempt to yaml load it. + if isinstance(inc_value, str) and 'str' not in vtype: + try: + inc_value = yaml.load(inc_value) + except Exception: + raise YeditException('Could not determine type of incoming ' + + 'value. value=[%s] vtype=[%s]' + % (type(inc_value), vtype)) + + return inc_value + + # pylint: disable=too-many-return-statements,too-many-branches + @staticmethod + def run_ansible(module): + '''perform the idempotent crud operations''' + yamlfile = Yedit(filename=module.params['src'], + backup=module.params['backup'], + separator=module.params['separator']) + + if module.params['src']: + rval = yamlfile.load() + + if yamlfile.yaml_dict is None and \ + module.params['state'] != 'present': + return {'failed': True, + 'msg': 'Error opening file [%s]. Verify that the ' + + 'file exists, that it is has correct' + + ' permissions, and is valid yaml.'} + + if module.params['state'] == 'list': + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + yamlfile.yaml_dict = content + + if module.params['key']: + rval = yamlfile.get(module.params['key']) or {} + + return {'changed': False, 'result': rval, 'state': "list"} + + elif module.params['state'] == 'absent': + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + yamlfile.yaml_dict = content + + if module.params['update']: + rval = yamlfile.pop(module.params['key'], + module.params['value']) + else: + rval = yamlfile.delete(module.params['key']) + + if rval[0] and module.params['src']: + yamlfile.write() + + return {'changed': rval[0], 'result': rval[1], 'state': "absent"} + + elif module.params['state'] == 'present': + # check if content is different than what is in the file + if module.params['content']: + content = Yedit.parse_value(module.params['content'], + module.params['content_type']) + + # We had no edits to make and the contents are the same + if yamlfile.yaml_dict == content and \ + module.params['value'] is None: + return {'changed': False, + 'result': yamlfile.yaml_dict, + 'state': "present"} + + yamlfile.yaml_dict = content + + # we were passed a value; parse it + if module.params['value']: + value = Yedit.parse_value(module.params['value'], + module.params['value_type']) + key = module.params['key'] + if module.params['update']: + # pylint: disable=line-too-long + curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501 + module.params['curr_value_format']) # noqa: E501 + + rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501 + + elif module.params['append']: + rval = yamlfile.append(key, value) + else: + rval = yamlfile.put(key, value) + + if rval[0] and module.params['src']: + yamlfile.write() + + return {'changed': rval[0], + 'result': rval[1], 'state': "present"} + + # no edits to make + if module.params['src']: + # pylint: disable=redefined-variable-type + rval = yamlfile.write() + return {'changed': rval[0], + 'result': rval[1], + 'state': "present"} + + return {'failed': True, 'msg': 'Unkown state passed'} + +# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*- +# pylint: disable=too-many-lines +# noqa: E301,E302,E303,T001 + + +class OpenShiftCLIError(Exception): + '''Exception class for openshiftcli''' + pass + + +# pylint: disable=too-few-public-methods +class OpenShiftCLI(object): + ''' Class to wrap the command line tools ''' + def __init__(self, + namespace, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False, + all_namespaces=False): + ''' Constructor for OpenshiftCLI ''' + self.namespace = namespace + self.verbose = verbose + self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig) + self.all_namespaces = all_namespaces + + # Pylint allows only 5 arguments to be passed. + # pylint: disable=too-many-arguments + def _replace_content(self, resource, rname, content, force=False, sep='.'): + ''' replace the current object with the content ''' + res = self._get(resource, rname) + if not res['results']: + return res + + fname = Utils.create_tmpfile(rname + '-') + + yed = Yedit(fname, res['results'][0], separator=sep) + changes = [] + for key, value in content.items(): + changes.append(yed.put(key, value)) + + if any([change[0] for change in changes]): + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self._replace(fname, force) + + return {'returncode': 0, 'updated': False} + + def _replace(self, fname, force=False): + '''replace the current object with oc replace''' + cmd = ['replace', '-f', fname] + if force: + cmd.append('--force') + return self.openshift_cmd(cmd) + + def _create_from_content(self, rname, content): + '''create a temporary file and then call oc create on it''' + fname = Utils.create_tmpfile(rname + '-') + yed = Yedit(fname, content=content) + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self._create(fname) + + def _create(self, fname): + '''call oc create on a filename''' + return self.openshift_cmd(['create', '-f', fname]) + + def _delete(self, resource, rname, selector=None): + '''call oc delete on a resource''' + cmd = ['delete', resource, rname] + if selector: + cmd.append('--selector=%s' % selector) + + return self.openshift_cmd(cmd) + + def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501 + '''process a template + + template_name: the name of the template to process + create: whether to send to oc create after processing + params: the parameters for the template + template_data: the incoming template's data; instead of a file + ''' + cmd = ['process'] + if template_data: + cmd.extend(['-f', '-']) + else: + cmd.append(template_name) + if params: + param_str = ["%s=%s" % (key, value) for key, value in params.items()] + cmd.append('-v') + cmd.extend(param_str) + + results = self.openshift_cmd(cmd, output=True, input_data=template_data) + + if results['returncode'] != 0 or not create: + return results + + fname = Utils.create_tmpfile(template_name + '-') + yed = Yedit(fname, results['results']) + yed.write() + + atexit.register(Utils.cleanup, [fname]) + + return self.openshift_cmd(['create', '-f', fname]) + + def _get(self, resource, rname=None, selector=None): + '''return a resource by name ''' + cmd = ['get', resource] + if selector: + cmd.append('--selector=%s' % selector) + elif rname: + cmd.append(rname) + + cmd.extend(['-o', 'json']) + + rval = self.openshift_cmd(cmd, output=True) + + # Ensure results are retuned in an array + if 'items' in rval: + rval['results'] = rval['items'] + elif not isinstance(rval['results'], list): + rval['results'] = [rval['results']] + + return rval + + def _schedulable(self, node=None, selector=None, schedulable=True): + ''' perform oadm manage-node scheduable ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + cmd.append('--schedulable=%s' % schedulable) + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501 + + def _list_pods(self, node=None, selector=None, pod_selector=None): + ''' perform oadm list pods + + node: the node in which to list pods + selector: the label selector filter if provided + pod_selector: the pod selector filter if provided + ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + if pod_selector: + cmd.append('--pod-selector=%s' % pod_selector) + + cmd.extend(['--list-pods', '-o', 'json']) + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') + + # pylint: disable=too-many-arguments + def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False): + ''' perform oadm manage-node evacuate ''' + cmd = ['manage-node'] + if node: + cmd.extend(node) + else: + cmd.append('--selector=%s' % selector) + + if dry_run: + cmd.append('--dry-run') + + if pod_selector: + cmd.append('--pod-selector=%s' % pod_selector) + + if grace_period: + cmd.append('--grace-period=%s' % int(grace_period)) + + if force: + cmd.append('--force') + + cmd.append('--evacuate') + + return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') + + def _version(self): + ''' return the openshift version''' + return self.openshift_cmd(['version'], output=True, output_type='raw') + + def _import_image(self, url=None, name=None, tag=None): + ''' perform image import ''' + cmd = ['import-image'] + + image = '{0}'.format(name) + if tag: + image += ':{0}'.format(tag) + + cmd.append(image) + + if url: + cmd.append('--from={0}/{1}'.format(url, image)) + + cmd.append('-n{0}'.format(self.namespace)) + + cmd.append('--confirm') + return self.openshift_cmd(cmd) + + def _run(self, cmds, input_data): + ''' Actually executes the command. This makes mocking easier. ''' + curr_env = os.environ.copy() + curr_env.update({'KUBECONFIG': self.kubeconfig}) + proc = subprocess.Popen(cmds, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=curr_env) + + stdout, stderr = proc.communicate(input_data) + + return proc.returncode, stdout.decode(), stderr.decode() + + # pylint: disable=too-many-arguments,too-many-branches + def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None): + '''Base command for oc ''' + cmds = [] + if oadm: + cmds = ['oadm'] + else: + cmds = ['oc'] + + if self.all_namespaces: + cmds.extend(['--all-namespaces']) + elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 + cmds.extend(['-n', self.namespace]) + + cmds.extend(cmd) + + rval = {} + results = '' + err = None + + if self.verbose: + print(' '.join(cmds)) + + returncode, stdout, stderr = self._run(cmds, input_data) + + rval = {"returncode": returncode, + "results": results, + "cmd": ' '.join(cmds)} + + if returncode == 0: + if output: + if output_type == 'json': + try: + rval['results'] = json.loads(stdout) + except ValueError as err: + if "No JSON object could be decoded" in err.args: + err = err.args + elif output_type == 'raw': + rval['results'] = stdout + + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if err: + rval.update({"err": err, + "stderr": stderr, + "stdout": stdout, + "cmd": cmds}) + + else: + rval.update({"stderr": stderr, + "stdout": stdout, + "results": {}}) + + return rval + + +class Utils(object): + ''' utilities for openshiftcli modules ''' + + @staticmethod + def _write(filename, contents): + ''' Actually write the file contents to disk. This helps with mocking. ''' + + with open(filename, 'w') as sfd: + sfd.write(contents) + + @staticmethod + def create_tmp_file_from_contents(rname, data, ftype='yaml'): + ''' create a file in tmp with name and contents''' + + tmp = Utils.create_tmpfile(prefix=rname) + + if ftype == 'yaml': + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripDumper'): + Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper)) + else: + Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False)) + + elif ftype == 'json': + Utils._write(tmp, json.dumps(data)) + else: + Utils._write(tmp, data) + + # Register cleanup when module is done + atexit.register(Utils.cleanup, [tmp]) + return tmp + + @staticmethod + def create_tmpfile_copy(inc_file): + '''create a temporary copy of a file''' + tmpfile = Utils.create_tmpfile('lib_openshift-') + Utils._write(tmpfile, open(inc_file).read()) + + # Cleanup the tmpfile + atexit.register(Utils.cleanup, [tmpfile]) + + return tmpfile + + @staticmethod + def create_tmpfile(prefix='tmp'): + ''' Generates and returns a temporary file name ''' + + with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp: + return tmp.name + + @staticmethod + def create_tmp_files_from_contents(content, content_type=None): + '''Turn an array of dict: filename, content into a files array''' + if not isinstance(content, list): + content = [content] + files = [] + for item in content: + path = Utils.create_tmp_file_from_contents(item['path'] + '-', + item['data'], + ftype=content_type) + files.append({'name': os.path.basename(item['path']), + 'path': path}) + return files + + @staticmethod + def cleanup(files): + '''Clean up on exit ''' + for sfile in files: + if os.path.exists(sfile): + if os.path.isdir(sfile): + shutil.rmtree(sfile) + elif os.path.isfile(sfile): + os.remove(sfile) + + @staticmethod + def exists(results, _name): + ''' Check to see if the results include the name ''' + if not results: + return False + + if Utils.find_result(results, _name): + return True + + return False + + @staticmethod + def find_result(results, _name): + ''' Find the specified result by name''' + rval = None + for result in results: + if 'metadata' in result and result['metadata']['name'] == _name: + rval = result + break + + return rval + + @staticmethod + def get_resource_file(sfile, sfile_type='yaml'): + ''' return the service file ''' + contents = None + with open(sfile) as sfd: + contents = sfd.read() + + if sfile_type == 'yaml': + # pylint: disable=no-member + if hasattr(yaml, 'RoundTripLoader'): + contents = yaml.load(contents, yaml.RoundTripLoader) + else: + contents = yaml.safe_load(contents) + elif sfile_type == 'json': + contents = json.loads(contents) + + return contents + + @staticmethod + def filter_versions(stdout): + ''' filter the oc version output ''' + + version_dict = {} + version_search = ['oc', 'openshift', 'kubernetes'] + + for line in stdout.strip().split('\n'): + for term in version_search: + if not line: + continue + if line.startswith(term): + version_dict[term] = line.split()[-1] + + # horrible hack to get openshift version in Openshift 3.2 + # By default "oc version in 3.2 does not return an "openshift" version + if "openshift" not in version_dict: + version_dict["openshift"] = version_dict["oc"] + + return version_dict + + @staticmethod + def add_custom_versions(versions): + ''' create custom versions strings ''' + + versions_dict = {} + + for tech, version in versions.items(): + # clean up "-" from version + if "-" in version: + version = version.split("-")[0] + + if version.startswith('v'): + versions_dict[tech + '_numeric'] = version[1:].split('+')[0] + # "v3.3.0.33" is what we have, we want "3.3" + versions_dict[tech + '_short'] = version[1:4] + + return versions_dict + + @staticmethod + def openshift_installed(): + ''' check if openshift is installed ''' + import yum + + yum_base = yum.YumBase() + if yum_base.rpmdb.searchNevra(name='atomic-openshift'): + return True + + return False + + # Disabling too-many-branches. This is a yaml dictionary comparison function + # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements + @staticmethod + def check_def_equal(user_def, result_def, skip_keys=None, debug=False): + ''' Given a user defined definition, compare it with the results given back by our query. ''' + + # Currently these values are autogenerated and we do not need to check them + skip = ['metadata', 'status'] + if skip_keys: + skip.extend(skip_keys) + + for key, value in result_def.items(): + if key in skip: + continue + + # Both are lists + if isinstance(value, list): + if key not in user_def: + if debug: + print('User data does not have key [%s]' % key) + print('User data: %s' % user_def) + return False + + if not isinstance(user_def[key], list): + if debug: + print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key])) + return False + + if len(user_def[key]) != len(value): + if debug: + print("List lengths are not equal.") + print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value))) + print("user_def: %s" % user_def[key]) + print("value: %s" % value) + return False + + for values in zip(user_def[key], value): + if isinstance(values[0], dict) and isinstance(values[1], dict): + if debug: + print('sending list - list') + print(type(values[0])) + print(type(values[1])) + result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug) + if not result: + print('list compare returned false') + return False + + elif value != user_def[key]: + if debug: + print('value should be identical') + print(value) + print(user_def[key]) + return False + + # recurse on a dictionary + elif isinstance(value, dict): + if key not in user_def: + if debug: + print("user_def does not have key [%s]" % key) + return False + if not isinstance(user_def[key], dict): + if debug: + print("dict returned false: not instance of dict") + return False + + # before passing ensure keys match + api_values = set(value.keys()) - set(skip) + user_values = set(user_def[key].keys()) - set(skip) + if api_values != user_values: + if debug: + print("keys are not equal in dict") + print(api_values) + print(user_values) + return False + + result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug) + if not result: + if debug: + print("dict returned false") + print(result) + return False + + # Verify each key, value pair is the same + else: + if key not in user_def or value != user_def[key]: + if debug: + print("value not equal; user_def does not have key") + print(key) + print(value) + if key in user_def: + print(user_def[key]) + return False + + if debug: + print('returning true') + return True + + +class OpenShiftCLIConfig(object): + '''Generic Config''' + def __init__(self, rname, namespace, kubeconfig, options): + self.kubeconfig = kubeconfig + self.name = rname + self.namespace = namespace + self._options = options + + @property + def config_options(self): + ''' return config options ''' + return self._options + + def to_option_list(self): + '''return all options as a string''' + return self.stringify() + + def stringify(self): + ''' return the options hash as cli params in a string ''' + rval = [] + for key, data in self.config_options.items(): + if data['include'] \ + and (data['value'] or isinstance(data['value'], int)): + rval.append('--%s=%s' % (key.replace('_', '-'), data['value'])) + + return rval + + +# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*- + + +# pylint: disable=too-many-instance-attributes +class ServiceConfig(object): + ''' Handle service options ''' + # pylint: disable=too-many-arguments + def __init__(self, + sname, + namespace, + ports, + selector=None, + labels=None, + cluster_ip=None, + portal_ip=None, + session_affinity=None, + service_type=None): + ''' constructor for handling service options ''' + self.name = sname + self.namespace = namespace + self.ports = ports + self.selector = selector + self.labels = labels + self.cluster_ip = cluster_ip + self.portal_ip = portal_ip + self.session_affinity = session_affinity + self.service_type = service_type + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' instantiates a service dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'Service' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + if self.labels: + for lab, lab_value in self.labels.items(): + self.data['metadata'][lab] = lab_value + self.data['spec'] = {} + + if self.ports: + self.data['spec']['ports'] = self.ports + else: + self.data['spec']['ports'] = [] + + if self.selector: + self.data['spec']['selector'] = self.selector + + self.data['spec']['sessionAffinity'] = self.session_affinity or 'None' + + if self.cluster_ip: + self.data['spec']['clusterIP'] = self.cluster_ip + + if self.portal_ip: + self.data['spec']['portalIP'] = self.portal_ip + + if self.service_type: + self.data['spec']['type'] = self.service_type + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class Service(Yedit): + ''' Class to model the oc service object ''' + port_path = "spec.ports" + portal_ip = "spec.portalIP" + cluster_ip = "spec.clusterIP" + kind = 'Service' + + def __init__(self, content): + '''Service constructor''' + super(Service, self).__init__(content=content) + + def get_ports(self): + ''' get a list of ports ''' + return self.get(Service.port_path) or [] + + def add_ports(self, inc_ports): + ''' add a port object to the ports list ''' + if not isinstance(inc_ports, list): + inc_ports = [inc_ports] + + ports = self.get_ports() + if not ports: + self.put(Service.port_path, inc_ports) + else: + ports.extend(inc_ports) + + return True + + def find_ports(self, inc_port): + ''' find a specific port ''' + for port in self.get_ports(): + if port['port'] == inc_port['port']: + return port + + return None + + def delete_ports(self, inc_ports): + ''' remove a port from a service ''' + if not isinstance(inc_ports, list): + inc_ports = [inc_ports] + + ports = self.get(Service.port_path) or [] + + if not ports: + return True + + removed = False + for inc_port in inc_ports: + port = self.find_ports(inc_port) + if port: + ports.remove(port) + removed = True + + return removed + + def add_cluster_ip(self, sip): + '''add cluster ip''' + self.put(Service.cluster_ip, sip) + + def add_portal_ip(self, pip): + '''add cluster ip''' + self.put(Service.portal_ip, pip) + +# -*- -*- -*- End included fragment: lib/service.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*- + + +# pylint: disable=too-many-public-methods +class DeploymentConfig(Yedit): + ''' Class to model an openshift DeploymentConfig''' + default_deployment_config = ''' +apiVersion: v1 +kind: DeploymentConfig +metadata: + name: default_dc + namespace: default +spec: + replicas: 0 + selector: + default_dc: default_dc + strategy: + resources: {} + rollingParams: + intervalSeconds: 1 + maxSurge: 0 + maxUnavailable: 25% + timeoutSeconds: 600 + updatePercent: -25 + updatePeriodSeconds: 1 + type: Rolling + template: + metadata: + spec: + containers: + - env: + - name: default + value: default + image: default + imagePullPolicy: IfNotPresent + name: default_dc + ports: + - containerPort: 8000 + hostPort: 8000 + protocol: TCP + name: default_port + resources: {} + terminationMessagePath: /dev/termination-log + dnsPolicy: ClusterFirst + hostNetwork: true + nodeSelector: + type: compute + restartPolicy: Always + securityContext: {} + serviceAccount: default + serviceAccountName: default + terminationGracePeriodSeconds: 30 + triggers: + - type: ConfigChange +''' + + replicas_path = "spec.replicas" + env_path = "spec.template.spec.containers[0].env" + volumes_path = "spec.template.spec.volumes" + container_path = "spec.template.spec.containers" + volume_mounts_path = "spec.template.spec.containers[0].volumeMounts" + + def __init__(self, content=None): + ''' Constructor for deploymentconfig ''' + if not content: + content = DeploymentConfig.default_deployment_config + + super(DeploymentConfig, self).__init__(content=content) + + # pylint: disable=no-member + def add_env_value(self, key, value): + ''' add key, value pair to env array ''' + rval = False + env = self.get_env_vars() + if env: + env.append({'name': key, 'value': value}) + rval = True + else: + result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value}) + rval = result[0] + + return rval + + def exists_env_value(self, key, value): + ''' return whether a key, value pair exists ''' + results = self.get_env_vars() + if not results: + return False + + for result in results: + if result['name'] == key and result['value'] == value: + return True + + return False + + def exists_env_key(self, key): + ''' return whether a key, value pair exists ''' + results = self.get_env_vars() + if not results: + return False + + for result in results: + if result['name'] == key: + return True + + return False + + def get_env_vars(self): + '''return a environment variables ''' + return self.get(DeploymentConfig.env_path) or [] + + def delete_env_var(self, keys): + '''delete a list of keys ''' + if not isinstance(keys, list): + keys = [keys] + + env_vars_array = self.get_env_vars() + modified = False + idx = None + for key in keys: + for env_idx, env_var in enumerate(env_vars_array): + if env_var['name'] == key: + idx = env_idx + break + + if idx: + modified = True + del env_vars_array[idx] + + if modified: + return True + + return False + + def update_env_var(self, key, value): + '''place an env in the env var list''' + + env_vars_array = self.get_env_vars() + idx = None + for env_idx, env_var in enumerate(env_vars_array): + if env_var['name'] == key: + idx = env_idx + break + + if idx: + env_vars_array[idx]['value'] = value + else: + self.add_env_value(key, value) + + return True + + def exists_volume_mount(self, volume_mount): + ''' return whether a volume mount exists ''' + exist_volume_mounts = self.get_volume_mounts() + + if not exist_volume_mounts: + return False + + volume_mount_found = False + for exist_volume_mount in exist_volume_mounts: + if exist_volume_mount['name'] == volume_mount['name']: + volume_mount_found = True + break + + return volume_mount_found + + def exists_volume(self, volume): + ''' return whether a volume exists ''' + exist_volumes = self.get_volumes() + + volume_found = False + for exist_volume in exist_volumes: + if exist_volume['name'] == volume['name']: + volume_found = True + break + + return volume_found + + def find_volume_by_name(self, volume, mounts=False): + ''' return the index of a volume ''' + volumes = [] + if mounts: + volumes = self.get_volume_mounts() + else: + volumes = self.get_volumes() + for exist_volume in volumes: + if exist_volume['name'] == volume['name']: + return exist_volume + + return None + + def get_replicas(self): + ''' return replicas setting ''' + return self.get(DeploymentConfig.replicas_path) + + def get_volume_mounts(self): + '''return volume mount information ''' + return self.get_volumes(mounts=True) + + def get_volumes(self, mounts=False): + '''return volume mount information ''' + if mounts: + return self.get(DeploymentConfig.volume_mounts_path) or [] + + return self.get(DeploymentConfig.volumes_path) or [] + + def delete_volume_by_name(self, volume): + '''delete a volume ''' + modified = False + exist_volume_mounts = self.get_volume_mounts() + exist_volumes = self.get_volumes() + del_idx = None + for idx, exist_volume in enumerate(exist_volumes): + if 'name' in exist_volume and exist_volume['name'] == volume['name']: + del_idx = idx + break + + if del_idx != None: + del exist_volumes[del_idx] + modified = True + + del_idx = None + for idx, exist_volume_mount in enumerate(exist_volume_mounts): + if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']: + del_idx = idx + break + + if del_idx != None: + del exist_volume_mounts[idx] + modified = True + + return modified + + def add_volume_mount(self, volume_mount): + ''' add a volume or volume mount to the proper location ''' + exist_volume_mounts = self.get_volume_mounts() + + if not exist_volume_mounts and volume_mount: + self.put(DeploymentConfig.volume_mounts_path, [volume_mount]) + else: + exist_volume_mounts.append(volume_mount) + + def add_volume(self, volume): + ''' add a volume or volume mount to the proper location ''' + exist_volumes = self.get_volumes() + if not volume: + return + + if not exist_volumes: + self.put(DeploymentConfig.volumes_path, [volume]) + else: + exist_volumes.append(volume) + + def update_replicas(self, replicas): + ''' update replicas value ''' + self.put(DeploymentConfig.replicas_path, replicas) + + def update_volume(self, volume): + '''place an env in the env var list''' + exist_volumes = self.get_volumes() + + if not volume: + return False + + # update the volume + update_idx = None + for idx, exist_vol in enumerate(exist_volumes): + if exist_vol['name'] == volume['name']: + update_idx = idx + break + + if update_idx != None: + exist_volumes[update_idx] = volume + else: + self.add_volume(volume) + + return True + + def update_volume_mount(self, volume_mount): + '''place an env in the env var list''' + modified = False + + exist_volume_mounts = self.get_volume_mounts() + + if not volume_mount: + return False + + # update the volume mount + for exist_vol_mount in exist_volume_mounts: + if exist_vol_mount['name'] == volume_mount['name']: + if 'mountPath' in exist_vol_mount and \ + str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']): + exist_vol_mount['mountPath'] = volume_mount['mountPath'] + modified = True + break + + if not modified: + self.add_volume_mount(volume_mount) + modified = True + + return modified + + def needs_update_volume(self, volume, volume_mount): + ''' verify a volume update is needed ''' + exist_volume = self.find_volume_by_name(volume) + exist_volume_mount = self.find_volume_by_name(volume, mounts=True) + results = [] + results.append(exist_volume['name'] == volume['name']) + + if 'secret' in volume: + results.append('secret' in exist_volume) + results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName']) + results.append(exist_volume_mount['name'] == volume_mount['name']) + results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath']) + + elif 'emptyDir' in volume: + results.append(exist_volume_mount['name'] == volume['name']) + results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath']) + + elif 'persistentVolumeClaim' in volume: + pvc = 'persistentVolumeClaim' + results.append(pvc in exist_volume) + if results[-1]: + results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName']) + + if 'claimSize' in volume[pvc]: + results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize']) + + elif 'hostpath' in volume: + results.append('hostPath' in exist_volume) + results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath']) + + return not all(results) + + def needs_update_replicas(self, replicas): + ''' verify whether a replica update is needed ''' + current_reps = self.get(DeploymentConfig.replicas_path) + return not current_reps == replicas + +# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/serviceaccount.py -*- -*- -*- + +class ServiceAccountConfig(object): + '''Service account config class + + This class stores the options and returns a default service account + ''' + + # pylint: disable=too-many-arguments + def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None): + self.name = sname + self.kubeconfig = kubeconfig + self.namespace = namespace + self.secrets = secrets or [] + self.image_pull_secrets = image_pull_secrets or [] + self.data = {} + self.create_dict() + + def create_dict(self): + ''' instantiate a properly structured volume ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'ServiceAccount' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + + self.data['secrets'] = [] + if self.secrets: + for sec in self.secrets: + self.data['secrets'].append({"name": sec}) + + self.data['imagePullSecrets'] = [] + if self.image_pull_secrets: + for sec in self.image_pull_secrets: + self.data['imagePullSecrets'].append({"name": sec}) + +class ServiceAccount(Yedit): + ''' Class to wrap the oc command line tools ''' + image_pull_secrets_path = "imagePullSecrets" + secrets_path = "secrets" + + def __init__(self, content): + '''ServiceAccount constructor''' + super(ServiceAccount, self).__init__(content=content) + self._secrets = None + self._image_pull_secrets = None + + @property + def image_pull_secrets(self): + ''' property for image_pull_secrets ''' + if self._image_pull_secrets is None: + self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or [] + return self._image_pull_secrets + + @image_pull_secrets.setter + def image_pull_secrets(self, secrets): + ''' property for secrets ''' + self._image_pull_secrets = secrets + + @property + def secrets(self): + ''' property for secrets ''' + if not self._secrets: + self._secrets = self.get(ServiceAccount.secrets_path) or [] + return self._secrets + + @secrets.setter + def secrets(self, secrets): + ''' property for secrets ''' + self._secrets = secrets + + def delete_secret(self, inc_secret): + ''' remove a secret ''' + remove_idx = None + for idx, sec in enumerate(self.secrets): + if sec['name'] == inc_secret: + remove_idx = idx + break + + if remove_idx: + del self.secrets[remove_idx] + return True + + return False + + def delete_image_pull_secret(self, inc_secret): + ''' remove a image_pull_secret ''' + remove_idx = None + for idx, sec in enumerate(self.image_pull_secrets): + if sec['name'] == inc_secret: + remove_idx = idx + break + + if remove_idx: + del self.image_pull_secrets[remove_idx] + return True + + return False + + def find_secret(self, inc_secret): + '''find secret''' + for secret in self.secrets: + if secret['name'] == inc_secret: + return secret + + return None + + def find_image_pull_secret(self, inc_secret): + '''find secret''' + for secret in self.image_pull_secrets: + if secret['name'] == inc_secret: + return secret + + return None + + def add_secret(self, inc_secret): + '''add secret''' + if self.secrets: + self.secrets.append({"name": inc_secret}) # pylint: disable=no-member + else: + self.put(ServiceAccount.secrets_path, [{"name": inc_secret}]) + + def add_image_pull_secret(self, inc_secret): + '''add image_pull_secret''' + if self.image_pull_secrets: + self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member + else: + self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}]) + +# -*- -*- -*- End included fragment: lib/serviceaccount.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*- + +# pylint: disable=too-many-instance-attributes +class SecretConfig(object): + ''' Handle secret options ''' + # pylint: disable=too-many-arguments + def __init__(self, + sname, + namespace, + kubeconfig, + secrets=None): + ''' constructor for handling secret options ''' + self.kubeconfig = kubeconfig + self.name = sname + self.namespace = namespace + self.secrets = secrets + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' assign the correct properties for a secret dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'Secret' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + self.data['data'] = {} + if self.secrets: + for key, value in self.secrets.items(): + self.data['data'][key] = value + +# pylint: disable=too-many-instance-attributes +class Secret(Yedit): + ''' Class to wrap the oc command line tools ''' + secret_path = "data" + kind = 'secret' + + def __init__(self, content): + '''secret constructor''' + super(Secret, self).__init__(content=content) + self._secrets = None + + @property + def secrets(self): + '''secret property getter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + @secrets.setter + def secrets(self): + '''secret property setter''' + if self._secrets is None: + self._secrets = self.get_secrets() + return self._secrets + + def get_secrets(self): + ''' returns all of the defined secrets ''' + return self.get(Secret.secret_path) or {} + + def add_secret(self, key, value): + ''' add a secret ''' + if self.secrets: + self.secrets[key] = value + else: + self.put(Secret.secret_path, {key: value}) + + return True + + def delete_secret(self, key): + ''' delete secret''' + try: + del self.secrets[key] + except KeyError as _: + return False + + return True + + def find_secret(self, key): + ''' find secret''' + rval = None + try: + rval = self.secrets[key] + except KeyError as _: + return None + + return {'key': key, 'value': rval} + + def update_secret(self, key, value): + ''' update a secret''' + # pylint: disable=no-member + if key in self.secrets: + self.secrets[key] = value + else: + self.add_secret(key, value) + + return True + +# -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/rolebinding.py -*- -*- -*- + +# pylint: disable=too-many-instance-attributes +class RoleBindingConfig(object): + ''' Handle rolebinding config ''' + # pylint: disable=too-many-arguments + def __init__(self, + name, + namespace, + kubeconfig, + group_names=None, + role_ref=None, + subjects=None, + usernames=None): + ''' constructor for handling rolebinding options ''' + self.kubeconfig = kubeconfig + self.name = name + self.namespace = namespace + self.group_names = group_names + self.role_ref = role_ref + self.subjects = subjects + self.usernames = usernames + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' create a default rolebinding as a dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'RoleBinding' + self.data['groupNames'] = self.group_names + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + + self.data['roleRef'] = self.role_ref + self.data['subjects'] = self.subjects + self.data['userNames'] = self.usernames + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class RoleBinding(Yedit): + ''' Class to model a rolebinding openshift object''' + group_names_path = "groupNames" + role_ref_path = "roleRef" + subjects_path = "subjects" + user_names_path = "userNames" + + kind = 'RoleBinding' + + def __init__(self, content): + '''RoleBinding constructor''' + super(RoleBinding, self).__init__(content=content) + self._subjects = None + self._role_ref = None + self._group_names = None + self._user_names = None + + @property + def subjects(self): + ''' subjects property ''' + if self._subjects is None: + self._subjects = self.get_subjects() + return self._subjects + + @subjects.setter + def subjects(self, data): + ''' subjects property setter''' + self._subjects = data + + @property + def role_ref(self): + ''' role_ref property ''' + if self._role_ref is None: + self._role_ref = self.get_role_ref() + return self._role_ref + + @role_ref.setter + def role_ref(self, data): + ''' role_ref property setter''' + self._role_ref = data + + @property + def group_names(self): + ''' group_names property ''' + if self._group_names is None: + self._group_names = self.get_group_names() + return self._group_names + + @group_names.setter + def group_names(self, data): + ''' group_names property setter''' + self._group_names = data + + @property + def user_names(self): + ''' user_names property ''' + if self._user_names is None: + self._user_names = self.get_user_names() + return self._user_names + + @user_names.setter + def user_names(self, data): + ''' user_names property setter''' + self._user_names = data + + def get_group_names(self): + ''' return groupNames ''' + return self.get(RoleBinding.group_names_path) or [] + + def get_user_names(self): + ''' return usernames ''' + return self.get(RoleBinding.user_names_path) or [] + + def get_role_ref(self): + ''' return role_ref ''' + return self.get(RoleBinding.role_ref_path) or {} + + def get_subjects(self): + ''' return subjects ''' + return self.get(RoleBinding.subjects_path) or [] + + #### ADD ##### + def add_subject(self, inc_subject): + ''' add a subject ''' + if self.subjects: + # pylint: disable=no-member + self.subjects.append(inc_subject) + else: + self.put(RoleBinding.subjects_path, [inc_subject]) + + return True + + def add_role_ref(self, inc_role_ref): + ''' add a role_ref ''' + if not self.role_ref: + self.put(RoleBinding.role_ref_path, {"name": inc_role_ref}) + return True + + return False + + def add_group_names(self, inc_group_names): + ''' add a group_names ''' + if self.group_names: + # pylint: disable=no-member + self.group_names.append(inc_group_names) + else: + self.put(RoleBinding.group_names_path, [inc_group_names]) + + return True + + def add_user_name(self, inc_user_name): + ''' add a username ''' + if self.user_names: + # pylint: disable=no-member + self.user_names.append(inc_user_name) + else: + self.put(RoleBinding.user_names_path, [inc_user_name]) + + return True + + #### /ADD ##### + + #### Remove ##### + def remove_subject(self, inc_subject): + ''' remove a subject ''' + try: + # pylint: disable=no-member + self.subjects.remove(inc_subject) + except ValueError as _: + return False + + return True + + def remove_role_ref(self, inc_role_ref): + ''' remove a role_ref ''' + if self.role_ref and self.role_ref['name'] == inc_role_ref: + del self.role_ref['name'] + return True + + return False + + def remove_group_name(self, inc_group_name): + ''' remove a groupname ''' + try: + # pylint: disable=no-member + self.group_names.remove(inc_group_name) + except ValueError as _: + return False + + return True + + def remove_user_name(self, inc_user_name): + ''' remove a username ''' + try: + # pylint: disable=no-member + self.user_names.remove(inc_user_name) + except ValueError as _: + return False + + return True + + #### /REMOVE ##### + + #### UPDATE ##### + def update_subject(self, inc_subject): + ''' update a subject ''' + try: + # pylint: disable=no-member + index = self.subjects.index(inc_subject) + except ValueError as _: + return self.add_subject(inc_subject) + + self.subjects[index] = inc_subject + + return True + + def update_group_name(self, inc_group_name): + ''' update a groupname ''' + try: + # pylint: disable=no-member + index = self.group_names.index(inc_group_name) + except ValueError as _: + return self.add_group_names(inc_group_name) + + self.group_names[index] = inc_group_name + + return True + + def update_user_name(self, inc_user_name): + ''' update a username ''' + try: + # pylint: disable=no-member + index = self.user_names.index(inc_user_name) + except ValueError as _: + return self.add_user_name(inc_user_name) + + self.user_names[index] = inc_user_name + + return True + + def update_role_ref(self, inc_role_ref): + ''' update a role_ref ''' + self.role_ref['name'] = inc_role_ref + + return True + + #### /UPDATE ##### + + #### FIND #### + def find_subject(self, inc_subject): + ''' find a subject ''' + index = None + try: + # pylint: disable=no-member + index = self.subjects.index(inc_subject) + except ValueError as _: + return index + + return index + + def find_group_name(self, inc_group_name): + ''' find a group_name ''' + index = None + try: + # pylint: disable=no-member + index = self.group_names.index(inc_group_name) + except ValueError as _: + return index + + return index + + def find_user_name(self, inc_user_name): + ''' find a user_name ''' + index = None + try: + # pylint: disable=no-member + index = self.user_names.index(inc_user_name) + except ValueError as _: + return index + + return index + + def find_role_ref(self, inc_role_ref): + ''' find a user_name ''' + if self.role_ref and self.role_ref['name'] == inc_role_ref['name']: + return self.role_ref + + return None + +# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: class/oc_adm_router.py -*- -*- -*- + + +class RouterException(Exception): + ''' Router exception''' + pass + + +class RouterConfig(OpenShiftCLIConfig): + ''' RouterConfig is a DTO for the router. ''' + def __init__(self, rname, namespace, kubeconfig, router_options): + super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options) + + +class Router(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + def __init__(self, + router_config, + verbose=False): + ''' Constructor for OpenshiftOC + + a router consists of 3 or more parts + - dc/router + - svc/router + - sa/router + - secret/router-certs + - clusterrolebinding/router-router-role + ''' + super(Router, self).__init__('default', router_config.kubeconfig, verbose) + self.config = router_config + self.verbose = verbose + self.router_parts = [{'kind': 'dc', 'name': self.config.name}, + {'kind': 'svc', 'name': self.config.name}, + {'kind': 'sa', 'name': self.config.config_options['service_account']['value']}, + {'kind': 'secret', 'name': self.config.name + '-certs'}, + {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'}, + ] + + self.__prepared_router = None + self.dconfig = None + self.svc = None + self._secret = None + self._serviceaccount = None + self._rolebinding = None + + @property + def prepared_router(self): + ''' property for the prepared router''' + if self.__prepared_router is None: + results = self._prepare_router() + if not results: + raise RouterException('Could not perform router preparation') + self.__prepared_router = results + + return self.__prepared_router + + @prepared_router.setter + def prepared_router(self, obj): + '''setter for the prepared_router''' + self.__prepared_router = obj + + @property + def deploymentconfig(self): + ''' property deploymentconfig''' + return self.dconfig + + @deploymentconfig.setter + def deploymentconfig(self, config): + ''' setter for property deploymentconfig ''' + self.dconfig = config + + @property + def service(self): + ''' property for service ''' + return self.svc + + @service.setter + def service(self, config): + ''' setter for property service ''' + self.svc = config + + @property + def secret(self): + ''' property secret ''' + return self._secret + + @secret.setter + def secret(self, config): + ''' setter for property secret ''' + self._secret = config + + @property + def serviceaccount(self): + ''' property for serviceaccount ''' + return self._serviceaccount + + @serviceaccount.setter + def serviceaccount(self, config): + ''' setter for property serviceaccount ''' + self._serviceaccount = config + + @property + def rolebinding(self): + ''' property rolebinding ''' + return self._rolebinding + + @rolebinding.setter + def rolebinding(self, config): + ''' setter for property rolebinding ''' + self._rolebinding = config + + def get(self): + ''' return the self.router_parts ''' + self.service = None + self.deploymentconfig = None + self.serviceaccount = None + self.secret = None + self.rolebinding = None + for part in self.router_parts: + result = self._get(part['kind'], rname=part['name']) + if result['returncode'] == 0 and part['kind'] == 'dc': + self.deploymentconfig = DeploymentConfig(result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'svc': + self.service = Service(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'sa': + self.serviceaccount = ServiceAccount(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'secret': + self.secret = Secret(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding': + self.rolebinding = RoleBinding(content=result['results'][0]) + + return {'deploymentconfig': self.deploymentconfig, + 'service': self.service, + 'serviceaccount': self.serviceaccount, + 'secret': self.secret, + 'clusterrolebinding': self.rolebinding, + } + + def exists(self): + '''return a whether svc or dc exists ''' + if self.deploymentconfig and self.service and self.secret and self.serviceaccount: + return True + + return False + + def delete(self): + '''return all pods ''' + parts = [] + for part in self.router_parts: + parts.append(self._delete(part['kind'], part['name'])) + + rval = 0 + for part in parts: + if part['returncode'] != 0 and not 'already exist' in part['stderr']: + rval = part['returncode'] + + return {'returncode': rval, 'results': parts} + + def add_modifications(self, deploymentconfig): + '''modify the deployment config''' + # We want modifications in the form of edits coming in from the module. + # Let's apply these here + edit_results = [] + for edit in self.config.config_options['edits'].get('value', []): + if edit['action'] == 'put': + edit_results.append(deploymentconfig.put(edit['key'], + edit['value'])) + if edit['action'] == 'update': + edit_results.append(deploymentconfig.update(edit['key'], + edit['value'], + edit.get('index', None), + edit.get('curr_value', None))) + if edit['action'] == 'append': + edit_results.append(deploymentconfig.append(edit['key'], + edit['value'])) + + if edit_results and not any([res[0] for res in edit_results]): + return None + + return deploymentconfig + + def _prepare_router(self): + '''prepare router for instantiation''' + # We need to create the pem file + if self.config.config_options['default_cert']['value'] is None: + router_pem = '/tmp/router.pem' + with open(router_pem, 'w') as rfd: + rfd.write(open(self.config.config_options['cert_file']['value']).read()) + rfd.write(open(self.config.config_options['key_file']['value']).read()) + if self.config.config_options['cacert_file']['value'] and \ + os.path.exists(self.config.config_options['cacert_file']['value']): + rfd.write(open(self.config.config_options['cacert_file']['value']).read()) + + atexit.register(Utils.cleanup, [router_pem]) + self.config.config_options['default_cert']['value'] = router_pem + + options = self.config.to_option_list() + + cmd = ['router', self.config.name, '-n', self.config.namespace] + cmd.extend(options) + cmd.extend(['--dry-run=True', '-o', 'json']) + + results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json') + + # pylint: disable=no-member + if results['returncode'] != 0 and 'items' in results['results']: + return results + + oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False}, + 'Secret': {'obj': None, 'path': None, 'update': False}, + 'ServiceAccount': {'obj': None, 'path': None, 'update': False}, + 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False}, + 'Service': {'obj': None, 'path': None, 'update': False}, + } + # pylint: disable=invalid-sequence-index + for res in results['results']['items']: + if res['kind'] == 'DeploymentConfig': + oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res) + elif res['kind'] == 'Service': + oc_objects['Service']['obj'] = Service(res) + elif res['kind'] == 'ServiceAccount': + oc_objects['ServiceAccount']['obj'] = ServiceAccount(res) + elif res['kind'] == 'Secret': + oc_objects['Secret']['obj'] = Secret(res) + elif res['kind'] == 'ClusterRoleBinding': + oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res) + + # Currently only deploymentconfig needs updating + # Verify we got a deploymentconfig + if not oc_objects['DeploymentConfig']['obj']: + return results + + # add modifications added + oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj']) + + for oc_type, oc_data in oc_objects.items(): + oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict) + + return oc_objects + + def create(self): + '''Create a deploymentconfig ''' + results = [] + + # pylint: disable=no-member + for _, oc_data in self.prepared_router.items(): + results.append(self._create(oc_data['path'])) + + rval = 0 + for result in results: + if result['returncode'] != 0 and not 'already exist' in result['stderr']: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def update(self): + '''run update for the router. This performs a replace''' + results = [] + + # pylint: disable=no-member + for _, oc_data in self.prepared_router.items(): + if oc_data['update']: + results.append(self._replace(oc_data['path'])) + + rval = 0 + for result in results: + if result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + # pylint: disable=too-many-return-statements,too-many-branches + def needs_update(self): + ''' check to see if we need to update ''' + if not self.deploymentconfig or not self.service or not self.serviceaccount or not self.secret: + return True + + # ServiceAccount: + # Need to determine changes from the pregenerated ones from the original + # Since these are auto generated, we can skip + skip = ['secrets', 'imagePullSecrets'] + if not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict, + self.serviceaccount.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['ServiceAccount']['update'] = True + + # Secret: + # See if one was generated from our dry-run and verify it if needed + if self.prepared_router['Secret']['obj']: + if not self.secret: + self.prepared_router['Secret']['update'] = True + + if not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict, + self.secret.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['Secret']['update'] = True + + # Service: + # Fix the ports to have protocol=TCP + for port in self.prepared_router['Service']['obj'].get('spec.ports'): + port['protocol'] = 'TCP' + + skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type'] + if not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict, + self.service.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['Service']['update'] = True + + # DeploymentConfig: + # Router needs some exceptions. + # We do not want to check the autogenerated password for stats admin + if not self.config.config_options['stats_password']['value']: + for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ + 'spec.template.spec.containers[0].env') or []): + if env_var['name'] == 'STATS_PASSWORD': + env_var['value'] = \ + self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx) + break + + # dry-run doesn't add the protocol to the ports section. We will manually do that. + for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ + 'spec.template.spec.containers[0].ports') or []): + if not 'protocol' in port: + port['protocol'] = 'TCP' + + # These are different when generating + skip = ['dnsPolicy', + 'terminationGracePeriodSeconds', + 'restartPolicy', 'timeoutSeconds', + 'livenessProbe', 'readinessProbe', + 'terminationMessagePath', 'hostPort', + 'defaultMode', + ] + + if not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict, + self.deploymentconfig.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['DeploymentConfig']['update'] = True + + # Check if any of the parts need updating, if so, return True + # else, no need to update + # pylint: disable=no-member + return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()]) + + @staticmethod + def run_ansible(params, check_mode): + '''run ansible idempotent code''' + + rconfig = RouterConfig(params['name'], + params['namespace'], + params['kubeconfig'], + {'default_cert': {'value': params['default_cert'], 'include': True}, + 'cert_file': {'value': params['cert_file'], 'include': False}, + 'key_file': {'value': params['key_file'], 'include': False}, + 'images': {'value': params['images'], 'include': True}, + 'latest_images': {'value': params['latest_images'], 'include': True}, + 'labels': {'value': params['labels'], 'include': True}, + 'ports': {'value': ','.join(params['ports']), 'include': True}, + 'replicas': {'value': params['replicas'], 'include': True}, + 'selector': {'value': params['selector'], 'include': True}, + 'service_account': {'value': params['service_account'], 'include': True}, + 'router_type': {'value': params['router_type'], 'include': False}, + 'host_network': {'value': params['host_network'], 'include': True}, + 'external_host': {'value': params['external_host'], 'include': True}, + 'external_host_vserver': {'value': params['external_host_vserver'], + 'include': True}, + 'external_host_insecure': {'value': params['external_host_insecure'], + 'include': True}, + 'external_host_partition_path': {'value': params['external_host_partition_path'], + 'include': True}, + 'external_host_username': {'value': params['external_host_username'], + 'include': True}, + 'external_host_password': {'value': params['external_host_password'], + 'include': True}, + 'external_host_private_key': {'value': params['external_host_private_key'], + 'include': True}, + 'expose_metrics': {'value': params['expose_metrics'], 'include': True}, + 'metrics_image': {'value': params['metrics_image'], 'include': True}, + 'stats_user': {'value': params['stats_user'], 'include': True}, + 'stats_password': {'value': params['stats_password'], 'include': True}, + 'stats_port': {'value': params['stats_port'], 'include': True}, + # extra + 'cacert_file': {'value': params['cacert_file'], 'include': False}, + # edits + 'edits': {'value': params['edits'], 'include': False}, + }) + + + state = params['state'] + + ocrouter = Router(rconfig, verbose=params['debug']) + + api_rval = ocrouter.get() + + ######## + # get + ######## + if state == 'list': + return {'changed': False, 'results': api_rval, 'state': state} + + ######## + # Delete + ######## + if state == 'absent': + if not ocrouter.exists(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'} + + # In case of delete we return a list of each object + # that represents a router and its result in a list + # pylint: disable=redefined-variable-type + api_rval = ocrouter.delete() + + return {'changed': True, 'results': api_rval, 'state': state} + + if state == 'present': + ######## + # Create + ######## + if not ocrouter.exists(): + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} + + api_rval = ocrouter.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + ######## + # Update + ######## + if not ocrouter.needs_update(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'} + + api_rval = ocrouter.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + +# -*- -*- -*- End included fragment: class/oc_adm_router.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ansible/oc_adm_router.py -*- -*- -*- + + +def main(): + ''' + ansible oc module for router + ''' + + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', type='str', + choices=['present', 'absent']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default='router', type='str'), + + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + default_cert=dict(default=None, type='str'), + cert_file=dict(default=None, type='str'), + key_file=dict(default=None, type='str'), + images=dict(default=None, type='str'), #'openshift3/ose-${component}:${version}' + latest_images=dict(default=False, type='bool'), + labels=dict(default=None, type='list'), + ports=dict(default=['80:80', '443:443'], type='list'), + replicas=dict(default=1, type='int'), + selector=dict(default=None, type='str'), + service_account=dict(default='router', type='str'), + router_type=dict(default='haproxy-router', type='str'), + host_network=dict(default=True, type='bool'), + # external host options + external_host=dict(default=None, type='str'), + external_host_vserver=dict(default=None, type='str'), + external_host_insecure=dict(default=False, type='bool'), + external_host_partition_path=dict(default=None, type='str'), + external_host_username=dict(default=None, type='str'), + external_host_password=dict(default=None, type='str'), + external_host_private_key=dict(default=None, type='str'), + # Metrics + expose_metrics=dict(default=False, type='bool'), + metrics_image=dict(default=None, type='str'), + # Stats + stats_user=dict(default=None, type='str'), + stats_password=dict(default=None, type='str'), + stats_port=dict(default=1936, type='int'), + # extra + cacert_file=dict(default=None, type='str'), + # edits + edits=dict(default=[], type='list'), + ), + mutually_exclusive=[["router_type", "images"], + ["key_file", "default_cert"], + ["cert_file", "default_cert"], + ], + + supports_check_mode=True, + ) + results = Router.run_ansible(module.params, module.check_mode) + + if 'failed' in results: + module.fail_json(**results) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() + +# -*- -*- -*- End included fragment: ansible/oc_adm_router.py -*- -*- -*- diff --git a/roles/lib_openshift/library/oc_env.py b/roles/lib_openshift/library/oc_env.py index 7c2ccb98f..1426565b4 100644 --- a/roles/lib_openshift/library/oc_env.py +++ b/roles/lib_openshift/library/oc_env.py @@ -1309,7 +1309,7 @@ class OpenShiftCLIConfig(object): # pylint: disable=too-many-public-methods class DeploymentConfig(Yedit): - ''' Class to wrap the oc command line tools ''' + ''' Class to model an openshift DeploymentConfig''' default_deployment_config = ''' apiVersion: v1 kind: DeploymentConfig diff --git a/roles/lib_openshift/library/oc_scale.py b/roles/lib_openshift/library/oc_scale.py index a37b2aba0..c73e96e10 100644 --- a/roles/lib_openshift/library/oc_scale.py +++ b/roles/lib_openshift/library/oc_scale.py @@ -1296,7 +1296,7 @@ class OpenShiftCLIConfig(object): # pylint: disable=too-many-public-methods class DeploymentConfig(Yedit): - ''' Class to wrap the oc command line tools ''' + ''' Class to model an openshift DeploymentConfig''' default_deployment_config = ''' apiVersion: v1 kind: DeploymentConfig @@ -1637,7 +1637,12 @@ spec: # pylint: disable=too-many-public-methods class ReplicationController(DeploymentConfig): - ''' Class to wrap the oc command line tools ''' + ''' Class to model a replicationcontroller openshift object. + + Currently we are modeled after a deployment config since they + are very similar. In the future, when the need arises we + will add functionality to this class. + ''' replicas_path = "spec.replicas" env_path = "spec.template.spec.containers[0].env" volumes_path = "spec.template.spec.volumes" diff --git a/roles/lib_openshift/library/oc_secret.py b/roles/lib_openshift/library/oc_secret.py index c423e9442..6ab5e81b2 100644 --- a/roles/lib_openshift/library/oc_secret.py +++ b/roles/lib_openshift/library/oc_secret.py @@ -1358,7 +1358,7 @@ class SecretConfig(object): self.create_dict() def create_dict(self): - ''' return a secret as a dict ''' + ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' self.data['metadata'] = {} diff --git a/roles/lib_openshift/library/oc_serviceaccount.py b/roles/lib_openshift/library/oc_serviceaccount.py index 0d1705414..7104355f0 100644 --- a/roles/lib_openshift/library/oc_serviceaccount.py +++ b/roles/lib_openshift/library/oc_serviceaccount.py @@ -1308,7 +1308,7 @@ class ServiceAccountConfig(object): self.create_dict() def create_dict(self): - ''' return a properly structured volume ''' + ''' instantiate a properly structured volume ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'ServiceAccount' self.data['metadata'] = {} diff --git a/roles/lib_openshift/library/oc_serviceaccount_secret.py b/roles/lib_openshift/library/oc_serviceaccount_secret.py index 5f07528a0..3a9380661 100644 --- a/roles/lib_openshift/library/oc_serviceaccount_secret.py +++ b/roles/lib_openshift/library/oc_serviceaccount_secret.py @@ -1308,7 +1308,7 @@ class ServiceAccountConfig(object): self.create_dict() def create_dict(self): - ''' return a properly structured volume ''' + ''' instantiate a properly structured volume ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'ServiceAccount' self.data['metadata'] = {} diff --git a/roles/lib_openshift/src/ansible/oc_adm_registry.py b/roles/lib_openshift/src/ansible/oc_adm_registry.py new file mode 100644 index 000000000..a49b84589 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_adm_registry.py @@ -0,0 +1,47 @@ +# pylint: skip-file +# flake8: noqa + +def main(): + ''' + ansible oc module for registry + ''' + + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', type='str', + choices=['present', 'absent']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default=None, required=True, type='str'), + + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + images=dict(default=None, type='str'), + latest_images=dict(default=False, type='bool'), + labels=dict(default=None, type='list'), + ports=dict(default=['5000'], type='list'), + replicas=dict(default=1, type='int'), + selector=dict(default=None, type='str'), + service_account=dict(default='registry', type='str'), + mount_host=dict(default=None, type='str'), + volume_mounts=dict(default=None, type='list'), + env_vars=dict(default=None, type='dict'), + edits=dict(default=None, type='list'), + enforce_quota=dict(default=False, type='bool'), + force=dict(default=False, type='bool'), + daemonset=dict(default=False, type='bool'), + tls_key=dict(default=None, type='str'), + tls_certificate=dict(default=None, type='str'), + ), + + supports_check_mode=True, + ) + + results = Registry.run_ansible(module.params, module.check_mode) + if 'failed' in results: + module.fail_json(**results) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/ansible/oc_adm_router.py b/roles/lib_openshift/src/ansible/oc_adm_router.py new file mode 100644 index 000000000..48c9f0ec1 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_adm_router.py @@ -0,0 +1,67 @@ +# pylint: skip-file +# flake8: noqa + + +def main(): + ''' + ansible oc module for router + ''' + + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', type='str', + choices=['present', 'absent']), + debug=dict(default=False, type='bool'), + namespace=dict(default='default', type='str'), + name=dict(default='router', type='str'), + + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + default_cert=dict(default=None, type='str'), + cert_file=dict(default=None, type='str'), + key_file=dict(default=None, type='str'), + images=dict(default=None, type='str'), #'openshift3/ose-${component}:${version}' + latest_images=dict(default=False, type='bool'), + labels=dict(default=None, type='list'), + ports=dict(default=['80:80', '443:443'], type='list'), + replicas=dict(default=1, type='int'), + selector=dict(default=None, type='str'), + service_account=dict(default='router', type='str'), + router_type=dict(default='haproxy-router', type='str'), + host_network=dict(default=True, type='bool'), + # external host options + external_host=dict(default=None, type='str'), + external_host_vserver=dict(default=None, type='str'), + external_host_insecure=dict(default=False, type='bool'), + external_host_partition_path=dict(default=None, type='str'), + external_host_username=dict(default=None, type='str'), + external_host_password=dict(default=None, type='str'), + external_host_private_key=dict(default=None, type='str'), + # Metrics + expose_metrics=dict(default=False, type='bool'), + metrics_image=dict(default=None, type='str'), + # Stats + stats_user=dict(default=None, type='str'), + stats_password=dict(default=None, type='str'), + stats_port=dict(default=1936, type='int'), + # extra + cacert_file=dict(default=None, type='str'), + # edits + edits=dict(default=[], type='list'), + ), + mutually_exclusive=[["router_type", "images"], + ["key_file", "default_cert"], + ["cert_file", "default_cert"], + ], + + supports_check_mode=True, + ) + results = Router.run_ansible(module.params, module.check_mode) + + if 'failed' in results: + module.fail_json(**results) + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/class/oc_adm_registry.py b/roles/lib_openshift/src/class/oc_adm_registry.py new file mode 100644 index 000000000..8cb7aea31 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_adm_registry.py @@ -0,0 +1,399 @@ +# pylint: skip-file +# flake8: noqa + +class RegistryException(Exception): + ''' Registry Exception Class ''' + pass + + +class RegistryConfig(OpenShiftCLIConfig): + ''' RegistryConfig is a DTO for the registry. ''' + def __init__(self, rname, namespace, kubeconfig, registry_options): + super(RegistryConfig, self).__init__(rname, namespace, kubeconfig, registry_options) + + +class Registry(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + + volume_mount_path = 'spec.template.spec.containers[0].volumeMounts' + volume_path = 'spec.template.spec.volumes' + env_path = 'spec.template.spec.containers[0].env' + + def __init__(self, + registry_config, + verbose=False): + ''' Constructor for Registry + + a registry consists of 3 or more parts + - dc/docker-registry + - svc/docker-registry + + Parameters: + :registry_config: + :verbose: + ''' + super(Registry, self).__init__(registry_config.namespace, registry_config.kubeconfig, verbose) + self.version = OCVersion(registry_config.kubeconfig, verbose) + self.svc_ip = None + self.portal_ip = None + self.config = registry_config + self.verbose = verbose + self.registry_parts = [{'kind': 'dc', 'name': self.config.name}, + {'kind': 'svc', 'name': self.config.name}, + ] + + self.__prepared_registry = None + self.volume_mounts = [] + self.volumes = [] + if self.config.config_options['volume_mounts']['value']: + for volume in self.config.config_options['volume_mounts']['value']: + volume_info = {'secret_name': volume.get('secret_name', None), + 'name': volume.get('name', None), + 'type': volume.get('type', None), + 'path': volume.get('path', None), + 'claimName': volume.get('claim_name', None), + 'claimSize': volume.get('claim_size', None), + } + + vol, vol_mount = Volume.create_volume_structure(volume_info) + self.volumes.append(vol) + self.volume_mounts.append(vol_mount) + + self.dconfig = None + self.svc = None + + @property + def deploymentconfig(self): + ''' deploymentconfig property ''' + return self.dconfig + + @deploymentconfig.setter + def deploymentconfig(self, config): + ''' setter for deploymentconfig property ''' + self.dconfig = config + + @property + def service(self): + ''' service property ''' + return self.svc + + @service.setter + def service(self, config): + ''' setter for service property ''' + self.svc = config + + @property + def prepared_registry(self): + ''' prepared_registry property ''' + if not self.__prepared_registry: + results = self.prepare_registry() + if not results: + raise RegistryException('Could not perform registry preparation.') + self.__prepared_registry = results + + return self.__prepared_registry + + @prepared_registry.setter + def prepared_registry(self, data): + ''' setter method for prepared_registry attribute ''' + self.__prepared_registry = data + + def get(self): + ''' return the self.registry_parts ''' + self.deploymentconfig = None + self.service = None + + rval = 0 + for part in self.registry_parts: + result = self._get(part['kind'], rname=part['name']) + if result['returncode'] == 0 and part['kind'] == 'dc': + self.deploymentconfig = DeploymentConfig(result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'svc': + self.service = Yedit(content=result['results'][0]) + + if result['returncode'] != 0: + rval = result['returncode'] + + + return {'returncode': rval, 'deploymentconfig': self.deploymentconfig, 'service': self.service} + + def exists(self): + '''does the object exist?''' + self.get() + if self.deploymentconfig or self.service: + return True + + return False + + def delete(self, complete=True): + '''return all pods ''' + parts = [] + for part in self.registry_parts: + if not complete and part['kind'] == 'svc': + continue + parts.append(self._delete(part['kind'], part['name'])) + + # Clean up returned results + rval = 0 + for part in parts: + # pylint: disable=invalid-sequence-index + if 'returncode' in part and part['returncode'] != 0: + rval = part['returncode'] + + return {'returncode': rval, 'results': parts} + + def prepare_registry(self): + ''' prepare a registry for instantiation ''' + options = self.config.to_option_list() + + cmd = ['registry', '-n', self.config.namespace] + cmd.extend(options) + cmd.extend(['--dry-run=True', '-o', 'json']) + + results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json') + # probably need to parse this + # pylint thinks results is a string + # pylint: disable=no-member + if results['returncode'] != 0 and results['results'].has_key('items'): + return results + + service = None + deploymentconfig = None + # pylint: disable=invalid-sequence-index + for res in results['results']['items']: + if res['kind'] == 'DeploymentConfig': + deploymentconfig = DeploymentConfig(res) + elif res['kind'] == 'Service': + service = Service(res) + + # Verify we got a service and a deploymentconfig + if not service or not deploymentconfig: + return results + + # results will need to get parsed here and modifications added + deploymentconfig = DeploymentConfig(self.add_modifications(deploymentconfig)) + + # modify service ip + if self.svc_ip: + service.put('spec.clusterIP', self.svc_ip) + if self.portal_ip: + service.put('spec.portalIP', self.portal_ip) + + # need to create the service and the deploymentconfig + service_file = Utils.create_tmp_file_from_contents('service', service.yaml_dict) + deployment_file = Utils.create_tmp_file_from_contents('deploymentconfig', deploymentconfig.yaml_dict) + + return {"service": service, + "service_file": service_file, + "service_update": False, + "deployment": deploymentconfig, + "deployment_file": deployment_file, + "deployment_update": False} + + def create(self): + '''Create a registry''' + results = [] + for config_file in ['deployment_file', 'service_file']: + results.append(self._create(self.prepared_registry[config_file])) + + # Clean up returned results + rval = 0 + for result in results: + # pylint: disable=invalid-sequence-index + if 'returncode' in result and result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def update(self): + '''run update for the registry. This performs a delete and then create ''' + # Store the current service IP + if self.service: + svcip = self.service.get('spec.clusterIP') + if svcip: + self.svc_ip = svcip + portip = self.service.get('spec.portalIP') + if portip: + self.portal_ip = portip + + results = [] + if self.prepared_registry['deployment_update']: + results.append(self._replace(self.prepared_registry['deployment_file'])) + if self.prepared_registry['service_update']: + results.append(self._replace(self.prepared_registry['service_file'])) + + # Clean up returned results + rval = 0 + for result in results: + if result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def add_modifications(self, deploymentconfig): + ''' update a deployment config with changes ''' + # Currently we know that our deployment of a registry requires a few extra modifications + # Modification 1 + # we need specific environment variables to be set + for key, value in self.config.config_options['env_vars']['value'].items(): + if not deploymentconfig.exists_env_key(key): + deploymentconfig.add_env_value(key, value) + else: + deploymentconfig.update_env_var(key, value) + + # Modification 2 + # we need specific volume variables to be set + for volume in self.volumes: + deploymentconfig.update_volume(volume) + + for vol_mount in self.volume_mounts: + deploymentconfig.update_volume_mount(vol_mount) + + # Modification 3 + # Edits + edit_results = [] + for edit in self.config.config_options['edits'].get('value', []): + if edit['action'] == 'put': + edit_results.append(deploymentconfig.put(edit['key'], + edit['value'])) + if edit['action'] == 'update': + edit_results.append(deploymentconfig.update(edit['key'], + edit['value'], + edit.get('index', None), + edit.get('curr_value', None))) + if edit['action'] == 'append': + edit_results.append(deploymentconfig.append(edit['key'], + edit['value'])) + + if edit_results and not any([res[0] for res in edit_results]): + return None + + return deploymentconfig.yaml_dict + + def needs_update(self): + ''' check to see if we need to update ''' + if not self.service or not self.deploymentconfig: + return True + + exclude_list = ['clusterIP', 'portalIP', 'type', 'protocol'] + if not Utils.check_def_equal(self.prepared_registry['service'].yaml_dict, + self.service.yaml_dict, + exclude_list, + debug=self.verbose): + self.prepared_registry['service_update'] = True + + exclude_list = ['dnsPolicy', + 'terminationGracePeriodSeconds', + 'restartPolicy', 'timeoutSeconds', + 'livenessProbe', 'readinessProbe', + 'terminationMessagePath', + 'securityContext', + 'imagePullPolicy', + 'protocol', # ports.portocol: TCP + 'type', # strategy: {'type': 'rolling'} + 'defaultMode', # added on secrets + 'activeDeadlineSeconds', # added in 1.5 for timeouts + ] + + if not Utils.check_def_equal(self.prepared_registry['deployment'].yaml_dict, + self.deploymentconfig.yaml_dict, + exclude_list, + debug=self.verbose): + self.prepared_registry['deployment_update'] = True + + return self.prepared_registry['deployment_update'] or self.prepared_registry['service_update'] or False + + # In the future, we would like to break out each ansible state into a function. + # pylint: disable=too-many-branches,too-many-return-statements + @staticmethod + def run_ansible(params, check_mode): + '''run idempotent ansible code''' + + rconfig = RegistryConfig(params['name'], + params['namespace'], + params['kubeconfig'], + {'images': {'value': params['images'], 'include': True}, + 'latest_images': {'value': params['latest_images'], 'include': True}, + 'labels': {'value': params['labels'], 'include': True}, + 'ports': {'value': ','.join(params['ports']), 'include': True}, + 'replicas': {'value': params['replicas'], 'include': True}, + 'selector': {'value': params['selector'], 'include': True}, + 'service_account': {'value': params['service_account'], 'include': True}, + 'mount_host': {'value': params['mount_host'], 'include': True}, + 'env_vars': {'value': params['env_vars'], 'include': False}, + 'volume_mounts': {'value': params['volume_mounts'], 'include': False}, + 'edits': {'value': params['edits'], 'include': False}, + 'enforce_quota': {'value': params['enforce_quota'], 'include': True}, + 'daemonset': {'value': params['daemonset'], 'include': True}, + 'tls_key': {'value': params['tls_key'], 'include': True}, + 'tls_certificate': {'value': params['tls_certificate'], 'include': True}, + }) + + + ocregistry = Registry(rconfig, params['debug']) + + api_rval = ocregistry.get() + + state = params['state'] + ######## + # get + ######## + if state == 'list': + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': False, 'results': api_rval, 'state': state} + + ######## + # Delete + ######## + if state == 'absent': + if not ocregistry.exists(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'} + + # Unsure as to why this is angry with the return type. + # pylint: disable=redefined-variable-type + api_rval = ocregistry.delete() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + if state == 'present': + ######## + # Create + ######## + if not ocregistry.exists(): + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} + + api_rval = ocregistry.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + ######## + # Update + ######## + if not params['force'] and not ocregistry.needs_update(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'} + + api_rval = ocregistry.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + return {'failed': True, 'msg': 'Unknown state passed. %s' % state} diff --git a/roles/lib_openshift/src/class/oc_adm_router.py b/roles/lib_openshift/src/class/oc_adm_router.py new file mode 100644 index 000000000..9d61cfdf2 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_adm_router.py @@ -0,0 +1,453 @@ +# pylint: skip-file +# flake8: noqa + + +class RouterException(Exception): + ''' Router exception''' + pass + + +class RouterConfig(OpenShiftCLIConfig): + ''' RouterConfig is a DTO for the router. ''' + def __init__(self, rname, namespace, kubeconfig, router_options): + super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options) + + +class Router(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + def __init__(self, + router_config, + verbose=False): + ''' Constructor for OpenshiftOC + + a router consists of 3 or more parts + - dc/router + - svc/router + - sa/router + - secret/router-certs + - clusterrolebinding/router-router-role + ''' + super(Router, self).__init__('default', router_config.kubeconfig, verbose) + self.config = router_config + self.verbose = verbose + self.router_parts = [{'kind': 'dc', 'name': self.config.name}, + {'kind': 'svc', 'name': self.config.name}, + {'kind': 'sa', 'name': self.config.config_options['service_account']['value']}, + {'kind': 'secret', 'name': self.config.name + '-certs'}, + {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'}, + ] + + self.__prepared_router = None + self.dconfig = None + self.svc = None + self._secret = None + self._serviceaccount = None + self._rolebinding = None + + @property + def prepared_router(self): + ''' property for the prepared router''' + if self.__prepared_router is None: + results = self._prepare_router() + if not results: + raise RouterException('Could not perform router preparation') + self.__prepared_router = results + + return self.__prepared_router + + @prepared_router.setter + def prepared_router(self, obj): + '''setter for the prepared_router''' + self.__prepared_router = obj + + @property + def deploymentconfig(self): + ''' property deploymentconfig''' + return self.dconfig + + @deploymentconfig.setter + def deploymentconfig(self, config): + ''' setter for property deploymentconfig ''' + self.dconfig = config + + @property + def service(self): + ''' property for service ''' + return self.svc + + @service.setter + def service(self, config): + ''' setter for property service ''' + self.svc = config + + @property + def secret(self): + ''' property secret ''' + return self._secret + + @secret.setter + def secret(self, config): + ''' setter for property secret ''' + self._secret = config + + @property + def serviceaccount(self): + ''' property for serviceaccount ''' + return self._serviceaccount + + @serviceaccount.setter + def serviceaccount(self, config): + ''' setter for property serviceaccount ''' + self._serviceaccount = config + + @property + def rolebinding(self): + ''' property rolebinding ''' + return self._rolebinding + + @rolebinding.setter + def rolebinding(self, config): + ''' setter for property rolebinding ''' + self._rolebinding = config + + def get(self): + ''' return the self.router_parts ''' + self.service = None + self.deploymentconfig = None + self.serviceaccount = None + self.secret = None + self.rolebinding = None + for part in self.router_parts: + result = self._get(part['kind'], rname=part['name']) + if result['returncode'] == 0 and part['kind'] == 'dc': + self.deploymentconfig = DeploymentConfig(result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'svc': + self.service = Service(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'sa': + self.serviceaccount = ServiceAccount(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'secret': + self.secret = Secret(content=result['results'][0]) + elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding': + self.rolebinding = RoleBinding(content=result['results'][0]) + + return {'deploymentconfig': self.deploymentconfig, + 'service': self.service, + 'serviceaccount': self.serviceaccount, + 'secret': self.secret, + 'clusterrolebinding': self.rolebinding, + } + + def exists(self): + '''return a whether svc or dc exists ''' + if self.deploymentconfig and self.service and self.secret and self.serviceaccount: + return True + + return False + + def delete(self): + '''return all pods ''' + parts = [] + for part in self.router_parts: + parts.append(self._delete(part['kind'], part['name'])) + + rval = 0 + for part in parts: + if part['returncode'] != 0 and not 'already exist' in part['stderr']: + rval = part['returncode'] + + return {'returncode': rval, 'results': parts} + + def add_modifications(self, deploymentconfig): + '''modify the deployment config''' + # We want modifications in the form of edits coming in from the module. + # Let's apply these here + edit_results = [] + for edit in self.config.config_options['edits'].get('value', []): + if edit['action'] == 'put': + edit_results.append(deploymentconfig.put(edit['key'], + edit['value'])) + if edit['action'] == 'update': + edit_results.append(deploymentconfig.update(edit['key'], + edit['value'], + edit.get('index', None), + edit.get('curr_value', None))) + if edit['action'] == 'append': + edit_results.append(deploymentconfig.append(edit['key'], + edit['value'])) + + if edit_results and not any([res[0] for res in edit_results]): + return None + + return deploymentconfig + + def _prepare_router(self): + '''prepare router for instantiation''' + # We need to create the pem file + if self.config.config_options['default_cert']['value'] is None: + router_pem = '/tmp/router.pem' + with open(router_pem, 'w') as rfd: + rfd.write(open(self.config.config_options['cert_file']['value']).read()) + rfd.write(open(self.config.config_options['key_file']['value']).read()) + if self.config.config_options['cacert_file']['value'] and \ + os.path.exists(self.config.config_options['cacert_file']['value']): + rfd.write(open(self.config.config_options['cacert_file']['value']).read()) + + atexit.register(Utils.cleanup, [router_pem]) + self.config.config_options['default_cert']['value'] = router_pem + + options = self.config.to_option_list() + + cmd = ['router', self.config.name, '-n', self.config.namespace] + cmd.extend(options) + cmd.extend(['--dry-run=True', '-o', 'json']) + + results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json') + + # pylint: disable=no-member + if results['returncode'] != 0 and 'items' in results['results']: + return results + + oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False}, + 'Secret': {'obj': None, 'path': None, 'update': False}, + 'ServiceAccount': {'obj': None, 'path': None, 'update': False}, + 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False}, + 'Service': {'obj': None, 'path': None, 'update': False}, + } + # pylint: disable=invalid-sequence-index + for res in results['results']['items']: + if res['kind'] == 'DeploymentConfig': + oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res) + elif res['kind'] == 'Service': + oc_objects['Service']['obj'] = Service(res) + elif res['kind'] == 'ServiceAccount': + oc_objects['ServiceAccount']['obj'] = ServiceAccount(res) + elif res['kind'] == 'Secret': + oc_objects['Secret']['obj'] = Secret(res) + elif res['kind'] == 'ClusterRoleBinding': + oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res) + + # Currently only deploymentconfig needs updating + # Verify we got a deploymentconfig + if not oc_objects['DeploymentConfig']['obj']: + return results + + # add modifications added + oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj']) + + for oc_type, oc_data in oc_objects.items(): + oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict) + + return oc_objects + + def create(self): + '''Create a deploymentconfig ''' + results = [] + + # pylint: disable=no-member + for _, oc_data in self.prepared_router.items(): + results.append(self._create(oc_data['path'])) + + rval = 0 + for result in results: + if result['returncode'] != 0 and not 'already exist' in result['stderr']: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + def update(self): + '''run update for the router. This performs a replace''' + results = [] + + # pylint: disable=no-member + for _, oc_data in self.prepared_router.items(): + if oc_data['update']: + results.append(self._replace(oc_data['path'])) + + rval = 0 + for result in results: + if result['returncode'] != 0: + rval = result['returncode'] + + return {'returncode': rval, 'results': results} + + # pylint: disable=too-many-return-statements,too-many-branches + def needs_update(self): + ''' check to see if we need to update ''' + if not self.deploymentconfig or not self.service or not self.serviceaccount or not self.secret: + return True + + # ServiceAccount: + # Need to determine changes from the pregenerated ones from the original + # Since these are auto generated, we can skip + skip = ['secrets', 'imagePullSecrets'] + if not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict, + self.serviceaccount.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['ServiceAccount']['update'] = True + + # Secret: + # See if one was generated from our dry-run and verify it if needed + if self.prepared_router['Secret']['obj']: + if not self.secret: + self.prepared_router['Secret']['update'] = True + + if not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict, + self.secret.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['Secret']['update'] = True + + # Service: + # Fix the ports to have protocol=TCP + for port in self.prepared_router['Service']['obj'].get('spec.ports'): + port['protocol'] = 'TCP' + + skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type'] + if not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict, + self.service.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['Service']['update'] = True + + # DeploymentConfig: + # Router needs some exceptions. + # We do not want to check the autogenerated password for stats admin + if not self.config.config_options['stats_password']['value']: + for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ + 'spec.template.spec.containers[0].env') or []): + if env_var['name'] == 'STATS_PASSWORD': + env_var['value'] = \ + self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx) + break + + # dry-run doesn't add the protocol to the ports section. We will manually do that. + for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ + 'spec.template.spec.containers[0].ports') or []): + if not 'protocol' in port: + port['protocol'] = 'TCP' + + # These are different when generating + skip = ['dnsPolicy', + 'terminationGracePeriodSeconds', + 'restartPolicy', 'timeoutSeconds', + 'livenessProbe', 'readinessProbe', + 'terminationMessagePath', 'hostPort', + 'defaultMode', + ] + + if not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict, + self.deploymentconfig.yaml_dict, + skip_keys=skip, + debug=self.verbose): + self.prepared_router['DeploymentConfig']['update'] = True + + # Check if any of the parts need updating, if so, return True + # else, no need to update + # pylint: disable=no-member + return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()]) + + @staticmethod + def run_ansible(params, check_mode): + '''run ansible idempotent code''' + + rconfig = RouterConfig(params['name'], + params['namespace'], + params['kubeconfig'], + {'default_cert': {'value': params['default_cert'], 'include': True}, + 'cert_file': {'value': params['cert_file'], 'include': False}, + 'key_file': {'value': params['key_file'], 'include': False}, + 'images': {'value': params['images'], 'include': True}, + 'latest_images': {'value': params['latest_images'], 'include': True}, + 'labels': {'value': params['labels'], 'include': True}, + 'ports': {'value': ','.join(params['ports']), 'include': True}, + 'replicas': {'value': params['replicas'], 'include': True}, + 'selector': {'value': params['selector'], 'include': True}, + 'service_account': {'value': params['service_account'], 'include': True}, + 'router_type': {'value': params['router_type'], 'include': False}, + 'host_network': {'value': params['host_network'], 'include': True}, + 'external_host': {'value': params['external_host'], 'include': True}, + 'external_host_vserver': {'value': params['external_host_vserver'], + 'include': True}, + 'external_host_insecure': {'value': params['external_host_insecure'], + 'include': True}, + 'external_host_partition_path': {'value': params['external_host_partition_path'], + 'include': True}, + 'external_host_username': {'value': params['external_host_username'], + 'include': True}, + 'external_host_password': {'value': params['external_host_password'], + 'include': True}, + 'external_host_private_key': {'value': params['external_host_private_key'], + 'include': True}, + 'expose_metrics': {'value': params['expose_metrics'], 'include': True}, + 'metrics_image': {'value': params['metrics_image'], 'include': True}, + 'stats_user': {'value': params['stats_user'], 'include': True}, + 'stats_password': {'value': params['stats_password'], 'include': True}, + 'stats_port': {'value': params['stats_port'], 'include': True}, + # extra + 'cacert_file': {'value': params['cacert_file'], 'include': False}, + # edits + 'edits': {'value': params['edits'], 'include': False}, + }) + + + state = params['state'] + + ocrouter = Router(rconfig, verbose=params['debug']) + + api_rval = ocrouter.get() + + ######## + # get + ######## + if state == 'list': + return {'changed': False, 'results': api_rval, 'state': state} + + ######## + # Delete + ######## + if state == 'absent': + if not ocrouter.exists(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'} + + # In case of delete we return a list of each object + # that represents a router and its result in a list + # pylint: disable=redefined-variable-type + api_rval = ocrouter.delete() + + return {'changed': True, 'results': api_rval, 'state': state} + + if state == 'present': + ######## + # Create + ######## + if not ocrouter.exists(): + + if check_mode: + return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} + + api_rval = ocrouter.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} + + ######## + # Update + ######## + if not ocrouter.needs_update(): + return {'changed': False, 'state': state} + + if check_mode: + return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'} + + api_rval = ocrouter.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': state} diff --git a/roles/lib_openshift/src/doc/registry b/roles/lib_openshift/src/doc/registry new file mode 100644 index 000000000..ebc714b7a --- /dev/null +++ b/roles/lib_openshift/src/doc/registry @@ -0,0 +1,192 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_adm_registry +short_description: Module to manage openshift registry +description: + - Manage openshift registry programmatically. +options: + state: + description: + - The desired action when managing openshift registry + - present - update or create the registry + - absent - tear down the registry service and deploymentconfig + - list - returns the current representiation of a registry + required: false + default: False + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + name: + description: + - The name of the registry + required: false + default: None + aliases: [] + namespace: + description: + - The selector when filtering on node labels + required: false + default: None + aliases: [] + images: + description: + - The image to base this registry on - ${component} will be replaced with --type + required: 'openshift3/ose-${component}:${version}' + default: None + aliases: [] + latest_images: + description: + - If true, attempt to use the latest image for the registry instead of the latest release. + required: false + default: False + aliases: [] + labels: + description: + - A set of labels to uniquely identify the registry and its components. + required: false + default: None + aliases: [] + enforce_quota: + description: + - If set, the registry will refuse to write blobs if they exceed quota limits + required: False + default: False + aliases: [] + mount_host: + description: + - If set, the registry volume will be created as a host-mount at this path. + required: False + default: False + aliases: [] + ports: + description: + - A comma delimited list of ports or port pairs to expose on the registry pod. The default is set for 5000. + required: False + default: [5000] + aliases: [] + replicas: + description: + - The replication factor of the registry; commonly 2 when high availability is desired. + required: False + default: 1 + aliases: [] + selector: + description: + - Selector used to filter nodes on deployment. Used to run registries on a specific set of nodes. + required: False + default: None + aliases: [] + service_account: + description: + - Name of the service account to use to run the registry pod. + required: False + default: 'registry' + aliases: [] + tls_certificate: + description: + - An optional path to a PEM encoded certificate (which may contain the private key) for serving over TLS + required: false + default: None + aliases: [] + tls_key: + description: + - An optional path to a PEM encoded private key for serving over TLS + required: false + default: None + aliases: [] + volume_mounts: + description: + - The volume mounts for the registry. + required: false + default: None + aliases: [] + daemonset: + description: + - Use a daemonset instead of a deployment config. + required: false + default: False + aliases: [] + edits: + description: + - A list of modifications to make on the deploymentconfig + required: false + default: None + aliases: [] + env_vars: + description: + - A dictionary of modifications to make on the deploymentconfig. e.g. FOO: BAR + required: false + default: None + aliases: [] + force: + description: + - Force a registry update. + required: false + default: False + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: create a secure registry + oc_adm_registry: + name: docker-registry + service_account: registry + replicas: 2 + namespace: default + selector: type=infra + images: "registry.ops.openshift.com/openshift3/ose-${component}:${version}" + env_vars: + REGISTRY_CONFIGURATION_PATH: /etc/registryconfig/config.yml + REGISTRY_HTTP_TLS_CERTIFICATE: /etc/secrets/registry.crt + REGISTRY_HTTP_TLS_KEY: /etc/secrets/registry.key + REGISTRY_HTTP_SECRET: supersecret + volume_mounts: + - path: /etc/secrets + name: dockercerts + type: secret + secret_name: registry-secret + - path: /etc/registryconfig + name: dockersecrets + type: secret + secret_name: docker-registry-config + edits: + - key: spec.template.spec.containers[0].livenessProbe.httpGet.scheme + value: HTTPS + action: put + - key: spec.template.spec.containers[0].readinessProbe.httpGet.scheme + value: HTTPS + action: put + - key: spec.strategy.rollingParams + value: + intervalSeconds: 1 + maxSurge: 50% + maxUnavailable: 50% + timeoutSeconds: 600 + updatePeriodSeconds: 1 + action: put + - key: spec.template.spec.containers[0].resources.limits.memory + value: 2G + action: update + - key: spec.template.spec.containers[0].resources.requests.memory + value: 1G + action: update + + register: registryout + +''' diff --git a/roles/lib_openshift/src/doc/router b/roles/lib_openshift/src/doc/router new file mode 100644 index 000000000..ed46e03a0 --- /dev/null +++ b/roles/lib_openshift/src/doc/router @@ -0,0 +1,217 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_adm_router +short_description: Module to manage openshift router +description: + - Manage openshift router programmatically. +options: + state: + description: + - Whether to create or delete the router + - present - create the router + - absent - remove the router + - list - return the current representation of a router + required: false + default: present + choices: + - present + - absent + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + name: + description: + - The name of the router + required: false + default: router + aliases: [] + namespace: + description: + - The namespace where to manage the router. + required: false + default: default + aliases: [] + images: + description: + - The image to base this router on - ${component} will be replaced with --type + required: 'openshift3/ose-${component}:${version}' + default: None + aliases: [] + latest_images: + description: + - If true, attempt to use the latest image for the registry instead of the latest release. + required: false + default: False + aliases: [] + labels: + description: + - A set of labels to uniquely identify the registry and its components. + required: false + default: None + aliases: [] + ports: + description: + - A list of strings in the 'port:port' format + required: False + default: + - 80:80 + - 443:443 + aliases: [] + replicas: + description: + - The replication factor of the registry; commonly 2 when high availability is desired. + required: False + default: 1 + aliases: [] + selector: + description: + - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes. + required: False + default: None + aliases: [] + service_account: + description: + - Name of the service account to use to run the router pod. + required: False + default: router + aliases: [] + router_type: + description: + - The router image to use - if you specify --images this flag may be ignored. + required: false + default: haproxy-router + aliases: [] + external_host: + description: + - If the underlying router implementation connects with an external host, this is the external host's hostname. + required: false + default: None + aliases: [] + external_host_vserver: + description: + - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections. + required: false + default: None + aliases: [] + external_host_insecure: + description: + - If the underlying router implementation connects with an external host + - over a secure connection, this causes the router to skip strict certificate verification with the external host. + required: false + default: False + aliases: [] + external_host_partition_path: + description: + - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition. + required: false + default: None + aliases: [] + external_host_username: + description: + - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host. + required: false + default: None + aliases: [] + external_host_password: + description: + - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host. + required: false + default: None + aliases: [] + external_host_private_key: + description: + - If the underlying router implementation requires an SSH private key, this is the path to the private key file. + required: false + default: None + aliases: [] + expose_metrics: + description: + - This is a hint to run an extra container in the pod to expose metrics - the image + - will either be set depending on the router implementation or provided with --metrics-image. + required: false + default: False + aliases: [] + metrics_image: + description: + - If expose_metrics is specified this is the image to use to run a sidecar container + - in the pod exposing metrics. If not set and --expose-metrics is true the image will + - depend on router implementation. + required: false + default: None + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: +- There are some exceptions to note when doing the idempotency in this module. +- The strategy is to use the oc adm router command to generate a default +- configuration when creating or updating a router. Often times there +- differences from the generated template and what is in memory in openshift. +- We make exceptions to not check these specific values when comparing objects. +- Here are a list of exceptions: +- - DeploymentConfig: + - dnsPolicy + - terminationGracePeriodSeconds + - restartPolicy + - timeoutSeconds + - livenessProbe + - readinessProbe + - terminationMessagePath + - hostPort + - defaultMode + - Service: + - portalIP + - clusterIP + - sessionAffinity + - type + - ServiceAccount: + - secrets + - imagePullSecrets +''' + +EXAMPLES = ''' +- name: create routers + oc_adm_router: + name: router + service_account: router + replicas: 2 + namespace: default + selector: type=infra + cert_file: /etc/origin/master/named_certificates/router.crt + key_file: /etc/origin/master/named_certificates/router.key + cacert_file: /etc/origin/master/named_certificates/router.ca + edits: + - key: spec.strategy.rollingParams + value: + intervalSeconds: 1 + maxSurge: 50% + maxUnavailable: 50% + timeoutSeconds: 600 + updatePeriodSeconds: 1 + action: put + - key: spec.template.spec.containers[0].resources.limits.memory + value: 2G + action: put + - key: spec.template.spec.containers[0].resources.requests.memory + value: 1G + action: put + - key: spec.template.spec.containers[0].env + value: + name: EXTENDED_VALIDATION + value: 'false' + action: update + register: router_out + run_once: True +''' diff --git a/roles/lib_openshift/src/lib/deploymentconfig.py b/roles/lib_openshift/src/lib/deploymentconfig.py index e060d3707..f10c6bb8b 100644 --- a/roles/lib_openshift/src/lib/deploymentconfig.py +++ b/roles/lib_openshift/src/lib/deploymentconfig.py @@ -4,7 +4,7 @@ # pylint: disable=too-many-public-methods class DeploymentConfig(Yedit): - ''' Class to wrap the oc command line tools ''' + ''' Class to model an openshift DeploymentConfig''' default_deployment_config = ''' apiVersion: v1 kind: DeploymentConfig diff --git a/roles/lib_openshift/src/lib/replicationcontroller.py b/roles/lib_openshift/src/lib/replicationcontroller.py index ae585a986..8bcc1e3cc 100644 --- a/roles/lib_openshift/src/lib/replicationcontroller.py +++ b/roles/lib_openshift/src/lib/replicationcontroller.py @@ -4,7 +4,12 @@ # pylint: disable=too-many-public-methods class ReplicationController(DeploymentConfig): - ''' Class to wrap the oc command line tools ''' + ''' Class to model a replicationcontroller openshift object. + + Currently we are modeled after a deployment config since they + are very similar. In the future, when the need arises we + will add functionality to this class. + ''' replicas_path = "spec.replicas" env_path = "spec.template.spec.containers[0].env" volumes_path = "spec.template.spec.volumes" diff --git a/roles/lib_openshift/src/lib/rolebinding.py b/roles/lib_openshift/src/lib/rolebinding.py new file mode 100644 index 000000000..69629f9f5 --- /dev/null +++ b/roles/lib_openshift/src/lib/rolebinding.py @@ -0,0 +1,289 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: disable=too-many-instance-attributes +class RoleBindingConfig(object): + ''' Handle rolebinding config ''' + # pylint: disable=too-many-arguments + def __init__(self, + name, + namespace, + kubeconfig, + group_names=None, + role_ref=None, + subjects=None, + usernames=None): + ''' constructor for handling rolebinding options ''' + self.kubeconfig = kubeconfig + self.name = name + self.namespace = namespace + self.group_names = group_names + self.role_ref = role_ref + self.subjects = subjects + self.usernames = usernames + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' create a default rolebinding as a dict ''' + self.data['apiVersion'] = 'v1' + self.data['kind'] = 'RoleBinding' + self.data['groupNames'] = self.group_names + self.data['metadata']['name'] = self.name + self.data['metadata']['namespace'] = self.namespace + + self.data['roleRef'] = self.role_ref + self.data['subjects'] = self.subjects + self.data['userNames'] = self.usernames + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class RoleBinding(Yedit): + ''' Class to model a rolebinding openshift object''' + group_names_path = "groupNames" + role_ref_path = "roleRef" + subjects_path = "subjects" + user_names_path = "userNames" + + kind = 'RoleBinding' + + def __init__(self, content): + '''RoleBinding constructor''' + super(RoleBinding, self).__init__(content=content) + self._subjects = None + self._role_ref = None + self._group_names = None + self._user_names = None + + @property + def subjects(self): + ''' subjects property ''' + if self._subjects is None: + self._subjects = self.get_subjects() + return self._subjects + + @subjects.setter + def subjects(self, data): + ''' subjects property setter''' + self._subjects = data + + @property + def role_ref(self): + ''' role_ref property ''' + if self._role_ref is None: + self._role_ref = self.get_role_ref() + return self._role_ref + + @role_ref.setter + def role_ref(self, data): + ''' role_ref property setter''' + self._role_ref = data + + @property + def group_names(self): + ''' group_names property ''' + if self._group_names is None: + self._group_names = self.get_group_names() + return self._group_names + + @group_names.setter + def group_names(self, data): + ''' group_names property setter''' + self._group_names = data + + @property + def user_names(self): + ''' user_names property ''' + if self._user_names is None: + self._user_names = self.get_user_names() + return self._user_names + + @user_names.setter + def user_names(self, data): + ''' user_names property setter''' + self._user_names = data + + def get_group_names(self): + ''' return groupNames ''' + return self.get(RoleBinding.group_names_path) or [] + + def get_user_names(self): + ''' return usernames ''' + return self.get(RoleBinding.user_names_path) or [] + + def get_role_ref(self): + ''' return role_ref ''' + return self.get(RoleBinding.role_ref_path) or {} + + def get_subjects(self): + ''' return subjects ''' + return self.get(RoleBinding.subjects_path) or [] + + #### ADD ##### + def add_subject(self, inc_subject): + ''' add a subject ''' + if self.subjects: + # pylint: disable=no-member + self.subjects.append(inc_subject) + else: + self.put(RoleBinding.subjects_path, [inc_subject]) + + return True + + def add_role_ref(self, inc_role_ref): + ''' add a role_ref ''' + if not self.role_ref: + self.put(RoleBinding.role_ref_path, {"name": inc_role_ref}) + return True + + return False + + def add_group_names(self, inc_group_names): + ''' add a group_names ''' + if self.group_names: + # pylint: disable=no-member + self.group_names.append(inc_group_names) + else: + self.put(RoleBinding.group_names_path, [inc_group_names]) + + return True + + def add_user_name(self, inc_user_name): + ''' add a username ''' + if self.user_names: + # pylint: disable=no-member + self.user_names.append(inc_user_name) + else: + self.put(RoleBinding.user_names_path, [inc_user_name]) + + return True + + #### /ADD ##### + + #### Remove ##### + def remove_subject(self, inc_subject): + ''' remove a subject ''' + try: + # pylint: disable=no-member + self.subjects.remove(inc_subject) + except ValueError as _: + return False + + return True + + def remove_role_ref(self, inc_role_ref): + ''' remove a role_ref ''' + if self.role_ref and self.role_ref['name'] == inc_role_ref: + del self.role_ref['name'] + return True + + return False + + def remove_group_name(self, inc_group_name): + ''' remove a groupname ''' + try: + # pylint: disable=no-member + self.group_names.remove(inc_group_name) + except ValueError as _: + return False + + return True + + def remove_user_name(self, inc_user_name): + ''' remove a username ''' + try: + # pylint: disable=no-member + self.user_names.remove(inc_user_name) + except ValueError as _: + return False + + return True + + #### /REMOVE ##### + + #### UPDATE ##### + def update_subject(self, inc_subject): + ''' update a subject ''' + try: + # pylint: disable=no-member + index = self.subjects.index(inc_subject) + except ValueError as _: + return self.add_subject(inc_subject) + + self.subjects[index] = inc_subject + + return True + + def update_group_name(self, inc_group_name): + ''' update a groupname ''' + try: + # pylint: disable=no-member + index = self.group_names.index(inc_group_name) + except ValueError as _: + return self.add_group_names(inc_group_name) + + self.group_names[index] = inc_group_name + + return True + + def update_user_name(self, inc_user_name): + ''' update a username ''' + try: + # pylint: disable=no-member + index = self.user_names.index(inc_user_name) + except ValueError as _: + return self.add_user_name(inc_user_name) + + self.user_names[index] = inc_user_name + + return True + + def update_role_ref(self, inc_role_ref): + ''' update a role_ref ''' + self.role_ref['name'] = inc_role_ref + + return True + + #### /UPDATE ##### + + #### FIND #### + def find_subject(self, inc_subject): + ''' find a subject ''' + index = None + try: + # pylint: disable=no-member + index = self.subjects.index(inc_subject) + except ValueError as _: + return index + + return index + + def find_group_name(self, inc_group_name): + ''' find a group_name ''' + index = None + try: + # pylint: disable=no-member + index = self.group_names.index(inc_group_name) + except ValueError as _: + return index + + return index + + def find_user_name(self, inc_user_name): + ''' find a user_name ''' + index = None + try: + # pylint: disable=no-member + index = self.user_names.index(inc_user_name) + except ValueError as _: + return index + + return index + + def find_role_ref(self, inc_role_ref): + ''' find a user_name ''' + if self.role_ref and self.role_ref['name'] == inc_role_ref['name']: + return self.role_ref + + return None diff --git a/roles/lib_openshift/src/lib/secret.py b/roles/lib_openshift/src/lib/secret.py index 1ba78ddd5..622290aa8 100644 --- a/roles/lib_openshift/src/lib/secret.py +++ b/roles/lib_openshift/src/lib/secret.py @@ -20,7 +20,7 @@ class SecretConfig(object): self.create_dict() def create_dict(self): - ''' return a secret as a dict ''' + ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' self.data['metadata'] = {} diff --git a/roles/lib_openshift/src/lib/serviceaccount.py b/roles/lib_openshift/src/lib/serviceaccount.py index 47a55757e..50c104d44 100644 --- a/roles/lib_openshift/src/lib/serviceaccount.py +++ b/roles/lib_openshift/src/lib/serviceaccount.py @@ -18,7 +18,7 @@ class ServiceAccountConfig(object): self.create_dict() def create_dict(self): - ''' return a properly structured volume ''' + ''' instantiate a properly structured volume ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'ServiceAccount' self.data['metadata'] = {} diff --git a/roles/lib_openshift/src/lib/volume.py b/roles/lib_openshift/src/lib/volume.py new file mode 100644 index 000000000..84ef1f705 --- /dev/null +++ b/roles/lib_openshift/src/lib/volume.py @@ -0,0 +1,37 @@ +# pylint: skip-file +# flake8: noqa + +class Volume(object): + ''' Class to model an openshift volume object''' + volume_mounts_path = {"pod": "spec.containers[0].volumeMounts", + "dc": "spec.template.spec.containers[0].volumeMounts", + "rc": "spec.template.spec.containers[0].volumeMounts", + } + volumes_path = {"pod": "spec.volumes", + "dc": "spec.template.spec.volumes", + "rc": "spec.template.spec.volumes", + } + + @staticmethod + def create_volume_structure(volume_info): + ''' return a properly structured volume ''' + volume_mount = None + volume = {'name': volume_info['name']} + if volume_info['type'] == 'secret': + volume['secret'] = {} + volume[volume_info['type']] = {'secretName': volume_info['secret_name']} + volume_mount = {'mountPath': volume_info['path'], + 'name': volume_info['name']} + elif volume_info['type'] == 'emptydir': + volume['emptyDir'] = {} + volume_mount = {'mountPath': volume_info['path'], + 'name': volume_info['name']} + elif volume_info['type'] == 'pvc': + volume['persistentVolumeClaim'] = {} + volume['persistentVolumeClaim']['claimName'] = volume_info['claimName'] + volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize'] + elif volume_info['type'] == 'hostpath': + volume['hostPath'] = {} + volume['hostPath']['path'] = volume_info['path'] + + return (volume, volume_mount) diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index 091aaef2e..fca1f4818 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -9,6 +9,36 @@ oadm_manage_node.py: - class/oadm_manage_node.py - ansible/oadm_manage_node.py +oc_adm_registry.py: +- doc/generated +- doc/license +- lib/import.py +- doc/registry +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/deploymentconfig.py +- lib/secret.py +- lib/service.py +- lib/volume.py +- class/oc_version.py +- class/oc_adm_registry.py +- ansible/oc_adm_registry.py + +oc_adm_router.py: +- doc/generated +- doc/license +- lib/import.py +- doc/router +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/service.py +- lib/deploymentconfig.py +- lib/serviceaccount.py +- lib/secret.py +- lib/rolebinding.py +- class/oc_adm_router.py +- ansible/oc_adm_router.py + oc_edit.py: - doc/generated - doc/license diff --git a/roles/lib_openshift/src/test/unit/test_oadm_manage_node.py b/roles/lib_openshift/src/test/unit/test_oadm_manage_node.py index b0786dfac..b0786dfac 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oadm_manage_node.py +++ b/roles/lib_openshift/src/test/unit/test_oadm_manage_node.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_env.py b/roles/lib_openshift/src/test/unit/test_oc_env.py index 15bd7e464..15bd7e464 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_env.py +++ b/roles/lib_openshift/src/test/unit/test_oc_env.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_label.py b/roles/lib_openshift/src/test/unit/test_oc_label.py index 3176987b0..3176987b0 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_label.py +++ b/roles/lib_openshift/src/test/unit/test_oc_label.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_process.py b/roles/lib_openshift/src/test/unit/test_oc_process.py index 450ff7071..450ff7071 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_process.py +++ b/roles/lib_openshift/src/test/unit/test_oc_process.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_route.py b/roles/lib_openshift/src/test/unit/test_oc_route.py index 361b61f4b..361b61f4b 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_route.py +++ b/roles/lib_openshift/src/test/unit/test_oc_route.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_scale.py b/roles/lib_openshift/src/test/unit/test_oc_scale.py index f15eb164d..f15eb164d 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_scale.py +++ b/roles/lib_openshift/src/test/unit/test_oc_scale.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_secret.py b/roles/lib_openshift/src/test/unit/test_oc_secret.py index 645aac82b..645aac82b 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_secret.py +++ b/roles/lib_openshift/src/test/unit/test_oc_secret.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_service.py b/roles/lib_openshift/src/test/unit/test_oc_service.py index 4a845e9f3..4a845e9f3 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_service.py +++ b/roles/lib_openshift/src/test/unit/test_oc_service.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py index 256b569eb..256b569eb 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py +++ b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py index 213c581aa..213c581aa 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py +++ b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py diff --git a/roles/lib_openshift/src/test/unit/test_oc_version.py b/roles/lib_openshift/src/test/unit/test_oc_version.py index 67dea415b..67dea415b 100644..100755 --- a/roles/lib_openshift/src/test/unit/test_oc_version.py +++ b/roles/lib_openshift/src/test/unit/test_oc_version.py diff --git a/roles/lib_openshift/tasks/main.yml b/roles/lib_openshift/tasks/main.yml index 77366c65e..b8af7c7c9 100644 --- a/roles/lib_openshift/tasks/main.yml +++ b/roles/lib_openshift/tasks/main.yml @@ -6,6 +6,6 @@ - name: lib_openshift ensure python-ruamel-yaml package is on target package: - name: python-ruamel-yaml + name: python2-ruamel-yaml state: present when: not ostree_booted.stat.exists diff --git a/roles/lib_utils/src/test/unit/test_repoquery.py b/roles/lib_utils/src/test/unit/test_repoquery.py index c487ab254..c487ab254 100644..100755 --- a/roles/lib_utils/src/test/unit/test_repoquery.py +++ b/roles/lib_utils/src/test/unit/test_repoquery.py diff --git a/roles/lib_utils/src/test/unit/test_yedit.py b/roles/lib_utils/src/test/unit/test_yedit.py index ed07ac96e..ed07ac96e 100644..100755 --- a/roles/lib_utils/src/test/unit/test_yedit.py +++ b/roles/lib_utils/src/test/unit/test_yedit.py diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py index 85671b164..b093d84fe 100644 --- a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -5,13 +5,32 @@ """For details on this module see DOCUMENTATION (below)""" import datetime +import io import os import subprocess +import sys +import tempfile +# File pointers from io.open require unicode inputs when using their +# `write` method +import six from six.moves import configparser import yaml -import OpenSSL.crypto +try: + # You can comment this import out and include a 'pass' in this + # block if you're manually testing this module on a NON-ATOMIC + # HOST (or any host that just doesn't have PyOpenSSL + # available). That will force the `load_and_handle_cert` function + # to use the Fake OpenSSL classes. + import OpenSSL.crypto +except ImportError: + # Some platforms (such as RHEL Atomic) may not have the Python + # OpenSSL library installed. In this case we will use a manual + # work-around to parse each certificate. + # + # Check for 'OpenSSL.crypto' in `sys.modules` later. + pass DOCUMENTATION = ''' --- @@ -66,6 +85,128 @@ EXAMPLES = ''' ''' +class FakeOpenSSLCertificate(object): + """This provides a rough mock of what you get from +`OpenSSL.crypto.load_certificate()`. This is a work-around for +platforms missing the Python OpenSSL library. + """ + def __init__(self, cert_string): + """`cert_string` is a certificate in the form you get from running a +.crt through 'openssl x509 -in CERT.cert -text'""" + self.cert_string = cert_string + self.serial = None + self.subject = None + self.extensions = [] + self.not_after = None + self._parse_cert() + + def _parse_cert(self): + """Manually parse the certificate line by line""" + self.extensions = [] + + PARSING_ALT_NAMES = False + for line in self.cert_string.split('\n'): + l = line.strip() + if PARSING_ALT_NAMES: + # We're parsing a 'Subject Alternative Name' line + self.extensions.append( + FakeOpenSSLCertificateSANExtension(l)) + + PARSING_ALT_NAMES = False + continue + + # parse out the bits that we can + if l.startswith('Serial Number:'): + # Serial Number: 11 (0xb) + # => 11 + self.serial = int(l.split()[-2]) + + elif l.startswith('Not After :'): + # Not After : Feb 7 18:19:35 2019 GMT + # => strptime(str, '%b %d %H:%M:%S %Y %Z') + # => strftime('%Y%m%d%H%M%SZ') + # => 20190207181935Z + not_after_raw = l.partition(' : ')[-1] + # Last item: ('Not After', ' : ', 'Feb 7 18:19:35 2019 GMT') + not_after_parsed = datetime.datetime.strptime(not_after_raw, '%b %d %H:%M:%S %Y %Z') + self.not_after = not_after_parsed.strftime('%Y%m%d%H%M%SZ') + + elif l.startswith('X509v3 Subject Alternative Name:'): + PARSING_ALT_NAMES = True + continue + + elif l.startswith('Subject:'): + # O=system:nodes, CN=system:node:m01.example.com + self.subject = FakeOpenSSLCertificateSubjects(l.partition(': ')[-1]) + + def get_serial_number(self): + """Return the serial number of the cert""" + return self.serial + + def get_subject(self): + """Subjects must implement get_components() and return dicts or +tuples. An 'openssl x509 -in CERT.cert -text' with 'Subject': + + Subject: Subject: O=system:nodes, CN=system:node:m01.example.com + +might return: [('O=system', 'nodes'), ('CN=system', 'node:m01.example.com')] + """ + return self.subject + + def get_extension(self, i): + """Extensions must implement get_short_name() and return the string +'subjectAltName'""" + return self.extensions[i] + + def get_notAfter(self): + """Returns a date stamp as a string in the form +'20180922170439Z'. strptime the result with format param: +'%Y%m%d%H%M%SZ'.""" + return self.not_after + + +class FakeOpenSSLCertificateSANExtension(object): # pylint: disable=too-few-public-methods + """Mocks what happens when `get_extension` is called on a certificate +object""" + + def __init__(self, san_string): + """With `san_string` as you get from: + + $ openssl x509 -in certificate.crt -text + """ + self.san_string = san_string + self.short_name = 'subjectAltName' + + def get_short_name(self): + """Return the 'type' of this extension. It's always the same though +because we only care about subjectAltName's""" + return self.short_name + + def __str__(self): + """Return this extension and the value as a simple string""" + return self.san_string + + +# pylint: disable=too-few-public-methods +class FakeOpenSSLCertificateSubjects(object): + """Mocks what happens when `get_subject` is called on a certificate +object""" + + def __init__(self, subject_string): + """With `subject_string` as you get from: + + $ openssl x509 -in certificate.crt -text + """ + self.subjects = [] + for s in subject_string.split(', '): + name, _, value = s.partition('=') + self.subjects.append((name, value)) + + def get_components(self): + """Returns a list of tuples""" + return self.subjects + + # We only need this for one thing, we don't care if it doesn't have # that many public methods # @@ -100,7 +241,10 @@ will be returned return [p for p in path_list if os.path.exists(os.path.realpath(p))] -def load_and_handle_cert(cert_string, now, base64decode=False): +# pylint: disable=too-many-locals,too-many-branches +# +# TODO: Break this function down into smaller chunks +def load_and_handle_cert(cert_string, now, base64decode=False, ans_module=None): """Load a certificate, split off the good parts, and return some useful data @@ -109,20 +253,45 @@ Params: - `cert_string` (string) - a certificate loaded into a string object - `now` (datetime) - a datetime object of the time to calculate the certificate 'time_remaining' against - `base64decode` (bool) - run .decode('base64') on the input? +- `ans_module` (AnsibleModule) - The AnsibleModule object for this module (so we can raise errors) Returns: -A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certificate_time_remaining) - +A tuple of the form: + (cert_subject, cert_expiry_date, time_remaining, cert_serial_number) """ if base64decode: _cert_string = cert_string.decode('base-64') else: _cert_string = cert_string - cert_loaded = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, _cert_string) - - cert_serial = cert_loaded.get_serial_number() + # Disable this. We 'redefine' the type because we are working + # around a missing library on the target host. + # + # pylint: disable=redefined-variable-type + if 'OpenSSL.crypto' in sys.modules: + # No work-around required + cert_loaded = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, _cert_string) + else: + # Missing library, work-around required. We need to write the + # cert out to disk temporarily so we can run the 'openssl' + # command on it to decode it + _, path = tempfile.mkstemp() + with io.open(path, 'w') as fp: + fp.write(six.u(_cert_string)) + fp.flush() + + cmd = 'openssl x509 -in {} -text'.format(path) + try: + openssl_decoded = subprocess.Popen(cmd.split(), + stdout=subprocess.PIPE) + except OSError: + ans_module.fail_json(msg="Error: The 'OpenSSL' python library and CLI command were not found on the target host. Unable to parse any certificates. This host will not be included in generated reports.") + else: + openssl_decoded = openssl_decoded.communicate()[0] + cert_loaded = FakeOpenSSLCertificate(openssl_decoded) + finally: + os.remove(path) ###################################################################### # Read all possible names from the cert @@ -172,15 +341,14 @@ A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certif ###################################################################### # Grab the expiration date - cert_expiry = cert_loaded.get_notAfter() cert_expiry_date = datetime.datetime.strptime( - cert_expiry, + cert_loaded.get_notAfter(), # example get_notAfter() => 20180922170439Z '%Y%m%d%H%M%SZ') time_remaining = cert_expiry_date - now - return (cert_subject, cert_expiry_date, time_remaining, cert_serial) + return (cert_subject, cert_expiry_date, time_remaining, cert_loaded.get_serial_number()) def classify_cert(cert_meta, now, time_remaining, expire_window, cert_list): @@ -379,7 +547,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(cert, now) + cert_serial) = load_and_handle_cert(cert, now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -428,7 +596,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now, base64decode=True) + cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -458,7 +626,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now, base64decode=True) + cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -512,7 +680,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now) + cert_serial) = load_and_handle_cert(c, now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -551,7 +719,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(etcd_fp.read(), now) + cert_serial) = load_and_handle_cert(etcd_fp.read(), now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -597,7 +765,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(router_c, now, base64decode=True) + cert_serial) = load_and_handle_cert(router_c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -628,7 +796,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(registry_c, now, base64decode=True) + cert_serial) = load_and_handle_cert(registry_c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, diff --git a/roles/openshift_certificate_expiry/test/master.server.crt b/roles/openshift_certificate_expiry/test/master.server.crt new file mode 100644 index 000000000..51aa85c8c --- /dev/null +++ b/roles/openshift_certificate_expiry/test/master.server.crt @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu +c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 +MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP +AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf +z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs +QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF +UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq +8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD +VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw +gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr +dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs +dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp +ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm +YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu +MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// +bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 +rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri +jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv +VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ +o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn +HEL2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu +c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM3WhcNMjIwMjA2 +MTgxMjM4WjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE0ODY0OTExNTgw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdyU8AD7sTXHP5jk04i1HY +cmUXuSiXdIByeIAtTZqiHU46Od0qnZib7tY1NSbo9FtGRl5YEvrfNL+1ig0hZjDh +gKZK4fNbsanuKgj2SWx11bt4yJH0YSbm9+H45y0E15IY1h30jGHnHFNFZDdYwxtO +8u+GShb4MOqZL9aUEfvfaoGJIIpfR+eW5NaBZQr6tiM89Z1wJYqoqzgzI/XIyUXR +zWLOayP1M/eeSXPvBncwZfTPLzphZjB2rz3MdloPrdYMm2b5tfbEOjD7L2aYOJJU +nVSkgjoFXAazL8KuXSIGcdrdDecyJ4ta8ijD4VIZRN9PnBlYiKaz0DsagkGjUVRd +AgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBCwUAA4IBAQAZ/Kerb5eJXbByQ29fq60V+MQgLIJ1iTo3+zfaXxGlmV9v +fTp3S1xQhdGyhww7UC0Ze940eRq6BQ5/I6qPcgSGNpUB064pnjSf0CexCY4qoGqK +4VSvHRrG9TP5V+YIlX9UR1zuPy//a+wuCwKaqiWedTMb4jpvj5jsEOGIrciSmurg +/9nKvvJXRbgqRYQeJGLT5QW5clHywsyTrE7oYytYSEcAvEs3UZT37H74wj2RFxk6 +KcEzsxUB3W+iYst0QdOPByt64OCwAaUJ96VJstaOYMmyWSShAxGAKDSjcrr4JJnF +KtqOC1K56x0ONuBsY4MB15TNGPp8SbOhVV6OfIWj +-----END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/master.server.crt.txt b/roles/openshift_certificate_expiry/test/master.server.crt.txt new file mode 100644 index 000000000..6b3c8fb03 --- /dev/null +++ b/roles/openshift_certificate_expiry/test/master.server.crt.txt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4 (0x4) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=openshift-signer@1486491158 + Validity + Not Before: Feb 7 18:12:39 2017 GMT + Not After : Feb 7 18:12:40 2019 GMT + Subject: CN=172.30.0.1 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:e3:89:fa:91:59:67:45:74:b0:82:72:a1:5d:7e: + c9:ad:1b:f1:66:6f:a7:0a:a1:04:fe:fa:4a:45:f3: + 6d:ac:c0:cf:02:e5:6d:70:b5:16:74:40:dd:4f:42: + fb:41:d7:1e:2d:30:81:15:ee:d5:ba:0a:df:a2:43: + e1:56:2d:17:6a:fa:c2:db:11:69:60:9e:be:c0:fa: + 6e:a3:b1:f2:05:18:5f:cf:ff:2b:3b:17:ed:02:73: + 1e:15:4e:49:84:50:da:78:bc:12:6c:38:e2:46:08: + c4:d5:96:18:cf:c1:dc:ab:d6:25:3e:e8:e9:b4:3e: + a3:e9:86:96:d4:89:da:7b:e5:6c:40:a5:dc:d6:e0: + 28:6d:e9:a1:0a:52:ca:6b:31:33:71:c5:46:bb:9d: + c2:69:21:de:e2:42:68:29:46:70:27:c5:23:b7:f9: + d1:1d:4a:fe:c1:e9:1b:92:9c:74:35:f9:85:50:41: + a0:35:15:cd:e3:a7:2a:ed:9c:24:2c:7b:23:b6:e4: + 76:a6:db:42:e8:98:45:f8:a1:e0:5d:34:72:58:cf: + 6c:b1:fb:cb:d8:c7:26:85:31:71:ad:3a:00:96:1b: + 2a:f0:cb:87:32:7d:32:f4:33:33:98:02:ba:12:7b: + 6f:94:2e:40:b1:cc:53:46:58:ed:c0:76:44:89:cb: + 29:17 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature, Key Encipherment + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Subject Alternative Name: + DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241 + Signature Algorithm: sha256WithRSAEncryption + d2:74:a0:69:51:50:79:4a:09:f5:24:1f:ff:6e:4f:34:e3:ec: + eb:51:fd:35:92:86:93:f3:7f:f5:ed:63:08:fb:31:bc:20:ec: + 02:67:d0:5e:e7:ec:c3:7b:84:e1:5c:7f:8f:40:2e:a8:6d:bc: + 14:8b:d7:67:f1:23:7a:ae:58:a1:be:b8:4d:bc:02:5a:92:76: + 35:61:18:d6:d3:b2:fb:6a:eb:36:44:52:97:a7:3b:0b:b9:6a: + 16:2d:59:4b:5c:14:e9:99:f5:a1:43:6c:38:d1:b0:a8:e0:aa: + e2:8d:0a:af:7b:30:50:fa:ed:68:b4:5c:e8:cd:69:85:ee:7a: + c8:fc:92:be:ee:8f:3d:84:bb:da:a1:bc:7e:98:38:f9:c0:23: + d0:2c:ef:9c:33:fa:b5:d4:97:33:de:1b:6f:54:e4:c5:b0:c8: + 76:56:7b:8a:3b:16:6a:2c:62:73:d8:25:e4:af:a6:17:bc:08: + 49:88:54:16:69:10:a7:24:46:80:da:88:13:61:b0:da:85:5e: + 62:c4:52:b0:aa:91:99:a3:ec:83:12:ba:0f:94:2c:39:e0:1c: + 6c:d0:fd:5e:c1:4c:78:4d:1b:2a:77:e4:33:86:7a:fb:df:18: + 85:05:0d:0c:ec:98:e1:28:71:7a:11:cc:c7:b7:ce:d7:3e:fb: + 27:1c:42:f6 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu +c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 +MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP +AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf +z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs +QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF +UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq +8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD +VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw +gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr +dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs +dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp +ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm +YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu +MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// +bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 +rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri +jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv +VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ +o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn +HEL2 +-----END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt new file mode 100644 index 000000000..cd13ddc38 --- /dev/null +++ b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu +c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 +MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl +bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf +CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 +N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg +hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 +mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh +d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL +BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD +MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO +MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen +gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo +/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx +ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= +-----END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt new file mode 100644 index 000000000..67a3eb81c --- /dev/null +++ b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 11 (0xb) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=openshift-signer@1486491158 + Validity + Not Before: Feb 7 18:19:34 2017 GMT + Not After : Feb 7 18:19:35 2019 GMT + Subject: O=system:nodes, CN=system:node:m01.example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:e7:15:74:36:92:99:e5:ee:4a:9f:bb:54:21:d4: + 8c:40:de:4c:fd:e3:f5:a1:f4:9e:b7:81:8f:7d:3b: + f6:c1:33:7a:d8:65:2e:84:26:c0:0a:9f:09:9a:76: + de:a5:c6:62:6d:11:f3:df:82:0b:9e:5b:b9:a1:f5: + 14:df:e5:b5:0f:10:0c:65:33:8b:8b:5d:a3:0f:40: + 7f:fd:0e:70:3d:04:20:d3:b6:ba:49:b2:1b:9f:fd: + 37:e5:fd:d3:99:a8:fb:2f:76:df:0a:69:be:b7:21: + 00:0e:80:73:1c:5e:0f:20:3a:df:3a:2e:19:2e:1e: + c4:8f:7c:8a:e6:65:71:d4:b8:f3:36:ec:18:44:64: + 44:f1:a0:86:81:82:a3:b1:e8:88:ab:54:34:be:57: + f0:c6:22:6e:71:58:9c:0e:04:52:78:1d:1d:9b:11: + 2a:e2:ad:7b:8b:7c:19:8e:0f:62:ae:1c:c4:83:98: + 5e:c2:6c:36:64:f9:9a:41:72:e1:38:46:15:f4:7d: + 20:68:3f:55:fc:9e:58:89:11:7b:65:56:c9:a5:20: + cc:bd:20:1f:2b:40:86:54:49:f6:5e:78:ca:7c:7d: + e9:81:16:ae:30:ad:a7:f9:21:77:72:9e:56:3d:08: + 51:c3:33:be:85:d7:e6:18:a9:61:43:e4:a1:ec:dc: + c4:8f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature, Key Encipherment + X509v3 Extended Key Usage: + TLS Web Client Authentication + X509v3 Basic Constraints: critical + CA:FALSE + Signature Algorithm: sha256WithRSAEncryption + cd:63:7b:12:ed:b8:e3:93:b0:e3:df:11:a9:7f:20:2c:df:3f: + d6:97:de:6d:11:ff:21:8f:8f:04:2f:e3:78:ce:73:cd:4d:70: + be:5d:f8:27:83:31:7d:fa:b4:a0:31:14:87:be:c2:f5:e8:d2: + 45:10:3b:34:ca:dc:3e:e8:97:8c:76:61:3b:41:e2:e0:f3:34: + 4e:6d:56:1e:5d:5a:2d:8b:ac:a6:12:a9:90:c8:ae:12:8e:30: + e9:89:45:64:77:00:51:28:a5:a6:86:42:9c:6c:0f:f1:52:7d: + 9e:4c:83:43:8c:30:13:c7:00:1c:4c:ae:c8:ac:7a:b1:ce:d6: + 78:78:6b:1d:fe:48:8d:22:ec:97:a7:81:5b:01:e0:1f:d7:c6: + 0f:4b:c3:d2:43:5f:7f:d7:31:c2:3e:7f:22:d5:c3:97:2d:3b: + ad:81:6a:5b:a5:8e:94:fe:1e:47:5b:45:69:c7:f1:aa:fb:4e: + 67:09:8f:34:e8:fd:3c:97:39:e3:8b:91:ba:33:c7:40:50:2b: + 5e:9d:fc:26:ed:2e:89:89:f8:f3:b8:71:dc:02:36:cd:99:e8: + 71:92:3d:32:fb:4b:dc:b4:a5:cb:79:d8:dd:24:7c:97:f1:64: + 65:cc:b1:6f:8d:eb:64:b6:bd:54:97:11:e9:0c:47:41:89:80: + 99:cb:35:2a +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu +c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 +MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl +bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf +CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 +N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg +hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 +mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh +d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL +BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD +MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO +MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen +gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo +/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx +ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= +-----END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py new file mode 100644 index 000000000..2e245191f --- /dev/null +++ b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +''' + Unit tests for the FakeOpenSSL classes +''' + +import os +import sys +import unittest +import pytest + +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error,wrong-import-position +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split(os.path.sep)[:-1]), 'library') +sys.path.insert(0, module_path) +openshift_cert_expiry = pytest.importorskip("openshift_cert_expiry") + + +@pytest.mark.skip('Skipping all tests because of unresolved import errors') +class TestFakeOpenSSLClasses(unittest.TestCase): + ''' + Test class for FakeOpenSSL classes + ''' + + def setUp(self): + ''' setup method for other tests ''' + with open('test/system-node-m01.example.com.crt.txt', 'r') as fp: + self.cert_string = fp.read() + + self.fake_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_string) + + with open('test/master.server.crt.txt', 'r') as fp: + self.cert_san_string = fp.read() + + self.fake_san_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_san_string) + + def test_FakeOpenSSLCertificate_get_serial_number(self): + """We can read the serial number from the cert""" + self.assertEqual(11, self.fake_cert.get_serial_number()) + + def test_FakeOpenSSLCertificate_get_notAfter(self): + """We can read the cert expiry date""" + expiry = self.fake_cert.get_notAfter() + self.assertEqual('20190207181935Z', expiry) + + def test_FakeOpenSSLCertificate_get_sans(self): + """We can read Subject Alt Names from a cert""" + ext = self.fake_san_cert.get_extension(0) + + if ext.get_short_name() == 'subjectAltName': + sans = str(ext) + + self.assertEqual('DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241', sans) + + def test_FakeOpenSSLCertificate_get_sans_no_sans(self): + """We can tell when there are no Subject Alt Names in a cert""" + with self.assertRaises(IndexError): + self.fake_cert.get_extension(0) + + def test_FakeOpenSSLCertificate_get_subject(self): + """We can read the Subject from a cert""" + # Subject: O=system:nodes, CN=system:node:m01.example.com + subject = self.fake_cert.get_subject() + subjects = [] + for name, value in subject.get_components(): + subjects.append('{}={}'.format(name, value)) + + self.assertEqual('O=system:nodes, CN=system:node:m01.example.com', ', '.join(subjects)) + + def test_FakeOpenSSLCertificate_get_subject_san_cert(self): + """We can read the Subject from a cert with sans""" + # Subject: O=system:nodes, CN=system:node:m01.example.com + subject = self.fake_san_cert.get_subject() + subjects = [] + for name, value in subject.get_components(): + subjects.append('{}={}'.format(name, value)) + + self.assertEqual('CN=172.30.0.1', ', '.join(subjects)) + + +if __name__ == "__main__": + unittest.main() diff --git a/roles/openshift_health_checker/HOWTO_CHECKS.md b/roles/openshift_health_checker/HOWTO_CHECKS.md index 1573c14da..6c5662a4e 100644 --- a/roles/openshift_health_checker/HOWTO_CHECKS.md +++ b/roles/openshift_health_checker/HOWTO_CHECKS.md @@ -10,7 +10,7 @@ Checks are typically implemented as two parts: 2. a custom Ansible module in [library/](library), for cases when the modules shipped with Ansible do not provide the required functionality. -The checks are called from an Ansible playbooks via the `openshift_health_check` +The checks are called from Ansible playbooks via the `openshift_health_check` action plugin. See [playbooks/byo/openshift-preflight/check.yml](../../playbooks/byo/openshift-preflight/check.yml) for an example. diff --git a/roles/openshift_logging/README.md b/roles/openshift_logging/README.md index 2e61696ab..c90a5bf20 100644 --- a/roles/openshift_logging/README.md +++ b/roles/openshift_logging/README.md @@ -63,7 +63,7 @@ When both `openshift_logging_install_logging` and `openshift_logging_upgrade_log - `openshift_logging_es_cluster_size`: The number of ES cluster members. Defaults to '1'. - `openshift_logging_es_cpu_limit`: The amount of CPU limit for the ES cluster. Unused if not set -- `openshift_logging_es_memory_limit`: The amount of RAM that should be assigned to ES. Defaults to '1024Mi'. +- `openshift_logging_es_memory_limit`: The amount of RAM that should be assigned to ES. Defaults to '8Gi'. - `openshift_logging_es_pv_selector`: A key/value map added to a PVC in order to select specific PVs. Defaults to 'None'. - `openshift_logging_es_pvc_dynamic`: Whether or not to add the dynamic PVC annotation for any generated PVCs. Defaults to 'False'. - `openshift_logging_es_pvc_size`: The requested size for the ES PVCs, when not provided the role will not generate any PVCs. Defaults to '""'. @@ -81,7 +81,7 @@ same as above for their non-ops counterparts, but apply to the OPS cluster insta - `openshift_logging_es_ops_client_key`: /etc/fluent/keys/key - `openshift_logging_es_ops_cluster_size`: 1 - `openshift_logging_es_ops_cpu_limit`: The amount of CPU limit for the ES cluster. Unused if not set -- `openshift_logging_es_ops_memory_limit`: 1024Mi +- `openshift_logging_es_ops_memory_limit`: 8Gi - `openshift_logging_es_ops_pvc_dynamic`: False - `openshift_logging_es_ops_pvc_size`: "" - `openshift_logging_es_ops_pvc_prefix`: logging-es-ops diff --git a/roles/openshift_logging/tasks/install_elasticsearch.yaml b/roles/openshift_logging/tasks/install_elasticsearch.yaml index f9c2c81fb..244949505 100644 --- a/roles/openshift_logging/tasks/install_elasticsearch.yaml +++ b/roles/openshift_logging/tasks/install_elasticsearch.yaml @@ -5,6 +5,7 @@ - name: Generate PersistentVolumeClaims include: "{{ role_path}}/tasks/generate_pvcs.yaml" vars: + es_pvc_pool: [] es_pvc_names: "{{openshift_logging_facts.elasticsearch.pvcs.keys()}}" es_dc_names: "{{openshift_logging_facts.elasticsearch.deploymentconfigs.keys()}}" @@ -63,6 +64,7 @@ - name: Generate PersistentVolumeClaims for Ops include: "{{ role_path}}/tasks/generate_pvcs.yaml" vars: + es_pvc_pool: [] es_pvc_names: "{{openshift_logging_facts.elasticsearch_ops.pvcs.keys()}}" es_dc_names: "{{openshift_logging_facts.elasticsearch_ops.deploymentconfigs.keys()}}" openshift_logging_es_pvc_prefix: "{{openshift_logging_es_ops_pvc_prefix}}" diff --git a/roles/openshift_logging/templates/elasticsearch.yml.j2 b/roles/openshift_logging/templates/elasticsearch.yml.j2 index dad78b844..8021a476d 100644 --- a/roles/openshift_logging/templates/elasticsearch.yml.j2 +++ b/roles/openshift_logging/templates/elasticsearch.yml.j2 @@ -38,6 +38,11 @@ gateway: io.fabric8.elasticsearch.authentication.users: ["system.logging.kibana", "system.logging.fluentd", "system.logging.curator", "system.admin"] +openshift.config: + use_common_data_model: true + project_index_prefix: "project" + time_field_name: "@timestamp" + openshift.searchguard: keystore.path: /etc/elasticsearch/secret/admin.jks truststore.path: /etc/elasticsearch/secret/searchguard.truststore |