diff --git a/ci/roles/project/defaults/main.yml b/ci/roles/project/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..3a57b6b40fc31860b02c0a1e1aa067a5f284747a --- /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 5fec2fac610a51635338a14aea7fc84c430d8457..ed64b26de1ad4a66f70504aa60a5576e495a6e07 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 9719452dc72d3989e3c53cde2e91b7d3e5000608..41fa3e0f6d056e0e6208ab067f04e18e014dbccd 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():