From 7ec8e4d0879bf97f1cbb8366c46bf0fe2accb10b Mon Sep 17 00:00:00 2001
From: Rafael Castillo <rcastill@redhat.com>
Date: Wed, 27 Apr 2022 14:18:21 -0700
Subject: [PATCH] Update project module to be compatible with new sdk

- Change sdk calls to use proxy objects
- Convert return values to dict before updating
- Adds additional test values

Change-Id: I187a27af4a5b8aa7cd4b60a1a876b5e5e6975144
---
 ci/roles/project/defaults/main.yml |  10 ++
 ci/roles/project/tasks/main.yml    | 171 +++++++++++++++++---
 plugins/modules/project.py         | 242 +++++++++++++++--------------
 3 files changed, 282 insertions(+), 141 deletions(-)
 create mode 100644 ci/roles/project/defaults/main.yml

diff --git a/ci/roles/project/defaults/main.yml b/ci/roles/project/defaults/main.yml
new file mode 100644
index 0000000..3a57b6b
--- /dev/null
+++ b/ci/roles/project/defaults/main.yml
@@ -0,0 +1,10 @@
+project_fields:
+  - description
+  - domain_id
+  - id
+  - is_domain
+  - is_enabled
+  - name
+  - options
+  - parent_id
+  - tags
diff --git a/ci/roles/project/tasks/main.yml b/ci/roles/project/tasks/main.yml
index 5fec2fa..ed64b26 100644
--- a/ci/roles/project/tasks/main.yml
+++ b/ci/roles/project/tasks/main.yml
@@ -1,28 +1,155 @@
 ---
-- name: Create project
+- name: Ensure project doesn't exist before tests
   openstack.cloud.project:
-     cloud: "{{ cloud }}"
-     state: present
-     name: ansible_project
-     description: dummy description
-     domain_id: default
-     enabled: True
-  register: project
+    cloud: "{{ cloud }}"
+    state: absent
+    name: ansible_project
 
-- debug: var=project
+- block:
+  - name: Create project
+    openstack.cloud.project:
+      cloud: "{{ cloud }}"
+      state: present
+      name: ansible_project
+      description: dummy description
+      domain: default
+      enabled: True
+    register: project
 
-- name: Update project
-  openstack.cloud.project:
-     cloud: "{{ cloud }}"
-     state: present
-     name: ansible_project
-     description: new description
-  register: updatedproject
+  - name: Assert project changed
+    assert:
+       that: project is changed
 
-- debug: var=updatedproject
+  - name: Assert project fields
+    assert:
+      that: item in project['project']
+    loop: "{{ project_fields }}"
 
-- name: Delete project
-  openstack.cloud.project:
-     cloud: "{{ cloud }}"
-     state: absent
-     name: ansible_project
+  - name: Get project
+    openstack.cloud.project_info:
+       cloud: "{{ cloud }}"
+       name: ansible_project
+    register: project_info
+
+  - name: Assert project
+    assert:
+      that:
+        - project_info.openstack_projects | length == 1
+        - project_info.openstack_projects[0]['name'] == 'ansible_project'
+        - project_info.openstack_projects[0]['description'] == 'dummy description'
+
+- block:
+  - name: Create identical project
+    openstack.cloud.project:
+      cloud: "{{ cloud }}"
+      state: present
+      name: ansible_project
+      description: dummy description
+      domain: default
+      enabled: True
+    register: project
+
+  - name: Assert project not changed
+    assert:
+       that: project is not changed
+
+  - name: Assert project fields
+    assert:
+      that: item in project['project']
+    loop: "{{ project_fields }}"
+
+
+- block:
+  - name: Update project
+    openstack.cloud.project:
+      cloud: "{{ cloud }}"
+      state: present
+      name: ansible_project
+      description: new description
+      properties:
+        tags:
+          - example_tag
+    register: project
+
+  - name: Assert project changed
+    assert:
+       that: project is changed
+
+  - name: Assert project fields
+    assert:
+      that: item in project['project']
+    loop: "{{ project_fields }}"
+
+  - name: Get project
+    openstack.cloud.project_info:
+       cloud: "{{ cloud }}"
+       name: ansible_project
+    register: project_info
+
+  - name: Assert project
+    assert:
+      that:
+        - project_info.openstack_projects | length == 1
+        - project_info.openstack_projects[0]['description'] == 'new description'
+
+- block:
+  - name: Delete project
+    openstack.cloud.project:
+       cloud: "{{ cloud }}"
+       state: absent
+       name: ansible_project
+    register: project
+
+  - name: Assert project changed
+    assert:
+       that: project is changed
+
+  - name: Get project
+    openstack.cloud.project_info:
+       cloud: "{{ cloud }}"
+       name: ansible_project
+    register: project_info
+
+  - name: Assert project deleted
+    assert:
+      that:
+        - project_info.openstack_projects | length == 0
+
+
+- block:
+  - name: Delete non existant project
+    openstack.cloud.project:
+       cloud: "{{ cloud }}"
+       state: absent
+       name: ansible_project
+    register: project
+
+  - name: Assert project not changed
+    assert:
+       that: project is not changed
+
+- block:
+  - name: Create project with properties
+    openstack.cloud.project:
+      cloud: "{{ cloud }}"
+      state: present
+      name: ansible_project
+      description: dummy description
+      domain: default
+      enabled: True
+      properties:
+        dummy_key: dummy_value
+    register: project
+
+- block:
+  - name: Update project with properties
+    openstack.cloud.project:
+      cloud: "{{ cloud }}"
+      state: present
+      name: ansible_project
+      description: dummy description
+      domain: default
+      enabled: True
+      properties:
+        dummy_key: other_dummy_value
+    register: project
diff --git a/plugins/modules/project.py b/plugins/modules/project.py
index 9719452..41fa3e0 100644
--- a/plugins/modules/project.py
+++ b/plugins/modules/project.py
@@ -14,37 +14,38 @@ description:
       The value for I(name) cannot be updated without deleting and
       re-creating the project.
 options:
