Browse Source

add oc_user ansible module

module to manipulate OpenShift users, and assign group membership to users
Joel Diaz 8 years ago
parent
commit
45fbfdad1b

File diff suppressed because it is too large
+ 1544 - 0
roles/lib_openshift/library/oc_user.py


+ 34 - 0
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()

+ 227 - 0
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"}

+ 66 - 0
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 <jdiaz@redhat.com>"
+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
+'''

+ 37 - 0
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)

+ 11 - 0
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

+ 240 - 0
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 }}"

+ 117 - 0
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()