From 4f8f6ffaf49a19a40370c81a5c1f86b05c89f826 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Thu, 10 Feb 2022 09:44:04 -0700 Subject: [PATCH] Backported changes to identity_user from master branch Renamed ci integration tests to match module name, updated return value documentation and refactored to simplify code. The module will no longer fail if no password is supplied since it is perfectly fine to create a user with an password. Reverted function calls which would break backward compatibility with previous collection releases. Change-Id: I97ee9b626f269abde3be7b2b9211d2bb5b7b3c26 (cherry picked from commit fd1b9fc0d2ff09e5fe18af30117e08679f158a3a) --- ci/roles/identity_user/defaults/main.yml | 9 ++ ci/roles/identity_user/tasks/main.yml | 197 +++++++++++++++++++++++ ci/roles/user/tasks/main.yml | 30 ---- ci/run-collection.yml | 2 +- plugins/modules/identity_user.py | 154 ++++++++---------- 5 files changed, 272 insertions(+), 120 deletions(-) create mode 100644 ci/roles/identity_user/defaults/main.yml create mode 100644 ci/roles/identity_user/tasks/main.yml delete mode 100644 ci/roles/user/tasks/main.yml diff --git a/ci/roles/identity_user/defaults/main.yml b/ci/roles/identity_user/defaults/main.yml new file mode 100644 index 00000000..5514e34c --- /dev/null +++ b/ci/roles/identity_user/defaults/main.yml @@ -0,0 +1,9 @@ +os_identity_user_fields: + - default_project_id + - description + - domain_id + - email + - enabled + - id + - name + - username diff --git a/ci/roles/identity_user/tasks/main.yml b/ci/roles/identity_user/tasks/main.yml new file mode 100644 index 00000000..7696fdbd --- /dev/null +++ b/ci/roles/identity_user/tasks/main.yml @@ -0,0 +1,197 @@ +--- +- name: setup + block: + - name: Delete user before running tests + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: "{{ item }}" + loop: + - ansible_user + - ansible_user2 + register: user + +- block: + - name: Delete unexistent user + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user + register: user + + - name: Ensure user was not changed + assert: + that: user is not changed + +- block: + - name: Create a user without a password + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + + - name: Ensure user was changed + assert: + that: user is changed + + - name: Ensure user has fields + assert: + that: item in user['user'] + loop: "{{ os_identity_user_fields }}" + + - name: Fail when update_password is always but no password specified + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + update_password: always + email: ansible.user@nowhere.net + domain: default + default_project: demo + register: user + ignore_errors: yes + + - assert: + that: user.msg == "update_password is always but a password value is missing" + + - name: Delete user + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user + +- block: + - name: Create user with a password + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + update_password: on_create + domain: default + default_project: demo + register: user + + - name: Assert user has fields + assert: + that: item in user['user'] + loop: "{{ os_identity_user_fields }}" + +- block: + - name: Create identical user + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret + email: ansible.user@nowhere.net + update_password: on_create + domain: default + default_project: demo + register: user + + - name: Assert user was not changed + assert: + that: user is not changed + + - name: Assert user has fields + assert: + that: item in user['user'] + loop: "{{ os_identity_user_fields }}" + +- block: + - name: Update user with password + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + password: secret2 + email: updated.ansible.user@nowhere.net + register: user + + - name: Ensure user was changed + assert: + that: user is changed + + - name: Ensure user has fields + assert: + that: item in user['user'] + loop: "{{ os_identity_user_fields }}" + +- name: Update user without password and update_password set to always + block: + - openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + update_password: always + email: updated.ansible.user@nowhere.net + register: user + ignore_errors: yes + + - assert: + that: user.msg == "update_password is always but a password value is missing" + +- block: + - name: Ensure user with update_password set to on_create + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + update_password: on_create + password: secret3 + email: updated.ansible.user@nowhere.net + register: user + + - name: Ensure user was not changed + assert: + that: user is not changed + +- block: + - name: Ensure user with update_password set to always + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user + update_password: always + password: secret3 + email: updated.ansible.user@nowhere.net + register: user + + - name: Ensure user was changed + assert: + that: user is changed + +- block: + - name: Create user without a password + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: present + name: ansible_user2 + password: secret + email: ansible.user2@nowhere.net + update_password: on_create + domain: default + default_project: demo + register: user + + - name: Assert user has fields + assert: + that: item in user['user'] + loop: "{{ os_identity_user_fields }}" + +- block: + - name: Delete user + openstack.cloud.identity_user: + cloud: "{{ cloud }}" + state: absent + name: ansible_user + + - name: Ensure user was changed + assert: + that: user is changed diff --git a/ci/roles/user/tasks/main.yml b/ci/roles/user/tasks/main.yml deleted file mode 100644 index 6e9c49e3..00000000 --- a/ci/roles/user/tasks/main.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- name: Create user - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: present - name: ansible_user - password: secret - email: ansible.user@nowhere.net - domain: default - default_project: demo - register: user - -- debug: var=user - -- name: Update user - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: present - name: ansible_user - password: secret - email: updated.ansible.user@nowhere.net - register: updateduser - -- debug: var=updateduser - -- name: Delete user - openstack.cloud.identity_user: - cloud: "{{ cloud }}" - state: absent - name: ansible_user diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 4a80e3cf..0d99531b 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -16,6 +16,7 @@ tags: dns when: sdk_version is version(0.28, '>=') - { role: floating_ip_info, tags: floating_ip_info } + - { role: identity_user, tags: identity_user } - { role: identity_user_info, tags: identity_user_info } - { role: identity_role, tags: identity_role } - { role: image, tags: image } @@ -49,7 +50,6 @@ - { role: server, tags: server } - { role: subnet, tags: subnet } - { role: subnet_pool, tags: subnet_pool } - - { role: user, tags: user } - { role: user_group, tags: user_group } - { role: user_role, tags: user_role } - { role: volume, tags: volume } diff --git a/plugins/modules/identity_user.py b/plugins/modules/identity_user.py index ac7a7585..047b3ed8 100644 --- a/plugins/modules/identity_user.py +++ b/plugins/modules/identity_user.py @@ -26,6 +26,7 @@ options: update_password: required: false choices: ['always', 'on_create'] + default: on_create description: - C(always) will attempt to update password. C(on_create) will only set the password for newly created users. @@ -108,28 +109,46 @@ RETURN = ''' user: description: Dictionary describing the user. returned: On success when I(state) is 'present' - type: complex + type: dict contains: default_project_id: description: User default project ID. Only present with Keystone >= v3. + returned: success type: str sample: "4427115787be45f08f0ec22a03bfc735" + description: + description: The description of this user + returned: success + type: str + sample: "a user" domain_id: description: User domain ID. Only present with Keystone >= v3. + returned: success type: str sample: "default" email: description: User email address + returned: success type: str sample: "demo@example.com" id: description: User ID + returned: success type: str sample: "f59382db809c43139982ca4189404650" + enabled: + description: Indicates whether the user is enabled + type: bool name: - description: User name + description: Unique user name, within the owning domain + returned: success type: str sample: "demouser" + username: + description: Username with Identity API v2 (OpenStack Pike or earlier) else Null + returned: success + type: str + ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule @@ -145,46 +164,37 @@ class IdentityUserModule(OpenStackModule): domain=dict(required=False, default=None), enabled=dict(default=True, type='bool'), state=dict(default='present', choices=['absent', 'present']), - update_password=dict(default=None, choices=['always', 'on_create']), + update_password=dict(default='on_create', choices=['always', 'on_create']), ) module_kwargs = dict() def _needs_update(self, params_dict, user): for k in params_dict: - if k not in ('password', 'update_password') and user[k] != params_dict[k]: + # We don't get password back in the user object, so assume any supplied + # password is a change. + if k == 'password': + return True + if k == 'default_project': + if user['default_project_id'] != params_dict['default_project']: + return True + else: + continue + if user[k] != params_dict[k]: return True - - # We don't get password back in the user object, so assume any supplied - # password is a change. - if ( - params_dict['password'] is not None - and params_dict['update_password'] == 'always' - ): - return True - return False def _get_domain_id(self, domain): - try: - # We assume admin is passing domain id - domain_id = self.conn.get_domain(domain)['id'] - except Exception: - # If we fail, maybe admin is passing a domain name. - # Note that domains have unique names, just like id. - try: - domain_id = self.conn.search_domains(filters={'name': domain})[0]['id'] - except Exception: - # Ok, let's hope the user is non-admin and passing a sane id - domain_id = domain - - return domain_id + dom_obj = self.conn.identity.find_domain(domain) + if dom_obj is None: + # Ok, let's hope the user is non-admin and passing a sane id + return domain + return dom_obj.id def _get_default_project_id(self, default_project, domain_id): - project = self.conn.get_project(default_project, domain_id=domain_id) + project = self.conn.identity.find_project(default_project, domain_id=domain_id) if not project: self.fail_json(msg='Default project %s is not valid' % default_project) - return project['id'] def run(self): @@ -198,84 +208,50 @@ class IdentityUserModule(OpenStackModule): update_password = self.params['update_password'] description = self.params['description'] - domain_id = None if domain: domain_id = self._get_domain_id(domain) user = self.conn.get_user(name, domain_id=domain_id) else: + domain_id = None user = self.conn.get_user(name) + changed = False if state == 'present': - if update_password in ('always', 'on_create'): - if not password: - msg = "update_password is %s but a password value is missing" % update_password - self.fail_json(msg=msg) - default_project_id = None + user_args = { + 'name': name, + 'email': email, + 'domain_id': domain_id, + 'description': description, + 'enabled': enabled, + } if default_project: default_project_id = self._get_default_project_id( default_project, domain_id) + user_args['default_project'] = default_project_id + user_args = {k: v for k, v in user_args.items() if v is not None} + changed = False if user is None: - if description is not None: - user = self.conn.create_user( - name=name, password=password, email=email, - default_project=default_project_id, domain_id=domain_id, - enabled=enabled, description=description) - else: - user = self.conn.create_user( - name=name, password=password, email=email, - default_project=default_project_id, domain_id=domain_id, - enabled=enabled) + if password: + user_args['password'] = password + + user = self.conn.create_user(**user_args) changed = True else: - params_dict = {'email': email, 'enabled': enabled, - 'password': password, - 'update_password': update_password} - if description is not None: - params_dict['description'] = description - if domain_id is not None: - params_dict['domain_id'] = domain_id - if default_project_id is not None: - params_dict['default_project_id'] = default_project_id + if update_password == 'always': + if not password: + self.fail_json(msg="update_password is always but a password value is missing") + user_args['password'] = password - if self._needs_update(params_dict, user): - if update_password == 'always': - if description is not None: - user = self.conn.update_user( - user['id'], password=password, email=email, - default_project=default_project_id, - domain_id=domain_id, enabled=enabled, description=description) - else: - user = self.conn.update_user( - user['id'], password=password, email=email, - default_project=default_project_id, - domain_id=domain_id, enabled=enabled) - else: - if description is not None: - user = self.conn.update_user( - user['id'], email=email, - default_project=default_project_id, - domain_id=domain_id, enabled=enabled, description=description) - else: - user = self.conn.update_user( - user['id'], email=email, - default_project=default_project_id, - domain_id=domain_id, enabled=enabled) + if self._needs_update(user_args, user): + user = self.conn.update_user(user['id'], **user_args) changed = True - else: - changed = False - self.exit_json(changed=changed, user=user) - elif state == 'absent': - if user is None: - changed = False - else: - if domain: - self.conn.delete_user(user['id'], domain_id=domain_id) - else: - self.conn.delete_user(user['id']) - changed = True - self.exit_json(changed=changed) + self.exit_json(changed=changed, user=user) + elif state == 'absent' and user is not None: + self.conn.identity.delete_user(user['id']) + changed = True + self.exit_json(changed=changed) def main():