-   name:
-     description:
-        - Name for the project
-     required: true
-     type: str
-   description:
-     description:
-        - Description for the project
-     type: str
-   domain_id:
-     description:
-        - Domain id to create the project in if the cloud supports domains.
-     aliases: ['domain']
-     type: str
-   enabled:
-     description:
-        - Is the project enabled
-     type: bool
-     default: 'yes'
-   properties:
-     description:
-        - Additional properties to be associated with this project. Requires
-          openstacksdk>0.45.
-     type: dict
-     required: false
-   state:
-     description:
-       - Should the resource be present or absent.
-     choices: [present, absent]
-     default: present
-     type: str
+  name:
+    description:
+      - Name for the project
+    required: true
+    type: str
+  description:
+    description:
+      - Description for the project
+    type: str
+  domain:
+    description:
+       - Domain name or id to create the project in if the cloud supports
+         domains.
+    aliases: ['domain_id']
+    type: str
+  is_enabled:
+    description:
+      - Is the project enabled
+    aliases: ['enabled']
+    type: bool
+    default: 'yes'
+  properties:
+    description:
+      - Additional properties to be associated with this project.
+    type: dict
+    required: false
+  state:
+    description:
+      - Should the resource be present or absent.
+    choices: [present, absent]
+    default: present
+    type: str
 requirements:
     - "python >= 3.6"
     - "openstacksdk"
@@ -61,8 +62,8 @@ EXAMPLES = '''
     state: present
     name: demoproject
     description: demodescription
-    domain_id: demoid
-    enabled: True
+    domain: demoid
+    is_enabled: True
     properties:
       internal_alias: demo_project
 
@@ -79,24 +80,40 @@ RETURN = '''
 project:
     description: Dictionary describing the project.
     returned: On success when I(state) is 'present'
-    type: complex
+    type: dict
     contains:
+        description:
+            description: Project description
+            type: str
+            sample: "demodescription"
+        domain_id:
+            description: domain to which the project belongs
+            type: str
+            sample: "default"
         id:
             description: Project ID
             type: str
             sample: "f59382db809c43139982ca4189404650"
+        is_domain:
+            description: Indicates whether the project also acts as a domain.
+            type: bool
+        is_enabled:
+            description: Indicates whether the project is enabled
+            type: bool
         name:
             description: Project name
             type: str
             sample: "demoproject"
-        description:
-            description: Project description
+        options:
+            description: The resource options for the project
+            type: dict
+        parent_id:
+            description: The ID of the parent of the project
             type: str
