From a11970d30c88d188392ec217c055b6b8169b3769 Mon Sep 17 00:00:00 2001 From: Joel Diaz Date: Tue, 28 Feb 2017 16:22:08 +0000 Subject: clean up and clarify docs/comments update unit tests --- roles/lib_openshift/library/oc_user.py | 232 +++++++++++++++++---- roles/lib_openshift/src/class/oc_user.py | 4 +- roles/lib_openshift/src/doc/user | 66 +++++- .../lib_openshift/src/test/integration/oc_user.yml | 4 +- roles/lib_openshift/src/test/unit/oc_user.py | 117 ----------- roles/lib_openshift/src/test/unit/test_oc_user.py | 127 +++++++++++ 6 files changed, 392 insertions(+), 158 deletions(-) delete mode 100755 roles/lib_openshift/src/test/unit/oc_user.py create mode 100755 roles/lib_openshift/src/test/unit/test_oc_user.py diff --git a/roles/lib_openshift/library/oc_user.py b/roles/lib_openshift/library/oc_user.py index ded594ac6..d64290a2e 100644 --- a/roles/lib_openshift/library/oc_user.py +++ b/roles/lib_openshift/library/oc_user.py @@ -33,6 +33,7 @@ from __future__ import print_function import atexit +import copy import json import os import re @@ -40,7 +41,11 @@ import shutil import subprocess import tempfile # pylint: disable=import-error -import ruamel.yaml as yaml +try: + import ruamel.yaml as yaml +except ImportError: + import yaml + from ansible.module_utils.basic import AnsibleModule # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*- @@ -83,13 +88,13 @@ options: aliases: [] full_name: description: - - String with the full name/description of th user. + - 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. + - 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: [] @@ -104,16 +109,79 @@ EXAMPLES = ''' 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 -*- -*- -*- +# pylint: disable=undefined-variable,missing-docstring # noqa: E301,E302 @@ -308,11 +376,17 @@ class Yedit(object): if self.backup and self.file_exists(): shutil.copy(self.filename, self.filename + '.orig') - # pylint: disable=no-member - if hasattr(self.yaml_dict, 'fa'): + # Try to set format attributes if supported + try: self.yaml_dict.fa.set_block_style() + except AttributeError: + pass - Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper)) + # 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) @@ -352,10 +426,24 @@ class Yedit(object): # check if it is yaml try: if content_type == 'yaml' and contents: - self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader) - # pylint: disable=no-member - if hasattr(self.yaml_dict, 'fa'): + # 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: @@ -384,14 +472,16 @@ class Yedit(object): return (False, self.yaml_dict) if isinstance(entry, dict): - # pylint: disable=no-member,maybe-no-member + # 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): - # pylint: disable=no-member,maybe-no-member + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=maybe-no-member ind = None try: ind = entry.index(key_or_item) @@ -459,7 +549,9 @@ class Yedit(object): if not isinstance(entry, list): return (False, self.yaml_dict) - # pylint: disable=no-member,maybe-no-member + # 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) @@ -472,7 +564,8 @@ class Yedit(object): entry = None if isinstance(entry, dict): - # pylint: disable=no-member,maybe-no-member + # 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 @@ -481,7 +574,8 @@ class Yedit(object): return (True, self.yaml_dict) elif isinstance(entry, list): - # pylint: disable=no-member,maybe-no-member + # AUDIT:maybe-no-member makes sense due to fuzzy types + # pylint: disable=maybe-no-member ind = None if curr_value: try: @@ -520,12 +614,20 @@ class Yedit(object): return (False, self.yaml_dict) # deepcopy didn't work - tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, - default_flow_style=False), - yaml.RoundTripLoader) - # pylint: disable=no-member - if hasattr(self.yaml_dict, 'fa'): + # 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 not result: return (False, self.yaml_dict) @@ -538,11 +640,20 @@ class Yedit(object): ''' create a yaml file ''' if not self.file_exists(): # deepcopy didn't work - tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict, default_flow_style=False), # noqa: E501 - yaml.RoundTripLoader) - # pylint: disable=no-member - if hasattr(self.yaml_dict, 'fa'): + # 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: self.yaml_dict = tmp_copy @@ -698,6 +809,32 @@ class OpenShiftCLIError(Exception): 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 ''' @@ -709,8 +846,9 @@ class OpenShiftCLI(object): ''' Constructor for OpenshiftCLI ''' self.namespace = namespace self.verbose = verbose - self.kubeconfig = kubeconfig + 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 @@ -907,16 +1045,15 @@ class OpenShiftCLI(object): stdout, stderr = proc.communicate(input_data) - return proc.returncode, stdout, stderr + return proc.returncode, stdout.decode(), stderr.decode() # pylint: disable=too-many-arguments,too-many-branches def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None): '''Base command for oc ''' - cmds = [] + cmds = [self.oc_binary] + if oadm: - cmds = ['oadm'] - else: - cmds = ['oc'] + cmds.append('adm') if self.all_namespaces: cmds.extend(['--all-namespaces']) @@ -932,7 +1069,10 @@ class OpenShiftCLI(object): if self.verbose: print(' '.join(cmds)) - returncode, stdout, stderr = self._run(cmds, input_data) + 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, "results": results, @@ -984,7 +1124,13 @@ class Utils(object): tmp = Utils.create_tmpfile(prefix=rname) if ftype == 'yaml': - Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper)) + # 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: @@ -995,7 +1141,18 @@ class Utils(object): return tmp @staticmethod - def create_tmpfile(prefix=None): + 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: @@ -1055,7 +1212,12 @@ class Utils(object): contents = sfd.read() if sfile_type == 'yaml': - contents = yaml.load(contents, yaml.RoundTripLoader) + # 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) @@ -1298,14 +1460,14 @@ class OCUser(OpenShiftCLI): @property def user(self): - ''' property function service''' + ''' property function user''' if not self._user: self.get() return self._user @user.setter def user(self, data): - ''' setter function for yedit var ''' + ''' setter function for user ''' self._user = data def exists(self): diff --git a/roles/lib_openshift/src/class/oc_user.py b/roles/lib_openshift/src/class/oc_user.py index 17b679289..d9e4eac13 100644 --- a/roles/lib_openshift/src/class/oc_user.py +++ b/roles/lib_openshift/src/class/oc_user.py @@ -19,14 +19,14 @@ class OCUser(OpenShiftCLI): @property def user(self): - ''' property function service''' + ''' property function user''' if not self._user: self.get() return self._user @user.setter def user(self, data): - ''' setter function for yedit var ''' + ''' setter function for user ''' self._user = data def exists(self): diff --git a/roles/lib_openshift/src/doc/user b/roles/lib_openshift/src/doc/user index cae108415..65ee01eb7 100644 --- a/roles/lib_openshift/src/doc/user +++ b/roles/lib_openshift/src/doc/user @@ -37,13 +37,13 @@ options: aliases: [] full_name: description: - - String with the full name/description of th user. + - 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. + - 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: [] @@ -58,9 +58,71 @@ EXAMPLES = ''' 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" + } +} ''' diff --git a/roles/lib_openshift/src/test/integration/oc_user.yml b/roles/lib_openshift/src/test/integration/oc_user.yml index 7d6221e64..ad1f9d188 100755 --- a/roles/lib_openshift/src/test/integration/oc_user.yml +++ b/roles/lib_openshift/src/test/integration/oc_user.yml @@ -142,8 +142,8 @@ register: user_out - name: assert test group created assert: - that: user_out['results'][0]['metadata']['name'] == "integration-test-group" and - user_out['results'][0]['users'] is not defined + that: user_out['results']['results'][0]['metadata']['name'] == "integration-test-group" + msg: "{{ user_out }}" - name: create user with group membership oc_user: diff --git a/roles/lib_openshift/src/test/unit/oc_user.py b/roles/lib_openshift/src/test/unit/oc_user.py deleted file mode 100755 index 920b06d0e..000000000 --- a/roles/lib_openshift/src/test/unit/oc_user.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python2 -''' - Unit tests for oc user -''' -# To run -# ./oc_user.py -# -# .. -# ---------------------------------------------------------------------- -# Ran 2 tests in 0.003s -# -# OK - -import os -import sys -import unittest -import mock - -# Removing invalid variable names for tests so that I can -# keep them brief -# pylint: disable=invalid-name,no-name-in-module -# Disable import-error b/c our libraries aren't loaded in jenkins -# pylint: disable=import-error -# place class in our python path -module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 -sys.path.insert(0, module_path) -from oc_user import OCUser # noqa: E402 - - -class OCUserTest(unittest.TestCase): - ''' - Test class for OCUser - ''' - - def setUp(self): - ''' setup method will create a file and set to known configuration ''' - pass - - @mock.patch('oc_user.OCUser._run') - def test_state_list(self, mock_cmd): - ''' Testing a user list ''' - params = {'username': 'testuser@email.com', - 'state': 'list', - 'kubeconfig': '/etc/origin/master/admin.kubeconfig', - 'full_name': None, - 'groups': [], - 'debug': False} - - user = '''{ - "kind": "User", - "apiVersion": "v1", - "metadata": { - "name": "testuser@email.com", - "selfLink": "/oapi/v1/users/testuser@email.com", - "uid": "02fee6c9-f20d-11e6-b83b-12e1a7285e80", - "resourceVersion": "38566887", - "creationTimestamp": "2017-02-13T16:53:58Z" - }, - "fullName": "Test User", - "identities": null, - "groups": null - }''' - - mock_cmd.side_effect = [ - (0, user, ''), - ] - - results = OCUser.run_ansible(params, False) - - self.assertFalse(results['changed']) - self.assertTrue(results['results'][0]['metadata']['name'] == "testuser@email.com") - - @mock.patch('oc_user.OCUser._run') - def test_state_present(self, mock_cmd): - ''' Testing a user list ''' - params = {'username': 'testuser@email.com', - 'state': 'present', - 'kubeconfig': '/etc/origin/master/admin.kubeconfig', - 'full_name': 'Test User', - 'groups': [], - 'debug': False} - - created_user = '''{ - "kind": "User", - "apiVersion": "v1", - "metadata": { - "name": "testuser@email.com", - "selfLink": "/oapi/v1/users/testuser@email.com", - "uid": "8d508039-f224-11e6-b83b-12e1a7285e80", - "resourceVersion": "38646241", - "creationTimestamp": "2017-02-13T19:42:28Z" - }, - "fullName": "Test User", - "identities": null, - "groups": null - }''' - - mock_cmd.side_effect = [ - (1, '', 'Error from server: users "testuser@email.com" not found'), # get - (1, '', 'Error from server: users "testuser@email.com" not found'), # get - (0, 'user "testuser@email.com" created', ''), # create - (0, created_user, ''), # get - ] - - results = OCUser.run_ansible(params, False) - - self.assertTrue(results['changed']) - self.assertTrue(results['results']['results'][0]['metadata']['name'] == - "testuser@email.com") - - def tearDown(self): - '''TearDown method''' - pass - - -if __name__ == "__main__": - unittest.main() diff --git a/roles/lib_openshift/src/test/unit/test_oc_user.py b/roles/lib_openshift/src/test/unit/test_oc_user.py new file mode 100755 index 000000000..933e96ae2 --- /dev/null +++ b/roles/lib_openshift/src/test/unit/test_oc_user.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python2 +''' + Unit tests for oc user +''' +# To run +# ./oc_user.py +# +# .. +# ---------------------------------------------------------------------- +# Ran 2 tests in 0.003s +# +# OK + +import os +import sys +import unittest +import mock + +# Removing invalid variable names for tests so that I can +# keep them brief +# pylint: disable=invalid-name,no-name-in-module +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 +sys.path.insert(0, module_path) +from oc_user import OCUser, locate_oc_binary # noqa: E402 + + +class OCUserTest(unittest.TestCase): + ''' + Test class for OCUser + ''' + + def setUp(self): + ''' setup method will create a file and set to known configuration ''' + pass + + @mock.patch('oc_user.Utils.create_tmpfile_copy') + @mock.patch('oc_user.OCUser._run') + def test_state_list(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a user list ''' + params = {'username': 'testuser@email.com', + 'state': 'list', + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'full_name': None, + 'groups': [], + 'debug': False} + + user = '''{ + "kind": "User", + "apiVersion": "v1", + "metadata": { + "name": "testuser@email.com", + "selfLink": "/oapi/v1/users/testuser@email.com", + "uid": "02fee6c9-f20d-11e6-b83b-12e1a7285e80", + "resourceVersion": "38566887", + "creationTimestamp": "2017-02-13T16:53:58Z" + }, + "fullName": "Test User", + "identities": null, + "groups": null + }''' + + mock_cmd.side_effect = [ + (0, user, ''), + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + results = OCUser.run_ansible(params, False) + + self.assertFalse(results['changed']) + self.assertTrue(results['results'][0]['metadata']['name'] == "testuser@email.com") + + @mock.patch('oc_user.Utils.create_tmpfile_copy') + @mock.patch('oc_user.OCUser._run') + def test_state_present(self, mock_cmd, mock_tmpfile_copy): + ''' Testing a user list ''' + params = {'username': 'testuser@email.com', + 'state': 'present', + 'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'full_name': 'Test User', + 'groups': [], + 'debug': False} + + created_user = '''{ + "kind": "User", + "apiVersion": "v1", + "metadata": { + "name": "testuser@email.com", + "selfLink": "/oapi/v1/users/testuser@email.com", + "uid": "8d508039-f224-11e6-b83b-12e1a7285e80", + "resourceVersion": "38646241", + "creationTimestamp": "2017-02-13T19:42:28Z" + }, + "fullName": "Test User", + "identities": null, + "groups": null + }''' + + mock_cmd.side_effect = [ + (1, '', 'Error from server: users "testuser@email.com" not found'), # get + (1, '', 'Error from server: users "testuser@email.com" not found'), # get + (0, 'user "testuser@email.com" created', ''), # create + (0, created_user, ''), # get + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + results = OCUser.run_ansible(params, False) + + self.assertTrue(results['changed']) + self.assertTrue(results['results']['results'][0]['metadata']['name'] == + "testuser@email.com") + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3