From fd1b9fc0d2ff09e5fe18af30117e08679f158a3a Mon Sep 17 00:00:00 2001 From: Rafael Castillo Date: Thu, 10 Feb 2022 09:44:04 -0700 Subject: [PATCH] Use proxy layer in identity_user module This patch changes the module to use the sdk proxy layer and does some general refactoring to simplify the code. It will no longer fail if no password is supplied since it is perfectly fine to create a user with an password. Renamed the test role from user to identity_user to match the module name Change-Id: I97ee9b626f269abde3be7b2b9211d2bb5b7b3c26 --- .zuul.yaml | 1 + ci/roles/identity_user/defaults/main.yml | 11 ++ 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 | 162 ++++++++----------- 6 files changed, 281 insertions(+), 122 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/.zuul.yaml b/.zuul.yaml index be1752c0..0e59a645 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -70,6 +70,7 @@ dns_zone_info floating_ip_info group + identity_user identity_user_info identity_role image diff --git a/ci/roles/identity_user/defaults/main.yml b/ci/roles/identity_user/defaults/main.yml new file mode 100644 index 00000000..53954500 --- /dev/null +++ b/ci/roles/identity_user/defaults/main.yml @@ -0,0 +1,11 @@ +os_identity_user_fields: + - default_project_id + - description + - domain_id + - email + - id + - is_enabled + - links + - name + - password + - password_expires_at 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..99aeda02 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,55 @@ 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" + is_enabled: + description: Indicates whether the user is enabled + type: bool + links: + description: The links for the user resource + returned: success + type: dict + elements: str name: - description: User name + description: Unique user name, within the owning domain + returned: success type: str sample: "demouser" + password: + description: Credential used during authentication + returned: success + type: str + password_expires_at: + description: The date and time when the password expires. The time zone is UTC. A none value means the password never expires + returned: success + type: str + ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule @@ -145,46 +173,32 @@ 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 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): @@ -201,81 +215,47 @@ class IdentityUserModule(OpenStackModule): domain_id = None if domain: domain_id = self._get_domain_id(domain) - user = self.conn.get_user(name, domain_id=domain_id) - else: - user = self.conn.get_user(name) + user = self.conn.identity.find_user(name, domain_id=domain_id) + 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, + 'is_enabled': enabled, + } if default_project: default_project_id = self._get_default_project_id( default_project, domain_id) + user_args['default_project_id'] = 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.identity.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 + # else we do not want to update the 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.identity.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) + user = user.to_dict(computed=False) + self.exit_json(changed=changed, user=user) + elif state == 'absent' and user is not None: + self.conn.identity.delete_user(user) + changed = True + self.exit_json(changed=changed) def main():