-            sample: "demodescription"
-        enabled:
-            description: Boolean to indicate if project is enabled
-            type: bool
-            sample: True
+        tags:
+            description: A list of associated tags
+            type: list
+            elements: str
 '''
 
 from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
@@ -106,109 +123,96 @@ class IdentityProjectModule(OpenStackModule):
     argument_spec = dict(
         name=dict(required=True),
         description=dict(required=False),
-        domain_id=dict(required=False, aliases=['domain']),
+        domain=dict(required=False, aliases=['domain_id']),
+        is_enabled=dict(default=True, type='bool', aliases=['enabled']),
         properties=dict(required=False, type='dict', min_ver='0.45.1'),
-        enabled=dict(default=True, type='bool'),
         state=dict(default='present', choices=['absent', 'present'])
     )
     module_kwargs = dict(
         supports_check_mode=True
     )
 
-    def _needs_update(self, project):
-        keys = ('description', 'enabled')
-        for key in keys:
-            if self.params[key] is not None and self.params[key] != project.get(key):
-                return True
+    def _needs_update(self, project, update, extra):
+        # We cannot update a project name because name find projects by name so
+        # only a project with an already matching name will be considered for
+        # updates
+        keys = ('description', 'is_enabled')
+        if any((k in update and update[k] != project[k]) for k in keys):
+            return True
 
-        properties = self.params['properties']
-        if properties:
-            project_properties = project.get('properties')
-            for k, v in properties.items():
-                if v is not None and (k not in project_properties or v != project_properties[k]):
-                    return True
+        # Additional keys passed by user will be checked completely
+        if extra and any(k not in project or extra[k] != project[k]
+                         for k in extra.keys()):
+            return True
 
         return False
 
-    def _system_state_change(self, project):
-        state = self.params['state']
+    def _get_domain_id(self, domain):
+        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 _system_state_change(self, state, project, attrs, extra_attrs):
         if state == 'present':
             if project is None:
-                changed = True
-            else:
-                if self._needs_update(project):
-                    changed = True
-                else:
-                    changed = False
-
-        elif state == 'absent':
-            changed = project is not None
-
-        return changed
+                return True
+            return self._needs_update(project, attrs, extra_attrs)
+        # Else state is absent
+        return project is not None
 
     def run(self):
         name = self.params['name']
-        description = self.params['description']
-        domain = self.params['domain_id']
-        enabled = self.params['enabled']
-        properties = self.params['properties'] or {}
+        domain = self.params['domain']
         state = self.params['state']
+        properties = self.params['properties']
+        enabled = self.params['is_enabled']
+        description = self.params['description']
 
+        find_project_kwargs = {}
+        domain_id = None
         if domain:
-            try:
-                # We assume admin is passing domain id
-                dom = self.conn.get_domain(domain)['id']
-                domain = dom
-            except Exception:
-                # If we fail, maybe admin is passing a domain name.
-                # Note that domains have unique names, just like id.
-                try:
-                    dom = self.conn.search_domains(filters={'name': domain})[0]['id']
-                    domain = dom
-                except Exception:
-                    # Ok, let's hope the user is non-admin and passing a sane id
-                    pass
-
-        if domain:
-            project = self.conn.get_project(name, domain_id=domain)
-        else:
-            project = self.conn.get_project(name)
+            domain_id = self._get_domain_id(domain)
+            find_project_kwargs['domain_id'] = domain_id
+
+        project = None
+        if name is not None:
+            project = self.conn.identity.find_project(
+                name, **find_project_kwargs)
+
+        project_attrs = {
+            'name': name,
+            'description': description,
+            'is_enabled': enabled,
+            'domain_id': domain_id,
+        }
+        project_attrs = {k: v for k, v in project_attrs.items()
+                         if v is not None}
+        # Add in arbitrary properties
+        if properties:
+            project_attrs.update(properties)
 
-        if self.ansible.check_mode:
-            self.exit_json(changed=self._system_state_change(project))
+        if self.check_mode:
+            self.exit_json(changed=self._system_state_change(state, project,
+                                                             project_attrs,
+                                                             properties))
 
+        changed = False
         if state == 'present':
             if project is None:
-                project = self.conn.create_project(
-                    name=name, description=description,
-                    domain_id=domain,
-                    enabled=enabled)
+                project = self.conn.identity.create_project(**project_attrs)
                 changed = True
-
-                project = self.conn.update_project(
-                    project['id'],
-                    description=description,
-                    enabled=enabled,
-                    **properties)
-            else:
-                if self._needs_update(project):
-                    project = self.conn.update_project(
-                        project['id'],
-                        description=description,
-                        enabled=enabled,
-                        **properties)
-                    changed = True
-                else:
-                    changed = False
-            self.exit_json(changed=changed, project=project)
-
-        elif state == 'absent':
-            if project is None:
-                changed = False
-            else:
-                self.conn.delete_project(project['id'])
+            elif self._needs_update(project, project_attrs, properties):
+                project = self.conn.identity.update_project(
+                    project, **project_attrs)
                 changed = True
-            self.exit_json(changed=changed)
+            self.exit_json(changed=changed,
+                           project=project.to_dict(computed=False))
+        elif state == 'absent' and project is not None:
+            self.conn.identity.delete_project(project['id'])
+            changed = True
+        self.exit_json(changed=changed)
 
 
 def main():
-- 
GitLab