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():