diff options
Diffstat (limited to 'roles/lib_openshift')
81 files changed, 6309 insertions, 1417 deletions
diff --git a/roles/lib_openshift/library/oc_adm_ca_server_cert.py b/roles/lib_openshift/library/oc_adm_ca_server_cert.py index a6273cfe4..05b2763d5 100644 --- a/roles/lib_openshift/library/oc_adm_ca_server_cert.py +++ b/roles/lib_openshift/library/oc_adm_ca_server_cert.py @@ -745,7 +745,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -952,7 +952,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1097,10 +1097,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1110,34 +1106,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1300,13 +1288,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1405,7 +1392,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1419,18 +1405,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_adm_csr.py b/roles/lib_openshift/library/oc_adm_csr.py new file mode 100644 index 000000000..324f52689 --- /dev/null +++ b/roles/lib_openshift/library/oc_adm_csr.py @@ -0,0 +1,1661 @@ +#!/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/csr -*- -*- -*- + +DOCUMENTATION = ''' +--- +module: oc_adm_csr +short_description: Module to approve or deny openshift certificate signing requests +description: + - Wrapper around the openshift `oc adm certificate approve|deny <csr>` command. +options: + state: + description: + - approve|deny|list Approve, deny, and list are the only supported states for certificates + required: false + default: present + choices: + - present + 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: [] + nodes: + description: + - A list of the names of the nodes in which to accept the certificates + required: false + default: None + aliases: [] + timeout: + description: + - This flag allows for a timeout value when approving nodes. + required: false + default: 30 + aliases: [] + timeout: + description: + - This flag allows for a timeout value when doing node approvals. + - A zero value for the timeout will block until the nodes have been accepted + required: false + default: 30 + aliases: [] + approve_all: + description: + - This flag allows for the module to approve all CSRs that are found. + - This facilitates testing. + required: false + default: False + aliases: [] + service_account: + description: + - This parameter tells the approval process which service account is being used for the requests + required: false + default: node-bootstrapper + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: Approve certificates for node xyz + oc_adm_scr: + nodes: + - xyz + timeout: 300 + +- name: Approve certificates for node xyz + oc_adm_scr: + nodes: + - xyz + timeout: 0 +''' + +# -*- -*- -*- End included fragment: doc/csr -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- + + +class YeditException(Exception): # pragma: no cover + ''' Exception class for Yedit ''' + pass + + +# pylint: disable=too-many-public-methods +class Yedit(object): # pragma: no cover + ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)" + 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 separator ''' + return self._separator + + @separator.setter + def separator(self, inc_sep): + ''' setter method for separator ''' + self._separator = inc_sep + + @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.format(''.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.format(''.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) + 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): + raise YeditException("Unexpected item type found while going through key " + + "path: {} (at key: {})".format(key, dict_key)) + + 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: + raise YeditException("Unexpected item type found while going through key path: {}".format(key)) + + 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 + + # didn't add/update to an existing list, nor add/update key to a dict + # so we must have been provided some syntax like a.b.c[<int>] = "data" for a + # non-existent array + else: + raise YeditException("Error adding to object at path: {}".format(key)) + + 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) + 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') + + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + # Try to use RoundTripDumper if supported. + try: + Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper)) + except AttributeError: + 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: + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + # Try to use RoundTripLoader if supported. + try: + self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader) + except AttributeError: + self.yaml_dict = yaml.safe_load(contents) + + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + 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. {}'.format(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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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) + + # AUDIT:maybe-no-member makes sense due to loading data from + # a serialized format. + # pylint: disable=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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=maybe-no-member + if not isinstance(value, dict): + raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' + + 'value=[{}] type=[{}]'.format(value, type(value))) + + entry.update(value) + return (True, self.yaml_dict) + + elif isinstance(entry, list): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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 + # Try to use ruamel.yaml and fallback to pyyaml + try: + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + except AttributeError: + tmp_copy = copy.deepcopy(self.yaml_dict) + + # set the format attributes if available + try: + tmp_copy.fa.set_block_style() + except AttributeError: + pass + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result is None: + return (False, self.yaml_dict) + + # When path equals "" it is a special case. + # "" refers to the root of the document + # Only update the root path (entire document) when its a list or dict + if path == '': + if isinstance(result, list) or isinstance(result, dict): + self.yaml_dict = result + return (True, self.yaml_dict) + + 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 + # Try to use ruamel.yaml and fallback to pyyaml + try: + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + except AttributeError: + tmp_copy = copy.deepcopy(self.yaml_dict) + + # set the format attributes if available + try: + tmp_copy.fa.set_block_style() + except AttributeError: + pass + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result is not None: + 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=[{}] vtype=[{}]'.format(inc_value, vtype)) + elif isinstance(inc_value, bool) and 'str' in vtype: + inc_value = str(inc_value) + + # There is a special case where '' will turn into None after yaml loading it so skip + if isinstance(inc_value, str) and inc_value == '': + pass + # If vtype is not str then go ahead and attempt to yaml load it. + elif isinstance(inc_value, str) and 'str' not in vtype: + try: + inc_value = yaml.safe_load(inc_value) + except Exception: + raise YeditException('Could not determine type of incoming value. ' + + 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype)) + + return inc_value + + @staticmethod + def process_edits(edits, yamlfile): + '''run through a list of edits and process them one-by-one''' + results = [] + for edit in edits: + value = Yedit.parse_value(edit['value'], edit.get('value_type', '')) + if edit.get('action') == 'update': + # pylint: disable=line-too-long + curr_value = Yedit.get_curr_value( + Yedit.parse_value(edit.get('curr_value')), + edit.get('curr_value_format')) + + rval = yamlfile.update(edit['key'], + value, + edit.get('index'), + curr_value) + + elif edit.get('action') == 'append': + rval = yamlfile.append(edit['key'], value) + + else: + rval = yamlfile.put(edit['key'], value) + + if rval[0]: + results.append({'key': edit['key'], 'edit': rval[1]}) + + return {'changed': len(results) > 0, 'results': results} + + # pylint: disable=too-many-return-statements,too-many-branches + @staticmethod + def run_ansible(params): + '''perform the idempotent crud operations''' + yamlfile = Yedit(filename=params['src'], + backup=params['backup'], + separator=params['separator']) + + state = params['state'] + + if params['src']: + rval = yamlfile.load() + + if yamlfile.yaml_dict is None and state != 'present': + return {'failed': True, + 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) + + 'file exists, that it is has correct permissions, and is valid yaml.'} + + if state == 'list': + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + yamlfile.yaml_dict = content + + if params['key']: + rval = yamlfile.get(params['key']) + + return {'changed': False, 'result': rval, 'state': state} + + elif state == 'absent': + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + yamlfile.yaml_dict = content + + if params['update']: + rval = yamlfile.pop(params['key'], params['value']) + else: + rval = yamlfile.delete(params['key']) + + if rval[0] and params['src']: + yamlfile.write() + + return {'changed': rval[0], 'result': rval[1], 'state': state} + + elif state == 'present': + # check if content is different than what is in the file + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + + # We had no edits to make and the contents are the same + if yamlfile.yaml_dict == content and \ + params['value'] is None: + return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state} + + yamlfile.yaml_dict = content + + # If we were passed a key, value then + # we enapsulate it in a list and process it + # Key, Value passed to the module : Converted to Edits list # + edits = [] + _edit = {} + if params['value'] is not None: + _edit['value'] = params['value'] + _edit['value_type'] = params['value_type'] + _edit['key'] = params['key'] + + if params['update']: + _edit['action'] = 'update' + _edit['curr_value'] = params['curr_value'] + _edit['curr_value_format'] = params['curr_value_format'] + _edit['index'] = params['index'] + + elif params['append']: + _edit['action'] = 'append' + + edits.append(_edit) + + elif params['edits'] is not None: + edits = params['edits'] + + if edits: + results = Yedit.process_edits(edits, yamlfile) + + # if there were changes and a src provided to us we need to write + if results['changed'] and params['src']: + yamlfile.write() + + return {'changed': results['changed'], 'result': results['results'], 'state': state} + + # no edits to make + if params['src']: + # pylint: disable=redefined-variable-type + rval = yamlfile.write() + return {'changed': rval[0], + 'result': rval[1], + 'state': state} + + # We were passed content but no src, key or value, or edits. Return contents in memory + return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state} + 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 + + +ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')] + + +def locate_oc_binary(): + ''' Find and return oc binary file ''' + # https://github.com/openshift/openshift-ansible/issues/3410 + # oc can be in /usr/local/bin in some cases, but that may not + # be in $PATH due to ansible/sudo + paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS + + oc_binary = 'oc' + + # Use shutil.which if it is available, otherwise fallback to a naive path search + try: + which_result = shutil.which(oc_binary, path=os.pathsep.join(paths)) + if which_result is not None: + oc_binary = which_result + except AttributeError: + for path in paths: + if os.path.exists(os.path.join(path, oc_binary)): + oc_binary = os.path.join(path, oc_binary) + break + + return oc_binary + + +# 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 + self.oc_binary = locate_oc_binary() + + # 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''' + # We are removing the 'resourceVersion' to handle + # a race condition when modifying oc objects + yed = Yedit(fname) + results = yed.delete('metadata.resourceVersion') + if results[0]: + yed.write() + + 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, name=None, selector=None): + '''call oc delete on a resource''' + cmd = ['delete', resource] + if selector is not None: + cmd.append('--selector={}'.format(selector)) + elif name is not None: + cmd.append(name) + else: + raise OpenShiftCLIError('Either name or selector is required when calling delete.') + + 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 = ["{}={}".format(key, str(value).replace("'", r'"')) 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, name=None, selector=None): + '''return a resource by name ''' + cmd = ['get', resource] + if selector is not None: + cmd.append('--selector={}'.format(selector)) + elif name is not None: + cmd.append(name) + + 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={}'.format(selector)) + + cmd.append('--schedulable={}'.format(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={}'.format(selector)) + + if pod_selector: + cmd.append('--pod-selector={}'.format(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={}'.format(selector)) + + if dry_run: + cmd.append('--dry-run') + + if pod_selector: + cmd.append('--pod-selector={}'.format(pod_selector)) + + if grace_period: + cmd.append('--grace-period={}'.format(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('utf-8'), stderr.decode('utf-8') + + # 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 = [self.oc_binary] + + if oadm: + cmds.append('adm') + + cmds.extend(cmd) + + 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]) + + if self.verbose: + print(' '.join(cmds)) + + try: + returncode, stdout, stderr = self._run(cmds, input_data) + except OSError as ex: + returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) + + rval = {"returncode": returncode, + "cmd": ' '.join(cmds)} + + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' + + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: + rval.update({"stderr": stderr, + "stdout": stdout}) + + return rval + + +class Utils(object): # pragma: no cover + ''' 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': + # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage + # 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': + # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage + # 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 rpm + + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") + + return rpmquery.count() > 0 + + # 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(user_def[key]) + print(value) + 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(user_values) + print(api_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, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' + rval = [] + for key in sorted(self.config_options.keys()): + data = self.config_options[key] + if data['include'] \ + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) + + return rval + + +# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: class/oc_adm_csr.py -*- -*- -*- + + +class OCcsr(OpenShiftCLI): + ''' Class to wrap the oc adm certificate command line''' + kind = 'csr' + + # pylint: disable=too-many-arguments + def __init__(self, + nodes=None, + approve_all=False, + service_account=None, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for oc adm certificate ''' + super(OCcsr, self).__init__(None, kubeconfig, verbose) + self.service_account = service_account + self.nodes = self.create_nodes(nodes) + self._csrs = [] + self.approve_all = approve_all + self.verbose = verbose + + @property + def csrs(self): + '''property for managing csrs''' + # any processing needed?? + self._csrs = self._get(resource=self.kind)['results'][0]['items'] + return self._csrs + + def create_nodes(self, nodes): + '''create a node object to track csr signing status''' + nodes_list = [] + + if nodes is None: + return nodes_list + + results = self._get(resource='nodes')['results'][0]['items'] + + for node in nodes: + nodes_list.append(dict(name=node, csrs={}, accepted=False, denied=False)) + + for ocnode in results: + if node in ocnode['metadata']['name']: + nodes_list[-1]['accepted'] = True + + return nodes_list + + def get(self): + '''get the current certificate signing requests''' + return self.csrs + + @staticmethod + def action_needed(csr, action): + '''check to see if csr is in desired state''' + if csr['status'] == {}: + return True + + state = csr['status']['conditions'][0]['type'] + + if action == 'approve' and state != 'Approved': + return True + + elif action == 'deny' and state != 'Denied': + return True + + return False + + def get_csr_request(self, request): + '''base64 decode the request object and call openssl to determine the + subject and specifically the CN: from the request + + Output: + (0, '... + Subject: O=system:nodes, CN=system:node:ip-172-31-54-54.ec2.internal + ...') + ''' + import base64 + return self._run(['openssl', 'req', '-noout', '-text'], base64.b64decode(request))[1] + + def match_node(self, csr): + '''match an inc csr to a node in self.nodes''' + for node in self.nodes: + # we need to match based upon the csr's request certificate's CN + if node['name'] in self.get_csr_request(csr['spec']['request']): + node['csrs'][csr['metadata']['name']] = csr + + # check that the username is the node and type is 'Approved' + if node['name'] in csr['spec']['username'] and csr['status']: + if csr['status']['conditions'][0]['type'] == 'Approved': + node['accepted'] = True + # check type is 'Denied' and mark node as such + if csr['status'] and csr['status']['conditions'][0]['type'] == 'Denied': + node['denied'] = True + + return node + + return None + + def finished(self): + '''determine if there are more csrs to sign''' + # if nodes is set and we have nodes then return if all nodes are 'accepted' + if self.nodes is not None and len(self.nodes) > 0: + return all([node['accepted'] or node['denied'] for node in self.nodes]) + + # we are approving everything or we still have nodes outstanding + return False + + def manage(self, action): + '''run openshift oc adm ca create-server-cert cmd and store results into self.nodes + + we attempt to verify if the node is one that was given to us to accept. + + action - (allow | deny) + ''' + + results = [] + # There are 2 types of requests: + # - node-bootstrapper-client-ip-172-31-51-246-ec2-internal + # The client request allows the client to talk to the api/controller + # - node-bootstrapper-server-ip-172-31-51-246-ec2-internal + # The server request allows the server to join the cluster + # Here we need to determine how to approve/deny + # we should query the csrs and verify they are from the nodes we thought + for csr in self.csrs: + node = self.match_node(csr) + # oc adm certificate <approve|deny> csr + # there are 3 known states: Denied, Aprroved, {} + # verify something is needed by OCcsr.action_needed + # if approve_all, then do it + # if you passed in nodes, you must have a node that matches + if self.approve_all or (node and OCcsr.action_needed(csr, action)): + result = self.openshift_cmd(['certificate', action, csr['metadata']['name']], oadm=True) + # client should have service account name in username field + # server should have node name in username field + if node and csr['metadata']['name'] not in node['csrs']: + node['csrs'][csr['metadata']['name']] = csr + + # accept node in cluster + if node['name'] in csr['spec']['username']: + node['accepted'] = True + + results.append(result) + + return results + + @staticmethod + def run_ansible(params, check_mode=False): + '''run the idempotent ansible code''' + + client = OCcsr(params['nodes'], + params['approve_all'], + params['service_account'], + params['kubeconfig'], + params['debug']) + + state = params['state'] + + api_rval = client.get() + + if state == 'list': + return {'changed': False, 'results': api_rval, 'state': state} + + if state in ['approve', 'deny']: + if check_mode: + return {'changed': True, + 'msg': "CHECK_MODE: Would have {} the certificate.".format(params['state']), + 'state': state} + + all_results = [] + finished = False + timeout = False + import time + # loop for timeout or block until all nodes pass + ctr = 0 + while True: + + all_results.extend(client.manage(params['state'])) + if client.finished(): + finished = True + break + + if params['timeout'] == 0: + if not params['approve_all']: + ctr = 0 + + if ctr * 2 > params['timeout']: + timeout = True + break + + # This provides time for the nodes to send their csr requests between approvals + time.sleep(2) + + ctr += 1 + + for result in all_results: + if result['returncode'] != 0: + return {'failed': True, 'msg': all_results} + + return dict(changed=len(all_results) > 0, + results=all_results, + nodes=client.nodes, + state=state, + finished=finished, + timeout=timeout) + + return {'failed': True, + 'msg': 'Unknown state passed. %s' % state} + + +# -*- -*- -*- End included fragment: class/oc_adm_csr.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ansible/oc_adm_csr.py -*- -*- -*- + +def main(): + ''' + ansible oc module for approving certificate signing requests + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='approve', type='str', + choices=['approve', 'deny', 'list']), + debug=dict(default=False, type='bool'), + nodes=dict(default=None, type='list'), + timeout=dict(default=30, type='int'), + approve_all=dict(default=False, type='bool'), + service_account=dict(default='node-bootstrapper', type='str'), + ), + supports_check_mode=True, + mutually_exclusive=[['approve_all', 'nodes']], + ) + + if module.params['nodes'] == []: + module.fail_json(**dict(failed=True, msg='Please specify hosts.')) + + rval = OCcsr.run_ansible(module.params, module.check_mode) + + if 'failed' in rval: + return module.fail_json(**rval) + + return module.exit_json(**rval) + + +if __name__ == '__main__': + main() + +# -*- -*- -*- End included fragment: ansible/oc_adm_csr.py -*- -*- -*- diff --git a/roles/lib_openshift/library/oc_adm_manage_node.py b/roles/lib_openshift/library/oc_adm_manage_node.py index 7493b5c3d..152f270ab 100644 --- a/roles/lib_openshift/library/oc_adm_manage_node.py +++ b/roles/lib_openshift/library/oc_adm_manage_node.py @@ -731,7 +731,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -938,7 +938,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1083,10 +1083,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1096,34 +1092,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1286,13 +1274,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1391,7 +1378,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1405,18 +1391,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_adm_policy_group.py b/roles/lib_openshift/library/oc_adm_policy_group.py index 5e72f5954..3082f5890 100644 --- a/roles/lib_openshift/library/oc_adm_policy_group.py +++ b/roles/lib_openshift/library/oc_adm_policy_group.py @@ -717,7 +717,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -924,7 +924,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1069,10 +1069,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1082,34 +1078,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1272,13 +1260,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1377,7 +1364,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1391,18 +1377,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1962,28 +1958,28 @@ class PolicyGroup(OpenShiftCLI): self.verbose = verbose self._rolebinding = None self._scc = None - self._cluster_policy_bindings = None - self._policy_bindings = None + self._cluster_role_bindings = None + self._role_bindings = None @property - def policybindings(self): - if self._policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def rolebindings(self): + if self._role_bindings is None: + results = self._get('rolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve policybindings') - self._policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve rolebindings') + self._role_bindings = results['results'][0]['items'] - return self._policy_bindings + return self._role_bindings @property - def clusterpolicybindings(self): - if self._cluster_policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def clusterrolebindings(self): + if self._cluster_role_bindings is None: + results = self._get('clusterrolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve clusterpolicybindings') - self._cluster_policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve clusterrolebindings') + self._cluster_role_bindings = results['results'][0]['items'] - return self._cluster_policy_bindings + return self._cluster_role_bindings @property def role_binding(self): @@ -2026,18 +2022,17 @@ class PolicyGroup(OpenShiftCLI): ''' return whether role_binding exists ''' bindings = None if self.config.config_options['resource_kind']['value'] == 'cluster-role': - bindings = self.clusterpolicybindings + bindings = self.clusterrolebindings else: - bindings = self.policybindings + bindings = self.rolebindings if bindings is None: return False - for binding in bindings['roleBindings']: - _rb = binding['roleBinding'] - if _rb['roleRef']['name'] == self.config.config_options['name']['value'] and \ - _rb['groupNames'] is not None and \ - self.config.config_options['group']['value'] in _rb['groupNames']: + for binding in bindings: + if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \ + binding['groupNames'] is not None and \ + self.config.config_options['group']['value'] in binding['groupNames']: self.role_binding = binding return True diff --git a/roles/lib_openshift/library/oc_adm_policy_user.py b/roles/lib_openshift/library/oc_adm_policy_user.py index 371a3953b..1ceaf5d0d 100644 --- a/roles/lib_openshift/library/oc_adm_policy_user.py +++ b/roles/lib_openshift/library/oc_adm_policy_user.py @@ -717,7 +717,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -924,7 +924,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1069,10 +1069,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1082,34 +1078,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1272,13 +1260,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1377,7 +1364,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1391,18 +1377,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1953,36 +1949,36 @@ class PolicyUser(OpenShiftCLI): ''' Class to handle attaching policies to users ''' def __init__(self, - policy_config, + config, verbose=False): ''' Constructor for PolicyUser ''' - super(PolicyUser, self).__init__(policy_config.namespace, policy_config.kubeconfig, verbose) - self.config = policy_config + super(PolicyUser, self).__init__(config.namespace, config.kubeconfig, verbose) + self.config = config self.verbose = verbose self._rolebinding = None self._scc = None - self._cluster_policy_bindings = None - self._policy_bindings = None + self._cluster_role_bindings = None + self._role_bindings = None @property - def policybindings(self): - if self._policy_bindings is None: - results = self._get('policybindings', None) + def rolebindings(self): + if self._role_bindings is None: + results = self._get('rolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve policybindings') - self._policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve rolebindings') + self._role_bindings = results['results'][0]['items'] - return self._policy_bindings + return self._role_bindings @property - def clusterpolicybindings(self): - if self._cluster_policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def clusterrolebindings(self): + if self._cluster_role_bindings is None: + results = self._get('clusterrolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve clusterpolicybindings') - self._cluster_policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve clusterrolebindings') + self._cluster_role_bindings = results['results'][0]['items'] - return self._cluster_policy_bindings + return self._cluster_role_bindings @property def role_binding(self): @@ -2020,18 +2016,17 @@ class PolicyUser(OpenShiftCLI): ''' return whether role_binding exists ''' bindings = None if self.config.config_options['resource_kind']['value'] == 'cluster-role': - bindings = self.clusterpolicybindings + bindings = self.clusterrolebindings else: - bindings = self.policybindings + bindings = self.rolebindings if bindings is None: return False - for binding in bindings['roleBindings']: - _rb = binding['roleBinding'] - if _rb['roleRef']['name'] == self.config.config_options['name']['value'] and \ - _rb['userNames'] is not None and \ - self.config.config_options['user']['value'] in _rb['userNames']: + for binding in bindings: + if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \ + binding['userNames'] is not None and \ + self.config.config_options['user']['value'] in binding['userNames']: self.role_binding = binding return True diff --git a/roles/lib_openshift/library/oc_adm_registry.py b/roles/lib_openshift/library/oc_adm_registry.py index 7240521c6..0771aa5a5 100644 --- a/roles/lib_openshift/library/oc_adm_registry.py +++ b/roles/lib_openshift/library/oc_adm_registry.py @@ -835,7 +835,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -1042,7 +1042,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1187,10 +1187,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1200,34 +1196,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1390,13 +1378,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1495,7 +1482,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1509,18 +1495,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1889,12 +1885,16 @@ class SecretConfig(object): sname, namespace, kubeconfig, - secrets=None): + secrets=None, + stype=None, + annotations=None): ''' constructor for handling secret options ''' self.kubeconfig = kubeconfig self.name = sname + self.type = stype self.namespace = namespace self.secrets = secrets + self.annotations = annotations self.data = {} self.create_dict() @@ -1903,6 +1903,7 @@ class SecretConfig(object): ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' + self.data['type'] = self.type self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace @@ -1910,6 +1911,8 @@ class SecretConfig(object): if self.secrets: for key, value in self.secrets.items(): self.data['data'][key] = value + if self.annotations: + self.data['metadata']['annotations'] = self.annotations # pylint: disable=too-many-instance-attributes class Secret(Yedit): @@ -1995,7 +1998,8 @@ class ServiceConfig(object): cluster_ip=None, portal_ip=None, session_affinity=None, - service_type=None): + service_type=None, + external_ips=None): ''' constructor for handling service options ''' self.name = sname self.namespace = namespace @@ -2006,6 +2010,7 @@ class ServiceConfig(object): self.portal_ip = portal_ip self.session_affinity = session_affinity self.service_type = service_type + self.external_ips = external_ips self.data = {} self.create_dict() @@ -2018,8 +2023,9 @@ class ServiceConfig(object): 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['metadata']['labels'] = {} + for lab, lab_value in self.labels.items(): + self.data['metadata']['labels'][lab] = lab_value self.data['spec'] = {} if self.ports: @@ -2041,6 +2047,10 @@ class ServiceConfig(object): if self.service_type: self.data['spec']['type'] = self.service_type + if self.external_ips: + self.data['spec']['externalIPs'] = self.external_ips + + # pylint: disable=too-many-instance-attributes,too-many-public-methods class Service(Yedit): ''' Class to model the oc service object ''' @@ -2049,6 +2059,7 @@ class Service(Yedit): cluster_ip = "spec.clusterIP" selector_path = 'spec.selector' kind = 'Service' + external_ips = "spec.externalIPs" def __init__(self, content): '''Service constructor''' @@ -2110,6 +2121,53 @@ class Service(Yedit): '''add cluster ip''' self.put(Service.portal_ip, pip) + def get_external_ips(self): + ''' get a list of external_ips ''' + return self.get(Service.external_ips) or [] + + def add_external_ips(self, inc_external_ips): + ''' add an external_ip to the external_ips list ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get_external_ips() + if not external_ips: + self.put(Service.external_ips, inc_external_ips) + else: + external_ips.extend(inc_external_ips) + + return True + + def find_external_ips(self, inc_external_ip): + ''' find a specific external IP ''' + val = None + try: + idx = self.get_external_ips().index(inc_external_ip) + val = self.get_external_ips()[idx] + except ValueError: + pass + + return val + + def delete_external_ips(self, inc_external_ips): + ''' remove an external IP from a service ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get(Service.external_ips) or [] + + if not external_ips: + return True + + removed = False + for inc_external_ip in inc_external_ips: + external_ip = self.find_external_ips(inc_external_ip) + if external_ip: + external_ips.remove(external_ip) + removed = True + + return removed + # -*- -*- -*- End included fragment: lib/service.py -*- -*- -*- # -*- -*- -*- Begin included fragment: lib/volume.py -*- -*- -*- @@ -2350,7 +2408,7 @@ class Registry(OpenShiftCLI): def prepare_registry(self): ''' prepare a registry for instantiation ''' - options = self.config.to_option_list() + options = self.config.to_option_list(ascommalist='labels') cmd = ['registry'] cmd.extend(options) @@ -2656,7 +2714,7 @@ def main(): 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'), + labels=dict(default=None, type='dict'), ports=dict(default=['5000'], type='list'), replicas=dict(default=1, type='int'), selector=dict(default=None, type='str'), diff --git a/roles/lib_openshift/library/oc_adm_router.py b/roles/lib_openshift/library/oc_adm_router.py index a54c62cd4..146f71f68 100644 --- a/roles/lib_openshift/library/oc_adm_router.py +++ b/roles/lib_openshift/library/oc_adm_router.py @@ -860,7 +860,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -1067,7 +1067,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1212,10 +1212,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1225,34 +1221,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1415,13 +1403,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1520,7 +1507,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1534,18 +1520,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1568,7 +1564,8 @@ class ServiceConfig(object): cluster_ip=None, portal_ip=None, session_affinity=None, - service_type=None): + service_type=None, + external_ips=None): ''' constructor for handling service options ''' self.name = sname self.namespace = namespace @@ -1579,6 +1576,7 @@ class ServiceConfig(object): self.portal_ip = portal_ip self.session_affinity = session_affinity self.service_type = service_type + self.external_ips = external_ips self.data = {} self.create_dict() @@ -1591,8 +1589,9 @@ class ServiceConfig(object): 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['metadata']['labels'] = {} + for lab, lab_value in self.labels.items(): + self.data['metadata']['labels'][lab] = lab_value self.data['spec'] = {} if self.ports: @@ -1614,6 +1613,10 @@ class ServiceConfig(object): if self.service_type: self.data['spec']['type'] = self.service_type + if self.external_ips: + self.data['spec']['externalIPs'] = self.external_ips + + # pylint: disable=too-many-instance-attributes,too-many-public-methods class Service(Yedit): ''' Class to model the oc service object ''' @@ -1622,6 +1625,7 @@ class Service(Yedit): cluster_ip = "spec.clusterIP" selector_path = 'spec.selector' kind = 'Service' + external_ips = "spec.externalIPs" def __init__(self, content): '''Service constructor''' @@ -1683,6 +1687,53 @@ class Service(Yedit): '''add cluster ip''' self.put(Service.portal_ip, pip) + def get_external_ips(self): + ''' get a list of external_ips ''' + return self.get(Service.external_ips) or [] + + def add_external_ips(self, inc_external_ips): + ''' add an external_ip to the external_ips list ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get_external_ips() + if not external_ips: + self.put(Service.external_ips, inc_external_ips) + else: + external_ips.extend(inc_external_ips) + + return True + + def find_external_ips(self, inc_external_ip): + ''' find a specific external IP ''' + val = None + try: + idx = self.get_external_ips().index(inc_external_ip) + val = self.get_external_ips()[idx] + except ValueError: + pass + + return val + + def delete_external_ips(self, inc_external_ips): + ''' remove an external IP from a service ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get(Service.external_ips) or [] + + if not external_ips: + return True + + removed = False + for inc_external_ip in inc_external_ips: + external_ip = self.find_external_ips(inc_external_ip) + if external_ip: + external_ips.remove(external_ip) + removed = True + + return removed + # -*- -*- -*- End included fragment: lib/service.py -*- -*- -*- # -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*- @@ -2178,12 +2229,16 @@ class SecretConfig(object): sname, namespace, kubeconfig, - secrets=None): + secrets=None, + stype=None, + annotations=None): ''' constructor for handling secret options ''' self.kubeconfig = kubeconfig self.name = sname + self.type = stype self.namespace = namespace self.secrets = secrets + self.annotations = annotations self.data = {} self.create_dict() @@ -2192,6 +2247,7 @@ class SecretConfig(object): ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' + self.data['type'] = self.type self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace @@ -2199,6 +2255,8 @@ class SecretConfig(object): if self.secrets: for key, value in self.secrets.items(): self.data['data'][key] = value + if self.annotations: + self.data['metadata']['annotations'] = self.annotations # pylint: disable=too-many-instance-attributes class Secret(Yedit): @@ -2782,7 +2840,7 @@ class Router(OpenShiftCLI): # No certificate was passed to us. do not pass one to oc adm router self.config.config_options['default_cert']['include'] = False - options = self.config.to_option_list() + options = self.config.to_option_list(ascommalist='labels') cmd = ['router', self.config.name] cmd.extend(options) @@ -3083,7 +3141,7 @@ def main(): 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'), + labels=dict(default=None, type='dict'), ports=dict(default=['80:80', '443:443'], type='list'), replicas=dict(default=1, type='int'), selector=dict(default=None, type='str'), diff --git a/roles/lib_openshift/library/oc_atomic_container.py b/roles/lib_openshift/library/oc_atomic_container.py index d2620b4cc..79bd08f4e 100644 --- a/roles/lib_openshift/library/oc_atomic_container.py +++ b/roles/lib_openshift/library/oc_atomic_container.py @@ -65,20 +65,25 @@ options: # -*- -*- -*- Begin included fragment: ansible/oc_atomic_container.py -*- -*- -*- -# pylint: disable=wrong-import-position,too-many-branches,invalid-name +# pylint: disable=wrong-import-position,too-many-branches,invalid-name,no-name-in-module, import-error import json + +from distutils.version import StrictVersion + from ansible.module_utils.basic import AnsibleModule def _install(module, container, image, values_list): ''' install a container using atomic CLI. values_list is the list of --set arguments. container is the name given to the container. image is the image to use for the installation. ''' - args = ['atomic', 'install', "--system", '--name=%s' % container] + values_list + [image] + # NOTE: system-package=no is hardcoded. This should be changed to an option in the future. + args = ['atomic', 'install', '--system', '--system-package=no', + '--name=%s' % container] + values_list + [image] rc, out, err = module.run_command(args, check_rc=False) if rc != 0: return rc, out, err, False else: - changed = "Extracting" in out + changed = "Extracting" in out or "Copying blob" in out return rc, out, err, changed def _uninstall(module, name): @@ -122,7 +127,7 @@ def do_update(module, container, old_image, image, values_list): if rc != 0: module.fail_json(rc=rc, msg=err) else: - changed = "Extracting" in out + changed = "Extracting" in out or "Copying blob" in out module.exit_json(msg=out, changed=changed) @@ -157,7 +162,9 @@ def core(module): module.fail_json(rc=rc, msg=err) return - containers = json.loads(out) + # NOTE: "or '[]' is a workaround until atomic containers list --json + # provides an empty list when no containers are present. + containers = json.loads(out or '[]') present = len(containers) > 0 old_image = containers[0]["image_name"] if present else None @@ -187,9 +194,15 @@ def main(): ) # Verify that the platform supports atomic command - rc, _, err = module.run_command('atomic -v', check_rc=False) + rc, version_out, err = module.run_command('rpm -q --queryformat "%{VERSION}\n" atomic', check_rc=False) if rc != 0: module.fail_json(msg="Error in running atomic command", err=err) + # This module requires atomic version 1.17.2 or later + atomic_version = StrictVersion(version_out.replace('\n', '')) + if atomic_version < StrictVersion('1.17.2'): + module.fail_json( + msg="atomic version 1.17.2+ is required", + err=str(atomic_version)) try: core(module) diff --git a/roles/lib_openshift/library/oc_clusterrole.py b/roles/lib_openshift/library/oc_clusterrole.py index 78c72ef26..9761b4b4e 100644 --- a/roles/lib_openshift/library/oc_clusterrole.py +++ b/roles/lib_openshift/library/oc_clusterrole.py @@ -709,7 +709,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -916,7 +916,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1061,10 +1061,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1074,34 +1070,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1264,13 +1252,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1369,7 +1356,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1383,18 +1369,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1674,7 +1670,7 @@ class OCClusterRole(OpenShiftCLI): self.clusterrole = ClusterRole(content=result['results'][0]) result['results'] = self.clusterrole.yaml_dict - elif 'clusterrole "{}" not found'.format(self.name) in result['stderr']: + elif '"{}" not found'.format(self.name) in result['stderr']: result['returncode'] = 0 self.clusterrole = None diff --git a/roles/lib_openshift/library/oc_configmap.py b/roles/lib_openshift/library/oc_configmap.py index c88f56fc6..047edffbb 100644 --- a/roles/lib_openshift/library/oc_configmap.py +++ b/roles/lib_openshift/library/oc_configmap.py @@ -715,7 +715,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -922,7 +922,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1067,10 +1067,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1080,34 +1076,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1270,13 +1258,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1375,7 +1362,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1389,18 +1375,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_edit.py b/roles/lib_openshift/library/oc_edit.py index 17e3f7dde..0b6a8436b 100644 --- a/roles/lib_openshift/library/oc_edit.py +++ b/roles/lib_openshift/library/oc_edit.py @@ -759,7 +759,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -966,7 +966,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1111,10 +1111,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1124,34 +1120,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1314,13 +1302,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1419,7 +1406,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1433,18 +1419,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_env.py b/roles/lib_openshift/library/oc_env.py index 18ab97bc0..1f52fba40 100644 --- a/roles/lib_openshift/library/oc_env.py +++ b/roles/lib_openshift/library/oc_env.py @@ -726,7 +726,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -933,7 +933,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1078,10 +1078,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1091,34 +1087,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1281,13 +1269,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1386,7 +1373,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1400,18 +1386,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_group.py b/roles/lib_openshift/library/oc_group.py index 88c6ef209..1b63a6c13 100644 --- a/roles/lib_openshift/library/oc_group.py +++ b/roles/lib_openshift/library/oc_group.py @@ -699,7 +699,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -906,7 +906,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1051,10 +1051,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1064,34 +1060,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1254,13 +1242,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1359,7 +1346,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1373,18 +1359,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_image.py b/roles/lib_openshift/library/oc_image.py index 45860cbe5..94b08d9ce 100644 --- a/roles/lib_openshift/library/oc_image.py +++ b/roles/lib_openshift/library/oc_image.py @@ -718,7 +718,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -925,7 +925,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1070,10 +1070,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1083,34 +1079,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1273,13 +1261,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1378,7 +1365,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1392,18 +1378,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_label.py b/roles/lib_openshift/library/oc_label.py index 65923a698..ad837fdb5 100644 --- a/roles/lib_openshift/library/oc_label.py +++ b/roles/lib_openshift/library/oc_label.py @@ -735,7 +735,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -942,7 +942,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1087,10 +1087,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1100,34 +1096,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1290,13 +1278,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1395,7 +1382,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1409,18 +1395,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_obj.py b/roles/lib_openshift/library/oc_obj.py index 1d75a21b9..892546e56 100644 --- a/roles/lib_openshift/library/oc_obj.py +++ b/roles/lib_openshift/library/oc_obj.py @@ -90,9 +90,9 @@ options: required: false default: str aliases: [] - all_namespace: + all_namespaces: description: - - The namespace where the object lives. + - Search in all namespaces for the object. required: false default: false aliases: [] @@ -738,7 +738,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -945,7 +945,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1090,10 +1090,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1103,34 +1099,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1293,13 +1281,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1398,7 +1385,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1412,18 +1398,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1464,7 +1460,12 @@ class OCObject(OpenShiftCLI): def delete(self): '''delete the object''' - return self._delete(self.kind, name=self.name, selector=self.selector) + results = self._delete(self.kind, name=self.name, selector=self.selector) + if (results['returncode'] != 0 and 'stderr' in results and + '\"{}\" not found'.format(self.name) in results['stderr']): + results['returncode'] = 0 + + return results def create(self, files=None, content=None): ''' @@ -1476,7 +1477,16 @@ class OCObject(OpenShiftCLI): if files: return self._create(files[0]) - content['data'] = yaml.dump(content['data']) + # pylint: disable=no-member + # The purpose of this change is twofold: + # - we need a check to only use the ruamel specific dumper if ruamel is loaded + # - the dumper or the flow style change is needed so openshift is able to parse + # the resulting yaml, at least until gopkg.in/yaml.v2 is updated + if hasattr(yaml, 'RoundTripDumper'): + content['data'] = yaml.dump(content['data'], Dumper=yaml.RoundTripDumper) + else: + content['data'] = yaml.safe_dump(content['data'], default_flow_style=False) + content_file = Utils.create_tmp_files_from_contents(content)[0] return self._create(content_file['path']) @@ -1548,7 +1558,8 @@ class OCObject(OpenShiftCLI): if state == 'absent': # verify its not in our results if (params['name'] is not None or params['selector'] is not None) and \ - (len(api_rval['results']) == 0 or len(api_rval['results'][0].get('items', [])) == 0): + (len(api_rval['results']) == 0 or \ + ('items' in api_rval['results'][0] and len(api_rval['results'][0]['items']) == 0)): return {'changed': False, 'state': state} if check_mode: diff --git a/roles/lib_openshift/library/oc_objectvalidator.py b/roles/lib_openshift/library/oc_objectvalidator.py index 72add01f4..38df585f0 100644 --- a/roles/lib_openshift/library/oc_objectvalidator.py +++ b/roles/lib_openshift/library/oc_objectvalidator.py @@ -670,7 +670,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -877,7 +877,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1022,10 +1022,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1035,34 +1031,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1225,13 +1213,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1330,7 +1317,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1344,18 +1330,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_process.py b/roles/lib_openshift/library/oc_process.py index 8e1ffe90f..70632f86d 100644 --- a/roles/lib_openshift/library/oc_process.py +++ b/roles/lib_openshift/library/oc_process.py @@ -727,7 +727,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -934,7 +934,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1079,10 +1079,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1092,34 +1088,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1282,13 +1270,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1387,7 +1374,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1401,18 +1387,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1450,7 +1446,7 @@ class OCProcess(OpenShiftCLI): if self._template is None: results = self._process(self.name, False, self.params, self.data) if results['returncode'] != 0: - raise OpenShiftCLIError('Error processing template [%s].' % self.name) + raise OpenShiftCLIError('Error processing template [%s]: %s' %(self.name, results)) self._template = results['results']['items'] return self._template diff --git a/roles/lib_openshift/library/oc_project.py b/roles/lib_openshift/library/oc_project.py index a06852fd8..4eee748d7 100644 --- a/roles/lib_openshift/library/oc_project.py +++ b/roles/lib_openshift/library/oc_project.py @@ -724,7 +724,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -931,7 +931,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1076,10 +1076,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1089,34 +1085,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1279,13 +1267,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1384,7 +1371,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1398,18 +1384,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1660,6 +1656,9 @@ class OCProject(OpenShiftCLI): # Create it here api_rval = oadm_project.create() + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + # return the created object api_rval = oadm_project.get() diff --git a/roles/lib_openshift/library/oc_pvc.py b/roles/lib_openshift/library/oc_pvc.py index 79673452d..2e73a7645 100644 --- a/roles/lib_openshift/library/oc_pvc.py +++ b/roles/lib_openshift/library/oc_pvc.py @@ -110,6 +110,18 @@ options: - ReadOnlyMany - ReadWriteMany aliases: [] + storage_class_name: + description: + - The storage class name for the PVC + required: false + default: None + aliases: [] + selector: + description: + - A hash of key/values for the matchLabels + required: false + default: None + aliases: [] author: - "Kenny Woodson <kwoodson@redhat.com>" extends_documentation_fragment: [] @@ -719,7 +731,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -926,7 +938,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1071,10 +1083,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1084,34 +1092,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1274,13 +1274,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1379,7 +1378,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1393,18 +1391,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1423,7 +1431,9 @@ class PersistentVolumeClaimConfig(object): namespace, kubeconfig, access_modes=None, - vol_capacity='1G'): + vol_capacity='1G', + selector=None, + storage_class_name=None): ''' constructor for handling pvc options ''' self.kubeconfig = kubeconfig self.name = sname @@ -1431,6 +1441,8 @@ class PersistentVolumeClaimConfig(object): self.access_modes = access_modes self.vol_capacity = vol_capacity self.data = {} + self.selector = selector + self.storage_class_name = storage_class_name self.create_dict() @@ -1448,12 +1460,16 @@ class PersistentVolumeClaimConfig(object): self.data['spec']['accessModes'] = ['ReadWriteOnce'] if self.access_modes: self.data['spec']['accessModes'] = self.access_modes + if self.selector: + self.data['spec']['selector'] = {'matchLabels': self.selector} # storage capacity self.data['spec']['resources'] = {} self.data['spec']['resources']['requests'] = {} self.data['spec']['resources']['requests']['storage'] = self.vol_capacity + if self.storage_class_name: + self.data['spec']['storageClassName'] = self.storage_class_name # pylint: disable=too-many-instance-attributes,too-many-public-methods class PersistentVolumeClaim(Yedit): @@ -1463,13 +1479,29 @@ class PersistentVolumeClaim(Yedit): volume_name_path = "spec.volumeName" bound_path = "status.phase" kind = 'PersistentVolumeClaim' + selector_path = "spec.selector.matchLabels" + storage_class_name_path = "spec.storageClassName" def __init__(self, content): - '''RoleBinding constructor''' + '''PersistentVolumeClaim constructor''' super(PersistentVolumeClaim, self).__init__(content=content) self._access_modes = None self._volume_capacity = None self._volume_name = None + self._selector = None + self._storage_class_name = None + + @property + def storage_class_name(self): + ''' storage_class_name property ''' + if self._storage_class_name is None: + self._storage_class_name = self.get_storage_class_name() + return self._storage_class_name + + @storage_class_name.setter + def storage_class_name(self, data): + ''' storage_class_name property setter''' + self._storage_class_name = data @property def volume_name(self): @@ -1484,6 +1516,24 @@ class PersistentVolumeClaim(Yedit): self._volume_name = data @property + def selector(self): + ''' selector property ''' + if self._selector is None: + self._selector = self.get_selector() + if not isinstance(self._selector, dict): + self._selector = dict(self._selector) + + return self._selector + + @selector.setter + def selector(self, data): + ''' selector property setter''' + if not isinstance(data, dict): + data = dict(data) + + self._selector = data + + @property def access_modes(self): ''' access_modes property ''' if self._access_modes is None: @@ -1513,6 +1563,14 @@ class PersistentVolumeClaim(Yedit): ''' volume_capacity property setter''' self._volume_capacity = data + def get_storage_class_name(self): + '''get storage_class_name''' + return self.get(PersistentVolumeClaim.storage_class_name_path) or [] + + def get_selector(self): + '''get selector''' + return self.get(PersistentVolumeClaim.selector_path) or [] + def get_access_modes(self): '''get access_modes''' return self.get(PersistentVolumeClaim.access_modes_path) or [] @@ -1666,6 +1724,8 @@ class OCPVC(OpenShiftCLI): params['kubeconfig'], params['access_modes'], params['volume_capacity'], + params['selector'], + params['storage_class_name'], ) oc_pvc = OCPVC(pconfig, verbose=params['debug']) @@ -1766,9 +1826,9 @@ def main(): name=dict(default=None, required=True, type='str'), namespace=dict(default=None, required=True, type='str'), volume_capacity=dict(default='1G', type='str'), - access_modes=dict(default='ReadWriteOnce', - choices=['ReadWriteOnce', 'ReadOnlyMany', 'ReadWriteMany'], - type='str'), + storage_class_name=dict(default=None, required=False, type='str'), + selector=dict(default=None, required=False, type='dict'), + access_modes=dict(default=['ReadWriteOnce'], type='list'), ), supports_check_mode=True, ) diff --git a/roles/lib_openshift/library/oc_route.py b/roles/lib_openshift/library/oc_route.py index ad705a6c5..e003770d8 100644 --- a/roles/lib_openshift/library/oc_route.py +++ b/roles/lib_openshift/library/oc_route.py @@ -90,6 +90,12 @@ options: required: false default: str aliases: [] + labels: + description: + - The labels to apply on the route + required: false + default: None + aliases: [] tls_termination: description: - The options for termination. e.g. reencrypt @@ -769,7 +775,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -976,7 +982,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1121,10 +1127,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1134,34 +1136,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1324,13 +1318,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1429,7 +1422,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1443,18 +1435,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1473,6 +1475,7 @@ class RouteConfig(object): sname, namespace, kubeconfig, + labels=None, destcacert=None, cacert=None, cert=None, @@ -1487,6 +1490,7 @@ class RouteConfig(object): self.kubeconfig = kubeconfig self.name = sname self.namespace = namespace + self.labels = labels self.host = host self.tls_termination = tls_termination self.destcacert = destcacert @@ -1512,6 +1516,8 @@ class RouteConfig(object): self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace + if self.labels: + self.data['metadata']['labels'] = self.labels self.data['spec'] = {} self.data['spec']['host'] = self.host @@ -1669,9 +1675,6 @@ class OCRoute(OpenShiftCLI): @staticmethod def get_cert_data(path, content): '''get the data for a particular value''' - if not path and not content: - return None - rval = None if path and os.path.exists(path) and os.access(path, os.R_OK): rval = open(path).read() @@ -1710,18 +1713,19 @@ class OCRoute(OpenShiftCLI): if params['tls_termination'] and params['tls_termination'].lower() != 'passthrough': # E501 for key, option in files.items(): - if key == 'destcacert' and params['tls_termination'] != 'reencrypt': + if not option['path'] and not option['content']: continue option['value'] = OCRoute.get_cert_data(option['path'], option['content']) # E501 if not option['value']: return {'failed': True, - 'msg': 'Verify that you pass a value for %s' % key} + 'msg': 'Verify that you pass a correct value for %s' % key} rconfig = RouteConfig(params['name'], params['namespace'], params['kubeconfig'], + params['labels'], files['destcacert']['value'], files['cacert']['value'], files['cert']['value'], @@ -1826,6 +1830,7 @@ def main(): state=dict(default='present', type='str', choices=['present', 'absent', 'list']), debug=dict(default=False, type='bool'), + labels=dict(default=None, type='dict'), name=dict(default=None, required=True, type='str'), namespace=dict(default=None, required=True, type='str'), tls_termination=dict(default=None, type='str'), diff --git a/roles/lib_openshift/library/oc_scale.py b/roles/lib_openshift/library/oc_scale.py index 291ac8b19..c142f1f43 100644 --- a/roles/lib_openshift/library/oc_scale.py +++ b/roles/lib_openshift/library/oc_scale.py @@ -713,7 +713,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -920,7 +920,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1065,10 +1065,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1078,34 +1074,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1268,13 +1256,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1373,7 +1360,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1387,18 +1373,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_secret.py b/roles/lib_openshift/library/oc_secret.py index df28df2bc..62bda33ad 100644 --- a/roles/lib_openshift/library/oc_secret.py +++ b/roles/lib_openshift/library/oc_secret.py @@ -90,6 +90,12 @@ options: required: false default: default aliases: [] + annotations: + description: + - Annotations to apply to the object + required: false + default: None + aliases: [] files: description: - A list of files provided for secrets @@ -108,6 +114,12 @@ options: required: false default: None aliases: [] + type: + description: + - The secret type. + required: false + default: None + aliases: [] force: description: - Whether or not to force the operation @@ -759,7 +771,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -966,7 +978,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1111,10 +1123,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1124,34 +1132,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1314,13 +1314,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1419,7 +1418,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1433,18 +1431,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1461,12 +1469,16 @@ class SecretConfig(object): sname, namespace, kubeconfig, - secrets=None): + secrets=None, + stype=None, + annotations=None): ''' constructor for handling secret options ''' self.kubeconfig = kubeconfig self.name = sname + self.type = stype self.namespace = namespace self.secrets = secrets + self.annotations = annotations self.data = {} self.create_dict() @@ -1475,6 +1487,7 @@ class SecretConfig(object): ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' + self.data['type'] = self.type self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace @@ -1482,6 +1495,8 @@ class SecretConfig(object): if self.secrets: for key, value in self.secrets.items(): self.data['data'][key] = value + if self.annotations: + self.data['metadata']['annotations'] = self.annotations # pylint: disable=too-many-instance-attributes class Secret(Yedit): @@ -1564,12 +1579,14 @@ class OCSecret(OpenShiftCLI): def __init__(self, namespace, secret_name=None, + secret_type=None, decode=False, kubeconfig='/etc/origin/master/admin.kubeconfig', verbose=False): ''' Constructor for OpenshiftOC ''' super(OCSecret, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose) self.name = secret_name + self.type = secret_type self.decode = decode def get(self): @@ -1593,13 +1610,17 @@ class OCSecret(OpenShiftCLI): '''delete a secret by name''' return self._delete('secrets', self.name) - def create(self, files=None, contents=None): + def create(self, files=None, contents=None, force=False): '''Create a secret ''' if not files: files = Utils.create_tmp_files_from_contents(contents) secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] cmd = ['secrets', 'new', self.name] + if self.type is not None: + cmd.append("--type=%s" % (self.type)) + if force: + cmd.append('--confirm') cmd.extend(secrets) results = self.openshift_cmd(cmd) @@ -1612,7 +1633,7 @@ class OCSecret(OpenShiftCLI): This receives a list of file names and converts it into a secret. The secret is then written to disk and passed into the `oc replace` command. ''' - secret = self.prep_secret(files) + secret = self.prep_secret(files, force=force) if secret['returncode'] != 0: return secret @@ -1624,7 +1645,7 @@ class OCSecret(OpenShiftCLI): return self._replace(sfile_path, force=force) - def prep_secret(self, files=None, contents=None): + def prep_secret(self, files=None, contents=None, force=False): ''' return what the secret would look like if created This is accomplished by passing -ojson. This will most likely change in the future ''' @@ -1633,6 +1654,10 @@ class OCSecret(OpenShiftCLI): secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] cmd = ['-ojson', 'secrets', 'new', self.name] + if self.type is not None: + cmd.extend(["--type=%s" % (self.type)]) + if force: + cmd.append('--confirm') cmd.extend(secrets) return self.openshift_cmd(cmd, output=True) @@ -1645,6 +1670,7 @@ class OCSecret(OpenShiftCLI): ocsecret = OCSecret(params['namespace'], params['name'], + params['type'], params['decode'], kubeconfig=params['kubeconfig'], verbose=params['debug']) @@ -1682,8 +1708,7 @@ class OCSecret(OpenShiftCLI): elif params['contents']: files = Utils.create_tmp_files_from_contents(params['contents']) else: - return {'failed': True, - 'msg': 'Either specify files or contents.'} + files = [{'name': 'null', 'path': os.devnull}] ######## # Create @@ -1694,7 +1719,7 @@ class OCSecret(OpenShiftCLI): return {'changed': True, 'msg': 'Would have performed a create.'} - api_rval = ocsecret.create(files, params['contents']) + api_rval = ocsecret.create(files, params['contents'], force=params['force']) # Remove files if files and params['delete_after']: @@ -1711,7 +1736,7 @@ class OCSecret(OpenShiftCLI): ######## # Update ######## - secret = ocsecret.prep_secret(params['files'], params['contents']) + secret = ocsecret.prep_secret(params['files'], params['contents'], force=params['force']) if secret['returncode'] != 0: return {'failed': True, 'msg': secret} @@ -1767,6 +1792,8 @@ def main(): debug=dict(default=False, type='bool'), namespace=dict(default='default', type='str'), name=dict(default=None, type='str'), + annotations=dict(default=None, type='dict'), + type=dict(default=None, type='str'), files=dict(default=None, type='list'), delete_after=dict(default=False, type='bool'), contents=dict(default=None, type='list'), diff --git a/roles/lib_openshift/library/oc_service.py b/roles/lib_openshift/library/oc_service.py index e98f83cc3..3e8aea4f1 100644 --- a/roles/lib_openshift/library/oc_service.py +++ b/roles/lib_openshift/library/oc_service.py @@ -140,6 +140,13 @@ options: - LoadBalancer - ExternalName aliases: [] + externalips: + description: + - A list of the external IPs that are exposed for this service. + - https://kubernetes.io/docs/concepts/services-networking/service/#external-ips + required: false + default: None + aliases: [] author: - "Kenny Woodson <kwoodson@redhat.com>" extends_documentation_fragment: [] @@ -765,7 +772,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -972,7 +979,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1117,10 +1124,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1130,34 +1133,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1320,13 +1315,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1425,7 +1419,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1439,18 +1432,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval @@ -1473,7 +1476,8 @@ class ServiceConfig(object): cluster_ip=None, portal_ip=None, session_affinity=None, - service_type=None): + service_type=None, + external_ips=None): ''' constructor for handling service options ''' self.name = sname self.namespace = namespace @@ -1484,6 +1488,7 @@ class ServiceConfig(object): self.portal_ip = portal_ip self.session_affinity = session_affinity self.service_type = service_type + self.external_ips = external_ips self.data = {} self.create_dict() @@ -1496,8 +1501,9 @@ class ServiceConfig(object): 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['metadata']['labels'] = {} + for lab, lab_value in self.labels.items(): + self.data['metadata']['labels'][lab] = lab_value self.data['spec'] = {} if self.ports: @@ -1519,6 +1525,10 @@ class ServiceConfig(object): if self.service_type: self.data['spec']['type'] = self.service_type + if self.external_ips: + self.data['spec']['externalIPs'] = self.external_ips + + # pylint: disable=too-many-instance-attributes,too-many-public-methods class Service(Yedit): ''' Class to model the oc service object ''' @@ -1527,6 +1537,7 @@ class Service(Yedit): cluster_ip = "spec.clusterIP" selector_path = 'spec.selector' kind = 'Service' + external_ips = "spec.externalIPs" def __init__(self, content): '''Service constructor''' @@ -1588,6 +1599,53 @@ class Service(Yedit): '''add cluster ip''' self.put(Service.portal_ip, pip) + def get_external_ips(self): + ''' get a list of external_ips ''' + return self.get(Service.external_ips) or [] + + def add_external_ips(self, inc_external_ips): + ''' add an external_ip to the external_ips list ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get_external_ips() + if not external_ips: + self.put(Service.external_ips, inc_external_ips) + else: + external_ips.extend(inc_external_ips) + + return True + + def find_external_ips(self, inc_external_ip): + ''' find a specific external IP ''' + val = None + try: + idx = self.get_external_ips().index(inc_external_ip) + val = self.get_external_ips()[idx] + except ValueError: + pass + + return val + + def delete_external_ips(self, inc_external_ips): + ''' remove an external IP from a service ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get(Service.external_ips) or [] + + if not external_ips: + return True + + removed = False + for inc_external_ip in inc_external_ips: + external_ip = self.find_external_ips(inc_external_ip) + if external_ip: + external_ips.remove(external_ip) + removed = True + + return removed + # -*- -*- -*- End included fragment: lib/service.py -*- -*- -*- # -*- -*- -*- Begin included fragment: class/oc_service.py -*- -*- -*- @@ -1610,13 +1668,15 @@ class OCService(OpenShiftCLI): ports, session_affinity, service_type, + external_ips, kubeconfig='/etc/origin/master/admin.kubeconfig', verbose=False): ''' Constructor for OCVolume ''' super(OCService, self).__init__(namespace, kubeconfig, verbose) self.namespace = namespace self.config = ServiceConfig(sname, namespace, ports, selector, labels, - cluster_ip, portal_ip, session_affinity, service_type) + cluster_ip, portal_ip, session_affinity, service_type, + external_ips) self.user_svc = Service(content=self.config.data) self.svc = None @@ -1685,6 +1745,7 @@ class OCService(OpenShiftCLI): params['ports'], params['session_affinity'], params['service_type'], + params['external_ips'], params['kubeconfig'], params['debug']) @@ -1786,6 +1847,7 @@ def main(): ports=dict(default=None, type='list'), session_affinity=dict(default='None', type='str'), service_type=dict(default='ClusterIP', type='str'), + external_ips=dict(default=None, type='list'), ), supports_check_mode=True, ) diff --git a/roles/lib_openshift/library/oc_serviceaccount.py b/roles/lib_openshift/library/oc_serviceaccount.py index f00e9e4f6..646a39224 100644 --- a/roles/lib_openshift/library/oc_serviceaccount.py +++ b/roles/lib_openshift/library/oc_serviceaccount.py @@ -711,7 +711,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -918,7 +918,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1063,10 +1063,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1076,34 +1072,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1266,13 +1254,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1371,7 +1358,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1385,18 +1371,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_serviceaccount_secret.py b/roles/lib_openshift/library/oc_serviceaccount_secret.py index 6691495a6..99a8e8f3d 100644 --- a/roles/lib_openshift/library/oc_serviceaccount_secret.py +++ b/roles/lib_openshift/library/oc_serviceaccount_secret.py @@ -711,7 +711,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -918,7 +918,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1063,10 +1063,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1076,34 +1072,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1266,13 +1254,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1371,7 +1358,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1385,18 +1371,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_storageclass.py b/roles/lib_openshift/library/oc_storageclass.py new file mode 100644 index 000000000..e88f3ae8d --- /dev/null +++ b/roles/lib_openshift/library/oc_storageclass.py @@ -0,0 +1,1684 @@ +#!/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/storageclass -*- -*- -*- + +DOCUMENTATION = ''' +--- +module: oc_storageclass +short_description: Create, modify, and idempotently manage openshift storageclasses. +description: + - Manage openshift storageclass objects programmatically. +options: + state: + description: + - State represents whether to create, modify, delete, or list + required: False + default: present + choices: ["present", "absent", "list"] + 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: + - Name of the object that is being queried. + required: false + default: None + aliases: [] + provisioner: + description: + - Any annotations to add to the storageclass + required: false + default: 'aws-ebs' + aliases: [] + default_storage_class: + description: + - Whether or not this is the default storage class + required: false + default: False + aliases: [] + parameters: + description: + - A dictionary with the parameters to configure the storageclass. This will be based on provisioner + required: false + default: None + aliases: [] + api_version: + description: + - The api version. + required: false + default: v1 + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: get storageclass + run_once: true + oc_storageclass: + name: gp2 + state: list + register: registry_sc_out + +- name: create the storageclass + oc_storageclass: + run_once: true + name: gp2 + parameters: + type: gp2 + encrypted: 'true' + kmsKeyId: '<full kms key arn>' + provisioner: aws-ebs + default_storage_class: False + register: sc_out + notify: + - restart openshift master services +''' + +# -*- -*- -*- End included fragment: doc/storageclass -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*- + + +class YeditException(Exception): # pragma: no cover + ''' Exception class for Yedit ''' + pass + + +# pylint: disable=too-many-public-methods +class Yedit(object): # pragma: no cover + ''' Class to modify yaml files ''' + re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$" + re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)" + 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 separator ''' + return self._separator + + @separator.setter + def separator(self, inc_sep): + ''' setter method for separator ''' + self._separator = inc_sep + + @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.format(''.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.format(''.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) + 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): + raise YeditException("Unexpected item type found while going through key " + + "path: {} (at key: {})".format(key, dict_key)) + + 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: + raise YeditException("Unexpected item type found while going through key path: {}".format(key)) + + 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 + + # didn't add/update to an existing list, nor add/update key to a dict + # so we must have been provided some syntax like a.b.c[<int>] = "data" for a + # non-existent array + else: + raise YeditException("Error adding to object at path: {}".format(key)) + + 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) + 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') + + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + # Try to use RoundTripDumper if supported. + try: + Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper)) + except AttributeError: + 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: + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + # Try to use RoundTripLoader if supported. + try: + self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader) + except AttributeError: + self.yaml_dict = yaml.safe_load(contents) + + # Try to set format attributes if supported + try: + self.yaml_dict.fa.set_block_style() + except AttributeError: + pass + + 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. {}'.format(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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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) + + # AUDIT:maybe-no-member makes sense due to loading data from + # a serialized format. + # pylint: disable=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): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=maybe-no-member + if not isinstance(value, dict): + raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' + + 'value=[{}] type=[{}]'.format(value, type(value))) + + entry.update(value) + return (True, self.yaml_dict) + + elif isinstance(entry, list): + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=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 + # Try to use ruamel.yaml and fallback to pyyaml + try: + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + except AttributeError: + tmp_copy = copy.deepcopy(self.yaml_dict) + + # set the format attributes if available + try: + tmp_copy.fa.set_block_style() + except AttributeError: + pass + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result is None: + return (False, self.yaml_dict) + + # When path equals "" it is a special case. + # "" refers to the root of the document + # Only update the root path (entire document) when its a list or dict + if path == '': + if isinstance(result, list) or isinstance(result, dict): + self.yaml_dict = result + return (True, self.yaml_dict) + + 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 + # Try to use ruamel.yaml and fallback to pyyaml + try: + tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, + default_flow_style=False), + yaml.RoundTripLoader) + except AttributeError: + tmp_copy = copy.deepcopy(self.yaml_dict) + + # set the format attributes if available + try: + tmp_copy.fa.set_block_style() + except AttributeError: + pass + + result = Yedit.add_entry(tmp_copy, path, value, self.separator) + if result is not None: + 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=[{}] vtype=[{}]'.format(inc_value, vtype)) + elif isinstance(inc_value, bool) and 'str' in vtype: + inc_value = str(inc_value) + + # There is a special case where '' will turn into None after yaml loading it so skip + if isinstance(inc_value, str) and inc_value == '': + pass + # If vtype is not str then go ahead and attempt to yaml load it. + elif isinstance(inc_value, str) and 'str' not in vtype: + try: + inc_value = yaml.safe_load(inc_value) + except Exception: + raise YeditException('Could not determine type of incoming value. ' + + 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype)) + + return inc_value + + @staticmethod + def process_edits(edits, yamlfile): + '''run through a list of edits and process them one-by-one''' + results = [] + for edit in edits: + value = Yedit.parse_value(edit['value'], edit.get('value_type', '')) + if edit.get('action') == 'update': + # pylint: disable=line-too-long + curr_value = Yedit.get_curr_value( + Yedit.parse_value(edit.get('curr_value')), + edit.get('curr_value_format')) + + rval = yamlfile.update(edit['key'], + value, + edit.get('index'), + curr_value) + + elif edit.get('action') == 'append': + rval = yamlfile.append(edit['key'], value) + + else: + rval = yamlfile.put(edit['key'], value) + + if rval[0]: + results.append({'key': edit['key'], 'edit': rval[1]}) + + return {'changed': len(results) > 0, 'results': results} + + # pylint: disable=too-many-return-statements,too-many-branches + @staticmethod + def run_ansible(params): + '''perform the idempotent crud operations''' + yamlfile = Yedit(filename=params['src'], + backup=params['backup'], + separator=params['separator']) + + state = params['state'] + + if params['src']: + rval = yamlfile.load() + + if yamlfile.yaml_dict is None and state != 'present': + return {'failed': True, + 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) + + 'file exists, that it is has correct permissions, and is valid yaml.'} + + if state == 'list': + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + yamlfile.yaml_dict = content + + if params['key']: + rval = yamlfile.get(params['key']) + + return {'changed': False, 'result': rval, 'state': state} + + elif state == 'absent': + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + yamlfile.yaml_dict = content + + if params['update']: + rval = yamlfile.pop(params['key'], params['value']) + else: + rval = yamlfile.delete(params['key']) + + if rval[0] and params['src']: + yamlfile.write() + + return {'changed': rval[0], 'result': rval[1], 'state': state} + + elif state == 'present': + # check if content is different than what is in the file + if params['content']: + content = Yedit.parse_value(params['content'], params['content_type']) + + # We had no edits to make and the contents are the same + if yamlfile.yaml_dict == content and \ + params['value'] is None: + return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state} + + yamlfile.yaml_dict = content + + # If we were passed a key, value then + # we enapsulate it in a list and process it + # Key, Value passed to the module : Converted to Edits list # + edits = [] + _edit = {} + if params['value'] is not None: + _edit['value'] = params['value'] + _edit['value_type'] = params['value_type'] + _edit['key'] = params['key'] + + if params['update']: + _edit['action'] = 'update' + _edit['curr_value'] = params['curr_value'] + _edit['curr_value_format'] = params['curr_value_format'] + _edit['index'] = params['index'] + + elif params['append']: + _edit['action'] = 'append' + + edits.append(_edit) + + elif params['edits'] is not None: + edits = params['edits'] + + if edits: + results = Yedit.process_edits(edits, yamlfile) + + # if there were changes and a src provided to us we need to write + if results['changed'] and params['src']: + yamlfile.write() + + return {'changed': results['changed'], 'result': results['results'], 'state': state} + + # no edits to make + if params['src']: + # pylint: disable=redefined-variable-type + rval = yamlfile.write() + return {'changed': rval[0], + 'result': rval[1], + 'state': state} + + # We were passed content but no src, key or value, or edits. Return contents in memory + return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state} + 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 + + +ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')] + + +def locate_oc_binary(): + ''' Find and return oc binary file ''' + # https://github.com/openshift/openshift-ansible/issues/3410 + # oc can be in /usr/local/bin in some cases, but that may not + # be in $PATH due to ansible/sudo + paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS + + oc_binary = 'oc' + + # Use shutil.which if it is available, otherwise fallback to a naive path search + try: + which_result = shutil.which(oc_binary, path=os.pathsep.join(paths)) + if which_result is not None: + oc_binary = which_result + except AttributeError: + for path in paths: + if os.path.exists(os.path.join(path, oc_binary)): + oc_binary = os.path.join(path, oc_binary) + break + + return oc_binary + + +# 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 + self.oc_binary = locate_oc_binary() + + # 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''' + # We are removing the 'resourceVersion' to handle + # a race condition when modifying oc objects + yed = Yedit(fname) + results = yed.delete('metadata.resourceVersion') + if results[0]: + yed.write() + + 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, name=None, selector=None): + '''call oc delete on a resource''' + cmd = ['delete', resource] + if selector is not None: + cmd.append('--selector={}'.format(selector)) + elif name is not None: + cmd.append(name) + else: + raise OpenShiftCLIError('Either name or selector is required when calling delete.') + + 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 = ["{}={}".format(key, str(value).replace("'", r'"')) 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, name=None, selector=None): + '''return a resource by name ''' + cmd = ['get', resource] + if selector is not None: + cmd.append('--selector={}'.format(selector)) + elif name is not None: + cmd.append(name) + + 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={}'.format(selector)) + + cmd.append('--schedulable={}'.format(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={}'.format(selector)) + + if pod_selector: + cmd.append('--pod-selector={}'.format(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={}'.format(selector)) + + if dry_run: + cmd.append('--dry-run') + + if pod_selector: + cmd.append('--pod-selector={}'.format(pod_selector)) + + if grace_period: + cmd.append('--grace-period={}'.format(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('utf-8'), stderr.decode('utf-8') + + # 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 = [self.oc_binary] + + if oadm: + cmds.append('adm') + + cmds.extend(cmd) + + 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]) + + if self.verbose: + print(' '.join(cmds)) + + try: + returncode, stdout, stderr = self._run(cmds, input_data) + except OSError as ex: + returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) + + rval = {"returncode": returncode, + "cmd": ' '.join(cmds)} + + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' + + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: + rval.update({"stderr": stderr, + "stdout": stdout}) + + return rval + + +class Utils(object): # pragma: no cover + ''' 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': + # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage + # 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': + # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage + # 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 rpm + + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") + + return rpmquery.count() > 0 + + # 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(user_def[key]) + print(value) + 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(user_values) + print(api_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, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' + rval = [] + for key in sorted(self.config_options.keys()): + data = self.config_options[key] + if data['include'] \ + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) + + return rval + + +# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: lib/storageclass.py -*- -*- -*- + + +# pylint: disable=too-many-instance-attributes +class StorageClassConfig(object): + ''' Handle service options ''' + # pylint: disable=too-many-arguments + def __init__(self, + name, + provisioner, + parameters=None, + annotations=None, + default_storage_class="false", + api_version='v1', + kubeconfig='/etc/origin/master/admin.kubeconfig'): + ''' constructor for handling storageclass options ''' + self.name = name + self.parameters = parameters + self.annotations = annotations + self.provisioner = provisioner + self.api_version = api_version + self.default_storage_class = str(default_storage_class).lower() + self.kubeconfig = kubeconfig + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' instantiates a storageclass dict ''' + self.data['apiVersion'] = self.api_version + self.data['kind'] = 'StorageClass' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + + self.data['metadata']['annotations'] = {} + if self.annotations is not None: + self.data['metadata']['annotations'] = self.annotations + + self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = \ + self.default_storage_class + + self.data['provisioner'] = self.provisioner + + self.data['parameters'] = {} + if self.parameters is not None: + self.data['parameters'].update(self.parameters) + + # default to aws if no params were passed + else: + self.data['parameters']['type'] = 'gp2' + + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class StorageClass(Yedit): + ''' Class to model the oc storageclass object ''' + annotations_path = "metadata.annotations" + provisioner_path = "provisioner" + parameters_path = "parameters" + kind = 'StorageClass' + + def __init__(self, content): + '''StorageClass constructor''' + super(StorageClass, self).__init__(content=content) + + def get_annotations(self): + ''' get a list of ports ''' + return self.get(StorageClass.annotations_path) or {} + + def get_parameters(self): + ''' get the service selector''' + return self.get(StorageClass.parameters_path) or {} + +# -*- -*- -*- End included fragment: lib/storageclass.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: class/oc_storageclass.py -*- -*- -*- + +# pylint: disable=too-many-instance-attributes +class OCStorageClass(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + kind = 'storageclass' + + # pylint allows 5 + # pylint: disable=too-many-arguments + def __init__(self, + config, + verbose=False): + ''' Constructor for OCStorageClass ''' + super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose) + self.config = config + self.storage_class = None + + def exists(self): + ''' return whether a storageclass exists''' + if self.storage_class: + return True + + return False + + def get(self): + '''return storageclass ''' + result = self._get(self.kind, self.config.name) + if result['returncode'] == 0: + self.storage_class = StorageClass(content=result['results'][0]) + elif '\"%s\" not found' % self.config.name in result['stderr']: + result['returncode'] = 0 + result['results'] = [{}] + + return result + + def delete(self): + '''delete the object''' + return self._delete(self.kind, self.config.name) + + def create(self): + '''create the object''' + return self._create_from_content(self.config.name, self.config.data) + + def update(self): + '''update the object''' + # parameters are currently unable to be updated. need to delete and recreate + self.delete() + # pause here and attempt to wait for delete. + # Better option would be to poll + import time + time.sleep(5) + return self.create() + + def needs_update(self): + ''' verify an update is needed ''' + # check if params have updated + if self.storage_class.get_parameters() != self.config.parameters: + return True + + for anno_key, anno_value in self.storage_class.get_annotations().items(): + if 'is-default-class' in anno_key and anno_value != self.config.default_storage_class: + return True + + return False + + @staticmethod + # pylint: disable=too-many-return-statements,too-many-branches + # TODO: This function should be refactored into its individual parts. + def run_ansible(params, check_mode): + '''run the ansible idempotent code''' + + rconfig = StorageClassConfig(params['name'], + provisioner="kubernetes.io/{}".format(params['provisioner']), + parameters=params['parameters'], + annotations=params['annotations'], + api_version="storage.k8s.io/{}".format(params['api_version']), + default_storage_class=params.get('default_storage_class', 'false'), + kubeconfig=params['kubeconfig'], + ) + + oc_sc = OCStorageClass(rconfig, verbose=params['debug']) + + state = params['state'] + + api_rval = oc_sc.get() + + ##### + # Get + ##### + if state == 'list': + return {'changed': False, 'results': api_rval['results'], 'state': 'list'} + + ######## + # Delete + ######## + if state == 'absent': + if oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a delete.'} + + api_rval = oc_sc.delete() + + return {'changed': True, 'results': api_rval, 'state': 'absent'} + + return {'changed': False, 'state': 'absent'} + + if state == 'present': + ######## + # Create + ######## + if not oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a create.'} + + # Create it here + api_rval = oc_sc.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + ######## + # Update + ######## + if oc_sc.needs_update(): + api_rval = oc_sc.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + return {'changed': False, 'results': api_rval, 'state': 'present'} + + + return {'failed': True, + 'changed': False, + 'msg': 'Unknown state passed. %s' % state, + 'state': 'unknown'} + +# -*- -*- -*- End included fragment: class/oc_storageclass.py -*- -*- -*- + +# -*- -*- -*- Begin included fragment: ansible/oc_storageclass.py -*- -*- -*- + +def main(): + ''' + ansible oc module for storageclass + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', choices=['present', 'absent', 'list']), + debug=dict(default=False, type='bool'), + name=dict(default=None, type='str'), + annotations=dict(default=None, type='dict'), + parameters=dict(default=None, type='dict'), + provisioner=dict(required=True, type='str', choices=['aws-ebs', 'gce-pd', 'glusterfs', 'cinder']), + api_version=dict(default='v1', type='str'), + default_storage_class=dict(default="false", type='str'), + ), + supports_check_mode=True, + ) + + rval = OCStorageClass.run_ansible(module.params, module.check_mode) + if 'failed' in rval: + return module.fail_json(**rval) + + return module.exit_json(**rval) + + +if __name__ == '__main__': + main() + +# -*- -*- -*- End included fragment: ansible/oc_storageclass.py -*- -*- -*- diff --git a/roles/lib_openshift/library/oc_user.py b/roles/lib_openshift/library/oc_user.py index 72f2fbf03..7bbe38819 100644 --- a/roles/lib_openshift/library/oc_user.py +++ b/roles/lib_openshift/library/oc_user.py @@ -771,7 +771,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -978,7 +978,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1123,10 +1123,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1136,34 +1132,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1326,13 +1314,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1431,7 +1418,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1445,18 +1431,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_version.py b/roles/lib_openshift/library/oc_version.py index bc3340a94..63adbd6ac 100644 --- a/roles/lib_openshift/library/oc_version.py +++ b/roles/lib_openshift/library/oc_version.py @@ -683,7 +683,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -890,7 +890,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1035,10 +1035,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1048,34 +1044,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1238,13 +1226,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1343,7 +1330,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1357,18 +1343,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/library/oc_volume.py b/roles/lib_openshift/library/oc_volume.py index 9dec0a6d4..3c07f8d4b 100644 --- a/roles/lib_openshift/library/oc_volume.py +++ b/roles/lib_openshift/library/oc_volume.py @@ -760,7 +760,7 @@ class Yedit(object): # pragma: no cover yamlfile.yaml_dict = content if params['key']: - rval = yamlfile.get(params['key']) or {} + rval = yamlfile.get(params['key']) return {'changed': False, 'result': rval, 'state': state} @@ -967,7 +967,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -1112,10 +1112,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -1125,34 +1121,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -1315,13 +1303,12 @@ class Utils(object): # pragma: no cover @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -1420,7 +1407,6 @@ class Utils(object): # pragma: no cover print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -1434,18 +1420,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/src/ansible/oc_adm_csr.py b/roles/lib_openshift/src/ansible/oc_adm_csr.py new file mode 100644 index 000000000..9e43a810b --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_adm_csr.py @@ -0,0 +1,36 @@ +# pylint: skip-file +# flake8: noqa + +def main(): + ''' + ansible oc module for approving certificate signing requests + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='approve', type='str', + choices=['approve', 'deny', 'list']), + debug=dict(default=False, type='bool'), + nodes=dict(default=None, type='list'), + timeout=dict(default=30, type='int'), + approve_all=dict(default=False, type='bool'), + service_account=dict(default='node-bootstrapper', type='str'), + ), + supports_check_mode=True, + mutually_exclusive=[['approve_all', 'nodes']], + ) + + if module.params['nodes'] == []: + module.fail_json(**dict(failed=True, msg='Please specify hosts.')) + + rval = OCcsr.run_ansible(module.params, module.check_mode) + + if 'failed' in rval: + return module.fail_json(**rval) + + return module.exit_json(**rval) + + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/ansible/oc_adm_registry.py b/roles/lib_openshift/src/ansible/oc_adm_registry.py index c85973c7d..d669a3488 100644 --- a/roles/lib_openshift/src/ansible/oc_adm_registry.py +++ b/roles/lib_openshift/src/ansible/oc_adm_registry.py @@ -17,7 +17,7 @@ def main(): 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'), + labels=dict(default=None, type='dict'), ports=dict(default=['5000'], type='list'), replicas=dict(default=1, type='int'), selector=dict(default=None, type='str'), diff --git a/roles/lib_openshift/src/ansible/oc_adm_router.py b/roles/lib_openshift/src/ansible/oc_adm_router.py index b6f8e90d0..c6563cc2f 100644 --- a/roles/lib_openshift/src/ansible/oc_adm_router.py +++ b/roles/lib_openshift/src/ansible/oc_adm_router.py @@ -21,7 +21,7 @@ def main(): 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'), + labels=dict(default=None, type='dict'), ports=dict(default=['80:80', '443:443'], type='list'), replicas=dict(default=1, type='int'), selector=dict(default=None, type='str'), diff --git a/roles/lib_openshift/src/ansible/oc_atomic_container.py b/roles/lib_openshift/src/ansible/oc_atomic_container.py index 20d75cb63..454d7c4b2 100644 --- a/roles/lib_openshift/src/ansible/oc_atomic_container.py +++ b/roles/lib_openshift/src/ansible/oc_atomic_container.py @@ -1,20 +1,25 @@ # pylint: skip-file # flake8: noqa -# pylint: disable=wrong-import-position,too-many-branches,invalid-name +# pylint: disable=wrong-import-position,too-many-branches,invalid-name,no-name-in-module, import-error import json + +from distutils.version import StrictVersion + from ansible.module_utils.basic import AnsibleModule def _install(module, container, image, values_list): ''' install a container using atomic CLI. values_list is the list of --set arguments. container is the name given to the container. image is the image to use for the installation. ''' - args = ['atomic', 'install', "--system", '--name=%s' % container] + values_list + [image] + # NOTE: system-package=no is hardcoded. This should be changed to an option in the future. + args = ['atomic', 'install', '--system', '--system-package=no', + '--name=%s' % container] + values_list + [image] rc, out, err = module.run_command(args, check_rc=False) if rc != 0: return rc, out, err, False else: - changed = "Extracting" in out + changed = "Extracting" in out or "Copying blob" in out return rc, out, err, changed def _uninstall(module, name): @@ -58,7 +63,7 @@ def do_update(module, container, old_image, image, values_list): if rc != 0: module.fail_json(rc=rc, msg=err) else: - changed = "Extracting" in out + changed = "Extracting" in out or "Copying blob" in out module.exit_json(msg=out, changed=changed) @@ -93,7 +98,9 @@ def core(module): module.fail_json(rc=rc, msg=err) return - containers = json.loads(out) + # NOTE: "or '[]' is a workaround until atomic containers list --json + # provides an empty list when no containers are present. + containers = json.loads(out or '[]') present = len(containers) > 0 old_image = containers[0]["image_name"] if present else None @@ -123,9 +130,15 @@ def main(): ) # Verify that the platform supports atomic command - rc, _, err = module.run_command('atomic -v', check_rc=False) + rc, version_out, err = module.run_command('rpm -q --queryformat "%{VERSION}\n" atomic', check_rc=False) if rc != 0: module.fail_json(msg="Error in running atomic command", err=err) + # This module requires atomic version 1.17.2 or later + atomic_version = StrictVersion(version_out.replace('\n', '')) + if atomic_version < StrictVersion('1.17.2'): + module.fail_json( + msg="atomic version 1.17.2+ is required", + err=str(atomic_version)) try: core(module) diff --git a/roles/lib_openshift/src/ansible/oc_pvc.py b/roles/lib_openshift/src/ansible/oc_pvc.py index a5181e281..c98d811d6 100644 --- a/roles/lib_openshift/src/ansible/oc_pvc.py +++ b/roles/lib_openshift/src/ansible/oc_pvc.py @@ -16,9 +16,9 @@ def main(): name=dict(default=None, required=True, type='str'), namespace=dict(default=None, required=True, type='str'), volume_capacity=dict(default='1G', type='str'), - access_modes=dict(default='ReadWriteOnce', - choices=['ReadWriteOnce', 'ReadOnlyMany', 'ReadWriteMany'], - type='str'), + storage_class_name=dict(default=None, required=False, type='str'), + selector=dict(default=None, required=False, type='dict'), + access_modes=dict(default=['ReadWriteOnce'], type='list'), ), supports_check_mode=True, ) diff --git a/roles/lib_openshift/src/ansible/oc_route.py b/roles/lib_openshift/src/ansible/oc_route.py index f2f5c5095..969cf8bcd 100644 --- a/roles/lib_openshift/src/ansible/oc_route.py +++ b/roles/lib_openshift/src/ansible/oc_route.py @@ -13,6 +13,7 @@ def main(): state=dict(default='present', type='str', choices=['present', 'absent', 'list']), debug=dict(default=False, type='bool'), + labels=dict(default=None, type='dict'), name=dict(default=None, required=True, type='str'), namespace=dict(default=None, required=True, type='str'), tls_termination=dict(default=None, type='str'), diff --git a/roles/lib_openshift/src/ansible/oc_secret.py b/roles/lib_openshift/src/ansible/oc_secret.py index 1337cbbe5..ee2827e69 100644 --- a/roles/lib_openshift/src/ansible/oc_secret.py +++ b/roles/lib_openshift/src/ansible/oc_secret.py @@ -15,6 +15,8 @@ def main(): debug=dict(default=False, type='bool'), namespace=dict(default='default', type='str'), name=dict(default=None, type='str'), + annotations=dict(default=None, type='dict'), + type=dict(default=None, type='str'), files=dict(default=None, type='list'), delete_after=dict(default=False, type='bool'), contents=dict(default=None, type='list'), diff --git a/roles/lib_openshift/src/ansible/oc_service.py b/roles/lib_openshift/src/ansible/oc_service.py index 9eb144e9c..b90c08255 100644 --- a/roles/lib_openshift/src/ansible/oc_service.py +++ b/roles/lib_openshift/src/ansible/oc_service.py @@ -21,6 +21,7 @@ def main(): ports=dict(default=None, type='list'), session_affinity=dict(default='None', type='str'), service_type=dict(default='ClusterIP', type='str'), + external_ips=dict(default=None, type='list'), ), supports_check_mode=True, ) diff --git a/roles/lib_openshift/src/ansible/oc_storageclass.py b/roles/lib_openshift/src/ansible/oc_storageclass.py new file mode 100644 index 000000000..e9f3ebbd3 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_storageclass.py @@ -0,0 +1,32 @@ +# pylint: skip-file +# flake8: noqa + +def main(): + ''' + ansible oc module for storageclass + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', choices=['present', 'absent', 'list']), + debug=dict(default=False, type='bool'), + name=dict(default=None, type='str'), + annotations=dict(default=None, type='dict'), + parameters=dict(default=None, type='dict'), + provisioner=dict(required=True, type='str', choices=['aws-ebs', 'gce-pd', 'glusterfs', 'cinder']), + api_version=dict(default='v1', type='str'), + default_storage_class=dict(default="false", type='str'), + ), + supports_check_mode=True, + ) + + rval = OCStorageClass.run_ansible(module.params, module.check_mode) + if 'failed' in rval: + return module.fail_json(**rval) + + return module.exit_json(**rval) + + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/class/oc_adm_csr.py b/roles/lib_openshift/src/class/oc_adm_csr.py new file mode 100644 index 000000000..22b8f9165 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_adm_csr.py @@ -0,0 +1,209 @@ +# pylint: skip-file +# flake8: noqa + + +class OCcsr(OpenShiftCLI): + ''' Class to wrap the oc adm certificate command line''' + kind = 'csr' + + # pylint: disable=too-many-arguments + def __init__(self, + nodes=None, + approve_all=False, + service_account=None, + kubeconfig='/etc/origin/master/admin.kubeconfig', + verbose=False): + ''' Constructor for oc adm certificate ''' + super(OCcsr, self).__init__(None, kubeconfig, verbose) + self.service_account = service_account + self.nodes = self.create_nodes(nodes) + self._csrs = [] + self.approve_all = approve_all + self.verbose = verbose + + @property + def csrs(self): + '''property for managing csrs''' + # any processing needed?? + self._csrs = self._get(resource=self.kind)['results'][0]['items'] + return self._csrs + + def create_nodes(self, nodes): + '''create a node object to track csr signing status''' + nodes_list = [] + + if nodes is None: + return nodes_list + + results = self._get(resource='nodes')['results'][0]['items'] + + for node in nodes: + nodes_list.append(dict(name=node, csrs={}, accepted=False, denied=False)) + + for ocnode in results: + if node in ocnode['metadata']['name']: + nodes_list[-1]['accepted'] = True + + return nodes_list + + def get(self): + '''get the current certificate signing requests''' + return self.csrs + + @staticmethod + def action_needed(csr, action): + '''check to see if csr is in desired state''' + if csr['status'] == {}: + return True + + state = csr['status']['conditions'][0]['type'] + + if action == 'approve' and state != 'Approved': + return True + + elif action == 'deny' and state != 'Denied': + return True + + return False + + def get_csr_request(self, request): + '''base64 decode the request object and call openssl to determine the + subject and specifically the CN: from the request + + Output: + (0, '... + Subject: O=system:nodes, CN=system:node:ip-172-31-54-54.ec2.internal + ...') + ''' + import base64 + return self._run(['openssl', 'req', '-noout', '-text'], base64.b64decode(request))[1] + + def match_node(self, csr): + '''match an inc csr to a node in self.nodes''' + for node in self.nodes: + # we need to match based upon the csr's request certificate's CN + if node['name'] in self.get_csr_request(csr['spec']['request']): + node['csrs'][csr['metadata']['name']] = csr + + # check that the username is the node and type is 'Approved' + if node['name'] in csr['spec']['username'] and csr['status']: + if csr['status']['conditions'][0]['type'] == 'Approved': + node['accepted'] = True + # check type is 'Denied' and mark node as such + if csr['status'] and csr['status']['conditions'][0]['type'] == 'Denied': + node['denied'] = True + + return node + + return None + + def finished(self): + '''determine if there are more csrs to sign''' + # if nodes is set and we have nodes then return if all nodes are 'accepted' + if self.nodes is not None and len(self.nodes) > 0: + return all([node['accepted'] or node['denied'] for node in self.nodes]) + + # we are approving everything or we still have nodes outstanding + return False + + def manage(self, action): + '''run openshift oc adm ca create-server-cert cmd and store results into self.nodes + + we attempt to verify if the node is one that was given to us to accept. + + action - (allow | deny) + ''' + + results = [] + # There are 2 types of requests: + # - node-bootstrapper-client-ip-172-31-51-246-ec2-internal + # The client request allows the client to talk to the api/controller + # - node-bootstrapper-server-ip-172-31-51-246-ec2-internal + # The server request allows the server to join the cluster + # Here we need to determine how to approve/deny + # we should query the csrs and verify they are from the nodes we thought + for csr in self.csrs: + node = self.match_node(csr) + # oc adm certificate <approve|deny> csr + # there are 3 known states: Denied, Aprroved, {} + # verify something is needed by OCcsr.action_needed + # if approve_all, then do it + # if you passed in nodes, you must have a node that matches + if self.approve_all or (node and OCcsr.action_needed(csr, action)): + result = self.openshift_cmd(['certificate', action, csr['metadata']['name']], oadm=True) + # client should have service account name in username field + # server should have node name in username field + if node and csr['metadata']['name'] not in node['csrs']: + node['csrs'][csr['metadata']['name']] = csr + + # accept node in cluster + if node['name'] in csr['spec']['username']: + node['accepted'] = True + + results.append(result) + + return results + + @staticmethod + def run_ansible(params, check_mode=False): + '''run the idempotent ansible code''' + + client = OCcsr(params['nodes'], + params['approve_all'], + params['service_account'], + params['kubeconfig'], + params['debug']) + + state = params['state'] + + api_rval = client.get() + + if state == 'list': + return {'changed': False, 'results': api_rval, 'state': state} + + if state in ['approve', 'deny']: + if check_mode: + return {'changed': True, + 'msg': "CHECK_MODE: Would have {} the certificate.".format(params['state']), + 'state': state} + + all_results = [] + finished = False + timeout = False + import time + # loop for timeout or block until all nodes pass + ctr = 0 + while True: + + all_results.extend(client.manage(params['state'])) + if client.finished(): + finished = True + break + + if params['timeout'] == 0: + if not params['approve_all']: + ctr = 0 + + if ctr * 2 > params['timeout']: + timeout = True + break + + # This provides time for the nodes to send their csr requests between approvals + time.sleep(2) + + ctr += 1 + + for result in all_results: + if result['returncode'] != 0: + return {'failed': True, 'msg': all_results} + + return dict(changed=len(all_results) > 0, + results=all_results, + nodes=client.nodes, + state=state, + finished=finished, + timeout=timeout) + + return {'failed': True, + 'msg': 'Unknown state passed. %s' % state} + diff --git a/roles/lib_openshift/src/class/oc_adm_policy_group.py b/roles/lib_openshift/src/class/oc_adm_policy_group.py index 1e51913e0..6ad57bdce 100644 --- a/roles/lib_openshift/src/class/oc_adm_policy_group.py +++ b/roles/lib_openshift/src/class/oc_adm_policy_group.py @@ -41,28 +41,28 @@ class PolicyGroup(OpenShiftCLI): self.verbose = verbose self._rolebinding = None self._scc = None - self._cluster_policy_bindings = None - self._policy_bindings = None + self._cluster_role_bindings = None + self._role_bindings = None @property - def policybindings(self): - if self._policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def rolebindings(self): + if self._role_bindings is None: + results = self._get('rolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve policybindings') - self._policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve rolebindings') + self._role_bindings = results['results'][0]['items'] - return self._policy_bindings + return self._role_bindings @property - def clusterpolicybindings(self): - if self._cluster_policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def clusterrolebindings(self): + if self._cluster_role_bindings is None: + results = self._get('clusterrolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve clusterpolicybindings') - self._cluster_policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve clusterrolebindings') + self._cluster_role_bindings = results['results'][0]['items'] - return self._cluster_policy_bindings + return self._cluster_role_bindings @property def role_binding(self): @@ -105,18 +105,17 @@ class PolicyGroup(OpenShiftCLI): ''' return whether role_binding exists ''' bindings = None if self.config.config_options['resource_kind']['value'] == 'cluster-role': - bindings = self.clusterpolicybindings + bindings = self.clusterrolebindings else: - bindings = self.policybindings + bindings = self.rolebindings if bindings is None: return False - for binding in bindings['roleBindings']: - _rb = binding['roleBinding'] - if _rb['roleRef']['name'] == self.config.config_options['name']['value'] and \ - _rb['groupNames'] is not None and \ - self.config.config_options['group']['value'] in _rb['groupNames']: + for binding in bindings: + if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \ + binding['groupNames'] is not None and \ + self.config.config_options['group']['value'] in binding['groupNames']: self.role_binding = binding return True diff --git a/roles/lib_openshift/src/class/oc_adm_policy_user.py b/roles/lib_openshift/src/class/oc_adm_policy_user.py index 37a685ebb..6fc8145c8 100644 --- a/roles/lib_openshift/src/class/oc_adm_policy_user.py +++ b/roles/lib_openshift/src/class/oc_adm_policy_user.py @@ -32,36 +32,36 @@ class PolicyUser(OpenShiftCLI): ''' Class to handle attaching policies to users ''' def __init__(self, - policy_config, + config, verbose=False): ''' Constructor for PolicyUser ''' - super(PolicyUser, self).__init__(policy_config.namespace, policy_config.kubeconfig, verbose) - self.config = policy_config + super(PolicyUser, self).__init__(config.namespace, config.kubeconfig, verbose) + self.config = config self.verbose = verbose self._rolebinding = None self._scc = None - self._cluster_policy_bindings = None - self._policy_bindings = None + self._cluster_role_bindings = None + self._role_bindings = None @property - def policybindings(self): - if self._policy_bindings is None: - results = self._get('policybindings', None) + def rolebindings(self): + if self._role_bindings is None: + results = self._get('rolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve policybindings') - self._policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve rolebindings') + self._role_bindings = results['results'][0]['items'] - return self._policy_bindings + return self._role_bindings @property - def clusterpolicybindings(self): - if self._cluster_policy_bindings is None: - results = self._get('clusterpolicybindings', None) + def clusterrolebindings(self): + if self._cluster_role_bindings is None: + results = self._get('clusterrolebindings', None) if results['returncode'] != 0: - raise OpenShiftCLIError('Could not retrieve clusterpolicybindings') - self._cluster_policy_bindings = results['results'][0]['items'][0] + raise OpenShiftCLIError('Could not retrieve clusterrolebindings') + self._cluster_role_bindings = results['results'][0]['items'] - return self._cluster_policy_bindings + return self._cluster_role_bindings @property def role_binding(self): @@ -99,18 +99,17 @@ class PolicyUser(OpenShiftCLI): ''' return whether role_binding exists ''' bindings = None if self.config.config_options['resource_kind']['value'] == 'cluster-role': - bindings = self.clusterpolicybindings + bindings = self.clusterrolebindings else: - bindings = self.policybindings + bindings = self.rolebindings if bindings is None: return False - for binding in bindings['roleBindings']: - _rb = binding['roleBinding'] - if _rb['roleRef']['name'] == self.config.config_options['name']['value'] and \ - _rb['userNames'] is not None and \ - self.config.config_options['user']['value'] in _rb['userNames']: + for binding in bindings: + if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \ + binding['userNames'] is not None and \ + self.config.config_options['user']['value'] in binding['userNames']: self.role_binding = binding return True diff --git a/roles/lib_openshift/src/class/oc_adm_registry.py b/roles/lib_openshift/src/class/oc_adm_registry.py index 3c130fe28..ad6869bb6 100644 --- a/roles/lib_openshift/src/class/oc_adm_registry.py +++ b/roles/lib_openshift/src/class/oc_adm_registry.py @@ -143,7 +143,7 @@ class Registry(OpenShiftCLI): def prepare_registry(self): ''' prepare a registry for instantiation ''' - options = self.config.to_option_list() + options = self.config.to_option_list(ascommalist='labels') cmd = ['registry'] cmd.extend(options) diff --git a/roles/lib_openshift/src/class/oc_adm_router.py b/roles/lib_openshift/src/class/oc_adm_router.py index 1a0b94b80..0d50116d1 100644 --- a/roles/lib_openshift/src/class/oc_adm_router.py +++ b/roles/lib_openshift/src/class/oc_adm_router.py @@ -222,7 +222,7 @@ class Router(OpenShiftCLI): # No certificate was passed to us. do not pass one to oc adm router self.config.config_options['default_cert']['include'] = False - options = self.config.to_option_list() + options = self.config.to_option_list(ascommalist='labels') cmd = ['router', self.config.name] cmd.extend(options) diff --git a/roles/lib_openshift/src/class/oc_clusterrole.py b/roles/lib_openshift/src/class/oc_clusterrole.py index ae6795446..328e5cb67 100644 --- a/roles/lib_openshift/src/class/oc_clusterrole.py +++ b/roles/lib_openshift/src/class/oc_clusterrole.py @@ -56,7 +56,7 @@ class OCClusterRole(OpenShiftCLI): self.clusterrole = ClusterRole(content=result['results'][0]) result['results'] = self.clusterrole.yaml_dict - elif 'clusterrole "{}" not found'.format(self.name) in result['stderr']: + elif '"{}" not found'.format(self.name) in result['stderr']: result['returncode'] = 0 self.clusterrole = None diff --git a/roles/lib_openshift/src/class/oc_obj.py b/roles/lib_openshift/src/class/oc_obj.py index 6f0da3d5c..68f7818e4 100644 --- a/roles/lib_openshift/src/class/oc_obj.py +++ b/roles/lib_openshift/src/class/oc_obj.py @@ -33,7 +33,12 @@ class OCObject(OpenShiftCLI): def delete(self): '''delete the object''' - return self._delete(self.kind, name=self.name, selector=self.selector) + results = self._delete(self.kind, name=self.name, selector=self.selector) + if (results['returncode'] != 0 and 'stderr' in results and + '\"{}\" not found'.format(self.name) in results['stderr']): + results['returncode'] = 0 + + return results def create(self, files=None, content=None): ''' @@ -45,7 +50,16 @@ class OCObject(OpenShiftCLI): if files: return self._create(files[0]) - content['data'] = yaml.dump(content['data']) + # pylint: disable=no-member + # The purpose of this change is twofold: + # - we need a check to only use the ruamel specific dumper if ruamel is loaded + # - the dumper or the flow style change is needed so openshift is able to parse + # the resulting yaml, at least until gopkg.in/yaml.v2 is updated + if hasattr(yaml, 'RoundTripDumper'): + content['data'] = yaml.dump(content['data'], Dumper=yaml.RoundTripDumper) + else: + content['data'] = yaml.safe_dump(content['data'], default_flow_style=False) + content_file = Utils.create_tmp_files_from_contents(content)[0] return self._create(content_file['path']) @@ -117,7 +131,8 @@ class OCObject(OpenShiftCLI): if state == 'absent': # verify its not in our results if (params['name'] is not None or params['selector'] is not None) and \ - (len(api_rval['results']) == 0 or len(api_rval['results'][0].get('items', [])) == 0): + (len(api_rval['results']) == 0 or \ + ('items' in api_rval['results'][0] and len(api_rval['results'][0]['items']) == 0)): return {'changed': False, 'state': state} if check_mode: diff --git a/roles/lib_openshift/src/class/oc_process.py b/roles/lib_openshift/src/class/oc_process.py index eba9a43cd..62a6bd571 100644 --- a/roles/lib_openshift/src/class/oc_process.py +++ b/roles/lib_openshift/src/class/oc_process.py @@ -30,7 +30,7 @@ class OCProcess(OpenShiftCLI): if self._template is None: results = self._process(self.name, False, self.params, self.data) if results['returncode'] != 0: - raise OpenShiftCLIError('Error processing template [%s].' % self.name) + raise OpenShiftCLIError('Error processing template [%s]: %s' %(self.name, results)) self._template = results['results']['items'] return self._template diff --git a/roles/lib_openshift/src/class/oc_project.py b/roles/lib_openshift/src/class/oc_project.py index 9ad8111a8..298597067 100644 --- a/roles/lib_openshift/src/class/oc_project.py +++ b/roles/lib_openshift/src/class/oc_project.py @@ -156,6 +156,9 @@ class OCProject(OpenShiftCLI): # Create it here api_rval = oadm_project.create() + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + # return the created object api_rval = oadm_project.get() diff --git a/roles/lib_openshift/src/class/oc_pvc.py b/roles/lib_openshift/src/class/oc_pvc.py index c73abc47c..6b566c301 100644 --- a/roles/lib_openshift/src/class/oc_pvc.py +++ b/roles/lib_openshift/src/class/oc_pvc.py @@ -85,6 +85,8 @@ class OCPVC(OpenShiftCLI): params['kubeconfig'], params['access_modes'], params['volume_capacity'], + params['selector'], + params['storage_class_name'], ) oc_pvc = OCPVC(pconfig, verbose=params['debug']) diff --git a/roles/lib_openshift/src/class/oc_route.py b/roles/lib_openshift/src/class/oc_route.py index 3935525f1..dc2f7977b 100644 --- a/roles/lib_openshift/src/class/oc_route.py +++ b/roles/lib_openshift/src/class/oc_route.py @@ -68,9 +68,6 @@ class OCRoute(OpenShiftCLI): @staticmethod def get_cert_data(path, content): '''get the data for a particular value''' - if not path and not content: - return None - rval = None if path and os.path.exists(path) and os.access(path, os.R_OK): rval = open(path).read() @@ -109,18 +106,19 @@ class OCRoute(OpenShiftCLI): if params['tls_termination'] and params['tls_termination'].lower() != 'passthrough': # E501 for key, option in files.items(): - if key == 'destcacert' and params['tls_termination'] != 'reencrypt': + if not option['path'] and not option['content']: continue option['value'] = OCRoute.get_cert_data(option['path'], option['content']) # E501 if not option['value']: return {'failed': True, - 'msg': 'Verify that you pass a value for %s' % key} + 'msg': 'Verify that you pass a correct value for %s' % key} rconfig = RouteConfig(params['name'], params['namespace'], params['kubeconfig'], + params['labels'], files['destcacert']['value'], files['cacert']['value'], files['cert']['value'], diff --git a/roles/lib_openshift/src/class/oc_secret.py b/roles/lib_openshift/src/class/oc_secret.py index deb36a9fa..89e70b6b2 100644 --- a/roles/lib_openshift/src/class/oc_secret.py +++ b/roles/lib_openshift/src/class/oc_secret.py @@ -13,12 +13,14 @@ class OCSecret(OpenShiftCLI): def __init__(self, namespace, secret_name=None, + secret_type=None, decode=False, kubeconfig='/etc/origin/master/admin.kubeconfig', verbose=False): ''' Constructor for OpenshiftOC ''' super(OCSecret, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose) self.name = secret_name + self.type = secret_type self.decode = decode def get(self): @@ -42,13 +44,17 @@ class OCSecret(OpenShiftCLI): '''delete a secret by name''' return self._delete('secrets', self.name) - def create(self, files=None, contents=None): + def create(self, files=None, contents=None, force=False): '''Create a secret ''' if not files: files = Utils.create_tmp_files_from_contents(contents) secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] cmd = ['secrets', 'new', self.name] + if self.type is not None: + cmd.append("--type=%s" % (self.type)) + if force: + cmd.append('--confirm') cmd.extend(secrets) results = self.openshift_cmd(cmd) @@ -61,7 +67,7 @@ class OCSecret(OpenShiftCLI): This receives a list of file names and converts it into a secret. The secret is then written to disk and passed into the `oc replace` command. ''' - secret = self.prep_secret(files) + secret = self.prep_secret(files, force=force) if secret['returncode'] != 0: return secret @@ -73,7 +79,7 @@ class OCSecret(OpenShiftCLI): return self._replace(sfile_path, force=force) - def prep_secret(self, files=None, contents=None): + def prep_secret(self, files=None, contents=None, force=False): ''' return what the secret would look like if created This is accomplished by passing -ojson. This will most likely change in the future ''' @@ -82,6 +88,10 @@ class OCSecret(OpenShiftCLI): secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files] cmd = ['-ojson', 'secrets', 'new', self.name] + if self.type is not None: + cmd.extend(["--type=%s" % (self.type)]) + if force: + cmd.append('--confirm') cmd.extend(secrets) return self.openshift_cmd(cmd, output=True) @@ -94,6 +104,7 @@ class OCSecret(OpenShiftCLI): ocsecret = OCSecret(params['namespace'], params['name'], + params['type'], params['decode'], kubeconfig=params['kubeconfig'], verbose=params['debug']) @@ -131,8 +142,7 @@ class OCSecret(OpenShiftCLI): elif params['contents']: files = Utils.create_tmp_files_from_contents(params['contents']) else: - return {'failed': True, - 'msg': 'Either specify files or contents.'} + files = [{'name': 'null', 'path': os.devnull}] ######## # Create @@ -143,7 +153,7 @@ class OCSecret(OpenShiftCLI): return {'changed': True, 'msg': 'Would have performed a create.'} - api_rval = ocsecret.create(files, params['contents']) + api_rval = ocsecret.create(files, params['contents'], force=params['force']) # Remove files if files and params['delete_after']: @@ -160,7 +170,7 @@ class OCSecret(OpenShiftCLI): ######## # Update ######## - secret = ocsecret.prep_secret(params['files'], params['contents']) + secret = ocsecret.prep_secret(params['files'], params['contents'], force=params['force']) if secret['returncode'] != 0: return {'failed': True, 'msg': secret} diff --git a/roles/lib_openshift/src/class/oc_service.py b/roles/lib_openshift/src/class/oc_service.py index 20cf23df5..7268a0c88 100644 --- a/roles/lib_openshift/src/class/oc_service.py +++ b/roles/lib_openshift/src/class/oc_service.py @@ -19,13 +19,15 @@ class OCService(OpenShiftCLI): ports, session_affinity, service_type, + external_ips, kubeconfig='/etc/origin/master/admin.kubeconfig', verbose=False): ''' Constructor for OCVolume ''' super(OCService, self).__init__(namespace, kubeconfig, verbose) self.namespace = namespace self.config = ServiceConfig(sname, namespace, ports, selector, labels, - cluster_ip, portal_ip, session_affinity, service_type) + cluster_ip, portal_ip, session_affinity, service_type, + external_ips) self.user_svc = Service(content=self.config.data) self.svc = None @@ -94,6 +96,7 @@ class OCService(OpenShiftCLI): params['ports'], params['session_affinity'], params['service_type'], + params['external_ips'], params['kubeconfig'], params['debug']) diff --git a/roles/lib_openshift/src/class/oc_storageclass.py b/roles/lib_openshift/src/class/oc_storageclass.py new file mode 100644 index 000000000..aced586ae --- /dev/null +++ b/roles/lib_openshift/src/class/oc_storageclass.py @@ -0,0 +1,155 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: disable=too-many-instance-attributes +class OCStorageClass(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + kind = 'storageclass' + + # pylint allows 5 + # pylint: disable=too-many-arguments + def __init__(self, + config, + verbose=False): + ''' Constructor for OCStorageClass ''' + super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose) + self.config = config + self.storage_class = None + + def exists(self): + ''' return whether a storageclass exists''' + if self.storage_class: + return True + + return False + + def get(self): + '''return storageclass ''' + result = self._get(self.kind, self.config.name) + if result['returncode'] == 0: + self.storage_class = StorageClass(content=result['results'][0]) + elif '\"%s\" not found' % self.config.name in result['stderr']: + result['returncode'] = 0 + result['results'] = [{}] + + return result + + def delete(self): + '''delete the object''' + return self._delete(self.kind, self.config.name) + + def create(self): + '''create the object''' + return self._create_from_content(self.config.name, self.config.data) + + def update(self): + '''update the object''' + # parameters are currently unable to be updated. need to delete and recreate + self.delete() + # pause here and attempt to wait for delete. + # Better option would be to poll + import time + time.sleep(5) + return self.create() + + def needs_update(self): + ''' verify an update is needed ''' + # check if params have updated + if self.storage_class.get_parameters() != self.config.parameters: + return True + + for anno_key, anno_value in self.storage_class.get_annotations().items(): + if 'is-default-class' in anno_key and anno_value != self.config.default_storage_class: + return True + + return False + + @staticmethod + # pylint: disable=too-many-return-statements,too-many-branches + # TODO: This function should be refactored into its individual parts. + def run_ansible(params, check_mode): + '''run the ansible idempotent code''' + + rconfig = StorageClassConfig(params['name'], + provisioner="kubernetes.io/{}".format(params['provisioner']), + parameters=params['parameters'], + annotations=params['annotations'], + api_version="storage.k8s.io/{}".format(params['api_version']), + default_storage_class=params.get('default_storage_class', 'false'), + kubeconfig=params['kubeconfig'], + ) + + oc_sc = OCStorageClass(rconfig, verbose=params['debug']) + + state = params['state'] + + api_rval = oc_sc.get() + + ##### + # Get + ##### + if state == 'list': + return {'changed': False, 'results': api_rval['results'], 'state': 'list'} + + ######## + # Delete + ######## + if state == 'absent': + if oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a delete.'} + + api_rval = oc_sc.delete() + + return {'changed': True, 'results': api_rval, 'state': 'absent'} + + return {'changed': False, 'state': 'absent'} + + if state == 'present': + ######## + # Create + ######## + if not oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a create.'} + + # Create it here + api_rval = oc_sc.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + ######## + # Update + ######## + if oc_sc.needs_update(): + api_rval = oc_sc.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + return {'changed': False, 'results': api_rval, 'state': 'present'} + + + return {'failed': True, + 'changed': False, + 'msg': 'Unknown state passed. %s' % state, + 'state': 'unknown'} diff --git a/roles/lib_openshift/src/doc/csr b/roles/lib_openshift/src/doc/csr new file mode 100644 index 000000000..db72dbda3 --- /dev/null +++ b/roles/lib_openshift/src/doc/csr @@ -0,0 +1,80 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_adm_csr +short_description: Module to approve or deny openshift certificate signing requests +description: + - Wrapper around the openshift `oc adm certificate approve|deny <csr>` command. +options: + state: + description: + - approve|deny|list Approve, deny, and list are the only supported states for certificates + required: false + default: present + choices: + - present + 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: [] + nodes: + description: + - A list of the names of the nodes in which to accept the certificates + required: false + default: None + aliases: [] + timeout: + description: + - This flag allows for a timeout value when approving nodes. + required: false + default: 30 + aliases: [] + timeout: + description: + - This flag allows for a timeout value when doing node approvals. + - A zero value for the timeout will block until the nodes have been accepted + required: false + default: 30 + aliases: [] + approve_all: + description: + - This flag allows for the module to approve all CSRs that are found. + - This facilitates testing. + required: false + default: False + aliases: [] + service_account: + description: + - This parameter tells the approval process which service account is being used for the requests + required: false + default: node-bootstrapper + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: Approve certificates for node xyz + oc_adm_scr: + nodes: + - xyz + timeout: 300 + +- name: Approve certificates for node xyz + oc_adm_scr: + nodes: + - xyz + timeout: 0 +''' diff --git a/roles/lib_openshift/src/doc/obj b/roles/lib_openshift/src/doc/obj index 4ff912b2d..c6504ed01 100644 --- a/roles/lib_openshift/src/doc/obj +++ b/roles/lib_openshift/src/doc/obj @@ -39,9 +39,9 @@ options: required: false default: str aliases: [] - all_namespace: + all_namespaces: description: - - The namespace where the object lives. + - Search in all namespaces for the object. required: false default: false aliases: [] diff --git a/roles/lib_openshift/src/doc/pvc b/roles/lib_openshift/src/doc/pvc index 9240f2a0f..268ad0b94 100644 --- a/roles/lib_openshift/src/doc/pvc +++ b/roles/lib_openshift/src/doc/pvc @@ -59,6 +59,18 @@ options: - ReadOnlyMany - ReadWriteMany aliases: [] + storage_class_name: + description: + - The storage class name for the PVC + required: false + default: None + aliases: [] + selector: + description: + - A hash of key/values for the matchLabels + required: false + default: None + aliases: [] author: - "Kenny Woodson <kwoodson@redhat.com>" extends_documentation_fragment: [] diff --git a/roles/lib_openshift/src/doc/route b/roles/lib_openshift/src/doc/route index a12999c9e..f0d38ab5f 100644 --- a/roles/lib_openshift/src/doc/route +++ b/roles/lib_openshift/src/doc/route @@ -39,6 +39,12 @@ options: required: false default: str aliases: [] + labels: + description: + - The labels to apply on the route + required: false + default: None + aliases: [] tls_termination: description: - The options for termination. e.g. reencrypt diff --git a/roles/lib_openshift/src/doc/secret b/roles/lib_openshift/src/doc/secret index 5c2bd9bc0..a27f90f38 100644 --- a/roles/lib_openshift/src/doc/secret +++ b/roles/lib_openshift/src/doc/secret @@ -39,6 +39,12 @@ options: required: false default: default aliases: [] + annotations: + description: + - Annotations to apply to the object + required: false + default: None + aliases: [] files: description: - A list of files provided for secrets @@ -57,6 +63,12 @@ options: required: false default: None aliases: [] + type: + description: + - The secret type. + required: false + default: None + aliases: [] force: description: - Whether or not to force the operation diff --git a/roles/lib_openshift/src/doc/service b/roles/lib_openshift/src/doc/service index 418f91dc5..ba9aa0b38 100644 --- a/roles/lib_openshift/src/doc/service +++ b/roles/lib_openshift/src/doc/service @@ -89,6 +89,13 @@ options: - LoadBalancer - ExternalName aliases: [] + externalips: + description: + - A list of the external IPs that are exposed for this service. + - https://kubernetes.io/docs/concepts/services-networking/service/#external-ips + required: false + default: None + aliases: [] author: - "Kenny Woodson <kwoodson@redhat.com>" extends_documentation_fragment: [] diff --git a/roles/lib_openshift/src/doc/storageclass b/roles/lib_openshift/src/doc/storageclass new file mode 100644 index 000000000..5a7320d55 --- /dev/null +++ b/roles/lib_openshift/src/doc/storageclass @@ -0,0 +1,86 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_storageclass +short_description: Create, modify, and idempotently manage openshift storageclasses. +description: + - Manage openshift storageclass objects programmatically. +options: + state: + description: + - State represents whether to create, modify, delete, or list + required: False + default: present + choices: ["present", "absent", "list"] + 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: + - Name of the object that is being queried. + required: false + default: None + aliases: [] + provisioner: + description: + - Any annotations to add to the storageclass + required: false + default: 'aws-ebs' + aliases: [] + default_storage_class: + description: + - Whether or not this is the default storage class + required: false + default: False + aliases: [] + parameters: + description: + - A dictionary with the parameters to configure the storageclass. This will be based on provisioner + required: false + default: None + aliases: [] + api_version: + description: + - The api version. + required: false + default: v1 + aliases: [] +author: +- "Kenny Woodson <kwoodson@redhat.com>" +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: get storageclass + run_once: true + oc_storageclass: + name: gp2 + state: list + register: registry_sc_out + +- name: create the storageclass + oc_storageclass: + run_once: true + name: gp2 + parameters: + type: gp2 + encrypted: 'true' + kmsKeyId: '<full kms key arn>' + provisioner: aws-ebs + default_storage_class: False + register: sc_out + notify: + - restart openshift master services +''' diff --git a/roles/lib_openshift/src/lib/base.py b/roles/lib_openshift/src/lib/base.py index 2bf795e25..1fb32164e 100644 --- a/roles/lib_openshift/src/lib/base.py +++ b/roles/lib_openshift/src/lib/base.py @@ -128,7 +128,7 @@ class OpenShiftCLI(object): else: cmd.append(template_name) if params: - param_str = ["{}={}".format(key, value) for key, value in params.items()] + param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()] cmd.append('-v') cmd.extend(param_str) @@ -273,10 +273,6 @@ class OpenShiftCLI(object): elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501 cmds.extend(['-n', self.namespace]) - rval = {} - results = '' - err = None - if self.verbose: print(' '.join(cmds)) @@ -286,34 +282,26 @@ class OpenShiftCLI(object): returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex) 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 verr: - if "No JSON object could be decoded" in verr.args: - err = verr.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}) + if output_type == 'json': + rval['results'] = {} + if output and stdout: + try: + rval['results'] = json.loads(stdout) + except ValueError as verr: + if "No JSON object could be decoded" in verr.args: + rval['err'] = verr.args + elif output_type == 'raw': + rval['results'] = stdout if output else '' - else: + if self.verbose: + print("STDOUT: {0}".format(stdout)) + print("STDERR: {0}".format(stderr)) + + if 'err' in rval or returncode != 0: rval.update({"stderr": stderr, - "stdout": stdout, - "results": {}}) + "stdout": stdout}) return rval @@ -476,13 +464,12 @@ class Utils(object): @staticmethod def openshift_installed(): ''' check if openshift is installed ''' - import yum + import rpm - yum_base = yum.YumBase() - if yum_base.rpmdb.searchNevra(name='atomic-openshift'): - return True + transaction_set = rpm.TransactionSet() + rpmquery = transaction_set.dbMatch("name", "atomic-openshift") - return False + return rpmquery.count() > 0 # Disabling too-many-branches. This is a yaml dictionary comparison function # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements @@ -581,7 +568,6 @@ class Utils(object): print('returning true') return True - class OpenShiftCLIConfig(object): '''Generic Config''' def __init__(self, rname, namespace, kubeconfig, options): @@ -595,18 +581,28 @@ class OpenShiftCLIConfig(object): ''' 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 ''' + def to_option_list(self, ascommalist=''): + '''return all options as a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs''' + return self.stringify(ascommalist) + + def stringify(self, ascommalist=''): + ''' return the options hash as cli params in a string + if ascommalist is set to the name of a key, and + the value of that key is a dict, format the dict + as a list of comma delimited key=value pairs ''' rval = [] for key in sorted(self.config_options.keys()): data = self.config_options[key] if data['include'] \ - and (data['value'] or isinstance(data['value'], int)): - rval.append('--{}={}'.format(key.replace('_', '-'), data['value'])) + and (data['value'] is not None or isinstance(data['value'], int)): + if key == ascommalist: + val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())]) + else: + val = data['value'] + rval.append('--{}={}'.format(key.replace('_', '-'), val)) return rval diff --git a/roles/lib_openshift/src/lib/pvc.py b/roles/lib_openshift/src/lib/pvc.py index 929b50990..d1e935c32 100644 --- a/roles/lib_openshift/src/lib/pvc.py +++ b/roles/lib_openshift/src/lib/pvc.py @@ -11,7 +11,9 @@ class PersistentVolumeClaimConfig(object): namespace, kubeconfig, access_modes=None, - vol_capacity='1G'): + vol_capacity='1G', + selector=None, + storage_class_name=None): ''' constructor for handling pvc options ''' self.kubeconfig = kubeconfig self.name = sname @@ -19,6 +21,8 @@ class PersistentVolumeClaimConfig(object): self.access_modes = access_modes self.vol_capacity = vol_capacity self.data = {} + self.selector = selector + self.storage_class_name = storage_class_name self.create_dict() @@ -36,12 +40,16 @@ class PersistentVolumeClaimConfig(object): self.data['spec']['accessModes'] = ['ReadWriteOnce'] if self.access_modes: self.data['spec']['accessModes'] = self.access_modes + if self.selector: + self.data['spec']['selector'] = {'matchLabels': self.selector} # storage capacity self.data['spec']['resources'] = {} self.data['spec']['resources']['requests'] = {} self.data['spec']['resources']['requests']['storage'] = self.vol_capacity + if self.storage_class_name: + self.data['spec']['storageClassName'] = self.storage_class_name # pylint: disable=too-many-instance-attributes,too-many-public-methods class PersistentVolumeClaim(Yedit): @@ -51,13 +59,29 @@ class PersistentVolumeClaim(Yedit): volume_name_path = "spec.volumeName" bound_path = "status.phase" kind = 'PersistentVolumeClaim' + selector_path = "spec.selector.matchLabels" + storage_class_name_path = "spec.storageClassName" def __init__(self, content): - '''RoleBinding constructor''' + '''PersistentVolumeClaim constructor''' super(PersistentVolumeClaim, self).__init__(content=content) self._access_modes = None self._volume_capacity = None self._volume_name = None + self._selector = None + self._storage_class_name = None + + @property + def storage_class_name(self): + ''' storage_class_name property ''' + if self._storage_class_name is None: + self._storage_class_name = self.get_storage_class_name() + return self._storage_class_name + + @storage_class_name.setter + def storage_class_name(self, data): + ''' storage_class_name property setter''' + self._storage_class_name = data @property def volume_name(self): @@ -72,6 +96,24 @@ class PersistentVolumeClaim(Yedit): self._volume_name = data @property + def selector(self): + ''' selector property ''' + if self._selector is None: + self._selector = self.get_selector() + if not isinstance(self._selector, dict): + self._selector = dict(self._selector) + + return self._selector + + @selector.setter + def selector(self, data): + ''' selector property setter''' + if not isinstance(data, dict): + data = dict(data) + + self._selector = data + + @property def access_modes(self): ''' access_modes property ''' if self._access_modes is None: @@ -101,6 +143,14 @@ class PersistentVolumeClaim(Yedit): ''' volume_capacity property setter''' self._volume_capacity = data + def get_storage_class_name(self): + '''get storage_class_name''' + return self.get(PersistentVolumeClaim.storage_class_name_path) or [] + + def get_selector(self): + '''get selector''' + return self.get(PersistentVolumeClaim.selector_path) or [] + def get_access_modes(self): '''get access_modes''' return self.get(PersistentVolumeClaim.access_modes_path) or [] diff --git a/roles/lib_openshift/src/lib/route.py b/roles/lib_openshift/src/lib/route.py index 3b54a24fb..b106866cb 100644 --- a/roles/lib_openshift/src/lib/route.py +++ b/roles/lib_openshift/src/lib/route.py @@ -11,6 +11,7 @@ class RouteConfig(object): sname, namespace, kubeconfig, + labels=None, destcacert=None, cacert=None, cert=None, @@ -25,6 +26,7 @@ class RouteConfig(object): self.kubeconfig = kubeconfig self.name = sname self.namespace = namespace + self.labels = labels self.host = host self.tls_termination = tls_termination self.destcacert = destcacert @@ -50,6 +52,8 @@ class RouteConfig(object): self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace + if self.labels: + self.data['metadata']['labels'] = self.labels self.data['spec'] = {} self.data['spec']['host'] = self.host diff --git a/roles/lib_openshift/src/lib/secret.py b/roles/lib_openshift/src/lib/secret.py index 75c32e8b1..ad4b6aa36 100644 --- a/roles/lib_openshift/src/lib/secret.py +++ b/roles/lib_openshift/src/lib/secret.py @@ -9,12 +9,16 @@ class SecretConfig(object): sname, namespace, kubeconfig, - secrets=None): + secrets=None, + stype=None, + annotations=None): ''' constructor for handling secret options ''' self.kubeconfig = kubeconfig self.name = sname + self.type = stype self.namespace = namespace self.secrets = secrets + self.annotations = annotations self.data = {} self.create_dict() @@ -23,6 +27,7 @@ class SecretConfig(object): ''' assign the correct properties for a secret dict ''' self.data['apiVersion'] = 'v1' self.data['kind'] = 'Secret' + self.data['type'] = self.type self.data['metadata'] = {} self.data['metadata']['name'] = self.name self.data['metadata']['namespace'] = self.namespace @@ -30,6 +35,8 @@ class SecretConfig(object): if self.secrets: for key, value in self.secrets.items(): self.data['data'][key] = value + if self.annotations: + self.data['metadata']['annotations'] = self.annotations # pylint: disable=too-many-instance-attributes class Secret(Yedit): diff --git a/roles/lib_openshift/src/lib/service.py b/roles/lib_openshift/src/lib/service.py index eef568779..0e8cc3aa5 100644 --- a/roles/lib_openshift/src/lib/service.py +++ b/roles/lib_openshift/src/lib/service.py @@ -15,7 +15,8 @@ class ServiceConfig(object): cluster_ip=None, portal_ip=None, session_affinity=None, - service_type=None): + service_type=None, + external_ips=None): ''' constructor for handling service options ''' self.name = sname self.namespace = namespace @@ -26,6 +27,7 @@ class ServiceConfig(object): self.portal_ip = portal_ip self.session_affinity = session_affinity self.service_type = service_type + self.external_ips = external_ips self.data = {} self.create_dict() @@ -38,8 +40,9 @@ class ServiceConfig(object): 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['metadata']['labels'] = {} + for lab, lab_value in self.labels.items(): + self.data['metadata']['labels'][lab] = lab_value self.data['spec'] = {} if self.ports: @@ -61,6 +64,10 @@ class ServiceConfig(object): if self.service_type: self.data['spec']['type'] = self.service_type + if self.external_ips: + self.data['spec']['externalIPs'] = self.external_ips + + # pylint: disable=too-many-instance-attributes,too-many-public-methods class Service(Yedit): ''' Class to model the oc service object ''' @@ -69,6 +76,7 @@ class Service(Yedit): cluster_ip = "spec.clusterIP" selector_path = 'spec.selector' kind = 'Service' + external_ips = "spec.externalIPs" def __init__(self, content): '''Service constructor''' @@ -129,3 +137,50 @@ class Service(Yedit): def add_portal_ip(self, pip): '''add cluster ip''' self.put(Service.portal_ip, pip) + + def get_external_ips(self): + ''' get a list of external_ips ''' + return self.get(Service.external_ips) or [] + + def add_external_ips(self, inc_external_ips): + ''' add an external_ip to the external_ips list ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get_external_ips() + if not external_ips: + self.put(Service.external_ips, inc_external_ips) + else: + external_ips.extend(inc_external_ips) + + return True + + def find_external_ips(self, inc_external_ip): + ''' find a specific external IP ''' + val = None + try: + idx = self.get_external_ips().index(inc_external_ip) + val = self.get_external_ips()[idx] + except ValueError: + pass + + return val + + def delete_external_ips(self, inc_external_ips): + ''' remove an external IP from a service ''' + if not isinstance(inc_external_ips, list): + inc_external_ips = [inc_external_ips] + + external_ips = self.get(Service.external_ips) or [] + + if not external_ips: + return True + + removed = False + for inc_external_ip in inc_external_ips: + external_ip = self.find_external_ips(inc_external_ip) + if external_ip: + external_ips.remove(external_ip) + removed = True + + return removed diff --git a/roles/lib_openshift/src/lib/storageclass.py b/roles/lib_openshift/src/lib/storageclass.py new file mode 100644 index 000000000..c49a3066a --- /dev/null +++ b/roles/lib_openshift/src/lib/storageclass.py @@ -0,0 +1,73 @@ +# pylint: skip-file +# flake8: noqa + + +# pylint: disable=too-many-instance-attributes +class StorageClassConfig(object): + ''' Handle service options ''' + # pylint: disable=too-many-arguments + def __init__(self, + name, + provisioner, + parameters=None, + annotations=None, + default_storage_class="false", + api_version='v1', + kubeconfig='/etc/origin/master/admin.kubeconfig'): + ''' constructor for handling storageclass options ''' + self.name = name + self.parameters = parameters + self.annotations = annotations + self.provisioner = provisioner + self.api_version = api_version + self.default_storage_class = str(default_storage_class).lower() + self.kubeconfig = kubeconfig + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' instantiates a storageclass dict ''' + self.data['apiVersion'] = self.api_version + self.data['kind'] = 'StorageClass' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + + self.data['metadata']['annotations'] = {} + if self.annotations is not None: + self.data['metadata']['annotations'] = self.annotations + + self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = \ + self.default_storage_class + + self.data['provisioner'] = self.provisioner + + self.data['parameters'] = {} + if self.parameters is not None: + self.data['parameters'].update(self.parameters) + + # default to aws if no params were passed + else: + self.data['parameters']['type'] = 'gp2' + + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class StorageClass(Yedit): + ''' Class to model the oc storageclass object ''' + annotations_path = "metadata.annotations" + provisioner_path = "provisioner" + parameters_path = "parameters" + kind = 'StorageClass' + + def __init__(self, content): + '''StorageClass constructor''' + super(StorageClass, self).__init__(content=content) + + def get_annotations(self): + ''' get a list of ports ''' + return self.get(StorageClass.annotations_path) or {} + + def get_parameters(self): + ''' get the service selector''' + return self.get(StorageClass.parameters_path) or {} diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index 9fa2a6c0e..4636f6b9b 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -9,6 +9,16 @@ oc_adm_ca_server_cert.py: - class/oc_adm_ca_server_cert.py - ansible/oc_adm_ca_server_cert.py +oc_adm_csr.py: +- doc/generated +- doc/license +- lib/import.py +- doc/csr +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- class/oc_adm_csr.py +- ansible/oc_adm_csr.py + oc_adm_manage_node.py: - doc/generated - doc/license @@ -263,6 +273,17 @@ oc_service.py: - class/oc_service.py - ansible/oc_service.py +oc_storageclass.py: +- doc/generated +- doc/license +- lib/import.py +- doc/storageclass +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/storageclass.py +- class/oc_storageclass.py +- ansible/oc_storageclass.py + oc_user.py: - doc/generated - doc/license diff --git a/roles/lib_openshift/src/test/integration/filter_plugins/filters.py b/roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py index f350bd25d..f350bd25d 100644 --- a/roles/lib_openshift/src/test/integration/filter_plugins/filters.py +++ b/roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py diff --git a/roles/lib_openshift/src/test/integration/oc_adm_csr.yml b/roles/lib_openshift/src/test/integration/oc_adm_csr.yml new file mode 100755 index 000000000..cad8e36f5 --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_adm_csr.yml @@ -0,0 +1,28 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +# ./oc_adm_csr.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER +--- +- hosts: masters + gather_facts: no + user: root + tasks: + - name: list csrs + oc_adm_csr: + state: list + register: csrout + + - debug: var=csrout + + - name: list csrs + oc_adm_csr: + state: approve + nodes: + - ip-172-31-51-0-ec2-internal + - ip-172-31-51-246-ec2-internal + - ip-172-31-54-12-ec2-internal + - ip-172-31-58-173-ec2-internal + - ip-172-31-58-212-ec2-internal + - ip-172-31-51-246-ec2-internal + - ip-172-31-54-12-ec2-internal + + register: csrout + - debug: var=csrout diff --git a/roles/lib_openshift/src/test/integration/oc_configmap.yml b/roles/lib_openshift/src/test/integration/oc_configmap.yml index c0d200e73..6a452ccec 100755 --- a/roles/lib_openshift/src/test/integration/oc_configmap.yml +++ b/roles/lib_openshift/src/test/integration/oc_configmap.yml @@ -55,7 +55,7 @@ config: "{{ filename }}" from_literal: foo: notbar - deployment_type: online + deployment_type: openshift-enterprise - name: fetch the updated configmap oc_configmap: @@ -70,7 +70,7 @@ assert: that: - cmout.results.results[0].metadata.name == 'configmaptest' - - cmout.results.results[0].data.deployment_type == 'online' + - cmout.results.results[0].data.deployment_type == 'openshift-enterprise' - cmout.results.results[0].data.foo == 'notbar' ###### end update test ########### diff --git a/roles/lib_openshift/src/test/integration/oc_pvc.yml b/roles/lib_openshift/src/test/integration/oc_pvc.yml new file mode 100755 index 000000000..fb3a4781f --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_pvc.yml @@ -0,0 +1,28 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +# ./oc_pvc.yml -e "cli_master_test=$OPENSHIFT_MASTER +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + tasks: + - name: create pvc + oc_pvc: + state: present + name: oc-pvc-create-test + namespace: default + volume_capacity: 3G + access_modes: + - ReadWriteOnce + selector: + foo: bar + storage_class_name: my-storage-class-name + register: pvcout + - debug: var=pvcout + + - assert: + that: + - pvcout.results.results[0]['metadata']['name'] == 'oc-pvc-create-test' + - pvcout.results.results[0]['spec']['storageClassName'] == 'my-storage-class-name' + - pvcout.results.results[0]['spec']['selector']['matchLabels']['foo'] == 'bar' + - pvcout.changed + msg: pvc create failed. diff --git a/roles/lib_openshift/src/test/integration/oc_service.yml b/roles/lib_openshift/src/test/integration/oc_service.yml index 3eb6facef..29535f24a 100755 --- a/roles/lib_openshift/src/test/integration/oc_service.yml +++ b/roles/lib_openshift/src/test/integration/oc_service.yml @@ -18,6 +18,9 @@ test-registtry: default session_affinity: ClientIP service_type: ClusterIP + labels: + component: test-registry + infra: registry register: svc_out - debug: var=svc_out @@ -25,6 +28,8 @@ that: - "svc_out.results.results[0]['metadata']['name'] == 'test-registry'" - svc_out.changed + - "svc_out.results.results[0]['metadata']['labels']['component'] == 'test-registry'" + - "svc_out.results.results[0]['metadata']['labels']['infra'] == 'registry'" msg: service create failed. # Test idempotent create diff --git a/roles/lib_openshift/src/test/integration/oc_storageclass.yml b/roles/lib_openshift/src/test/integration/oc_storageclass.yml new file mode 100755 index 000000000..c82f9dedb --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_storageclass.yml @@ -0,0 +1,87 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +# ./oc_storageclass.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + tasks: + - name: create a storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + default_storage_class: "true" + register: sc_out + - debug: var=sc_out + + - assert: + that: + - "sc_out.results.results[0]['metadata']['name'] == 'testsc'" + - sc_out.changed + - "sc_out.results.results[0]['parameters']['type'] == 'gp2'" + msg: storageclass create failed. + + # Test idempotent create + - name: NOOP create the storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + default_storage_class: "true" + register: sc_out + + - assert: + that: + - "sc_out.results.results[0]['metadata']['name'] == 'testsc'" + - sc_out.changed == False + msg: storageclass create failed. No changes expected + + - name: test list storageclass + oc_storageclass: + name: testsc + state: list + register: sc_out + - debug: var=sc_out + + - assert: + that: "sc_out.results[0]['metadata']['name'] == 'testsc'" + msg: storageclass list failed + + - name: update the storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + encrypted: "true" + default_storage_class: "true" + register: sc_out + + - assert: + that: "sc_out.results.results[0]['parameters']['encrypted'] == 'true'" + msg: storageclass update failed + + - name: oc delete storageclass + oc_storageclass: + name: testsc + state: absent + register: sc_out + - debug: var=sc_out + + - assert: + that: + - "sc_out.results['returncode'] == 0" + - "sc_out.results.results == {}" + msg: storageclass delete failed + + - name: oc get storageclass + oc_storageclass: + name: testsc + state: list + register: sc_out + - debug: var=sc_out + + - assert: + that: + - sc_out.changed == False + - "sc_out.results == [{}]" + msg: storageclass get failed diff --git a/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py b/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py index 97cf86170..77787fe87 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py +++ b/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py @@ -218,7 +218,7 @@ class RegistryTest(unittest.TestCase): 'kubeconfig': '/etc/origin/master/admin.kubeconfig', 'images': None, 'latest_images': None, - 'labels': None, + 'labels': {"docker-registry": "default", "another-label": "val"}, 'ports': ['5000'], 'replicas': 1, 'selector': 'type=infra', @@ -255,6 +255,7 @@ class RegistryTest(unittest.TestCase): mock.call(['oc', 'get', 'dc', 'docker-registry', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'get', 'svc', 'docker-registry', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'adm', 'registry', + "--labels=another-label=val,docker-registry=default", '--ports=5000', '--replicas=1', '--selector=type=infra', '--service-account=registry', '--dry-run=True', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None), diff --git a/roles/lib_openshift/src/test/unit/test_oc_adm_router.py b/roles/lib_openshift/src/test/unit/test_oc_adm_router.py index 5481ac623..dcf768e08 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_adm_router.py +++ b/roles/lib_openshift/src/test/unit/test_oc_adm_router.py @@ -300,7 +300,7 @@ class RouterTest(unittest.TestCase): 'cert_file': None, 'key_file': None, 'cacert_file': None, - 'labels': None, + 'labels': {"router": "router", "another-label": "val"}, 'ports': ['80:80', '443:443'], 'images': None, 'latest_images': None, @@ -363,6 +363,7 @@ class RouterTest(unittest.TestCase): mock.call(['oc', 'get', 'secret', 'router-certs', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'get', 'clusterrolebinding', 'router-router-role', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'adm', 'router', 'router', '--expose-metrics=False', '--external-host-insecure=False', + "--labels=another-label=val,router=router", '--ports=80:80,443:443', '--replicas=2', '--selector=type=infra', '--service-account=router', '--stats-port=1936', '--dry-run=True', '-o', 'json', '-n', 'default'], None), mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None), diff --git a/roles/lib_openshift/src/test/unit/test_oc_configmap.py b/roles/lib_openshift/src/test/unit/test_oc_configmap.py index 318fd6167..27042c64b 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_configmap.py +++ b/roles/lib_openshift/src/test/unit/test_oc_configmap.py @@ -79,7 +79,7 @@ class OCConfigMapTest(unittest.TestCase): ''' Testing a configmap create ''' params = copy.deepcopy(OCConfigMapTest.params) params['from_file'] = {'test': '/root/file'} - params['from_literal'] = {'foo': 'bar', 'deployment_type': 'online'} + params['from_literal'] = {'foo': 'bar', 'deployment_type': 'openshift-enterprise'} configmap = '''{ "apiVersion": "v1", @@ -100,7 +100,7 @@ class OCConfigMapTest(unittest.TestCase): "apiVersion": "v1", "data": { "foo": "bar", - "deployment_type": "online", + "deployment_type": "openshift-enterprise", "test": "this is a file\\n" }, "kind": "ConfigMap", @@ -128,7 +128,7 @@ class OCConfigMapTest(unittest.TestCase): self.assertTrue(results['changed']) self.assertEqual(results['results']['results'][0]['metadata']['name'], 'configmap') - self.assertEqual(results['results']['results'][0]['data']['deployment_type'], 'online') + self.assertEqual(results['results']['results'][0]['data']['deployment_type'], 'openshift-enterprise') @unittest.skipIf(six.PY3, 'py2 test only') @mock.patch('os.path.exists') diff --git a/roles/lib_openshift/src/test/unit/test_oc_pvc.py b/roles/lib_openshift/src/test/unit/test_oc_pvc.py index 82187917d..a96f2e4a7 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_pvc.py +++ b/roles/lib_openshift/src/test/unit/test_oc_pvc.py @@ -30,6 +30,8 @@ class OCPVCTest(unittest.TestCase): 'name': 'mypvc', 'namespace': 'test', 'volume_capacity': '1G', + 'selector': {'foo': 'bar', 'abc': 'a123'}, + 'storage_class_name': 'mystorage', 'access_modes': 'ReadWriteMany'} @mock.patch('oc_pvc.Utils.create_tmpfile_copy') @@ -65,6 +67,13 @@ class OCPVCTest(unittest.TestCase): "storage": "1Gi" } }, + "selector": { + "matchLabels": { + "foo": "bar", + "abc": "a123" + } + }, + "storageClassName": "myStorage", "volumeName": "pv-aws-ow5vl" }, "status": { @@ -93,6 +102,8 @@ class OCPVCTest(unittest.TestCase): self.assertTrue(results['changed']) self.assertEqual(results['results']['results'][0]['metadata']['name'], 'mypvc') + self.assertEqual(results['results']['results'][0]['spec']['storageClassName'], 'myStorage') + self.assertEqual(results['results']['results'][0]['spec']['selector']['matchLabels']['foo'], 'bar') @mock.patch('oc_pvc.Utils.create_tmpfile_copy') @mock.patch('oc_pvc.OCPVC._run') 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 afdb5e4dc..5699f123b 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_route.py +++ b/roles/lib_openshift/src/test/unit/test_oc_route.py @@ -39,6 +39,7 @@ class OCRouteTest(unittest.TestCase): 'debug': False, 'name': 'test', 'namespace': 'default', + 'labels': {'route': 'route'}, 'tls_termination': 'passthrough', 'dest_cacert_path': None, 'cacert_path': None, @@ -64,7 +65,10 @@ class OCRouteTest(unittest.TestCase): "selfLink": "/oapi/v1/namespaces/default/routes/test", "uid": "1b127c67-ecd9-11e6-96eb-0e0d9bdacd26", "resourceVersion": "439182", - "creationTimestamp": "2017-02-07T01:59:48Z" + "creationTimestamp": "2017-02-07T01:59:48Z", + "labels": { + "route": "route" + } }, "spec": { "host": "test.example", @@ -141,6 +145,7 @@ class OCRouteTest(unittest.TestCase): 'debug': False, 'name': 'test', 'namespace': 'default', + 'labels': {'route': 'route'}, 'tls_termination': 'edge', 'dest_cacert_path': None, 'cacert_path': None, @@ -166,7 +171,8 @@ class OCRouteTest(unittest.TestCase): "namespace": "default", "resourceVersion": "517745", "selfLink": "/oapi/v1/namespaces/default/routes/test", - "uid": "b6f25898-ed77-11e6-9755-0e737db1e63a" + "uid": "b6f25898-ed77-11e6-9755-0e737db1e63a", + "labels": {"route": "route"} }, "spec": { "host": "test.openshift.com", @@ -250,6 +256,7 @@ metadata: self.assertTrue(results['changed']) self.assertEqual(results['state'], 'present') self.assertEqual(results['results']['results'][0]['metadata']['name'], 'test') + self.assertEqual(results['results']['results'][0]['metadata']['labels']['route'], 'route') # Making sure our mock was called as we expected mock_cmd.assert_has_calls([ 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 e31393793..323b3423c 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_secret.py +++ b/roles/lib_openshift/src/test/unit/test_oc_secret.py @@ -38,6 +38,7 @@ class OCSecretTest(unittest.TestCase): 'state': 'present', 'namespace': 'default', 'name': 'testsecretname', + 'type': 'Opaque', 'contents': [{ 'path': "/tmp/somesecret.json", 'data': "{'one': 1, 'two': 2, 'three': 3}", @@ -47,6 +48,7 @@ class OCSecretTest(unittest.TestCase): 'debug': False, 'files': None, 'delete_after': True, + 'force': False, } # Return values of our mocked function call. These get returned once per call. @@ -74,7 +76,7 @@ class OCSecretTest(unittest.TestCase): # Making sure our mock was called as we expected mock_cmd.assert_has_calls([ mock.call(['oc', 'get', 'secrets', 'testsecretname', '-o', 'json', '-n', 'default'], None), - mock.call(['oc', 'secrets', 'new', 'testsecretname', mock.ANY, '-n', 'default'], None), + mock.call(['oc', 'secrets', 'new', 'testsecretname', '--type=Opaque', mock.ANY, '-n', 'default'], None), ]) mock_write.assert_has_calls([ 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 e74c66665..9c21a262f 100755 --- a/roles/lib_openshift/src/test/unit/test_oc_service.py +++ b/roles/lib_openshift/src/test/unit/test_oc_service.py @@ -39,6 +39,7 @@ class OCServiceTest(unittest.TestCase): 'selector': None, 'session_affinity': None, 'service_type': None, + 'external_ips': None, 'kubeconfig': '/etc/origin/master/admin.kubeconfig', 'debug': False} @@ -124,6 +125,7 @@ class OCServiceTest(unittest.TestCase): 'selector': {'router': 'router'}, 'session_affinity': 'ClientIP', 'service_type': 'ClusterIP', + 'external_ips': None, 'kubeconfig': '/etc/origin/master/admin.kubeconfig', 'debug': False} @@ -303,3 +305,183 @@ class OCServiceTest(unittest.TestCase): mock_shutil_which.side_effect = lambda _f, path=None: oc_bin self.assertEqual(locate_oc_binary(), oc_bin) + + @mock.patch('oc_service.Utils.create_tmpfile_copy') + @mock.patch('oc_service.OCService._run') + def test_create_with_labels(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a create service ''' + params = {'name': 'router', + 'namespace': 'default', + 'ports': {'name': '9000-tcp', + 'port': 9000, + 'protocol': 'TCP', + 'targetPOrt': 9000}, + 'state': 'present', + 'labels': {'component': 'some_component', 'infra': 'true'}, + 'clusterip': None, + 'portalip': None, + 'selector': {'router': 'router'}, + 'session_affinity': 'ClientIP', + 'service_type': 'ClusterIP', + 'external_ips': None, + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'debug': False} + + service = '''{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "router", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/services/router", + "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa", + "resourceVersion": "3206", + "creationTimestamp": "2017-01-26T15:06:14Z", + "labels": {"component": "some_component", "infra": "true"} + }, + "spec": { + "ports": [ + { + "name": "80-tcp", + "protocol": "TCP", + "port": 80, + "targetPort": 80 + }, + { + "name": "443-tcp", + "protocol": "TCP", + "port": 443, + "targetPort": 443 + }, + { + "name": "1936-tcp", + "protocol": "TCP", + "port": 1936, + "targetPort": 1936 + }, + { + "name": "5000-tcp", + "protocol": "TCP", + "port": 5000, + "targetPort": 5000 + } + ], + "selector": { + "router": "router" + }, + "clusterIP": "172.30.129.161", + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } + }''' + mock_cmd.side_effect = [ + (1, '', 'Error from server: services "router" not found'), + (1, '', 'Error from server: services "router" not found'), + (0, service, ''), + (0, service, '') + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + results = OCService.run_ansible(params, False) + + self.assertTrue(results['changed']) + self.assertTrue(results['results']['returncode'] == 0) + self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router') + self.assertEqual(results['results']['results'][0]['metadata']['labels'], {"component": "some_component", "infra": "true"}) + + @mock.patch('oc_service.Utils.create_tmpfile_copy') + @mock.patch('oc_service.OCService._run') + def test_create_with_external_ips(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a create service ''' + params = {'name': 'router', + 'namespace': 'default', + 'ports': {'name': '9000-tcp', + 'port': 9000, + 'protocol': 'TCP', + 'targetPOrt': 9000}, + 'state': 'present', + 'labels': {'component': 'some_component', 'infra': 'true'}, + 'clusterip': None, + 'portalip': None, + 'selector': {'router': 'router'}, + 'session_affinity': 'ClientIP', + 'service_type': 'ClusterIP', + 'external_ips': ['1.2.3.4', '5.6.7.8'], + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'debug': False} + + service = '''{ + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "router", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/services/router", + "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa", + "resourceVersion": "3206", + "creationTimestamp": "2017-01-26T15:06:14Z", + "labels": {"component": "some_component", "infra": "true"} + }, + "spec": { + "ports": [ + { + "name": "80-tcp", + "protocol": "TCP", + "port": 80, + "targetPort": 80 + }, + { + "name": "443-tcp", + "protocol": "TCP", + "port": 443, + "targetPort": 443 + }, + { + "name": "1936-tcp", + "protocol": "TCP", + "port": 1936, + "targetPort": 1936 + }, + { + "name": "5000-tcp", + "protocol": "TCP", + "port": 5000, + "targetPort": 5000 + } + ], + "selector": { + "router": "router" + }, + "clusterIP": "172.30.129.161", + "externalIPs": ["1.2.3.4", "5.6.7.8"], + "type": "ClusterIP", + "sessionAffinity": "None" + }, + "status": { + "loadBalancer": {} + } + }''' + mock_cmd.side_effect = [ + (1, '', 'Error from server: services "router" not found'), + (1, '', 'Error from server: services "router" not found'), + (0, service, ''), + (0, service, '') + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + results = OCService.run_ansible(params, False) + + self.assertTrue(results['changed']) + self.assertTrue(results['results']['returncode'] == 0) + self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router') + self.assertEqual(results['results']['results'][0]['metadata']['labels'], {"component": "some_component", "infra": "true"}) + self.assertEqual(results['results']['results'][0]['spec']['externalIPs'], ["1.2.3.4", "5.6.7.8"]) diff --git a/roles/lib_openshift/src/test/unit/test_oc_storageclass.py b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py new file mode 100755 index 000000000..4fd02a8b1 --- /dev/null +++ b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py @@ -0,0 +1,93 @@ +''' + Unit tests for oc serviceaccount +''' + +import os +import sys +import unittest +import mock + +# Removing invalid variable names for tests so that I can +# keep them brief +# pylint: disable=invalid-name,no-name-in-module +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 +sys.path.insert(0, module_path) +from oc_storageclass import OCStorageClass # noqa: E402 + + +class OCStorageClassTest(unittest.TestCase): + ''' + Test class for OCStorageClass + ''' + params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'state': 'present', + 'debug': False, + 'name': 'testsc', + 'provisioner': 'kubernetes.io/aws-ebs', + 'annotations': {'storageclass.beta.kubernetes.io/is-default-class': "true"}, + 'parameters': {'type': 'gp2'}, + 'api_version': 'v1', + 'default_storage_class': 'true'} + + @mock.patch('oc_storageclass.locate_oc_binary') + @mock.patch('oc_storageclass.Utils.create_tmpfile_copy') + @mock.patch('oc_storageclass.OCStorageClass._run') + def test_adding_a_storageclass(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary): + ''' Testing adding a storageclass ''' + + # Arrange + + # run_ansible input parameters + + valid_result_json = '''{ + "kind": "StorageClass", + "apiVersion": "v1", + "metadata": { + "name": "testsc", + "selfLink": "/apis/storage.k8s.io/v1/storageclasses/gp2", + "uid": "4d8320c9-e66f-11e6-8edc-0eece8f2ce22", + "resourceVersion": "2828", + "creationTimestamp": "2017-01-29T22:07:19Z", + "annotations": {"storageclass.beta.kubernetes.io/is-default-class": "true"} + }, + "provisioner": "kubernetes.io/aws-ebs", + "parameters": {"type": "gp2"} + }''' + + # Return values of our mocked function call. These get returned once per call. + mock_cmd.side_effect = [ + # First call to mock + (1, '', 'Error from server: storageclass "testsc" not found'), + + # Second call to mock + (0, 'storageclass "testsc" created', ''), + + # Third call to mock + (0, valid_result_json, ''), + ] + + mock_oc_binary.side_effect = [ + 'oc' + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + # Act + results = OCStorageClass.run_ansible(OCStorageClassTest.params, False) + + # Assert + self.assertTrue(results['changed']) + self.assertEqual(results['results']['returncode'], 0) + self.assertEqual(results['state'], 'present') + + # Making sure our mock was called as we expected + mock_cmd.assert_has_calls([ + mock.call(['oc', 'get', 'storageclass', 'testsc', '-o', 'json'], None), + mock.call(['oc', 'create', '-f', mock.ANY], None), + mock.call(['oc', 'get', 'storageclass', 'testsc', '-o', 'json'], None), + ]) |