From 45fbfdad1b80c50276a9da3841d6e4089b109e35 Mon Sep 17 00:00:00 2001 From: Joel Diaz Date: Fri, 10 Feb 2017 19:30:37 +0000 Subject: add oc_user ansible module module to manipulate OpenShift users, and assign group membership to users --- roles/lib_openshift/src/ansible/oc_user.py | 34 +++ roles/lib_openshift/src/class/oc_user.py | 227 +++++++++++++++++++ roles/lib_openshift/src/doc/user | 66 ++++++ roles/lib_openshift/src/lib/user.py | 37 ++++ roles/lib_openshift/src/sources.yml | 11 + .../lib_openshift/src/test/integration/oc_user.yml | 240 +++++++++++++++++++++ roles/lib_openshift/src/test/unit/oc_user.py | 117 ++++++++++ 7 files changed, 732 insertions(+) create mode 100644 roles/lib_openshift/src/ansible/oc_user.py create mode 100644 roles/lib_openshift/src/class/oc_user.py create mode 100644 roles/lib_openshift/src/doc/user create mode 100644 roles/lib_openshift/src/lib/user.py create mode 100755 roles/lib_openshift/src/test/integration/oc_user.yml create mode 100755 roles/lib_openshift/src/test/unit/oc_user.py (limited to 'roles/lib_openshift/src') diff --git a/roles/lib_openshift/src/ansible/oc_user.py b/roles/lib_openshift/src/ansible/oc_user.py new file mode 100644 index 000000000..6b1440796 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_user.py @@ -0,0 +1,34 @@ +# pylint: skip-file +# flake8: noqa + +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() diff --git a/roles/lib_openshift/src/class/oc_user.py b/roles/lib_openshift/src/class/oc_user.py new file mode 100644 index 000000000..17b679289 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_user.py @@ -0,0 +1,227 @@ +# pylint: skip-file +# flake8: noqa + +# 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 service''' + if not self._user: + self.get() + return self._user + + @user.setter + def user(self, data): + ''' setter function for yedit var ''' + 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"} diff --git a/roles/lib_openshift/src/doc/user b/roles/lib_openshift/src/doc/user new file mode 100644 index 000000000..cae108415 --- /dev/null +++ b/roles/lib_openshift/src/doc/user @@ -0,0 +1,66 @@ +# flake8: noqa +# pylint: skip-file + +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 th user. + required: false + default: None + aliases: [] + groups: + description: + - List of groups the user should be a member of. + required: false + default: [] + aliases: [] +author: +- "Joel Diaz " +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: Ensure user exists + oc_user: + state: present + username: johndoe + full_name "John Doe" + +- name: Ensure user does not exist + oc_user: + state: absent + username: johndoe +''' diff --git a/roles/lib_openshift/src/lib/user.py b/roles/lib_openshift/src/lib/user.py new file mode 100644 index 000000000..a14d5fc91 --- /dev/null +++ b/roles/lib_openshift/src/lib/user.py @@ -0,0 +1,37 @@ +# pylint: skip-file +# flake8: noqa + + +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) diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index 91ee86750..a7204dce1 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -219,6 +219,17 @@ oc_service.py: - class/oc_service.py - ansible/oc_service.py +oc_user.py: +- doc/generated +- doc/license +- lib/import.py +- doc/user +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/user.py +- class/oc_user.py +- ansible/oc_user.py + oc_version.py: - doc/generated - doc/license diff --git a/roles/lib_openshift/src/test/integration/oc_user.yml b/roles/lib_openshift/src/test/integration/oc_user.yml new file mode 100755 index 000000000..7d6221e64 --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_user.yml @@ -0,0 +1,240 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +# +# ./oc_user.yml -e "cli_master_test=$OPENSHIFT_MASTER +# +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + + vars: + test_user: testuser@email.com + test_user_fullname: "Test User" + pre_tasks: + - name: ensure needed vars are defined + fail: + msg: "{{ item }} no defined" + when: "{{ item}} is not defined" + with_items: + - cli_master_test # ansible inventory instance to run playbook against + + tasks: + - name: delete test user (so future tests work) + oc_user: + state: absent + username: "{{ test_user }}" + + - name: get user list + oc_user: + state: list + username: "{{ test_user }}" + register: user_out + - name: "assert test user does not exist" + assert: + that: user_out['results'][0] == {} + msg: "{{ user_out }}" + + - name: get all list + oc_user: + state: list + register: user_out + #- debug: var=user_out + + - name: add test user + oc_user: + state: present + username: "{{ test_user }}" + full_name: "{{ test_user_fullname }}" + register: user_out + - name: assert result set to changed + assert: + that: user_out['changed'] == True + msg: "{{ user_out }}" + + - name: check test user actually added + oc_user: + state: list + username: "{{ test_user }}" + register: user_out + - name: assert user actually added + assert: + that: user_out['results'][0]['metadata']['name'] == "{{ test_user }}" and + user_out['results'][0]['fullName'] == "{{ test_user_fullname }}" + msg: "{{ user_out }}" + + - name: re-add test user + oc_user: + state: present + username: "{{ test_user }}" + full_name: "{{ test_user_fullname }}" + register: user_out + - name: assert re-add result set to not changed + assert: + that: user_out['changed'] == False + msg: "{{ user_out }}" + + - name: modify existing user + oc_user: + state: present + username: "{{ test_user }}" + full_name: 'Something Different' + register: user_out + - name: assert modify existing user result set to changed + assert: + that: user_out['changed'] == True + msg: "{{ user_out }}" + + - name: check modify test user + oc_user: + state: list + username: "{{ test_user }}" + register: user_out + - name: assert modification successful + assert: + that: user_out['results'][0]['metadata']['name'] == "{{ test_user }}" and + user_out['results'][0]['fullName'] == 'Something Different' + msg: "{{ user_out }}" + + - name: delete test user + oc_user: + state: absent + username: "{{ test_user }}" + register: user_out + - name: assert delete marked changed + assert: + that: user_out['changed'] == True + msg: "{{ user_out }}" + + - name: check delete user + oc_user: + state: list + username: "{{ test_user }}" + register: user_out + - name: assert deletion successful + assert: + that: user_out['results'][0] == {} + msg: "{{ user_out }}" + + - name: re-delete test user + oc_user: + state: absent + username: "{{ test_user }}" + register: user_out + - name: check re-delete marked not changed + assert: + that: user_out['changed'] == False + msg: "{{ user_out }}" + + - name: delete test group + oc_obj: + kind: group + state: absent + name: integration-test-group + + - name: create test group + command: oadm groups new integration-test-group + + - name: check group creation + oc_obj: + kind: group + state: list + name: integration-test-group + 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 + + - name: create user with group membership + oc_user: + state: present + username: "{{ test_user }}" + groups: + - "integration-test-group" + register: user_out + - debug: var=user_out + - name: get group user members + oc_obj: + kind: group + state: list + name: integration-test-group + register: user_out + - name: assert user group membership + assert: + that: "'{{ test_user }}' in user_out['results']['results'][0]['users'][0]" + msg: "{{ user_out }}" + + - name: delete second test group + oc_obj: + kind: group + state: absent + name: integration-test-group2 + + - name: create empty second group + command: oadm groups new integration-test-group2 + + - name: update user with second group membership + oc_user: + state: present + username: "{{ test_user }}" + groups: + - "integration-test-group" + - "integration-test-group2" + register: user_out + - name: assert adding more group changed + assert: + that: user_out['changed'] == True + + - name: get group memberships + oc_obj: + kind: group + state: list + name: "{{ item }}" + with_items: + - integration-test-group + - integration-test-group2 + register: user_out + - name: assert user member of above groups + assert: + that: "'{{ test_user }}' in user_out['results'][0]['results']['results'][0]['users'] and \ + '{{ test_user }}' in user_out['results'][1]['results']['results'][0]['users']" + msg: "{{ user_out }}" + + - name: update user with only one group + oc_user: + state: present + username: "{{ test_user }}" + groups: + - "integration-test-group2" + register: user_out + - assert: + that: user_out['changed'] == True + + - name: get group memberships + oc_obj: + kind: group + state: list + name: "{{ item }}" + with_items: + - "integration-test-group" + - "integration-test-group2" + register: user_out + - debug: var=user_out + - name: assert proper user membership + assert: + that: "'{{ test_user }}' not in user_out['results'][0]['results']['results'][0]['users'] and \ + '{{ test_user }}' in user_out['results'][1]['results']['results'][0]['users']" + + - name: clean up test groups + oc_obj: + kind: group + state: absent + name: "{{ item }}" + with_items: + - "integration-test-group" + - "integration-test-group2" + + - name: clean up test user + oc_user: + state: absent + username: "{{ test_user }}" diff --git a/roles/lib_openshift/src/test/unit/oc_user.py b/roles/lib_openshift/src/test/unit/oc_user.py new file mode 100755 index 000000000..920b06d0e --- /dev/null +++ b/roles/lib_openshift/src/test/unit/oc_user.py @@ -0,0 +1,117 @@ +#!/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() -- cgit v1.2.3