From a69a84339e1bf187aa970316a4148138cb7f82fb Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Fri, 10 Mar 2017 16:52:50 -0500 Subject: Added the ability to do multiple edits --- roles/lib_utils/src/ansible/yedit.py | 3 +- roles/lib_utils/src/class/yedit.py | 142 +++++++++++++++---------- roles/lib_utils/src/doc/yedit | 16 +++ roles/lib_utils/src/test/integration/yedit.yml | 31 +++++- 4 files changed, 136 insertions(+), 56 deletions(-) (limited to 'roles/lib_utils/src') diff --git a/roles/lib_utils/src/ansible/yedit.py b/roles/lib_utils/src/ansible/yedit.py index 8a1a7c2dc..ea112ac83 100644 --- a/roles/lib_utils/src/ansible/yedit.py +++ b/roles/lib_utils/src/ansible/yedit.py @@ -26,12 +26,13 @@ def main(): type='str'), backup=dict(default=True, type='bool'), separator=dict(default='.', type='str'), + edits=dict(default=None, type='list'), ), mutually_exclusive=[["curr_value", "index"], ['update', "append"]], required_one_of=[["content", "src"]], ) - rval = Yedit.run_ansible(module) + rval = Yedit.run_ansible(module.params) if 'failed' in rval and rval['failed']: module.fail_json(**rval) diff --git a/roles/lib_utils/src/class/yedit.py b/roles/lib_utils/src/class/yedit.py index 533665db2..9f37a9244 100644 --- a/roles/lib_utils/src/class/yedit.py +++ b/roles/lib_utils/src/class/yedit.py @@ -512,13 +512,15 @@ 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=[%s] vtype=[%s]' % (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) except Exception: @@ -528,96 +530,128 @@ class Yedit(object): 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 'action' in edit and edit['action'] == 'update': + # pylint: disable=line-too-long + curr_value = Yedit.get_curr_value(Yedit.parse_value(edit.get('curr_value', None)), # noqa: E501 + edit.get('curr_value_format', None)) # noqa: E501 + + rval = yamlfile.update(edit['key'], + value, + edit.get('index', None), + curr_value) + + elif 'action' in edit and edit['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']) + 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} return {'failed': True, 'msg': 'Unkown state passed'} diff --git a/roles/lib_utils/src/doc/yedit b/roles/lib_utils/src/doc/yedit index 16b44943e..82af1f675 100644 --- a/roles/lib_utils/src/doc/yedit +++ b/roles/lib_utils/src/doc/yedit @@ -135,4 +135,20 @@ EXAMPLES = ''' # a: # b: # c: d +# +# multiple edits at the same time +- name: perform multiple edits + yedit: + src: somefile.yml + edits: + - key: a#b#c + value: d + - key: a#b#c#d + value: e + state: present +# Results: +# a: +# b: +# c: +# d: e ''' diff --git a/roles/lib_utils/src/test/integration/yedit.yml b/roles/lib_utils/src/test/integration/yedit.yml index e3dfd490b..c960c9856 100755 --- a/roles/lib_utils/src/test/integration/yedit.yml +++ b/roles/lib_utils/src/test/integration/yedit.yml @@ -219,4 +219,33 @@ assert: that: results.result == [1, 2, 3] msg: "Test: '[1, 2, 3]' != [{{ results.result }}]" -###### end test create list value ##### + ###### end test create list value ##### + + ###### test create multiple list value ##### + - name: test multiple edits + yedit: + src: "{{ test_file }}" + edits: + - key: z.x.y + value: + - 1 + - 2 + - 3 + - key: z.x.y + value: 4 + action: append + + - name: retrieve the key + yedit: + src: "{{ test_file }}" + state: list + key: z#x#y + separator: '#' + register: results + - debug: var=results + + - name: Assert that the key was created + assert: + that: results.result == [1, 2, 3, 4] + msg: "Test: '[1, 2, 3, 4]' != [{{ results.result }}]" + ###### end test create multiple list value ##### -- cgit v1.2.3 From aa9e19c20b745f91a47292f8754a8cf52017c3e4 Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Fri, 24 Mar 2017 13:07:24 -0400 Subject: Adding a few more test cases. Fixed a bug when key was empty. Safeguard against yedit module being passed an empty key --- roles/lib_utils/src/ansible/yedit.py | 3 + roles/lib_utils/src/class/yedit.py | 29 ++++++--- roles/lib_utils/src/test/integration/yedit.yml | 2 +- roles/lib_utils/src/test/unit/test_yedit.py | 86 ++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 10 deletions(-) (limited to 'roles/lib_utils/src') diff --git a/roles/lib_utils/src/ansible/yedit.py b/roles/lib_utils/src/ansible/yedit.py index ea112ac83..bdb9915d6 100644 --- a/roles/lib_utils/src/ansible/yedit.py +++ b/roles/lib_utils/src/ansible/yedit.py @@ -32,6 +32,9 @@ def main(): required_one_of=[["content", "src"]], ) + if module.params['src'] is not None and module.params['key'] in [None, '']: + module.fail_json(failed=True, msg='Empty value for parameter key not allowed.') + rval = Yedit.run_ansible(module.params) if 'failed' in rval and rval['failed']: module.fail_json(**rval) diff --git a/roles/lib_utils/src/class/yedit.py b/roles/lib_utils/src/class/yedit.py index 9f37a9244..65472afc0 100644 --- a/roles/lib_utils/src/class/yedit.py +++ b/roles/lib_utils/src/class/yedit.py @@ -1,6 +1,5 @@ # flake8: noqa -# pylint: disable=undefined-variable,missing-docstring -# noqa: E301,E302 +# pylint: skip-file class YeditException(Exception): @@ -34,13 +33,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): @@ -454,7 +453,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 @@ -480,7 +489,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) @@ -522,7 +531,7 @@ class Yedit(object): # 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.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]' @@ -654,4 +663,6 @@ class Yedit(object): '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'} diff --git a/roles/lib_utils/src/test/integration/yedit.yml b/roles/lib_utils/src/test/integration/yedit.yml index c960c9856..65209bade 100755 --- a/roles/lib_utils/src/test/integration/yedit.yml +++ b/roles/lib_utils/src/test/integration/yedit.yml @@ -248,4 +248,4 @@ assert: that: results.result == [1, 2, 3, 4] msg: "Test: '[1, 2, 3, 4]' != [{{ results.result }}]" - ###### end test create multiple list value ##### + ###### end test create multiple list value ##### diff --git a/roles/lib_utils/src/test/unit/test_yedit.py b/roles/lib_utils/src/test/unit/test_yedit.py index 23a3f7353..f9f42843a 100755 --- a/roles/lib_utils/src/test/unit/test_yedit.py +++ b/roles/lib_utils/src/test/unit/test_yedit.py @@ -5,6 +5,7 @@ import os import sys import unittest +import mock # Removing invalid variable names for tests so that I can # keep them brief @@ -277,6 +278,91 @@ class YeditTest(unittest.TestCase): with self.assertRaises(YeditException): yed.put('new.stuff.here[0]', 'item') + def test_empty_key_with_int_value(self): + '''test editing top level with not list or dict''' + yed = Yedit(content={'a': {'b': 12}}) + result = yed.put('', 'b') + self.assertFalse(result[0]) + + def test_setting_separator(self): + '''test editing top level with not list or dict''' + yed = Yedit(content={'a': {'b': 12}}) + yed.separator = ':' + self.assertEqual(yed.separator, ':') + + def test_remove_all(self): + '''test removing all data''' + data = Yedit.remove_entry({'a': {'b': 12}}, '') + self.assertTrue(data) + + def test_remove_list_entry(self): + '''test removing list entry''' + data = {'a': {'b': [{'c': 3}]}} + results = Yedit.remove_entry(data, 'a.b[0]') + self.assertTrue(results) + self.assertTrue(data, {'a': {'b': []}}) + + def test_parse_value_string_true(self): + '''test parse_value''' + results = Yedit.parse_value('true', 'str') + self.assertEqual(results, 'true') + + def test_parse_value_bool_true(self): + '''test parse_value''' + results = Yedit.parse_value('true', 'bool') + self.assertTrue(results) + + def test_parse_value_bool_exception(self): + '''test parse_value''' + with self.assertRaises(YeditException): + Yedit.parse_value('TTT', 'bool') + + @mock.patch('yedit.Yedit.write') + def test_run_ansible_basic(self, mock_write): + '''test parse_value''' + params = { + 'src': None, + 'backup': False, + 'separator': '.', + 'state': 'present', + 'edits': [], + 'value': None, + 'key': None, + 'content': {'a': {'b': {'c': 1}}}, + 'content_type': '', + } + + results = Yedit.run_ansible(params) + + mock_write.side_effect = [ + (True, params['content']), + ] + + self.assertFalse(results['changed']) + + @mock.patch('yedit.Yedit.write') + def test_run_ansible_and_write(self, mock_write): + '''test parse_value''' + params = { + 'src': '/tmp/test', + 'backup': False, + 'separator': '.', + 'state': 'present', + 'edits': [], + 'value': None, + 'key': None, + 'content': {'a': {'b': {'c': 1}}}, + 'content_type': '', + } + + results = Yedit.run_ansible(params) + + mock_write.side_effect = [ + (True, params['content']), + ] + + self.assertTrue(results['changed']) + def tearDown(self): '''TearDown method''' os.unlink(YeditTest.filename) -- cgit v1.2.3 From b7af36569f4f02a6320833759b48e15df25a9b06 Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Mon, 27 Mar 2017 09:41:15 -0400 Subject: Fixed a bug in oc_volume. --- roles/lib_utils/src/ansible/yedit.py | 22 ++++++++- roles/lib_utils/src/class/yedit.py | 37 ++++++++------- .../test/integration/kube-manager-test.yaml.orig | 52 ---------------------- 3 files changed, 38 insertions(+), 73 deletions(-) delete mode 100644 roles/lib_utils/src/test/integration/kube-manager-test.yaml.orig (limited to 'roles/lib_utils/src') diff --git a/roles/lib_utils/src/ansible/yedit.py b/roles/lib_utils/src/ansible/yedit.py index bdb9915d6..c4b818cf1 100644 --- a/roles/lib_utils/src/ansible/yedit.py +++ b/roles/lib_utils/src/ansible/yedit.py @@ -32,8 +32,26 @@ def main(): required_one_of=[["content", "src"]], ) - if module.params['src'] is not None and module.params['key'] in [None, '']: - module.fail_json(failed=True, msg='Empty value for parameter key not allowed.') + # Verify we recieved either a valid key or edits with valid keys when receiving a src file. + # A valid key being not None or not ''. + if module.params['src'] is not None: + key_error = False + edit_error = False + + if module.params['key'] in [None, '']: + key_error = True + + if module.params['edits'] in [None, []]: + edit_error = True + + else: + for edit in module.params['edits']: + if edit.get('key') in [None, '']: + edit_error = True + break + + if key_error and edit_error: + module.fail_json(failed=True, msg='Empty value for parameter key not allowed.') rval = Yedit.run_ansible(module.params) if 'failed' in rval and rval['failed']: diff --git a/roles/lib_utils/src/class/yedit.py b/roles/lib_utils/src/class/yedit.py index 65472afc0..e0a27012f 100644 --- a/roles/lib_utils/src/class/yedit.py +++ b/roles/lib_utils/src/class/yedit.py @@ -55,13 +55,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 @@ -83,7 +83,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)] @@ -172,7 +172,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)] @@ -272,7 +272,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 @@ -391,8 +391,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) @@ -521,7 +521,7 @@ 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) @@ -533,9 +533,8 @@ class Yedit(object): try: 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 @@ -545,17 +544,18 @@ class Yedit(object): results = [] for edit in edits: value = Yedit.parse_value(edit['value'], edit.get('value_type', '')) - if 'action' in edit and edit['action'] == 'update': + if edit.get('action') == 'update': # pylint: disable=line-too-long - curr_value = Yedit.get_curr_value(Yedit.parse_value(edit.get('curr_value', None)), # noqa: E501 - edit.get('curr_value_format', None)) # noqa: E501 + 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', None), + edit.get('index'), curr_value) - elif 'action' in edit and edit['action'] == 'append': + elif edit.get('action') == 'append': rval = yamlfile.append(edit['key'], value) else: @@ -581,9 +581,8 @@ class Yedit(object): 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.'} + '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']: diff --git a/roles/lib_utils/src/test/integration/kube-manager-test.yaml.orig b/roles/lib_utils/src/test/integration/kube-manager-test.yaml.orig deleted file mode 100644 index 5541c3dae..000000000 --- a/roles/lib_utils/src/test/integration/kube-manager-test.yaml.orig +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: kube-controller-manager - namespace: kube-system -spec: - hostNetwork: true - containers: - - name: kube-controller-manager - image: openshift/kube:v1.0.0 - command: - - /hyperkube - - controller-manager - - --master=http://127.0.0.1:8080 - - --leader-elect=true - - --service-account-private-key-file=/etc/kubernetes/ssl/apiserver-key.pem - - --root-ca-file=/etc/k8s/ssl/my.pem - - --my-new-parameter=openshift - livenessProbe: - httpGet: - host: 127.0.0.1 - path: /healthz - port: 10252 - initialDelaySeconds: 15 - timeoutSeconds: 1 - volumeMounts: - - mountPath: /etc/kubernetes/ssl - name: ssl-certs-kubernetes - readOnly: true - - mountPath: /etc/ssl/certs - name: ssl-certs-host - readOnly: 'true' - volumes: - - hostPath: - path: /etc/kubernetes/ssl - name: ssl-certs-kubernetes - - hostPath: - path: /usr/share/ca-certificates - name: ssl-certs-host -yedittest: yedittest -metadata-namespace: openshift-is-awesome -nonexistingkey: -- --my-new-parameter=openshift -a: - b: - c: d -e: - f: - g: - h: - i: - j: k -- cgit v1.2.3