summaryrefslogtreecommitdiffstats
path: root/roles/lib_openshift/library
diff options
context:
space:
mode:
Diffstat (limited to 'roles/lib_openshift/library')
-rw-r--r--roles/lib_openshift/library/oc_adm_ca_server_cert.py368
-rw-r--r--roles/lib_openshift/library/oc_adm_csr.py1649
-rw-r--r--roles/lib_openshift/library/oc_adm_manage_node.py (renamed from roles/lib_openshift/library/oadm_manage_node.py)360
-rw-r--r--roles/lib_openshift/library/oc_adm_policy_group.py2177
-rw-r--r--roles/lib_openshift/library/oc_adm_policy_user.py2171
-rw-r--r--roles/lib_openshift/library/oc_adm_registry.py478
-rw-r--r--roles/lib_openshift/library/oc_adm_router.py418
-rw-r--r--roles/lib_openshift/library/oc_atomic_container.py21
-rw-r--r--roles/lib_openshift/library/oc_clusterrole.py1814
-rw-r--r--roles/lib_openshift/library/oc_configmap.py1631
-rw-r--r--roles/lib_openshift/library/oc_edit.py344
-rw-r--r--roles/lib_openshift/library/oc_env.py344
-rw-r--r--roles/lib_openshift/library/oc_group.py1610
-rw-r--r--roles/lib_openshift/library/oc_image.py1536
-rw-r--r--roles/lib_openshift/library/oc_label.py348
-rw-r--r--roles/lib_openshift/library/oc_obj.py409
-rw-r--r--roles/lib_openshift/library/oc_objectvalidator.py (renamed from roles/lib_openshift/library/oc_sdnvalidator.py)426
-rw-r--r--roles/lib_openshift/library/oc_process.py358
-rw-r--r--roles/lib_openshift/library/oc_project.py1732
-rw-r--r--roles/lib_openshift/library/oc_pvc.py1847
-rw-r--r--roles/lib_openshift/library/oc_route.py351
-rw-r--r--roles/lib_openshift/library/oc_scale.py344
-rw-r--r--roles/lib_openshift/library/oc_secret.py377
-rw-r--r--roles/lib_openshift/library/oc_service.py418
-rw-r--r--roles/lib_openshift/library/oc_serviceaccount.py344
-rw-r--r--roles/lib_openshift/library/oc_serviceaccount_secret.py344
-rw-r--r--roles/lib_openshift/library/oc_storageclass.py1684
-rw-r--r--roles/lib_openshift/library/oc_user.py1764
-rw-r--r--roles/lib_openshift/library/oc_version.py344
-rw-r--r--roles/lib_openshift/library/oc_volume.py2086
30 files changed, 25542 insertions, 2555 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 9390ea7c1..45d7444a4 100644
--- a/roles/lib_openshift/library/oc_adm_ca_server_cert.py
+++ b/roles/lib_openshift/library/oc_adm_ca_server_cert.py
@@ -130,6 +130,12 @@ options:
required: false
default: True
aliases: []
+ expire_days:
+ description
+ - Validity of the certificate in days
+ required: false
+ default: None
+ aliases: []
author:
- "Kenny Woodson <kwoodson@redhat.com>"
extends_documentation_fragment: []
@@ -149,20 +155,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/ca_server_cert -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -184,13 +188,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -206,13 +210,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -234,7 +238,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -275,7 +279,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -284,7 +289,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -298,6 +303,12 @@ class Yedit(object):
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
@@ -316,7 +327,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -416,7 +427,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -535,8 +546,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -597,7 +608,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -623,7 +644,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -655,114 +676,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -844,6 +900,13 @@ class OpenShiftCLI(object):
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')
@@ -863,11 +926,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -885,7 +952,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -902,13 +969,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -928,9 +995,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -945,10 +1012,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -961,16 +1028,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1013,7 +1080,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1030,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))
@@ -1043,39 +1106,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1233,13 +1288,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
@@ -1338,7 +1392,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1352,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1447,7 +1511,10 @@ class CAServerCert(OpenShiftCLI):
x509output, _ = proc.communicate()
if proc.returncode == 0:
regex = re.compile(r"^\s*X509v3 Subject Alternative Name:\s*?\n\s*(.*)\s*\n", re.MULTILINE)
- match = regex.search(x509output) # E501
+ match = regex.search(x509output.decode()) # E501
+ if not match:
+ return False
+
for entry in re.split(r", *", match.group(1)):
if entry.startswith('DNS') or entry.startswith('IP Address'):
cert_names.append(entry.split(':')[1])
@@ -1463,6 +1530,10 @@ class CAServerCert(OpenShiftCLI):
def run_ansible(params, check_mode):
'''run the idempotent ansible code'''
+ # Filter non-strings from hostnames list s.t. the omit filter
+ # may be used to conditionally add a hostname.
+ params['hostnames'] = [host for host in params['hostnames'] if isinstance(host, string_types)]
+
config = CAServerCertConfig(params['kubeconfig'],
params['debug'],
{'cert': {'value': params['cert'], 'include': True},
@@ -1472,6 +1543,7 @@ class CAServerCert(OpenShiftCLI):
'signer_cert': {'value': params['signer_cert'], 'include': True},
'signer_key': {'value': params['signer_key'], 'include': True},
'signer_serial': {'value': params['signer_serial'], 'include': True},
+ 'expire_days': {'value': params['expire_days'], 'include': True},
'backup': {'value': params['backup'], 'include': False},
})
@@ -1492,6 +1564,9 @@ class CAServerCert(OpenShiftCLI):
api_rval = server_cert.create()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
return {'changed': True, 'results': api_rval, 'state': state}
########
@@ -1508,6 +1583,10 @@ class CAServerCert(OpenShiftCLI):
# -*- -*- -*- Begin included fragment: ansible/oc_adm_ca_server_cert.py -*- -*- -*-
+
+# pylint: disable=wrong-import-position
+from ansible.module_utils.six import string_types
+
def main():
'''
ansible oc adm module for ca create-server-cert
@@ -1527,6 +1606,7 @@ def main():
signer_key=dict(default='/etc/origin/master/ca.key', type='str'),
signer_serial=dict(default='/etc/origin/master/ca.serial.txt', type='str'),
hostnames=dict(default=[], type='list'),
+ expire_days=dict(default=None, type='int'),
),
supports_check_mode=True,
)
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..231857cca
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_csr.py
@@ -0,0 +1,1649 @@
+#!/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']) or {}
+
+ 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'] 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 match_node(self, csr):
+ '''match an inc csr to a node in self.nodes'''
+ for node in self.nodes:
+ # we have a match
+ if node['name'] in csr['metadata']['name']:
+ 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/oadm_manage_node.py b/roles/lib_openshift/library/oc_adm_manage_node.py
index 0c29c1b91..44f3f57d8 100644
--- a/roles/lib_openshift/library/oadm_manage_node.py
+++ b/roles/lib_openshift/library/oc_adm_manage_node.py
@@ -54,7 +54,7 @@ from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = '''
---
-module: oadm_manage_node
+module: oc_adm_manage_node
short_description: Module to manage openshift nodes
description:
- Manage openshift nodes programmatically.
@@ -126,13 +126,13 @@ extends_documentation_fragment: []
EXAMPLES = '''
- name: oadm manage-node --schedulable=true --selector=ops_node=new
- oadm_manage_node:
+ oc_adm_manage_node:
selector: ops_node=new
schedulable: True
register: schedout
- name: oadm manage-node my-k8s-node-5 --evacuate
- oadm_manage_node:
+ oc_adm_manage_node:
node: my-k8s-node-5
evacuate: True
force: True
@@ -141,20 +141,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/manage_node -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -176,13 +174,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -198,13 +196,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -226,7 +224,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -267,7 +265,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -276,7 +275,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -290,6 +289,12 @@ class Yedit(object):
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
@@ -308,7 +313,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -408,7 +413,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -527,8 +532,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -589,7 +594,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -615,7 +630,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -647,114 +662,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -836,6 +886,13 @@ class OpenShiftCLI(object):
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')
@@ -855,11 +912,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -877,7 +938,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -894,13 +955,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -920,9 +981,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -937,10 +998,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -953,16 +1014,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1005,7 +1066,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1022,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))
@@ -1035,39 +1092,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1225,13 +1274,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
@@ -1330,7 +1378,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1344,24 +1391,35 @@ 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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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/oadm_manage_node.py -*- -*- -*-
+# -*- -*- -*- Begin included fragment: class/oc_adm_manage_node.py -*- -*- -*-
class ManageNodeException(Exception):
@@ -1406,7 +1464,7 @@ class ManageNode(OpenShiftCLI):
if selector:
_sel = selector
- results = self._get('node', rname=_node, selector=_sel)
+ results = self._get('node', name=_node, selector=_sel)
if results['returncode'] != 0:
return results
@@ -1570,9 +1628,9 @@ class ManageNode(OpenShiftCLI):
return {'changed': changed, 'results': results, 'state': "present"}
-# -*- -*- -*- End included fragment: class/oadm_manage_node.py -*- -*- -*-
+# -*- -*- -*- End included fragment: class/oc_adm_manage_node.py -*- -*- -*-
-# -*- -*- -*- Begin included fragment: ansible/oadm_manage_node.py -*- -*- -*-
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_manage_node.py -*- -*- -*-
def main():
@@ -1610,4 +1668,4 @@ def main():
if __name__ == "__main__":
main()
-# -*- -*- -*- End included fragment: ansible/oadm_manage_node.py -*- -*- -*-
+# -*- -*- -*- End included fragment: ansible/oc_adm_manage_node.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_policy_group.py b/roles/lib_openshift/library/oc_adm_policy_group.py
new file mode 100644
index 000000000..687cff579
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_policy_group.py
@@ -0,0 +1,2177 @@
+#!/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/policy_group -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_group
+short_description: Module to manage openshift policy for groups
+description:
+ - Manage openshift policy for groups.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ group:
+ description:
+ - The name of the group
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-group an-scc agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-group system:build-strategy-docker agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
+
+# -*- -*- -*- End included fragment: doc/policy_group -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/rolebinding.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
+
+# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/scc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class SecurityContextConstraintsConfig(object):
+ ''' Handle scc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ kubeconfig,
+ options=None,
+ fs_group='MustRunAs',
+ default_add_capabilities=None,
+ groups=None,
+ priority=None,
+ required_drop_capabilities=None,
+ run_as_user='MustRunAsRange',
+ se_linux_context='MustRunAs',
+ supplemental_groups='RunAsAny',
+ users=None,
+ annotations=None):
+ ''' constructor for handling scc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.options = options
+ self.fs_group = fs_group
+ self.default_add_capabilities = default_add_capabilities
+ self.groups = groups
+ self.priority = priority
+ self.required_drop_capabilities = required_drop_capabilities
+ self.run_as_user = run_as_user
+ self.se_linux_context = se_linux_context
+ self.supplemental_groups = supplemental_groups
+ self.users = users
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a scc dict '''
+ # allow options
+ if self.options:
+ for key, value in self.options.items():
+ self.data[key] = value
+ else:
+ self.data['allowHostDirVolumePlugin'] = False
+ self.data['allowHostIPC'] = False
+ self.data['allowHostNetwork'] = False
+ self.data['allowHostPID'] = False
+ self.data['allowHostPorts'] = False
+ self.data['allowPrivilegedContainer'] = False
+ self.data['allowedCapabilities'] = None
+
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'SecurityContextConstraints'
+ # defaultAddCapabilities
+ self.data['defaultAddCapabilities'] = self.default_add_capabilities
+ # fsGroup
+ self.data['fsGroup']['type'] = self.fs_group
+ # groups
+ self.data['groups'] = []
+ if self.groups:
+ self.data['groups'] = self.groups
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ if self.annotations:
+ for key, value in self.annotations.items():
+ self.data['metadata'][key] = value
+ # priority
+ self.data['priority'] = self.priority
+ # requiredDropCapabilities
+ self.data['requiredDropCapabilities'] = self.required_drop_capabilities
+ # runAsUser
+ self.data['runAsUser'] = {'type': self.run_as_user}
+ # seLinuxContext
+ self.data['seLinuxContext'] = {'type': self.se_linux_context}
+ # supplementalGroups
+ self.data['supplementalGroups'] = {'type': self.supplemental_groups}
+ # users
+ self.data['users'] = []
+ if self.users:
+ self.data['users'] = self.users
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods,no-member
+class SecurityContextConstraints(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ default_add_capabilities_path = "defaultAddCapabilities"
+ fs_group_path = "fsGroup"
+ groups_path = "groups"
+ priority_path = "priority"
+ required_drop_capabilities_path = "requiredDropCapabilities"
+ run_as_user_path = "runAsUser"
+ se_linux_context_path = "seLinuxContext"
+ supplemental_groups_path = "supplementalGroups"
+ users_path = "users"
+ kind = 'SecurityContextConstraints'
+
+ def __init__(self, content):
+ '''SecurityContextConstraints constructor'''
+ super(SecurityContextConstraints, self).__init__(content=content)
+ self._users = None
+ self._groups = None
+
+ @property
+ def users(self):
+ ''' users property getter '''
+ if self._users is None:
+ self._users = self.get_users()
+ return self._users
+
+ @property
+ def groups(self):
+ ''' groups property getter '''
+ if self._groups is None:
+ self._groups = self.get_groups()
+ return self._groups
+
+ @users.setter
+ def users(self, data):
+ ''' users property setter'''
+ self._users = data
+
+ @groups.setter
+ def groups(self, data):
+ ''' groups property setter'''
+ self._groups = data
+
+ def get_users(self):
+ '''get scc users'''
+ return self.get(SecurityContextConstraints.users_path) or []
+
+ def get_groups(self):
+ '''get scc groups'''
+ return self.get(SecurityContextConstraints.groups_path) or []
+
+ def add_user(self, inc_user):
+ ''' add a user '''
+ if self.users:
+ self.users.append(inc_user)
+ else:
+ self.put(SecurityContextConstraints.users_path, [inc_user])
+
+ return True
+
+ def add_group(self, inc_group):
+ ''' add a group '''
+ if self.groups:
+ self.groups.append(inc_group)
+ else:
+ self.put(SecurityContextConstraints.groups_path, [inc_group])
+
+ return True
+
+ def remove_user(self, inc_user):
+ ''' remove a user '''
+ try:
+ self.users.remove(inc_user)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_group(self, inc_group):
+ ''' remove a group '''
+ try:
+ self.groups.remove(inc_group)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def update_user(self, inc_user):
+ ''' update a user '''
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return self.add_user(inc_user)
+
+ self.users[index] = inc_user
+
+ return True
+
+ def update_group(self, inc_group):
+ ''' update a group '''
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return self.add_group(inc_group)
+
+ self.groups[index] = inc_group
+
+ return True
+
+ def find_user(self, inc_user):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group(self, inc_group):
+ ''' find a group '''
+ index = None
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/scc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_policy_group.py -*- -*- -*-
+
+
+class PolicyGroupException(Exception):
+ ''' PolicyGroup exception'''
+ pass
+
+
+class PolicyGroupConfig(OpenShiftCLIConfig):
+ ''' PolicyGroupConfig is a DTO for group related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyGroupConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyGroup(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyGroup '''
+ super(PolicyGroup, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding getter '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' role_binding setter '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint getter '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' security_context_constraint setter '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind'''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ # oc adm policy add-... creates policy bindings with the name
+ # "[resource_name]-binding", however some bindings in the system
+ # simply use "[resource_name]". So try both.
+
+ results = self._get(self.config.kind, resource_name)
+ if results['returncode'] == 0:
+ return results
+
+ # Now try -binding naming convention
+ return self._get(self.config.kind, resource_name + "-binding")
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ 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
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_group(self.config.config_options['group']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['group']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-group'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-group'
+
+ nconfig = PolicyGroupConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'group': {'value': params['group'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policygroup = PolicyGroup(nconfig, params['debug'])
+
+ # Run the oc adm policy group related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policygroup.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policygroup.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_adm_policy_group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_policy_group.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc adm module for group policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ group=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyGroup.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_policy_group.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_policy_user.py b/roles/lib_openshift/library/oc_adm_policy_user.py
new file mode 100644
index 000000000..ddf5d90b7
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_policy_user.py
@@ -0,0 +1,2171 @@
+#!/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/policy_user -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_user
+short_description: Module to manage openshift policy for users
+description:
+ - Manage openshift policy for users.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ user:
+ description:
+ - The name of the user
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-user an-scc ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-user system:build-strategy-docker ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
+
+# -*- -*- -*- End included fragment: doc/policy_user -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/rolebinding.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
+
+# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/scc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class SecurityContextConstraintsConfig(object):
+ ''' Handle scc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ kubeconfig,
+ options=None,
+ fs_group='MustRunAs',
+ default_add_capabilities=None,
+ groups=None,
+ priority=None,
+ required_drop_capabilities=None,
+ run_as_user='MustRunAsRange',
+ se_linux_context='MustRunAs',
+ supplemental_groups='RunAsAny',
+ users=None,
+ annotations=None):
+ ''' constructor for handling scc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.options = options
+ self.fs_group = fs_group
+ self.default_add_capabilities = default_add_capabilities
+ self.groups = groups
+ self.priority = priority
+ self.required_drop_capabilities = required_drop_capabilities
+ self.run_as_user = run_as_user
+ self.se_linux_context = se_linux_context
+ self.supplemental_groups = supplemental_groups
+ self.users = users
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a scc dict '''
+ # allow options
+ if self.options:
+ for key, value in self.options.items():
+ self.data[key] = value
+ else:
+ self.data['allowHostDirVolumePlugin'] = False
+ self.data['allowHostIPC'] = False
+ self.data['allowHostNetwork'] = False
+ self.data['allowHostPID'] = False
+ self.data['allowHostPorts'] = False
+ self.data['allowPrivilegedContainer'] = False
+ self.data['allowedCapabilities'] = None
+
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'SecurityContextConstraints'
+ # defaultAddCapabilities
+ self.data['defaultAddCapabilities'] = self.default_add_capabilities
+ # fsGroup
+ self.data['fsGroup']['type'] = self.fs_group
+ # groups
+ self.data['groups'] = []
+ if self.groups:
+ self.data['groups'] = self.groups
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ if self.annotations:
+ for key, value in self.annotations.items():
+ self.data['metadata'][key] = value
+ # priority
+ self.data['priority'] = self.priority
+ # requiredDropCapabilities
+ self.data['requiredDropCapabilities'] = self.required_drop_capabilities
+ # runAsUser
+ self.data['runAsUser'] = {'type': self.run_as_user}
+ # seLinuxContext
+ self.data['seLinuxContext'] = {'type': self.se_linux_context}
+ # supplementalGroups
+ self.data['supplementalGroups'] = {'type': self.supplemental_groups}
+ # users
+ self.data['users'] = []
+ if self.users:
+ self.data['users'] = self.users
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods,no-member
+class SecurityContextConstraints(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ default_add_capabilities_path = "defaultAddCapabilities"
+ fs_group_path = "fsGroup"
+ groups_path = "groups"
+ priority_path = "priority"
+ required_drop_capabilities_path = "requiredDropCapabilities"
+ run_as_user_path = "runAsUser"
+ se_linux_context_path = "seLinuxContext"
+ supplemental_groups_path = "supplementalGroups"
+ users_path = "users"
+ kind = 'SecurityContextConstraints'
+
+ def __init__(self, content):
+ '''SecurityContextConstraints constructor'''
+ super(SecurityContextConstraints, self).__init__(content=content)
+ self._users = None
+ self._groups = None
+
+ @property
+ def users(self):
+ ''' users property getter '''
+ if self._users is None:
+ self._users = self.get_users()
+ return self._users
+
+ @property
+ def groups(self):
+ ''' groups property getter '''
+ if self._groups is None:
+ self._groups = self.get_groups()
+ return self._groups
+
+ @users.setter
+ def users(self, data):
+ ''' users property setter'''
+ self._users = data
+
+ @groups.setter
+ def groups(self, data):
+ ''' groups property setter'''
+ self._groups = data
+
+ def get_users(self):
+ '''get scc users'''
+ return self.get(SecurityContextConstraints.users_path) or []
+
+ def get_groups(self):
+ '''get scc groups'''
+ return self.get(SecurityContextConstraints.groups_path) or []
+
+ def add_user(self, inc_user):
+ ''' add a user '''
+ if self.users:
+ self.users.append(inc_user)
+ else:
+ self.put(SecurityContextConstraints.users_path, [inc_user])
+
+ return True
+
+ def add_group(self, inc_group):
+ ''' add a group '''
+ if self.groups:
+ self.groups.append(inc_group)
+ else:
+ self.put(SecurityContextConstraints.groups_path, [inc_group])
+
+ return True
+
+ def remove_user(self, inc_user):
+ ''' remove a user '''
+ try:
+ self.users.remove(inc_user)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_group(self, inc_group):
+ ''' remove a group '''
+ try:
+ self.groups.remove(inc_group)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def update_user(self, inc_user):
+ ''' update a user '''
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return self.add_user(inc_user)
+
+ self.users[index] = inc_user
+
+ return True
+
+ def update_group(self, inc_group):
+ ''' update a group '''
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return self.add_group(inc_group)
+
+ self.groups[index] = inc_group
+
+ return True
+
+ def find_user(self, inc_user):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group(self, inc_group):
+ ''' find a group '''
+ index = None
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/scc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_policy_user.py -*- -*- -*-
+
+
+class PolicyUserException(Exception):
+ ''' PolicyUser exception'''
+ pass
+
+
+class PolicyUserConfig(OpenShiftCLIConfig):
+ ''' PolicyUserConfig is a DTO for user related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyUserConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyUser(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyUser '''
+ super(PolicyUser, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding property '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' setter for role_binding property '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint property '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' setter for security_context_constraint property '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind
+
+ This is only used for scc objects.
+ The {cluster}rolebindings happen in exists.
+ '''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ return self._get(self.config.kind, resource_name)
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ 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
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_user(self.config.config_options['user']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['user']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-user'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-user'
+
+ nconfig = PolicyUserConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'user': {'value': params['user'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policyuser = PolicyUser(nconfig, params['debug'])
+
+ # Run the oc adm policy user related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policyuser.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policyuser.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_adm_policy_user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_policy_user.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc adm module for user policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ user=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_policy_user.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_registry.py b/roles/lib_openshift/library/oc_adm_registry.py
index 62018d758..c00eee381 100644
--- a/roles/lib_openshift/library/oc_adm_registry.py
+++ b/roles/lib_openshift/library/oc_adm_registry.py
@@ -245,20 +245,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/registry -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -280,13 +278,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -302,13 +300,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -330,7 +328,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -371,7 +369,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -380,7 +379,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -394,6 +393,12 @@ class Yedit(object):
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
@@ -412,7 +417,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -512,7 +517,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -631,8 +636,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -693,7 +698,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -719,7 +734,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -751,114 +766,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -940,6 +990,13 @@ class OpenShiftCLI(object):
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')
@@ -959,11 +1016,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -981,7 +1042,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -998,13 +1059,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -1024,9 +1085,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -1041,10 +1102,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -1057,16 +1118,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1109,7 +1170,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1126,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))
@@ -1139,39 +1196,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1329,13 +1378,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
@@ -1434,7 +1482,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1448,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1827,10 +1885,12 @@ class SecretConfig(object):
sname,
namespace,
kubeconfig,
- secrets=None):
+ secrets=None,
+ stype=None):
''' constructor for handling secret options '''
self.kubeconfig = kubeconfig
self.name = sname
+ self.type = stype
self.namespace = namespace
self.secrets = secrets
self.data = {}
@@ -1841,6 +1901,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
@@ -1933,7 +1994,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
@@ -1944,6 +2006,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()
@@ -1956,8 +2019,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:
@@ -1979,6 +2043,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 '''
@@ -1987,6 +2055,7 @@ class Service(Yedit):
cluster_ip = "spec.clusterIP"
selector_path = 'spec.selector'
kind = 'Service'
+ external_ips = "spec.externalIPs"
def __init__(self, content):
'''Service constructor'''
@@ -2048,12 +2117,59 @@ 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 -*- -*- -*-
class Volume(object):
- ''' Class to model an openshift volume object'''
+ ''' Class to represent an openshift volume object'''
volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
"dc": "spec.template.spec.containers[0].volumeMounts",
"rc": "spec.template.spec.containers[0].volumeMounts",
@@ -2085,6 +2201,11 @@ class Volume(object):
elif volume_type == 'hostpath':
volume['hostPath'] = {}
volume['hostPath']['path'] = volume_info['path']
+ elif volume_type == 'configmap':
+ volume['configMap'] = {}
+ volume['configMap']['name'] = volume_info['configmap_name']
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
return (volume, volume_mount)
@@ -2227,8 +2348,8 @@ class Registry(OpenShiftCLI):
''' prepared_registry property '''
if not self.__prepared_registry:
results = self.prepare_registry()
- if not results:
- raise RegistryException('Could not perform registry preparation.')
+ if not results or ('returncode' in results and results['returncode'] != 0):
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
self.__prepared_registry = results
return self.__prepared_registry
@@ -2245,7 +2366,7 @@ class Registry(OpenShiftCLI):
rval = 0
for part in self.registry_parts:
- result = self._get(part['kind'], rname=part['name'])
+ result = self._get(part['kind'], name=part['name'])
if result['returncode'] == 0 and part['kind'] == 'dc':
self.deploymentconfig = DeploymentConfig(result['results'][0])
elif result['returncode'] == 0 and part['kind'] == 'svc':
@@ -2259,7 +2380,6 @@ class Registry(OpenShiftCLI):
def exists(self):
'''does the object exist?'''
- self.get()
if self.deploymentconfig and self.service:
return True
@@ -2284,9 +2404,9 @@ 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', '-n', self.config.namespace]
+ cmd = ['registry']
cmd.extend(options)
cmd.extend(['--dry-run=True', '-o', 'json'])
@@ -2294,8 +2414,8 @@ class Registry(OpenShiftCLI):
# probably need to parse this
# pylint thinks results is a string
# pylint: disable=no-member
- if results['returncode'] != 0 and 'items' in results['results']:
- return results
+ if results['returncode'] != 0 and 'items' not in results['results']:
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
service = None
deploymentconfig = None
@@ -2320,7 +2440,8 @@ class Registry(OpenShiftCLI):
service.put('spec.portalIP', self.portal_ip)
# the dry-run doesn't apply the selector correctly
- service.put('spec.selector', self.service.get_selector())
+ if self.service:
+ service.put('spec.selector', self.service.get_selector())
# need to create the service and the deploymentconfig
service_file = Utils.create_tmp_file_from_contents('service', service.yaml_dict)
@@ -2471,25 +2592,34 @@ class Registry(OpenShiftCLI):
def run_ansible(params, check_mode):
'''run idempotent ansible code'''
+ registry_options = {'images': {'value': params['images'], 'include': True},
+ 'latest_images': {'value': params['latest_images'], 'include': True},
+ 'labels': {'value': params['labels'], 'include': True},
+ 'ports': {'value': ','.join(params['ports']), 'include': True},
+ 'replicas': {'value': params['replicas'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'service_account': {'value': params['service_account'], 'include': True},
+ 'mount_host': {'value': params['mount_host'], 'include': True},
+ 'env_vars': {'value': params['env_vars'], 'include': False},
+ 'volume_mounts': {'value': params['volume_mounts'], 'include': False},
+ 'edits': {'value': params['edits'], 'include': False},
+ 'tls_key': {'value': params['tls_key'], 'include': True},
+ 'tls_certificate': {'value': params['tls_certificate'], 'include': True},
+ }
+
+ # Do not always pass the daemonset and enforce-quota parameters because they are not understood
+ # by old versions of oc.
+ # Default value is false. So, it's safe to not pass an explicit false value to oc versions which
+ # understand these parameters.
+ if params['daemonset']:
+ registry_options['daemonset'] = {'value': params['daemonset'], 'include': True}
+ if params['enforce_quota']:
+ registry_options['enforce_quota'] = {'value': params['enforce_quota'], 'include': True}
+
rconfig = RegistryConfig(params['name'],
params['namespace'],
params['kubeconfig'],
- {'images': {'value': params['images'], 'include': True},
- 'latest_images': {'value': params['latest_images'], 'include': True},
- 'labels': {'value': params['labels'], 'include': True},
- 'ports': {'value': ','.join(params['ports']), 'include': True},
- 'replicas': {'value': params['replicas'], 'include': True},
- 'selector': {'value': params['selector'], 'include': True},
- 'service_account': {'value': params['service_account'], 'include': True},
- 'mount_host': {'value': params['mount_host'], 'include': True},
- 'env_vars': {'value': params['env_vars'], 'include': False},
- 'volume_mounts': {'value': params['volume_mounts'], 'include': False},
- 'edits': {'value': params['edits'], 'include': False},
- 'enforce_quota': {'value': params['enforce_quota'], 'include': True},
- 'daemonset': {'value': params['daemonset'], 'include': True},
- 'tls_key': {'value': params['tls_key'], 'include': True},
- 'tls_certificate': {'value': params['tls_certificate'], 'include': True},
- })
+ registry_options)
ocregistry = Registry(rconfig, params['debug'])
@@ -2580,7 +2710,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 bb4ce5e70..0c925ab0b 100644
--- a/roles/lib_openshift/library/oc_adm_router.py
+++ b/roles/lib_openshift/library/oc_adm_router.py
@@ -270,20 +270,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/router -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -305,13 +303,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -327,13 +325,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -355,7 +353,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -396,7 +394,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -405,7 +404,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -419,6 +418,12 @@ class Yedit(object):
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
@@ -437,7 +442,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -537,7 +542,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -656,8 +661,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -718,7 +723,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -744,7 +759,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -776,114 +791,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ 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 rval[0] and module.params['src']:
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
yamlfile.write()
- return {'changed': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -965,6 +1015,13 @@ class OpenShiftCLI(object):
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')
@@ -984,11 +1041,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -1006,7 +1067,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -1023,13 +1084,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -1049,9 +1110,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -1066,10 +1127,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -1082,16 +1143,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1134,7 +1195,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1151,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))
@@ -1164,39 +1221,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1354,13 +1403,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
@@ -1459,7 +1507,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1473,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1506,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
@@ -1517,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()
@@ -1529,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:
@@ -1552,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 '''
@@ -1560,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'''
@@ -1621,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 -*- -*- -*-
@@ -2116,10 +2229,12 @@ class SecretConfig(object):
sname,
namespace,
kubeconfig,
- secrets=None):
+ secrets=None,
+ stype=None):
''' constructor for handling secret options '''
self.kubeconfig = kubeconfig
self.name = sname
+ self.type = stype
self.namespace = namespace
self.secrets = secrets
self.data = {}
@@ -2130,6 +2245,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
@@ -2634,7 +2750,7 @@ class Router(OpenShiftCLI):
self.secret = None
self.rolebinding = None
for part in self.router_parts:
- result = self._get(part['kind'], rname=part['name'])
+ result = self._get(part['kind'], name=part['name'])
if result['returncode'] == 0 and part['kind'] == 'dc':
self.deploymentconfig = DeploymentConfig(result['results'][0])
elif result['returncode'] == 0 and part['kind'] == 'svc':
@@ -2720,9 +2836,9 @@ 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, '-n', self.config.namespace]
+ cmd = ['router', self.config.name]
cmd.extend(options)
cmd.extend(['--dry-run=True', '-o', 'json'])
@@ -3021,7 +3137,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 ac48c8c70..79bd08f4e 100644
--- a/roles/lib_openshift/library/oc_atomic_container.py
+++ b/roles/lib_openshift/library/oc_atomic_container.py
@@ -65,15 +65,20 @@ 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
@@ -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
new file mode 100644
index 000000000..567ecfd4e
--- /dev/null
+++ b/roles/lib_openshift/library/oc_clusterrole.py
@@ -0,0 +1,1814 @@
+#!/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/clusterrole -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_clusterrole
+short_description: Modify, and idempotently manage openshift clusterroles
+description:
+ - Manage openshift clusterroles
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a clusterrole
+ - absent - will remove a clusterrole
+ 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: []
+ rules:
+ description:
+ - A list of dictionaries that have the rule parameters.
+ - e.g. rules=[{'apiGroups': [""], 'attributeRestrictions': None, 'verbs': ['get'], 'resources': []}]
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: query a list of env vars on dc
+ oc_clusterrole:
+ name: myclusterrole
+ state: list
+
+- name: Set the following variables.
+ oc_clusterrole:
+ name: myclusterrole
+ rules:
+ apiGroups:
+ - ""
+ attributeRestrictions: null
+ verbs: []
+ resources: []
+'''
+
+# -*- -*- -*- End included fragment: doc/clusterrole -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/rule.py -*- -*- -*-
+
+
+class Rule(object):
+ '''class to represent a clusterrole rule
+
+ Example Rule Object's yaml:
+ - apiGroups:
+ - ""
+ attributeRestrictions: null
+ resources:
+ - persistentvolumes
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+
+ '''
+ def __init__(self,
+ api_groups=None,
+ attr_restrictions=None,
+ resources=None,
+ verbs=None):
+ self.__api_groups = api_groups if api_groups is not None else [""]
+ self.__verbs = verbs if verbs is not None else []
+ self.__resources = resources if resources is not None else []
+ self.__attribute_restrictions = attr_restrictions if attr_restrictions is not None else None
+
+ @property
+ def verbs(self):
+ '''property for verbs'''
+ if self.__verbs is None:
+ return []
+
+ return self.__verbs
+
+ @verbs.setter
+ def verbs(self, data):
+ '''setter for verbs'''
+ self.__verbs = data
+
+ @property
+ def api_groups(self):
+ '''property for api_groups'''
+ if self.__api_groups is None:
+ return []
+ return self.__api_groups
+
+ @api_groups.setter
+ def api_groups(self, data):
+ '''setter for api_groups'''
+ self.__api_groups = data
+
+ @property
+ def resources(self):
+ '''property for resources'''
+ if self.__resources is None:
+ return []
+
+ return self.__resources
+
+ @resources.setter
+ def resources(self, data):
+ '''setter for resources'''
+ self.__resources = data
+
+ @property
+ def attribute_restrictions(self):
+ '''property for attribute_restrictions'''
+ return self.__attribute_restrictions
+
+ @attribute_restrictions.setter
+ def attribute_restrictions(self, data):
+ '''setter for attribute_restrictions'''
+ self.__attribute_restrictions = data
+
+ def add_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ self.verbs.append(inc_verb)
+
+ def add_api_group(self, inc_apigroup):
+ '''add an api_group to the api_groups array'''
+ self.api_groups.append(inc_apigroup)
+
+ def add_resource(self, inc_resource):
+ '''add an resource to the resources array'''
+ self.resources.append(inc_resource)
+
+ def remove_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ try:
+ self.verbs.remove(inc_verb)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_api_group(self, inc_api_group):
+ '''add a verb to the verbs array'''
+ try:
+ self.api_groups.remove(inc_api_group)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_resource(self, inc_resource):
+ '''add a verb to the verbs array'''
+ try:
+ self.resources.remove(inc_resource)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def __eq__(self, other):
+ '''return whether rules are equal'''
+ return (self.attribute_restrictions == other.attribute_restrictions and
+ self.api_groups == other.api_groups and
+ self.resources == other.resources and
+ self.verbs == other.verbs)
+
+
+ @staticmethod
+ def parse_rules(inc_rules):
+ '''create rules from an array'''
+
+ results = []
+ for rule in inc_rules:
+ results.append(Rule(rule.get('apiGroups', ['']),
+ rule.get('attributeRestrictions', None),
+ rule.get('resources', []),
+ rule.get('verbs', [])))
+
+ return results
+
+# -*- -*- -*- End included fragment: lib/rule.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/clusterrole.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class ClusterRole(Yedit):
+ ''' Class to model an openshift ClusterRole'''
+ rules_path = "rules"
+
+ def __init__(self, name=None, content=None):
+ ''' Constructor for clusterrole '''
+ if content is None:
+ content = ClusterRole.builder(name).yaml_dict
+
+ super(ClusterRole, self).__init__(content=content)
+
+ self.__rules = Rule.parse_rules(self.get(ClusterRole.rules_path)) or []
+
+ @property
+ def rules(self):
+ return self.__rules
+
+ @rules.setter
+ def rules(self, data):
+ self.__rules = data
+ self.put(ClusterRole.rules_path, self.__rules)
+
+ def rule_exists(self, inc_rule):
+ '''attempt to find the inc_rule in the rules list'''
+ for rule in self.rules:
+ if rule == inc_rule:
+ return True
+
+ return False
+
+ def compare(self, other, verbose=False):
+ '''compare function for clusterrole'''
+ for rule in other.rules:
+ if rule not in self.rules:
+ if verbose:
+ print('Rule in other not found in self. [{}]'.format(rule))
+ return False
+
+ for rule in self.rules:
+ if rule not in other.rules:
+ if verbose:
+ print('Rule in self not found in other. [{}]'.format(rule))
+ return False
+
+ return True
+
+ @staticmethod
+ def builder(name='default_clusterrole', rules=None):
+ '''return a clusterrole with name and/or rules'''
+ if rules is None:
+ rules = [{'apiGroups': [""],
+ 'attributeRestrictions': None,
+ 'verbs': [],
+ 'resources': []}]
+ content = {
+ 'apiVersion': 'v1',
+ 'kind': 'ClusterRole',
+ 'metadata': {'name': '{}'.format(name)},
+ 'rules': rules,
+ }
+
+ return ClusterRole(content=content)
+
+
+# -*- -*- -*- End included fragment: lib/clusterrole.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_clusterrole.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCClusterRole(OpenShiftCLI):
+ ''' Class to manage clusterrole objects'''
+ kind = 'clusterrole'
+
+ def __init__(self,
+ name,
+ rules=None,
+ kubeconfig=None,
+ verbose=False):
+ ''' Constructor for OCClusterRole '''
+ super(OCClusterRole, self).__init__(None, kubeconfig=kubeconfig, verbose=verbose)
+ self.verbose = verbose
+ self.name = name
+ self._clusterrole = None
+ self._inc_clusterrole = ClusterRole.builder(name, rules)
+
+ @property
+ def clusterrole(self):
+ ''' property for clusterrole'''
+ if self._clusterrole is None:
+ self.get()
+ return self._clusterrole
+
+ @clusterrole.setter
+ def clusterrole(self, data):
+ ''' setter function for clusterrole property'''
+ self._clusterrole = data
+
+ @property
+ def inc_clusterrole(self):
+ ''' property for inc_clusterrole'''
+ return self._inc_clusterrole
+
+ @inc_clusterrole.setter
+ def inc_clusterrole(self, data):
+ ''' setter function for inc_clusterrole property'''
+ self._inc_clusterrole = data
+
+ def exists(self):
+ ''' return whether a clusterrole exists '''
+ if self.clusterrole:
+ return True
+
+ return False
+
+ def get(self):
+ '''return a clusterrole '''
+ result = self._get(self.kind, self.name)
+
+ if result['returncode'] == 0:
+ self.clusterrole = ClusterRole(content=result['results'][0])
+ result['results'] = self.clusterrole.yaml_dict
+
+ elif '"{}" not found'.format(self.name) in result['stderr']:
+ result['returncode'] = 0
+ self.clusterrole = None
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.name)
+
+ def create(self):
+ '''create a clusterrole from the proposed incoming clusterrole'''
+ return self._create_from_content(self.name, self.inc_clusterrole.yaml_dict)
+
+ def update(self):
+ '''update a project'''
+ return self._replace_content(self.kind, self.name, self.inc_clusterrole.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed'''
+ return not self.clusterrole.compare(self.inc_clusterrole, self.verbose)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ oc_clusterrole = OCClusterRole(params['name'],
+ params['rules'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_clusterrole.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_clusterrole.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_clusterrole.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_clusterrole.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oc_clusterrole.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
+
+# -*- -*- -*- End included fragment: class/oc_clusterrole.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_clusterrole.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for clusterrole
+ '''
+
+ 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'),
+ rules=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCClusterRole.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_clusterrole.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_configmap.py b/roles/lib_openshift/library/oc_configmap.py
new file mode 100644
index 000000000..9515de569
--- /dev/null
+++ b/roles/lib_openshift/library/oc_configmap.py
@@ -0,0 +1,1631 @@
+#!/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/configmap -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_configmap
+short_description: Modify, and idempotently manage openshift configmaps
+description:
+ - Modify openshift configmaps programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a configmap
+ - absent - will remove the configmap
+ 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: True
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ from_file:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the file path.
+ required: false
+ default: None
+ aliases: []
+ from_literal:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the string content
+ required: false
+ default: None
+ aliases: []
+author:
+- "kenny woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_configmap:
+ state: present
+ name: testmap
+ from_file:
+ secret: /path/to/secret
+ from_literal:
+ title: systemadmin
+ register: configout
+'''
+
+# -*- -*- -*- End included fragment: doc/configmap -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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_configmap.py -*- -*- -*-
+
+
+# pylint: disable=too-many-arguments
+class OCConfigMap(OpenShiftCLI):
+ ''' Openshift ConfigMap Class
+
+ ConfigMaps are a way to store data inside of objects
+ '''
+ def __init__(self,
+ name,
+ from_file,
+ from_literal,
+ state,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCConfigMap, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = name
+ self.state = state
+ self._configmap = None
+ self._inc_configmap = None
+ self.from_file = from_file if from_file is not None else {}
+ self.from_literal = from_literal if from_literal is not None else {}
+
+ @property
+ def configmap(self):
+ if self._configmap is None:
+ self._configmap = self.get()
+
+ return self._configmap
+
+ @configmap.setter
+ def configmap(self, inc_map):
+ self._configmap = inc_map
+
+ @property
+ def inc_configmap(self):
+ if self._inc_configmap is None:
+ results = self.create(dryrun=True, output=True)
+ self._inc_configmap = results['results']
+
+ return self._inc_configmap
+
+ @inc_configmap.setter
+ def inc_configmap(self, inc_map):
+ self._inc_configmap = inc_map
+
+ def from_file_to_params(self):
+ '''return from_files in a string ready for cli'''
+ return ["--from-file={}={}".format(key, value) for key, value in self.from_file.items()]
+
+ def from_literal_to_params(self):
+ '''return from_literal in a string ready for cli'''
+ return ["--from-literal={}={}".format(key, value) for key, value in self.from_literal.items()]
+
+ def get(self):
+ '''return a configmap by name '''
+ results = self._get('configmap', self.name)
+ if results['returncode'] == 0 and results['results'][0]:
+ self.configmap = results['results'][0]
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete a configmap by name'''
+ return self._delete('configmap', self.name)
+
+ def create(self, dryrun=False, output=False):
+ '''Create a configmap
+
+ :dryrun: Product what you would have done. default: False
+ :output: Whether to parse output. default: False
+ '''
+
+ cmd = ['create', 'configmap', self.name]
+ if self.from_literal is not None:
+ cmd.extend(self.from_literal_to_params())
+
+ if self.from_file is not None:
+ cmd.extend(self.from_file_to_params())
+
+ if dryrun:
+ cmd.extend(['--dry-run', '-ojson'])
+
+ results = self.openshift_cmd(cmd, output=output)
+
+ return results
+
+ def update(self):
+ '''run update configmap '''
+ return self._replace_content('configmap', self.name, self.inc_configmap)
+
+ def needs_update(self):
+ '''compare the current configmap with the proposed and return if they are equal'''
+ return not Utils.check_def_equal(self.inc_configmap, self.configmap, debug=self.verbose)
+
+ @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'''
+
+ oc_cm = OCConfigMap(params['name'],
+ params['from_file'],
+ params['from_literal'],
+ params['state'],
+ params['namespace'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_cm.get()
+
+ if 'failed' in api_rval:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if not params['name']:
+ return {'failed': True,
+ 'msg': 'Please specify a name when state is absent|present.'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_cm.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ api_rval = oc_cm.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_cm.needs_update():
+
+ api_rval = oc_cm.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_configmap.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_configmap.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for managing OpenShift configmap objects
+ '''
+
+ 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'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ from_file=dict(default=None, type='dict'),
+ from_literal=dict(default=None, type='dict'),
+ ),
+ supports_check_mode=True,
+ )
+
+
+ rval = OCConfigMap.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_configmap.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_edit.py b/roles/lib_openshift/library/oc_edit.py
index aec64202f..d461e5ae9 100644
--- a/roles/lib_openshift/library/oc_edit.py
+++ b/roles/lib_openshift/library/oc_edit.py
@@ -169,20 +169,18 @@ oc_edit:
# -*- -*- -*- End included fragment: doc/edit -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -204,13 +202,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -226,13 +224,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -254,7 +252,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -295,7 +293,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -304,7 +303,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -318,6 +317,12 @@ class Yedit(object):
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
@@ -336,7 +341,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -436,7 +441,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -555,8 +560,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -617,7 +622,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -643,7 +658,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -675,114 +690,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ 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 rval[0] and module.params['src']:
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
yamlfile.write()
- return {'changed': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -864,6 +914,13 @@ class OpenShiftCLI(object):
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')
@@ -883,11 +940,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -905,7 +966,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -922,13 +983,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -948,9 +1009,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -965,10 +1026,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -981,16 +1042,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1033,7 +1094,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1050,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))
@@ -1063,39 +1120,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1253,13 +1302,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
@@ -1358,7 +1406,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1372,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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 e164ecf95..22ad58725 100644
--- a/roles/lib_openshift/library/oc_env.py
+++ b/roles/lib_openshift/library/oc_env.py
@@ -136,20 +136,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/env -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -171,13 +169,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -193,13 +191,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -221,7 +219,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -262,7 +260,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -271,7 +270,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -285,6 +284,12 @@ class Yedit(object):
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
@@ -303,7 +308,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -403,7 +408,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -522,8 +527,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -584,7 +589,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -610,7 +625,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -642,114 +657,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -831,6 +881,13 @@ class OpenShiftCLI(object):
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')
@@ -850,11 +907,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -872,7 +933,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -889,13 +950,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -915,9 +976,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -932,10 +993,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -948,16 +1009,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1000,7 +1061,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1017,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))
@@ -1030,39 +1087,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1220,13 +1269,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
@@ -1325,7 +1373,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1339,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
new file mode 100644
index 000000000..b6c6e47d9
--- /dev/null
+++ b/roles/lib_openshift/library/oc_group.py
@@ -0,0 +1,1610 @@
+#!/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/group -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_group
+short_description: Modify, and idempotently manage openshift groups.
+description:
+ - Modify openshift groups programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a group
+ - absent - will remove the group
+ 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: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_group:
+ state: present
+ name: acme_org
+ register: group_out
+'''
+
+# -*- -*- -*- End included fragment: doc/group -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/group.py -*- -*- -*-
+
+
+class GroupConfig(object):
+ ''' Handle route options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig):
+ ''' constructor for handling group options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Group'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['users'] = None
+
+
+# pylint: disable=too-many-instance-attributes
+class Group(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self, content):
+ '''Group constructor'''
+ super(Group, self).__init__(content=content)
+
+# -*- -*- -*- End included fragment: lib/group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_group.py -*- -*- -*-
+
+
+class OCGroup(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCGroup '''
+ super(OCGroup, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._group = None
+
+ @property
+ def group(self):
+ ''' property function service'''
+ if not self._group:
+ self.get()
+ return self._group
+
+ @group.setter
+ def group(self, data):
+ ''' setter function for yedit var '''
+ self._group = data
+
+ def exists(self):
+ ''' return whether a group exists '''
+ if self.group:
+ return True
+
+ return False
+
+ def get(self):
+ '''return group information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.group = Group(content=result['results'][0])
+ elif 'groups \"{}\" not found'.format(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'''
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return not Utils.check_def_equal(self.config.data, self.group.yaml_dict, skip_keys=[], debug=True)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+
+ gconfig = GroupConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ )
+ oc_group = OCGroup(gconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_group.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_group.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_group.needs_update():
+ api_rval = oc_group.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_group.py -*- -*- -*-
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for group
+ '''
+
+ 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'),
+ namespace=dict(default='default', type='str'),
+ # addind users to a group is handled through the oc_users module
+ #users=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCGroup.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_group.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_image.py b/roles/lib_openshift/library/oc_image.py
new file mode 100644
index 000000000..f7fc286e0
--- /dev/null
+++ b/roles/lib_openshift/library/oc_image.py
@@ -0,0 +1,1536 @@
+#!/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/image -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_image
+short_description: Create, modify, and idempotently manage openshift labels.
+description:
+ - Modify openshift labels programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create. Does _not_ support update.
+ - 'list' will read the labels
+ default: present
+ choices: ["present", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace where this object lives
+ required: false
+ default: default
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ registry_url:
+ description:
+ - The url for the registry so that openshift can pull the image
+ required: false
+ default: None
+ aliases: []
+ image_name:
+ description:
+ - The name of the image being imported
+ required: false
+ default: False
+ aliases: []
+ image_tag:
+ description:
+ - The tag of the image being imported
+ required: false
+ default: None
+ aliases: []
+author:
+- "Ivan Horvath<ihorvath@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Get an imagestream
+ oc_image:
+ name: php55
+ state: list
+ register: imageout
+
+- name: create an imagestream
+ oc_image:
+ state: present
+ image_name: php55
+ image_tag: int
+ registry_url: registry.example.com
+ namespace: default
+ register: imageout
+'''
+
+# -*- -*- -*- End included fragment: doc/image -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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_image.py -*- -*- -*-
+
+
+# pylint: disable=too-many-arguments
+class OCImage(OpenShiftCLI):
+ ''' Class to import and create an imagestream object'''
+ def __init__(self,
+ namespace,
+ registry_url,
+ image_name,
+ image_tag,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCImage'''
+ super(OCImage, self).__init__(namespace, kubeconfig)
+ self.registry_url = registry_url
+ self.image_name = image_name
+ self.image_tag = image_tag
+ self.verbose = verbose
+
+ def get(self):
+ '''return a image by name '''
+ results = self._get('imagestream', self.image_name)
+ results['exists'] = False
+ if results['returncode'] == 0 and results['results'][0]:
+ results['exists'] = True
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.image_name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def create(self, url=None, name=None, tag=None):
+ '''Create an image '''
+ return self._import_image(url, name, tag)
+
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ ''' run the ansible idempotent code '''
+
+ ocimage = OCImage(params['namespace'],
+ params['registry_url'],
+ params['image_name'],
+ params['image_tag'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocimage.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+ return {"changed": False, "results": api_rval, "state": "list"}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+
+ if not Utils.exists(api_rval['results'], params['image_name']):
+
+ if check_mode:
+ return {"changed": False, "msg": 'CHECK_MODE: Would have performed a create'}
+
+ api_rval = ocimage.create(params['registry_url'],
+ params['image_name'],
+ params['image_tag'])
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ # return the newly created object
+ api_rval = ocimage.get()
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ return {"changed": True, "results": api_rval, "state": "present"}
+
+ # image exists, no change
+ return {"changed": False, "results": api_rval, "state": "present"}
+
+ return {"failed": True, "changed": False, "msg": "Unknown state passed. {0}".format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_image.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_image.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for image import
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ registry_url=dict(default=None, type='str'),
+ image_name=dict(default=None, required=True, type='str'),
+ image_tag=dict(default=None, type='str'),
+ force=dict(default=False, type='bool'),
+ ),
+
+ supports_check_mode=True,
+ )
+
+ rval = OCImage.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_image.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_label.py b/roles/lib_openshift/library/oc_label.py
index fd6674d41..2206878a4 100644
--- a/roles/lib_openshift/library/oc_label.py
+++ b/roles/lib_openshift/library/oc_label.py
@@ -145,20 +145,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/label -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -180,13 +178,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -202,13 +200,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -230,7 +228,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -271,7 +269,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -280,7 +279,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -294,6 +293,12 @@ class Yedit(object):
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
@@ -312,7 +317,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -412,7 +417,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -531,8 +536,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -593,7 +598,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -619,7 +634,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -651,114 +666,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -840,6 +890,13 @@ class OpenShiftCLI(object):
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')
@@ -859,11 +916,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -881,7 +942,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -898,13 +959,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -924,9 +985,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -941,10 +1002,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -957,16 +1018,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1009,7 +1070,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1026,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))
@@ -1039,39 +1096,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1229,13 +1278,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
@@ -1334,7 +1382,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1348,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1500,9 +1558,9 @@ class OCLabel(OpenShiftCLI):
label_list = []
if self.name:
- result = self._get(resource=self.kind, rname=self.name, selector=self.selector)
+ result = self._get(resource=self.kind, name=self.name, selector=self.selector)
- if 'labels' in result['results'][0]['metadata']:
+ if result['results'][0] and 'labels' in result['results'][0]['metadata']:
label_list.append(result['results'][0]['metadata']['labels'])
else:
label_list.append({})
diff --git a/roles/lib_openshift/library/oc_obj.py b/roles/lib_openshift/library/oc_obj.py
index 24397d725..126d7a617 100644
--- a/roles/lib_openshift/library/oc_obj.py
+++ b/roles/lib_openshift/library/oc_obj.py
@@ -90,15 +90,15 @@ 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: []
kind:
description:
- - The kind attribute of the object. e.g. dc, bc, svc, route
+ - The kind attribute of the object. e.g. dc, bc, svc, route. May be a comma-separated list, e.g. "dc,po,svc".
required: True
default: None
aliases: []
@@ -148,20 +148,18 @@ register: router_output
# -*- -*- -*- End included fragment: doc/obj -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -183,13 +181,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -205,13 +203,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -233,7 +231,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -274,7 +272,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -283,7 +282,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -297,6 +296,12 @@ class Yedit(object):
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
@@ -315,7 +320,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -415,7 +420,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -534,8 +539,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -596,7 +601,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -622,7 +637,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -654,114 +669,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ 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 rval[0] and module.params['src']:
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
yamlfile.write()
- return {'changed': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -843,6 +893,13 @@ class OpenShiftCLI(object):
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')
@@ -862,11 +919,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -884,7 +945,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -901,13 +962,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -927,9 +988,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -944,10 +1005,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -960,16 +1021,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1012,7 +1073,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1029,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))
@@ -1042,39 +1099,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1232,13 +1281,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
@@ -1337,7 +1385,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1351,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1379,7 +1437,7 @@ class OCObject(OpenShiftCLI):
def __init__(self,
kind,
namespace,
- rname=None,
+ name=None,
selector=None,
kubeconfig='/etc/origin/master/admin.kubeconfig',
verbose=False,
@@ -1388,21 +1446,26 @@ class OCObject(OpenShiftCLI):
super(OCObject, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose,
all_namespaces=all_namespaces)
self.kind = kind
- self.name = rname
+ self.name = name
self.selector = selector
def get(self):
'''return a kind by name '''
- results = self._get(self.kind, rname=self.name, selector=self.selector)
- if results['returncode'] != 0 and 'stderr' in results and \
- '\"%s\" not found' % self.name in results['stderr']:
+ results = self._get(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 delete(self):
- '''return all pods '''
- return self._delete(self.kind, self.name)
+ '''delete the object'''
+ 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):
'''
@@ -1414,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'])
@@ -1478,24 +1550,31 @@ class OCObject(OpenShiftCLI):
# Get
#####
if state == 'list':
- return {'changed': False, 'results': api_rval, 'state': 'list'}
-
- if not params['name']:
- return {'failed': True, 'msg': 'Please specify a name when state is absent|present.'} # noqa: E501
+ return {'changed': False, 'results': api_rval, 'state': state}
########
# Delete
########
if state == 'absent':
- if not Utils.exists(api_rval['results'], params['name']):
- return {'changed': False, '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 \
+ ('items' in api_rval['results'][0] and len(api_rval['results'][0]['items']) == 0)):
+ return {'changed': False, 'state': state}
if check_mode:
return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete'}
api_rval = ocobj.delete()
- return {'changed': True, 'results': api_rval, 'state': 'absent'}
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ # create/update: Must define a name beyond this point
+ if not params['name']:
+ return {'failed': True, 'msg': 'Please specify a name when state is present.'}
if state == 'present':
########
@@ -1521,7 +1600,7 @@ class OCObject(OpenShiftCLI):
if params['files'] and params['delete_after']:
Utils.cleanup(params['files'])
- return {'changed': True, 'results': api_rval, 'state': "present"}
+ return {'changed': True, 'results': api_rval, 'state': state}
########
# Update
@@ -1536,7 +1615,7 @@ class OCObject(OpenShiftCLI):
if params['files'] and params['delete_after']:
Utils.cleanup(params['files'])
- return {'changed': False, 'results': api_rval['results'][0], 'state': "present"}
+ return {'changed': False, 'results': api_rval['results'][0], 'state': state}
if check_mode:
return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
@@ -1555,7 +1634,7 @@ class OCObject(OpenShiftCLI):
if api_rval['returncode'] != 0:
return {'failed': True, 'msg': api_rval}
- return {'changed': True, 'results': api_rval, 'state': "present"}
+ return {'changed': True, 'results': api_rval, 'state': state}
# -*- -*- -*- End included fragment: class/oc_obj.py -*- -*- -*-
@@ -1583,7 +1662,7 @@ def main():
force=dict(default=False, type='bool'),
selector=dict(default=None, type='str'),
),
- mutually_exclusive=[["content", "files"]],
+ mutually_exclusive=[["content", "files"], ["selector", "name"]],
supports_check_mode=True,
)
diff --git a/roles/lib_openshift/library/oc_sdnvalidator.py b/roles/lib_openshift/library/oc_objectvalidator.py
index 795b775f3..d20904d0d 100644
--- a/roles/lib_openshift/library/oc_sdnvalidator.py
+++ b/roles/lib_openshift/library/oc_objectvalidator.py
@@ -50,14 +50,14 @@ from ansible.module_utils.basic import AnsibleModule
# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
-# -*- -*- -*- Begin included fragment: doc/sdnvalidator -*- -*- -*-
+# -*- -*- -*- Begin included fragment: doc/objectvalidator -*- -*- -*-
DOCUMENTATION = '''
---
-module: oc_sdnvalidator
-short_description: Validate SDN objects
+module: oc_objectvalidator
+short_description: Validate OpenShift objects
description:
- - Validate SDN objects
+ - Validate OpenShift objects
options:
kubeconfig:
description:
@@ -71,29 +71,27 @@ extends_documentation_fragment: []
'''
EXAMPLES = '''
-oc_version:
-- name: get oc sdnvalidator
- sdnvalidator:
- register: oc_sdnvalidator
+oc_objectvalidator:
+- name: run oc_objectvalidator
+ oc_objectvalidator:
+ register: oc_objectvalidator
'''
-# -*- -*- -*- End included fragment: doc/sdnvalidator -*- -*- -*-
+# -*- -*- -*- End included fragment: doc/objectvalidator -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -115,13 +113,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -137,13 +135,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -165,7 +163,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -206,7 +204,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -215,7 +214,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -229,6 +228,12 @@ class Yedit(object):
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
@@ -247,7 +252,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -347,7 +352,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -466,8 +471,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -528,7 +533,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -554,7 +569,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -586,114 +601,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -775,6 +825,13 @@ class OpenShiftCLI(object):
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')
@@ -794,11 +851,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -816,7 +877,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -833,13 +894,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -859,9 +920,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -876,10 +937,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -892,16 +953,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -944,7 +1005,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -961,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))
@@ -974,39 +1031,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1164,13 +1213,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
@@ -1269,7 +1317,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1283,42 +1330,53 @@ 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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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_sdnvalidator.py -*- -*- -*-
+# -*- -*- -*- Begin included fragment: class/oc_objectvalidator.py -*- -*- -*-
# pylint: disable=too-many-instance-attributes
-class OCSDNValidator(OpenShiftCLI):
+class OCObjectValidator(OpenShiftCLI):
''' Class to wrap the oc command line tools '''
def __init__(self, kubeconfig):
- ''' Constructor for OCSDNValidator '''
- # namespace has no meaning for SDN validation, hardcode to 'default'
- super(OCSDNValidator, self).__init__('default', kubeconfig)
+ ''' Constructor for OCObjectValidator '''
+ # namespace has no meaning for object validation, hardcode to 'default'
+ super(OCObjectValidator, self).__init__('default', kubeconfig)
- def get(self, kind, invalid_filter):
- ''' return SDN information '''
+ def get_invalid(self, kind, invalid_filter):
+ ''' return invalid object information '''
rval = self._get(kind)
if rval['returncode'] != 0:
return False, rval, []
- return True, rval, filter(invalid_filter, rval['results'][0]['items'])
+ return True, rval, list(filter(invalid_filter, rval['results'][0]['items'])) # wrap filter with list for py3
# pylint: disable=too-many-return-statements
@staticmethod
@@ -1328,10 +1386,26 @@ class OCSDNValidator(OpenShiftCLI):
params comes from the ansible portion of this module
'''
- sdnvalidator = OCSDNValidator(params['kubeconfig'])
+ objectvalidator = OCObjectValidator(params['kubeconfig'])
all_invalid = {}
failed = False
+ def _is_invalid_namespace(namespace):
+ # check if it uses a reserved name
+ name = namespace['metadata']['name']
+ if not any((name == 'kube',
+ name == 'kubernetes',
+ name == 'openshift',
+ name.startswith('kube-'),
+ name.startswith('kubernetes-'),
+ name.startswith('openshift-'),)):
+ return False
+
+ # determine if the namespace was created by a user
+ if 'annotations' not in namespace['metadata']:
+ return False
+ return 'openshift.io/requester' in namespace['metadata']['annotations']
+
checks = (
(
'hostsubnet',
@@ -1343,10 +1417,15 @@ class OCSDNValidator(OpenShiftCLI):
lambda x: x['metadata']['name'] != x['netname'],
u'netnamespaces where metadata.name != netname',
),
+ (
+ 'namespace',
+ _is_invalid_namespace,
+ u'namespaces that use reserved names and were not created by infrastructure components',
+ ),
)
for resource, invalid_filter, invalid_msg in checks:
- success, rval, invalid = sdnvalidator.get(resource, invalid_filter)
+ success, rval, invalid = objectvalidator.get_invalid(resource, invalid_filter)
if not success:
return {'failed': True, 'msg': 'Failed to GET {}.'.format(resource), 'state': 'list', 'results': rval}
if invalid:
@@ -1354,17 +1433,26 @@ class OCSDNValidator(OpenShiftCLI):
all_invalid[invalid_msg] = invalid
if failed:
- return {'failed': True, 'msg': 'All SDN objects are not valid.', 'state': 'list', 'results': all_invalid}
+ return {
+ 'failed': True,
+ 'msg': (
+ "All objects are not valid. If you are a supported customer please contact "
+ "Red Hat Support providing the complete output above. If you are not a customer "
+ "please contact users@lists.openshift.redhat.com for assistance."
+ ),
+ 'state': 'list',
+ 'results': all_invalid
+ }
- return {'msg': 'All SDN objects are valid.'}
+ return {'msg': 'All objects are valid.'}
-# -*- -*- -*- End included fragment: class/oc_sdnvalidator.py -*- -*- -*-
+# -*- -*- -*- End included fragment: class/oc_objectvalidator.py -*- -*- -*-
-# -*- -*- -*- Begin included fragment: ansible/oc_sdnvalidator.py -*- -*- -*-
+# -*- -*- -*- Begin included fragment: ansible/oc_objectvalidator.py -*- -*- -*-
def main():
'''
- ansible oc module for validating OpenShift SDN objects
+ ansible oc module for validating OpenShift objects
'''
module = AnsibleModule(
@@ -1375,7 +1463,7 @@ def main():
)
- rval = OCSDNValidator.run_ansible(module.params)
+ rval = OCObjectValidator.run_ansible(module.params)
if 'failed' in rval:
module.fail_json(**rval)
@@ -1384,4 +1472,4 @@ def main():
if __name__ == '__main__':
main()
-# -*- -*- -*- End included fragment: ansible/oc_sdnvalidator.py -*- -*- -*-
+# -*- -*- -*- End included fragment: ansible/oc_objectvalidator.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_process.py b/roles/lib_openshift/library/oc_process.py
index 0a4f2058a..91199d093 100644
--- a/roles/lib_openshift/library/oc_process.py
+++ b/roles/lib_openshift/library/oc_process.py
@@ -137,20 +137,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/process -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -172,13 +170,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -194,13 +192,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -222,7 +220,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -263,7 +261,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -272,7 +271,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -286,6 +285,12 @@ class Yedit(object):
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
@@ -304,7 +309,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -404,7 +409,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -523,8 +528,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -585,7 +590,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -611,7 +626,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -643,114 +658,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -832,6 +882,13 @@ class OpenShiftCLI(object):
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')
@@ -851,11 +908,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -873,7 +934,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -890,13 +951,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -916,9 +977,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -933,10 +994,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -949,16 +1010,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1001,7 +1062,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1018,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))
@@ -1031,39 +1088,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1221,13 +1270,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
@@ -1326,7 +1374,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1340,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1388,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
@@ -1494,7 +1552,7 @@ class OCProcess(OpenShiftCLI):
if api_rval['returncode'] != 0:
return {"failed": True, "msg" : api_rval}
- return {"changed" : False, "results": api_rval, "state": "list"}
+ return {"changed" : False, "results": api_rval, "state": state}
elif state == 'present':
if check_mode and params['create']:
@@ -1516,9 +1574,9 @@ class OCProcess(OpenShiftCLI):
return {"failed": True, "msg": api_rval}
if params['create']:
- return {"changed": True, "results": api_rval, "state": "present"}
+ return {"changed": True, "results": api_rval, "state": state}
- return {"changed": False, "results": api_rval, "state": "present"}
+ return {"changed": False, "results": api_rval, "state": state}
# verify results
update = False
@@ -1533,13 +1591,13 @@ class OCProcess(OpenShiftCLI):
update = True
if not update:
- return {"changed": update, "results": api_rval, "state": "present"}
+ return {"changed": update, "results": api_rval, "state": state}
for cmd in rval:
if cmd['returncode'] != 0:
- return {"failed": True, "changed": update, "results": rval, "state": "present"}
+ return {"failed": True, "changed": update, "msg": rval, "state": state}
- return {"changed": update, "results": rval, "state": "present"}
+ return {"changed": update, "results": rval, "state": state}
# -*- -*- -*- End included fragment: class/oc_process.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_project.py b/roles/lib_openshift/library/oc_project.py
new file mode 100644
index 000000000..f9b2d81fa
--- /dev/null
+++ b/roles/lib_openshift/library/oc_project.py
@@ -0,0 +1,1732 @@
+#!/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/project -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_project
+short_description: Module to manage openshift projects
+description:
+ - Manage openshift projects programmatically.
+options:
+ state:
+ description:
+ - If present, the project will be created if it doesn't exist or update if different. If absent, the project will be removed if present. If list, information about the project will be gathered and returned as part of the Ansible call results.
+ 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: []
+ display_name:
+ description:
+ - The display name attribute for a project
+ required: false
+ default: None
+ aliases: []
+ description:
+ description:
+ - The description attribute for a project
+ required: false
+ default: None
+ aliases: []
+ admin:
+ description:
+ - The project admin username
+ required: false
+ default: false
+ aliases: []
+ admin_role:
+ description:
+ - The project admin username
+ required: false
+ default: 'admin'
+ aliases: []
+ node_selector:
+ description:
+ - The node selector for this project.
+ - This allows certain pods in this project to run on certain nodes.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create secret
+ oc_project:
+ state: present
+ name: openshift-ops
+ display_name: operations team project
+ node_selector:
+ - top=secret
+ - noncustomer=True
+'''
+
+# -*- -*- -*- End included fragment: doc/project -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/project.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class ProjectConfig(OpenShiftCLIConfig):
+ ''' project config object '''
+ def __init__(self, rname, namespace, kubeconfig, project_options):
+ super(ProjectConfig, self).__init__(rname, None, kubeconfig, project_options)
+
+
+class Project(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ annotations_path = "metadata.annotations"
+ kind = 'Project'
+ annotation_prefix = 'openshift.io/'
+
+ def __init__(self, content):
+ '''Project constructor'''
+ super(Project, self).__init__(content=content)
+
+ def get_annotations(self):
+ ''' return the annotations'''
+ return self.get(Project.annotations_path) or {}
+
+ def add_annotations(self, inc_annos):
+ ''' add an annotation to the other annotations'''
+ if not isinstance(inc_annos, list):
+ inc_annos = [inc_annos]
+
+ annos = self.get_annotations()
+ if not annos:
+ self.put(Project.annotations_path, inc_annos)
+ else:
+ for anno in inc_annos:
+ for key, value in anno.items():
+ annos[key] = value
+
+ return True
+
+ def find_annotation(self, key):
+ ''' find an annotation'''
+ annotations = self.get_annotations()
+ for anno in annotations:
+ if Project.annotation_prefix + key == anno:
+ return annotations[anno]
+
+ return None
+
+ def delete_annotation(self, inc_anno_keys):
+ ''' remove an annotation from a project'''
+ if not isinstance(inc_anno_keys, list):
+ inc_anno_keys = [inc_anno_keys]
+
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ removed = False
+ for inc_anno in inc_anno_keys:
+ anno = self.find_annotation(inc_anno)
+ if anno:
+ del annos[Project.annotation_prefix + anno]
+ removed = True
+
+ return removed
+
+ def update_annotation(self, key, value):
+ ''' remove an annotation for a project'''
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ updated = False
+ anno = self.find_annotation(key)
+ if anno:
+ annos[Project.annotation_prefix + key] = value
+ updated = True
+
+ else:
+ self.add_annotations({Project.annotation_prefix + key: value})
+
+ return updated
+
+# -*- -*- -*- End included fragment: lib/project.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_project.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCProject(OpenShiftCLI):
+ ''' Project Class to manage project/namespace objects'''
+ kind = 'namespace'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCProject '''
+ super(OCProject, self).__init__(None, config.kubeconfig)
+ self.config = config
+ self._project = None
+
+ @property
+ def project(self):
+ ''' property for project'''
+ if not self._project:
+ self.get()
+ return self._project
+
+ @project.setter
+ def project(self, data):
+ ''' setter function for project propeorty'''
+ self._project = data
+
+ def exists(self):
+ ''' return whether a project exists '''
+ if self.project:
+ return True
+
+ return False
+
+ def get(self):
+ '''return project '''
+ result = self._get(self.kind, self.config.name)
+
+ if result['returncode'] == 0:
+ self.project = Project(content=result['results'][0])
+ result['results'] = self.project.yaml_dict
+
+ elif 'namespaces "%s" not found' % self.config.name in result['stderr']:
+ result = {'results': [], 'returncode': 0}
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create a project '''
+ cmd = ['new-project', self.config.name]
+ cmd.extend(self.config.to_option_list())
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ def update(self):
+ '''update a project '''
+
+ if self.config.config_options['display_name']['value'] is not None:
+ self.project.update_annotation('display-name', self.config.config_options['display_name']['value'])
+
+ if self.config.config_options['description']['value'] is not None:
+ self.project.update_annotation('description', self.config.config_options['description']['value'])
+
+ # work around for immutable project field
+ if self.config.config_options['node_selector']['value'] is not None:
+ self.project.update_annotation('node-selector', self.config.config_options['node_selector']['value'])
+
+ return self._replace_content(self.kind, self.config.name, self.project.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.config.config_options['display_name']['value'] is not None:
+ result = self.project.find_annotation("display-name")
+ if result != self.config.config_options['display_name']['value']:
+ return True
+
+ if self.config.config_options['description']['value'] is not None:
+ result = self.project.find_annotation("description")
+ if result != self.config.config_options['description']['value']:
+ return True
+
+ if self.config.config_options['node_selector']['value'] is not None:
+ result = self.project.find_annotation("node-selector")
+ if result != self.config.config_options['node_selector']['value']:
+ return True
+
+ return False
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ node_selector = None
+ if params['node_selector'] is not None:
+ node_selector = ','.join(params['node_selector'])
+
+ pconfig = ProjectConfig(
+ params['name'],
+ 'None',
+ params['kubeconfig'],
+ {
+ 'admin': {'value': params['admin'], 'include': True},
+ 'admin_role': {'value': params['admin_role'], 'include': True},
+ 'description': {'value': params['description'], 'include': True},
+ 'display_name': {'value': params['display_name'], 'include': True},
+ 'node_selector': {'value': node_selector, 'include': True},
+ },
+ )
+
+ oadm_project = OCProject(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oadm_project.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oadm_project.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # 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()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oadm_project.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oadm_project.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oadm_project.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
+
+# -*- -*- -*- End included fragment: class/oc_project.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_project.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for project
+ '''
+
+ 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, require=True, type='str'),
+ display_name=dict(default=None, type='str'),
+ node_selector=dict(default=None, type='list'),
+ description=dict(default=None, type='str'),
+ admin=dict(default=None, type='str'),
+ admin_role=dict(default='admin', type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCProject.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_project.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_pvc.py b/roles/lib_openshift/library/oc_pvc.py
new file mode 100644
index 000000000..895322ba5
--- /dev/null
+++ b/roles/lib_openshift/library/oc_pvc.py
@@ -0,0 +1,1847 @@
+#!/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/pvc -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_pvc
+short_description: Modify, and idempotently manage openshift persistent volume claims
+description:
+ - Modify openshift persistent volume claims programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a pvc
+ - absent - will remove a pvc
+ 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: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ volume_capacity:
+ description:
+ - The requested volume capacity
+ required: False
+ default: 1G
+ aliases: []
+ access_modes:
+ description:
+ - The access modes allowed for the pvc
+ - Expects a list
+ required: False
+ default: ReadWriteOnce
+ choices:
+ - ReadWriteOnce
+ - 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: []
+'''
+
+EXAMPLES = '''
+- name: create a pvc
+ oc_pvc:
+ namespace: awesomeapp
+ name: dbstorage
+ access_modes:
+ - ReadWriteOnce
+ volume_capacity: 5G
+ register: pvcout
+'''
+
+# -*- -*- -*- End included fragment: doc/pvc -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/pvc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class PersistentVolumeClaimConfig(object):
+ ''' Handle pvc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ access_modes=None,
+ vol_capacity='1G',
+ selector=None,
+ storage_class_name=None):
+ ''' constructor for handling pvc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ 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()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'PersistentVolumeClaim'
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ # spec
+ self.data['spec'] = {}
+ 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):
+ ''' Class to wrap the oc command line tools '''
+ access_modes_path = "spec.accessModes"
+ volume_capacity_path = "spec.requests.storage"
+ 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):
+ '''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):
+ ''' volume_name property '''
+ if self._volume_name is None:
+ self._volume_name = self.get_volume_name()
+ return self._volume_name
+
+ @volume_name.setter
+ def volume_name(self, data):
+ ''' volume_name property setter'''
+ 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:
+ self._access_modes = self.get_access_modes()
+ if not isinstance(self._access_modes, list):
+ self._access_modes = list(self._access_modes)
+
+ return self._access_modes
+
+ @access_modes.setter
+ def access_modes(self, data):
+ ''' access_modes property setter'''
+ if not isinstance(data, list):
+ data = list(data)
+
+ self._access_modes = data
+
+ @property
+ def volume_capacity(self):
+ ''' volume_capacity property '''
+ if self._volume_capacity is None:
+ self._volume_capacity = self.get_volume_capacity()
+ return self._volume_capacity
+
+ @volume_capacity.setter
+ def volume_capacity(self, data):
+ ''' 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 []
+
+ def get_volume_capacity(self):
+ '''get volume_capacity'''
+ return self.get(PersistentVolumeClaim.volume_capacity_path) or []
+
+ def get_volume_name(self):
+ '''get volume_name'''
+ return self.get(PersistentVolumeClaim.volume_name_path) or []
+
+ def is_bound(self):
+ '''return whether volume is bound'''
+ return self.get(PersistentVolumeClaim.bound_path) or []
+
+ #### ADD #####
+ def add_access_mode(self, inc_mode):
+ ''' add an access_mode'''
+ if self.access_modes:
+ self.access_modes.append(inc_mode)
+ else:
+ self.put(PersistentVolumeClaim.access_modes_path, [inc_mode])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_access_mode(self, inc_mode):
+ ''' remove an access_mode'''
+ try:
+ self.access_modes.remove(inc_mode)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_access_mode(self, inc_mode):
+ ''' update an access_mode'''
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return self.add_access_mode(inc_mode)
+
+ self.access_modes[index] = inc_mode
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_access_mode(self, inc_mode):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/pvc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_pvc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCPVC(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'pvc'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCPVC, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._pvc = None
+
+ @property
+ def pvc(self):
+ ''' property function pvc'''
+ if not self._pvc:
+ self.get()
+ return self._pvc
+
+ @pvc.setter
+ def pvc(self, data):
+ ''' setter function for yedit var '''
+ self._pvc = data
+
+ def bound(self):
+ '''return whether the pvc is bound'''
+ if self.pvc.get_volume_name():
+ return True
+
+ return False
+
+ def exists(self):
+ ''' return whether a pvc exists '''
+ if self.pvc:
+ return True
+
+ return False
+
+ def get(self):
+ '''return pvc information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.pvc = PersistentVolumeClaim(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'''
+ # need to update the tls information and the service name
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.pvc.get_volume_name() or self.pvc.is_bound():
+ return False
+
+ skip = []
+ return not Utils.check_def_equal(self.config.data, self.pvc.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+ pconfig = PersistentVolumeClaimConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['access_modes'],
+ params['volume_capacity'],
+ params['selector'],
+ params['storage_class_name'],
+ )
+ oc_pvc = OCPVC(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_pvc.get()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_pvc.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_pvc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_pvc.pvc.is_bound() or oc_pvc.pvc.get_volume_name():
+ api_rval['msg'] = '##### - This volume is currently bound. Will not update - ####'
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if oc_pvc.needs_update():
+ api_rval = oc_pvc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_pvc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_pvc.py -*- -*- -*-
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for pvc
+ '''
+
+ 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, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ volume_capacity=dict(default='1G', 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,
+ )
+
+ rval = OCPVC.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_pvc.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_route.py b/roles/lib_openshift/library/oc_route.py
index 21e7e175b..8f8e46e1e 100644
--- a/roles/lib_openshift/library/oc_route.py
+++ b/roles/lib_openshift/library/oc_route.py
@@ -179,20 +179,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/route -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -214,13 +212,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -236,13 +234,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -264,7 +262,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -305,7 +303,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -314,7 +313,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -328,6 +327,12 @@ class Yedit(object):
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
@@ -346,7 +351,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -446,7 +451,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -565,8 +570,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -627,7 +632,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -653,7 +668,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -685,114 +700,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -874,6 +924,13 @@ class OpenShiftCLI(object):
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')
@@ -893,11 +950,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -915,7 +976,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -932,13 +993,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -958,9 +1019,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -975,10 +1036,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -991,16 +1052,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1043,7 +1104,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1060,10 +1121,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))
@@ -1073,39 +1130,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1263,13 +1312,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
@@ -1368,7 +1416,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1382,17 +1429,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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1607,9 +1665,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()
@@ -1648,14 +1703,14 @@ 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'],
diff --git a/roles/lib_openshift/library/oc_scale.py b/roles/lib_openshift/library/oc_scale.py
index 0c2d86a18..7130cc5fc 100644
--- a/roles/lib_openshift/library/oc_scale.py
+++ b/roles/lib_openshift/library/oc_scale.py
@@ -123,20 +123,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/scale -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -158,13 +156,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -180,13 +178,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -208,7 +206,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -249,7 +247,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -258,7 +257,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -272,6 +271,12 @@ class Yedit(object):
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
@@ -290,7 +295,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -390,7 +395,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -509,8 +514,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -571,7 +576,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -597,7 +612,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -629,114 +644,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -818,6 +868,13 @@ class OpenShiftCLI(object):
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')
@@ -837,11 +894,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -859,7 +920,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -876,13 +937,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -902,9 +963,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -919,10 +980,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -935,16 +996,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -987,7 +1048,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1004,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))
@@ -1017,39 +1074,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1207,13 +1256,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
@@ -1312,7 +1360,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1326,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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 b6dcd9fff..0c4b99e30 100644
--- a/roles/lib_openshift/library/oc_secret.py
+++ b/roles/lib_openshift/library/oc_secret.py
@@ -108,6 +108,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
@@ -169,20 +175,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/secret -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -204,13 +208,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -226,13 +230,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -254,7 +258,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -295,7 +299,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -304,7 +309,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -318,6 +323,12 @@ class Yedit(object):
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
@@ -336,7 +347,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -436,7 +447,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -555,8 +566,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -617,7 +628,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -643,7 +664,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -675,114 +696,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -864,6 +920,13 @@ class OpenShiftCLI(object):
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')
@@ -883,11 +946,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -905,7 +972,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -922,13 +989,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -948,9 +1015,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -965,10 +1032,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -981,16 +1048,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1033,7 +1100,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1050,10 +1117,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))
@@ -1063,39 +1126,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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))
- else:
+ if 'err' in rval or returncode != 0:
rval.update({"stderr": stderr,
- "stdout": stdout,
- "results": {}})
+ "stdout": stdout})
return rval
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1253,13 +1308,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
@@ -1358,7 +1412,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1372,17 +1425,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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1399,10 +1463,12 @@ class SecretConfig(object):
sname,
namespace,
kubeconfig,
- secrets=None):
+ secrets=None,
+ stype=None):
''' constructor for handling secret options '''
self.kubeconfig = kubeconfig
self.name = sname
+ self.type = stype
self.namespace = namespace
self.secrets = secrets
self.data = {}
@@ -1413,6 +1479,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
@@ -1502,12 +1569,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):
@@ -1531,13 +1600,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)
@@ -1550,7 +1623,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)
if secret['returncode'] != 0:
return secret
@@ -1562,7 +1635,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
'''
@@ -1571,6 +1644,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)
@@ -1583,6 +1660,7 @@ class OCSecret(OpenShiftCLI):
ocsecret = OCSecret(params['namespace'],
params['name'],
+ params['type'],
params['decode'],
kubeconfig=params['kubeconfig'],
verbose=params['debug'])
@@ -1632,7 +1710,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']:
@@ -1649,7 +1727,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}
@@ -1705,6 +1783,7 @@ def main():
debug=dict(default=False, type='bool'),
namespace=dict(default='default', type='str'),
name=dict(default=None, type='str'),
+ 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 83d0579a5..7ab139e85 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: []
@@ -175,20 +182,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/service -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -210,13 +215,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -232,13 +237,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -260,7 +265,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -301,7 +306,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -310,7 +316,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -324,6 +330,12 @@ class Yedit(object):
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
@@ -342,7 +354,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -442,7 +454,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -561,8 +573,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -623,7 +635,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -649,7 +671,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -681,114 +703,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -870,6 +927,13 @@ class OpenShiftCLI(object):
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')
@@ -889,11 +953,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -911,7 +979,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -928,13 +996,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -954,9 +1022,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -971,10 +1039,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -987,16 +1055,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -1039,7 +1107,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1056,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))
@@ -1069,39 +1133,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1259,13 +1315,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
@@ -1364,7 +1419,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1378,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
@@ -1411,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
@@ -1422,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()
@@ -1434,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:
@@ -1457,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 '''
@@ -1465,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'''
@@ -1526,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 -*- -*- -*-
@@ -1548,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
@@ -1623,6 +1745,7 @@ class OCService(OpenShiftCLI):
params['ports'],
params['session_affinity'],
params['service_type'],
+ params['external_ips'],
params['kubeconfig'],
params['debug'])
@@ -1724,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 9b0a6e060..5d539ced4 100644
--- a/roles/lib_openshift/library/oc_serviceaccount.py
+++ b/roles/lib_openshift/library/oc_serviceaccount.py
@@ -121,20 +121,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/serviceaccount -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -156,13 +154,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -178,13 +176,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -206,7 +204,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -247,7 +245,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -256,7 +255,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -270,6 +269,12 @@ class Yedit(object):
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
@@ -288,7 +293,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -388,7 +393,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -507,8 +512,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -569,7 +574,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -595,7 +610,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -627,114 +642,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -816,6 +866,13 @@ class OpenShiftCLI(object):
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')
@@ -835,11 +892,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -857,7 +918,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -874,13 +935,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -900,9 +961,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -917,10 +978,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -933,16 +994,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -985,7 +1046,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1002,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))
@@ -1015,39 +1072,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1205,13 +1254,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
@@ -1310,7 +1358,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1324,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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 413b8e358..97e213f46 100644
--- a/roles/lib_openshift/library/oc_serviceaccount_secret.py
+++ b/roles/lib_openshift/library/oc_serviceaccount_secret.py
@@ -121,20 +121,18 @@ EXAMPLES = '''
# -*- -*- -*- End included fragment: doc/serviceaccount_secret -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -156,13 +154,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -178,13 +176,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -206,7 +204,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -247,7 +245,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -256,7 +255,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -270,6 +269,12 @@ class Yedit(object):
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
@@ -288,7 +293,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -388,7 +393,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -507,8 +512,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -569,7 +574,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -595,7 +610,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -627,114 +642,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
- if module.params['src']:
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -816,6 +866,13 @@ class OpenShiftCLI(object):
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')
@@ -835,11 +892,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -857,7 +918,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -874,13 +935,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -900,9 +961,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -917,10 +978,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -933,16 +994,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -985,7 +1046,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -1002,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))
@@ -1015,39 +1072,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1205,13 +1254,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
@@ -1310,7 +1358,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1324,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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..9339a85e5
--- /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']) or {}
+
+ 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'] 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
new file mode 100644
index 000000000..2fa349547
--- /dev/null
+++ b/roles/lib_openshift/library/oc_user.py
@@ -0,0 +1,1764 @@
+#!/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/user -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_user
+short_description: Create, modify, and idempotently manage openshift users.
+description:
+ - Modify openshift users programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update a user to the desired state
+ - 'absent' will ensure user is removed
+ - 'list' will read and return a list of users
+ 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: []
+ username:
+ description:
+ - Short username to query/modify.
+ required: false
+ default: None
+ aliases: []
+ full_name:
+ description:
+ - String with the full name/description of the user.
+ required: false
+ default: None
+ aliases: []
+ groups:
+ description:
+ - List of groups the user should be a member of. This does not add/update the legacy 'groups' field in the OpenShift user object, but makes user entries into the appropriate OpenShift group object for the given user.
+ required: false
+ default: []
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Ensure user exists
+ oc_user:
+ state: present
+ username: johndoe
+ full_name "John Doe"
+ groups:
+ - dedicated-admins
+ register: user_johndoe
+
+user_johndoe variable will have contents like:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": true,
+ "results": {
+ "cmd": "oc -n default get users johndoe -o json",
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John DOe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:09:21Z",
+ "name": "johndoe",
+ "resourceVersion": "848781",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "e23d3300-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "returncode": 0
+ },
+ "state": "present"
+ }
+}
+'groups' is empty because this field is the OpenShift user object's 'group' field.
+
+- name: Ensure user does not exist
+ oc_user:
+ state: absent
+ username: johndoe
+
+- name: List user's info
+ oc_user:
+ state: list
+ username: johndoe
+ register: user_johndoe
+
+user_johndoe will have contents similar to:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": false,
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John Doe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:04:44Z",
+ "name": "johndoe",
+ "resourceVersion": "848280",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "3d479ad2-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "state": "list"
+ }
+}
+'''
+
+# -*- -*- -*- End included fragment: doc/user -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/user.py -*- -*- -*-
+
+
+class UserConfig(object):
+ ''' Handle user options '''
+ def __init__(self,
+ kubeconfig,
+ username,
+ full_name):
+ ''' constructor for handling user options '''
+ self.kubeconfig = kubeconfig
+ self.username = username
+ self.full_name = full_name
+
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a user as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['fullName'] = self.full_name
+ self.data['groups'] = None
+ self.data['identities'] = None
+ self.data['kind'] = 'User'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.username
+
+
+# pylint: disable=too-many-instance-attributes
+class User(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'user'
+
+ def __init__(self, content):
+ '''User constructor'''
+ super(User, self).__init__(content=content)
+
+# -*- -*- -*- End included fragment: lib/user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_user.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCUser(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'users'
+
+ def __init__(self,
+ config,
+ groups=None,
+ verbose=False):
+ ''' Constructor for OCUser '''
+ # namespace has no meaning for user operations, hardcode to 'default'
+ super(OCUser, self).__init__('default', config.kubeconfig)
+ self.config = config
+ self.groups = groups
+ self._user = None
+
+ @property
+ def user(self):
+ ''' property function user'''
+ if not self._user:
+ self.get()
+ return self._user
+
+ @user.setter
+ def user(self, data):
+ ''' setter function for user '''
+ self._user = data
+
+ def exists(self):
+ ''' return whether a user exists '''
+ if self.user:
+ return True
+
+ return False
+
+ def get(self):
+ ''' return user information '''
+ result = self._get(self.kind, self.config.username)
+ if result['returncode'] == 0:
+ self.user = User(content=result['results'][0])
+ elif 'users \"%s\" not found' % self.config.username in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ ''' delete the object '''
+ return self._delete(self.kind, self.config.username)
+
+ def create_group_entries(self):
+ ''' make entries for user to the provided group list '''
+ if self.groups != None:
+ for group in self.groups:
+ cmd = ['groups', 'add-users', group, self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ return {'returncode': 0}
+
+ def create(self):
+ ''' create the object '''
+ rval = self.create_group_entries()
+ if rval['returncode'] != 0:
+ return rval
+
+ return self._create_from_content(self.config.username, self.config.data)
+
+ def group_update(self):
+ ''' update group membership '''
+ rval = {'returncode': 0}
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ cmd = ['groups', 'add-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ cmd = ['groups', 'remove-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ def update(self):
+ ''' update the object '''
+ rval = self.group_update()
+ if rval['returncode'] != 0:
+ return rval
+
+ # need to update the user's info
+ return self._replace_content(self.kind, self.config.username, self.config.data, force=True)
+
+ def needs_group_update(self):
+ ''' check if there are group membership changes '''
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ return True
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ return True
+
+ return False
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = []
+ if self.needs_group_update():
+ return True
+
+ return not Utils.check_def_equal(self.config.data, self.user.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent ansible code
+
+ params comes from the ansible portion of this module
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+
+ uconfig = UserConfig(params['kubeconfig'],
+ params['username'],
+ params['full_name'],
+ )
+
+ oc_user = OCUser(uconfig, params['groups'],
+ verbose=params['debug'])
+ state = params['state']
+
+ api_rval = oc_user.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_user.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_user.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_user.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ ########
+ # Update
+ ########
+ if oc_user.needs_update():
+ api_rval = oc_user.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ orig_cmd = api_rval['cmd']
+ # return the created object
+ api_rval = oc_user.get()
+ # overwrite the get/list cmd
+ api_rval['cmd'] = orig_cmd
+
+ 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,
+ 'results': 'Unknown state passed. %s' % state,
+ 'state': "unknown"}
+
+# -*- -*- -*- End included fragment: class/oc_user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_user.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for user
+ '''
+
+ 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'),
+ username=dict(default=None, type='str'),
+ full_name=dict(default=None, type='str'),
+ # setting groups for user data will not populate the
+ # 'groups' field in the user data.
+ # it will call out to the group data and make the user
+ # entry there
+ groups=dict(default=[], type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_user.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_version.py b/roles/lib_openshift/library/oc_version.py
index 1aae7a8ea..55e1054e7 100644
--- a/roles/lib_openshift/library/oc_version.py
+++ b/roles/lib_openshift/library/oc_version.py
@@ -93,20 +93,18 @@ oc_version:
# -*- -*- -*- End included fragment: doc/version -*- -*- -*-
# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
-# pylint: disable=undefined-variable,missing-docstring
-# noqa: E301,E302
-class YeditException(Exception):
+class YeditException(Exception): # pragma: no cover
''' Exception class for Yedit '''
pass
# pylint: disable=too-many-public-methods
-class Yedit(object):
+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%s/_-]+)"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
com_sep = set(['.', '#', '|', ':'])
# pylint: disable=too-many-arguments
@@ -128,13 +126,13 @@ class Yedit(object):
@property
def separator(self):
- ''' getter method for yaml_dict '''
+ ''' getter method for separator '''
return self._separator
@separator.setter
- def separator(self):
- ''' getter method for yaml_dict '''
- return self._separator
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
@property
def yaml_dict(self):
@@ -150,13 +148,13 @@ class Yedit(object):
def parse_key(key, sep='.'):
'''parse the key allowing the appropriate separator'''
common_separators = list(Yedit.com_sep - set([sep]))
- return re.findall(Yedit.re_key % ''.join(common_separators), key)
+ 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 % ''.join(common_separators), key):
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
return False
return True
@@ -178,7 +176,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes[:-1]:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -219,7 +217,8 @@ class Yedit(object):
continue
elif data and not isinstance(data, dict):
- return None
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
data[dict_key] = {}
data = data[dict_key]
@@ -228,7 +227,7 @@ class Yedit(object):
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
else:
- return None
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
if key == '':
data = item
@@ -242,6 +241,12 @@ class Yedit(object):
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
@@ -260,7 +265,7 @@ class Yedit(object):
key_indexes = Yedit.parse_key(key, sep)
for arr_ind, dict_key in key_indexes:
if dict_key and isinstance(data, dict):
- data = data.get(dict_key, None)
+ data = data.get(dict_key)
elif (arr_ind and isinstance(data, list) and
int(arr_ind) <= len(data) - 1):
data = data[int(arr_ind)]
@@ -360,7 +365,7 @@ class Yedit(object):
self.yaml_dict = json.loads(contents)
except yaml.YAMLError as err:
# Error loading yaml or json
- raise YeditException('Problem with loading yaml file. %s' % err)
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
return self.yaml_dict
@@ -479,8 +484,8 @@ class Yedit(object):
# 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=[%s] [%s]' % (value, type(value))) # noqa: E501
+ 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)
@@ -541,7 +546,17 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if not result:
+ 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
@@ -567,7 +582,7 @@ class Yedit(object):
pass
result = Yedit.add_entry(tmp_copy, path, value, self.separator)
- if result:
+ if result is not None:
self.yaml_dict = tmp_copy
return (True, self.yaml_dict)
@@ -599,114 +614,149 @@ class Yedit(object):
# we will convert to bool if it matches any of the above cases
if isinstance(inc_value, str) and 'bool' in vtype:
if inc_value not in true_bools and inc_value not in false_bools:
- raise YeditException('Not a boolean type. str=[%s] vtype=[%s]'
- % (inc_value, vtype))
+ 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.
- if isinstance(inc_value, str) and 'str' not in vtype:
+ elif isinstance(inc_value, str) and 'str' not in vtype:
try:
- inc_value = yaml.load(inc_value)
+ inc_value = yaml.safe_load(inc_value)
except Exception:
- raise YeditException('Could not determine type of incoming ' +
- 'value. value=[%s] vtype=[%s]'
- % (type(inc_value), vtype))
+ 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(module):
+ def run_ansible(params):
'''perform the idempotent crud operations'''
- yamlfile = Yedit(filename=module.params['src'],
- backup=module.params['backup'],
- separator=module.params['separator'])
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
- if module.params['src']:
+ state = params['state']
+
+ if params['src']:
rval = yamlfile.load()
- if yamlfile.yaml_dict is None and \
- module.params['state'] != 'present':
+ if yamlfile.yaml_dict is None and state != 'present':
return {'failed': True,
- 'msg': 'Error opening file [%s]. Verify that the ' +
- 'file exists, that it is has correct' +
- ' permissions, and is valid yaml.'}
-
- if module.params['state'] == 'list':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ '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 module.params['key']:
- rval = yamlfile.get(module.params['key']) or {}
+ if params['key']:
+ rval = yamlfile.get(params['key']) or {}
- return {'changed': False, 'result': rval, 'state': "list"}
+ return {'changed': False, 'result': rval, 'state': state}
- elif module.params['state'] == 'absent':
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
yamlfile.yaml_dict = content
- if module.params['update']:
- rval = yamlfile.pop(module.params['key'],
- module.params['value'])
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
else:
- rval = yamlfile.delete(module.params['key'])
+ rval = yamlfile.delete(params['key'])
- if rval[0] and module.params['src']:
+ if rval[0] and params['src']:
yamlfile.write()
- return {'changed': rval[0], 'result': rval[1], 'state': "absent"}
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
- elif module.params['state'] == 'present':
+ elif state == 'present':
# check if content is different than what is in the file
- if module.params['content']:
- content = Yedit.parse_value(module.params['content'],
- module.params['content_type'])
+ 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 \
- module.params['value'] is None:
- return {'changed': False,
- 'result': yamlfile.yaml_dict,
- 'state': "present"}
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
yamlfile.yaml_dict = content
- # we were passed a value; parse it
- if module.params['value']:
- value = Yedit.parse_value(module.params['value'],
- module.params['value_type'])
- key = module.params['key']
- if module.params['update']:
- # pylint: disable=line-too-long
- curr_value = Yedit.get_curr_value(Yedit.parse_value(module.params['curr_value']), # noqa: E501
- module.params['curr_value_format']) # noqa: E501
+ # 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']
- rval = yamlfile.update(key, value, module.params['index'], curr_value) # noqa: E501
+ 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 module.params['append']:
- rval = yamlfile.append(key, value)
- else:
- rval = yamlfile.put(key, value)
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
- if rval[0] and module.params['src']:
+ 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': rval[0],
- 'result': rval[1], 'state': "present"}
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
# no edits to make
- if module.params['src']:
+ if params['src']:
# pylint: disable=redefined-variable-type
rval = yamlfile.write()
return {'changed': rval[0],
'result': rval[1],
- 'state': "present"}
+ '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 -*- -*- -*-
@@ -788,6 +838,13 @@ class OpenShiftCLI(object):
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')
@@ -807,11 +864,15 @@ class OpenShiftCLI(object):
'''call oc create on a filename'''
return self.openshift_cmd(['create', '-f', fname])
- def _delete(self, resource, rname, selector=None):
+ def _delete(self, resource, name=None, selector=None):
'''call oc delete on a resource'''
- cmd = ['delete', resource, rname]
- if selector:
- cmd.append('--selector=%s' % selector)
+ 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)
@@ -829,7 +890,7 @@ class OpenShiftCLI(object):
else:
cmd.append(template_name)
if params:
- param_str = ["%s=%s" % (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)
@@ -846,13 +907,13 @@ class OpenShiftCLI(object):
return self.openshift_cmd(['create', '-f', fname])
- def _get(self, resource, rname=None, selector=None):
+ def _get(self, resource, name=None, selector=None):
'''return a resource by name '''
cmd = ['get', resource]
- if selector:
- cmd.append('--selector=%s' % selector)
- elif rname:
- cmd.append(rname)
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
cmd.extend(['-o', 'json'])
@@ -872,9 +933,9 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
- cmd.append('--schedulable=%s' % schedulable)
+ cmd.append('--schedulable={}'.format(schedulable))
return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
@@ -889,10 +950,10 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
cmd.extend(['--list-pods', '-o', 'json'])
@@ -905,16 +966,16 @@ class OpenShiftCLI(object):
if node:
cmd.extend(node)
else:
- cmd.append('--selector=%s' % selector)
+ cmd.append('--selector={}'.format(selector))
if dry_run:
cmd.append('--dry-run')
if pod_selector:
- cmd.append('--pod-selector=%s' % pod_selector)
+ cmd.append('--pod-selector={}'.format(pod_selector))
if grace_period:
- cmd.append('--grace-period=%s' % int(grace_period))
+ cmd.append('--grace-period={}'.format(int(grace_period)))
if force:
cmd.append('--force')
@@ -957,7 +1018,7 @@ class OpenShiftCLI(object):
stdout, stderr = proc.communicate(input_data)
- return proc.returncode, stdout.decode(), stderr.decode()
+ 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):
@@ -974,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))
@@ -987,39 +1044,31 @@ 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 err:
- if "No JSON object could be decoded" in err.args:
- err = err.args
- elif output_type == 'raw':
- rval['results'] = stdout
-
- if self.verbose:
- print("STDOUT: {0}".format(stdout))
- print("STDERR: {0}".format(stderr))
-
- if err:
- rval.update({"err": err,
- "stderr": stderr,
- "stdout": stdout,
- "cmd": cmds})
+ 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
-class Utils(object):
+class Utils(object): # pragma: no cover
''' utilities for openshiftcli modules '''
@staticmethod
@@ -1177,13 +1226,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
@@ -1282,7 +1330,6 @@ class Utils(object):
print('returning true')
return True
-
class OpenShiftCLIConfig(object):
'''Generic Config'''
def __init__(self, rname, namespace, kubeconfig, options):
@@ -1296,17 +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, data in self.config_options.items():
+ 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('--%s=%s' % (key.replace('_', '-'), data['value']))
+ 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
new file mode 100644
index 000000000..63bad57b4
--- /dev/null
+++ b/roles/lib_openshift/library/oc_volume.py
@@ -0,0 +1,2086 @@
+#!/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/volume -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_volume
+short_description: Create, modify, and idempotently manage openshift volumes.
+description:
+ - Modify openshift volumes programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update and object to the desired state
+ - 'absent' will ensure volumes are removed
+ - 'list' will read the volumes
+ 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: []
+ vol_name:
+ description:
+ - Name of the volume that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The name of the namespace where the object lives
+ required: false
+ default: default
+ aliases: []
+ kind:
+ description:
+ - The kind of object that can be managed.
+ default: dc
+ choices:
+ - dc
+ - rc
+ - pods
+ aliases: []
+ mount_type:
+ description:
+ - The type of volume to be used
+ required: false
+ default: None
+ choices:
+ - emptydir
+ - hostpath
+ - secret
+ - pvc
+ - configmap
+ aliases: []
+ mount_path:
+ description:
+ - The path to where the mount will be attached
+ required: false
+ default: None
+ aliases: []
+ secret_name:
+ description:
+ - The name of the secret. Used when mount_type is secret.
+ required: false
+ default: None
+ aliases: []
+ claim_size:
+ description:
+ - The size in GB of the pv claim. e.g. 100G
+ required: false
+ default: None
+ aliases: []
+ claim_name:
+ description:
+ - The name of the pv claim
+ required: false
+ default: None
+ aliases: []
+ configmap_name:
+ description:
+ - The name of the configmap
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: attach storage volumes to deploymentconfig
+ oc_volume:
+ namespace: logging
+ kind: dc
+ name: name_of_the_dc
+ mount_type: pvc
+ claim_name: loggingclaim
+ claim_size: 100G
+ vol_name: logging-storage
+ run_once: true
+'''
+
+# -*- -*- -*- End included fragment: doc/volume -*- -*- -*-
+
+# -*- -*- -*- 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']) or {}
+
+ 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'] 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/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/volume.py -*- -*- -*-
+
+class Volume(object):
+ ''' Class to represent an openshift volume object'''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ @staticmethod
+ def create_volume_structure(volume_info):
+ ''' return a properly structured volume '''
+ volume_mount = None
+ volume = {'name': volume_info['name']}
+ volume_type = volume_info['type'].lower()
+ if volume_type == 'secret':
+ volume['secret'] = {}
+ volume[volume_info['type']] = {'secretName': volume_info['secret_name']}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'emptydir':
+ volume['emptyDir'] = {}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'pvc' or volume_type == 'persistentvolumeclaim':
+ volume['persistentVolumeClaim'] = {}
+ volume['persistentVolumeClaim']['claimName'] = volume_info['claimName']
+ volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize']
+ elif volume_type == 'hostpath':
+ volume['hostPath'] = {}
+ volume['hostPath']['path'] = volume_info['path']
+ elif volume_type == 'configmap':
+ volume['configMap'] = {}
+ volume['configMap']['name'] = volume_info['configmap_name']
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+
+ return (volume, volume_mount)
+
+# -*- -*- -*- End included fragment: lib/volume.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_volume.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVolume(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ resource_name,
+ namespace,
+ vol_name,
+ mount_path,
+ mount_type,
+ secret_name,
+ claim_size,
+ claim_name,
+ configmap_name,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCVolume, self).__init__(namespace, kubeconfig)
+ self.kind = kind
+ self.volume_info = {'name': vol_name,
+ 'secret_name': secret_name,
+ 'path': mount_path,
+ 'type': mount_type,
+ 'claimSize': claim_size,
+ 'claimName': claim_name,
+ 'configmap_name': configmap_name}
+ self.volume, self.volume_mount = Volume.create_volume_structure(self.volume_info)
+ self.name = resource_name
+ self.namespace = namespace
+ self.kubeconfig = kubeconfig
+ self.verbose = verbose
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var '''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var '''
+ self._resource = data
+
+ def exists(self):
+ ''' return whether a volume exists '''
+ volume_mount_found = False
+ volume_found = self.resource.exists_volume(self.volume)
+ if not self.volume_mount and volume_found:
+ return True
+
+ if self.volume_mount:
+ volume_mount_found = self.resource.exists_volume_mount(self.volume_mount)
+
+ if volume_found and self.volume_mount and volume_mount_found:
+ return True
+
+ return False
+
+ def get(self):
+ '''return volume information '''
+ vol = self._get(self.kind, self.name)
+ if vol['returncode'] == 0:
+ if self.kind == 'dc':
+ self.resource = DeploymentConfig(content=vol['results'][0])
+ vol['results'] = self.resource.get_volumes()
+
+ return vol
+
+ def delete(self):
+ '''remove a volume'''
+ self.resource.delete_volume_by_name(self.volume)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def put(self):
+ '''place volume into dc '''
+ self.resource.update_volume(self.volume)
+ self.resource.get_volumes()
+ self.resource.update_volume_mount(self.volume_mount)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return self.resource.needs_update_volume(self.volume, self.volume_mount)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+ oc_volume = OCVolume(params['kind'],
+ params['name'],
+ params['namespace'],
+ params['vol_name'],
+ params['mount_path'],
+ params['mount_type'],
+ # secrets
+ params['secret_name'],
+ # pvc
+ params['claim_size'],
+ params['claim_name'],
+ # configmap
+ params['configmap_name'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_volume.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_volume.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_volume.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_volume.needs_update():
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: state}
+
+ return {'changed': False, 'results': api_rval, state: state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_volume.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_volume.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for volumes
+ '''
+
+ 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'),
+ kind=dict(default='dc', choices=['dc', 'rc', 'pods'], type='str'),
+ namespace=dict(default='default', type='str'),
+ vol_name=dict(default=None, type='str'),
+ name=dict(default=None, type='str'),
+ mount_type=dict(default=None,
+ choices=['emptydir', 'hostpath', 'secret', 'pvc', 'configmap'],
+ type='str'),
+ mount_path=dict(default=None, type='str'),
+ # secrets require a name
+ secret_name=dict(default=None, type='str'),
+ # pvc requires a size
+ claim_size=dict(default=None, type='str'),
+ claim_name=dict(default=None, type='str'),
+ # configmap requires a name
+ configmap_name=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+ rval = OCVolume.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_volume.py -*- -*- -*-