From f0cb7f680231bf17fdf043de4e5e8117c1aef097 Mon Sep 17 00:00:00 2001 From: Rafael Castillo <rcastill@redhat.com> Date: Thu, 28 Jul 2022 17:30:30 -0700 Subject: [PATCH] Update quota for 2.0.0 Add a test role to validate module functionality Replace calls to the sdk cloud layer to use the proxy layer. Update module parameters to use names matching the sdk. Keep aliases for old values. Remove _scrub_results, no longer necessary with proxy layer. Move check mode outside of main flow to keep the module readable. Refactor code to handle fields that should be ignored. Simplify return value from _system_state_change_details. Inline calls to fetch existing quotas. Remove metaprogramming calls to cloud layer methods, as the proxy layer doesn't have the same consistent API. Remove handling for case where neutron throws exception when unsetting quotas that aren't set. This is validated in the test role. Ensure return values are dicts. Replace exception handler with conditionals which allows us to drop the dependency on keystoneauth1 library and is much more correct than catching all exceptions and always printing the same error even on unrelated exceptions. Story: 2010099 Task: 45654 Change-Id: I5eda8e476a4e779382e6c63f5982504d5951501d --- .zuul.yaml | 1 + ci/roles/quota/defaults/main.yml | 33 ++ ci/roles/quota/tasks/main.yml | 131 +++++++ ci/run-collection.yml | 1 + plugins/modules/quota.py | 597 ++++++++++++++++--------------- 5 files changed, 483 insertions(+), 280 deletions(-) create mode 100644 ci/roles/quota/defaults/main.yml create mode 100644 ci/roles/quota/tasks/main.yml diff --git a/.zuul.yaml b/.zuul.yaml index 33f368a..32d47c9 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -96,6 +96,7 @@ port project project_info + quota recordset role_assignment router diff --git a/ci/roles/quota/defaults/main.yml b/ci/roles/quota/defaults/main.yml new file mode 100644 index 0000000..c9a1ce7 --- /dev/null +++ b/ci/roles/quota/defaults/main.yml @@ -0,0 +1,33 @@ +test_project: ansible_project +quota_name_frag: + cloud: "{{ cloud }}" + name: "{{ test_project }}" +test_network_quota: + floating_ips: 5 + networks: 50 + ports: 300 + rbac_policies: 5 + routers: 5 + security_group_rules: 5 + security_groups: 5 + subnet_pools: 5 + subnets: 5 +test_volume_quota: + backup_gigabytes: 500 + backups: 5 + gigabytes: 500 + groups: 1 + per_volume_gigabytes: 10 + snapshots: 5 + volumes: 5 +test_compute_quota: + cores: 5 + injected_file_content_bytes: 5 + injected_file_path_bytes: 5 + injected_files: 5 + instances: 5 + key_pairs: 5 + metadata_items: 5 + ram: 5 + server_group_members: 5 + server_groups: 5 diff --git a/ci/roles/quota/tasks/main.yml b/ci/roles/quota/tasks/main.yml new file mode 100644 index 0000000..0415db4 --- /dev/null +++ b/ci/roles/quota/tasks/main.yml @@ -0,0 +1,131 @@ +--- +- name: Create test project + openstack.cloud.project: + cloud: "{{ cloud }}" + state: present + name: "{{ test_project }}" + +- name: Clear quotas before tests + openstack.cloud.quota: + cloud: "{{ cloud }}" + state: absent + name: "{{ test_project }}" + register: default_quotas + +- name: Set network quota + openstack.cloud.quota: "{{ test_network_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Assert changed + assert: + that: quotas is changed + +- name: Assert field values + assert: + that: quotas.quotas.network[item.key] == item.value + loop: "{{ test_network_quota | dict2items }}" + +- name: Set network quota again + openstack.cloud.quota: "{{ test_network_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Assert not changed + assert: + that: quotas is not changed + +- name: Set volume quotas + openstack.cloud.quota: "{{ test_volume_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Assert changed + assert: + that: quotas is changed + +- name: Assert field values + assert: + that: quotas.quotas.volume[item.key] == item.value + loop: "{{ test_volume_quota | dict2items }}" + +- name: Set volume quotas again + openstack.cloud.quota: "{{ test_volume_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Assert not changed + assert: + that: quotas is not changed + +- name: Set compute quotas + openstack.cloud.quota: "{{ test_compute_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Assert changed + assert: + that: quotas is changed + +- name: Assert field values + assert: + that: quotas.quotas.compute[item.key] == item.value + loop: "{{ test_compute_quota | dict2items }}" + +- name: Set compute quotas again + openstack.cloud.quota: "{{ test_compute_quota | combine(quota_name_frag)}}" + register: quotas + +- name: Unset all quotas + openstack.cloud.quota: + cloud: "{{ cloud }}" + name: "{{ test_project }}" + state: absent + register: quotas + +- name: Assert defaults restore + assert: + that: quotas.quotas == default_quotas.quotas + +- name: Set all quotas at once + openstack.cloud.quota: + "{{ [test_network_quota, test_volume_quota, test_compute_quota, + quota_name_frag] | combine }}" + register: quotas + +- name: Assert changed + assert: + that: quotas is changed + +- name: Assert volume values + assert: + that: quotas.quotas.volume[item.key] == item.value + loop: "{{ test_volume_quota | dict2items }}" + +- name: Assert network values + assert: + that: quotas.quotas.network[item.key] == item.value + loop: "{{ test_network_quota | dict2items }}" + +- name: Assert compute values + assert: + that: quotas.quotas.compute[item.key] == item.value + loop: "{{ test_compute_quota | dict2items }}" + +- name: Set all quotas at once again + openstack.cloud.quota: + "{{ [test_network_quota, test_volume_quota, test_compute_quota, + quota_name_frag] | combine }}" + register: quotas + +- name: Assert not changed + assert: + that: quotas is not changed + +- name: Unset all quotas + openstack.cloud.quota: + cloud: "{{ cloud }}" + name: "{{ test_project }}" + state: absent + register: quotas + +- name: Delete test project + openstack.cloud.project: + cloud: "{{ cloud }}" + state: absent + name: "{{ test_project }}" diff --git a/ci/run-collection.yml b/ci/run-collection.yml index ed5efec..0725441 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -69,3 +69,4 @@ - role: loadbalancer tags: loadbalancer - { role: floating_ip, tags: floating_ip } + - { role: quota, tags: quota } diff --git a/plugins/modules/quota.py b/plugins/modules/quota.py index af9769c..7f25df4 100644 --- a/plugins/modules/quota.py +++ b/plugins/modules/quota.py @@ -14,17 +14,6 @@ description: updated or deleted using this module. A quota will be updated if matches an existing project and is present. options: - name: - description: - - Name of the OpenStack Project to manage. - required: true - type: str - state: - description: - - A value of present sets the quota and a value of absent resets the quota to system defaults. - default: present - type: str - choices: ['absent', 'present'] backup_gigabytes: description: Maximum size of backups in GB's. type: int @@ -35,75 +24,89 @@ options: description: Maximum number of CPU's per project. type: int fixed_ips: - description: Number of fixed IP's to allow. + description: + - Number of fixed IP's to allow. + - Available until Nova API version 2.35. type: int floating_ips: - description: Number of floating IP's to allow in Compute. - aliases: ['compute_floating_ips'] - type: int - floatingip: - description: Number of floating IP's to allow in Network. - aliases: ['network_floating_ips'] + description: Number of floating IP's to allow. + aliases: [compute_floating_ips, floatingip, network_floating_ips] type: int gigabytes: description: Maximum volume storage allowed for project. type: int - gigabytes_types: + groups: + description: Number of groups that are allowed for the project + type: int + injected_file_content_bytes: description: - - Per driver volume storage quotas. Keys should be - prefixed with C(gigabytes_) values should be ints. - type: dict - injected_file_size: - description: Maximum file size in bytes. + - Maximum file size in bytes. + - Available until Nova API version 2.56. type: int + aliases: [injected_file_size] injected_files: - description: Number of injected files to allow. + description: + - Number of injected files to allow. + - Available until Nova API version 2.56. type: int - injected_path_size: - description: Maximum path size. + injected_file_path_bytes: + description: + - Maximum path size. + - Available until Nova API version 2.56. type: int + aliases: [injected_path_size] instances: description: Maximum number of instances allowed. type: int key_pairs: description: Number of key pairs to allow. type: int - loadbalancer: - description: Number of load balancers to allow. + load_balancers: + description: The maximum amount of load balancers you can create type: int + aliases: [loadbalancer] metadata_items: description: Number of metadata items allowed per instance. type: int - network: + name: + description: Name of the OpenStack Project to manage. + required: true + type: str + networks: description: Number of networks to allow. type: int + aliases: [network] per_volume_gigabytes: description: Maximum size in GB's of individual volumes. type: int - pool: - description: Number of load balancer pools to allow. + pools: + description: The maximum number of pools you can create type: int - port: - description: Number of Network ports to allow, this needs to be greater than the instances limit. - type: int - properties: - description: Number of properties to allow. + aliases: [pool] + ports: + description: Number of Network ports to allow, this needs to be greater + than the instances limit. type: int + aliases: [port] ram: description: Maximum amount of ram in MB to allow. type: int - rbac_policy: + rbac_policies: description: Number of policies to allow. type: int - router: + aliases: [rbac_policy] + routers: description: Number of routers to allow. type: int - security_group_rule: + aliases: [router] + security_group_rules: description: Number of rules per security group to allow. type: int - security_group: + aliases: [security_group_rule] + security_groups: description: Number of security groups to allow. type: int + aliases: [security_group] server_group_members: description: Number of server group members to allow. type: int @@ -113,112 +116,190 @@ options: snapshots: description: Number of snapshots to allow. type: int - snapshots_types: - description: - - Per-driver volume snapshot quotas. Keys should be - prefixed with C(snapshots_) values should be ints. - type: dict - subnet: + state: + description: A value of C(present) sets the quota and a value of + C(absent) resets the quota to defaults. + default: present + type: str + choices: [absent, present] + subnets: description: Number of subnets to allow. type: int - subnetpool: + aliases: [subnet] + subnet_pools: description: Number of subnet pools to allow. type: int + aliases: [subnetpool] volumes: description: Number of volumes to allow. type: int - volumes_types: - description: - - Per-driver volume count quotas. Keys should be - prefixed with C(volumes_) values should be ints. - type: dict - project: - description: Unused, kept for compatability - type: int requirements: - "python >= 3.6" - - "openstacksdk >= 0.13.0" - - "keystoneauth1 >= 3.4.0" + - openstacksdk extends_documentation_fragment: - openstack.cloud.openstack ''' EXAMPLES = ''' -# List a Project Quota -- openstack.cloud.quota: +- name: Fetch current project quota + openstack.cloud.quota: cloud: mycloud name: demoproject -# Set a Project back to the defaults -- openstack.cloud.quota: +- name: Reset project quota back to defaults + openstack.cloud.quota: cloud: mycloud name: demoproject state: absent -# Update a Project Quota for cores -- openstack.cloud.quota: +- name: Change number of cores and volumes + openstack.cloud.quota: cloud: mycloud name: demoproject cores: 100 - -# Update a Project Quota -- openstack.cloud.quota: - name: demoproject - cores: 1000 volumes: 20 - volumes_type: - - volume_lvm: 10 -# Complete example based on list of projects -- name: Update quotas +- name: Update quota again openstack.cloud.quota: - name: "{{ item.name }}" - backup_gigabytes: "{{ item.backup_gigabytes }}" - backups: "{{ item.backups }}" - cores: "{{ item.cores }}" - fixed_ips: "{{ item.fixed_ips }}" - floating_ips: "{{ item.floating_ips }}" - floatingip: "{{ item.floatingip }}" - gigabytes: "{{ item.gigabytes }}" - injected_file_size: "{{ item.injected_file_size }}" - injected_files: "{{ item.injected_files }}" - injected_path_size: "{{ item.injected_path_size }}" - instances: "{{ item.instances }}" - key_pairs: "{{ item.key_pairs }}" - loadbalancer: "{{ item.loadbalancer }}" - metadata_items: "{{ item.metadata_items }}" - per_volume_gigabytes: "{{ item.per_volume_gigabytes }}" - pool: "{{ item.pool }}" - port: "{{ item.port }}" - properties: "{{ item.properties }}" - ram: "{{ item.ram }}" - security_group_rule: "{{ item.security_group_rule }}" - security_group: "{{ item.security_group }}" - server_group_members: "{{ item.server_group_members }}" - server_groups: "{{ item.server_groups }}" - snapshots: "{{ item.snapshots }}" - volumes: "{{ item.volumes }}" - volumes_types: - volumes_lvm: "{{ item.volumes_lvm }}" - snapshots_types: - snapshots_lvm: "{{ item.snapshots_lvm }}" - gigabytes_types: - gigabytes_lvm: "{{ item.gigabytes_lvm }}" - with_items: - - "{{ projects }}" - when: item.state == "present" + cloud: mycloud + name: demo_project + floating_ips: 5 + networks: 50 + ports: 300 + rbac_policies: 5 + routers: 5 + subnets: 5 + subnet_pools: 5 + security_group_rules: 5 + security_groups: 5 + backup_gigabytes: 500 + backups: 5 + gigabytes: 500 + groups: 1 + pools: 5 + per_volume_gigabytes: 10 + snapshots: 5 + volumes: 5 + cores: 5 + instances: 5 + key_pairs: 5 + metadata_items: 5 + ram: 5 + server_groups: 5 + server_group_members: 5 + ''' RETURN = ''' -openstack_quotas: +quotas: description: Dictionary describing the project quota. returned: Regardless if changes where made or not type: dict + contains: + compute: + description: Compute service quotas + type: dict + contains: + cores: + description: Maximum number of CPU's per project. + type: int + injected_file_content_bytes: + description: Maximum file size in bytes. + type: int + injected_files: + description: Number of injected files to allow. + type: int + injected_file_path_bytes: + description: Maximum path size. + type: int + instances: + description: Maximum number of instances allowed. + type: int + key_pairs: + description: Number of key pairs to allow. + type: int + metadata_items: + description: Number of metadata items allowed per instance. + type: int + ram: + description: Maximum amount of ram in MB to allow. + type: int + server_group_members: + description: Number of server group members to allow. + type: int + server_groups: + description: Number of server groups to allow. + type: int + network: + description: Network service quotas + type: dict + contains: + floating_ips: + description: Number of floating IP's to allow. + type: int + load_balancers: + description: The maximum amount of load balancers one can + create + type: int + networks: + description: Number of networks to allow. + type: int + pools: + description: The maximum amount of pools one can create. + type: int + ports: + description: Number of Network ports to allow, this needs + to be greater than the instances limit. + type: int + rbac_policies: + description: Number of policies to allow. + type: int + routers: + description: Number of routers to allow. + type: int + security_group_rules: + description: Number of rules per security group to allow. + type: int + security_groups: + description: Number of security groups to allow. + type: int + subnet_pools: + description: Number of subnet pools to allow. + type: int + subnets: + description: Number of subnets to allow. + type: int + volume: + description: Block storage service quotas + type: dict + contains: + backup_gigabytes: + description: Maximum size of backups in GB's. + type: int + backups: + description: Maximum number of backups allowed. + type: int + gigabytes: + description: Maximum volume storage allowed for project. + type: int + groups: + description: Number of groups that are allowed for the + project + type: int + per_volume_gigabytes: + description: Maximum size in GB's of individual volumes. + type: int + snapshots: + description: Number of snapshots to allow. + type: int + volumes: + description: Number of volumes to allow. + type: int sample: - openstack_quotas: { - compute: { + quotas: + compute: cores: 150, fixed_ips: -1, floating_ips: 10, @@ -228,146 +309,134 @@ openstack_quotas: instances: 100, key_pairs: 100, metadata_items: 128, + networks: -1, ram: 153600, - security_group_rules: 20, - security_groups: 10, + security_group_rules: -1, + security_groups: -1, server_group_members: 10, - server_groups: 10 - }, - network: { - floatingip: 50, - loadbalancer: 10, - network: 10, - pool: 10, - port: 160, - rbac_policy: 10, - router: 10, - security_group: 10, - security_group_rule: 100, - subnet: 10, - subnetpool: -1 - }, - volume: { + server_groups: 10, + network: + floating_ips: 50, + load_balancers: 10, + networks: 10, + pools: 10, + ports: 160, + rbac_policies: 10, + routers: 10, + security_group_rules: 100, + security_groups: 10, + subnet_pools: -1, + subnets: 10, + volume: backup_gigabytes: 1000, backups: 10, gigabytes: 1000, - gigabytes_lvm: -1, + groups: 10, per_volume_gigabytes: -1, snapshots: 10, - snapshots_lvm: -1, volumes: 10, - volumes_lvm: -1 - } - } - ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +from collections import defaultdict class QuotaModule(OpenStackModule): + # TODO: Add missing network quota options 'check_limit', 'health_monitors', + # 'l7_policies', 'listeners' to argument_spec, DOCUMENTATION and + # RETURN docstrings argument_spec = dict( - name=dict(required=True), - state=dict(default='present', choices=['absent', 'present']), backup_gigabytes=dict(type='int'), backups=dict(type='int'), cores=dict(type='int'), fixed_ips=dict(type='int'), - floating_ips=dict(type='int', aliases=['compute_floating_ips']), - floatingip=dict(type='int', aliases=['network_floating_ips']), + floating_ips=dict( + type='int', aliases=['floatingip', 'compute_floating_ips', + 'network_floating_ips']), gigabytes=dict(type='int'), - gigabytes_types=dict(type='dict', default={}), - injected_file_size=dict(type='int'), + groups=dict(type='int'), + injected_file_content_bytes=dict(type='int', + aliases=['injected_file_size']), + injected_file_path_bytes=dict(type='int', + aliases=['injected_path_size']), injected_files=dict(type='int'), - injected_path_size=dict(type='int'), instances=dict(type='int'), key_pairs=dict(type='int', no_log=False), - loadbalancer=dict(type='int'), + load_balancers=dict(type='int', aliases=['loadbalancer']), metadata_items=dict(type='int'), - network=dict(type='int'), + name=dict(required=True), + networks=dict(type='int', aliases=['network']), per_volume_gigabytes=dict(type='int'), - pool=dict(type='int'), - port=dict(type='int'), - project=dict(type='int'), - properties=dict(type='int'), + pools=dict(type='int', aliases=['pool']), + ports=dict(type='int', aliases=['port']), ram=dict(type='int'), - rbac_policy=dict(type='int'), - router=dict(type='int'), - security_group_rule=dict(type='int'), - security_group=dict(type='int'), + rbac_policies=dict(type='int', aliases=['rbac_policy']), + routers=dict(type='int', aliases=['router']), + security_group_rules=dict(type='int', aliases=['security_group_rule']), + security_groups=dict(type='int', aliases=['security_group']), server_group_members=dict(type='int'), server_groups=dict(type='int'), snapshots=dict(type='int'), - snapshots_types=dict(type='dict', default={}), - subnet=dict(type='int'), - subnetpool=dict(type='int'), + state=dict(default='present', choices=['absent', 'present']), + subnet_pools=dict(type='int', aliases=['subnetpool']), + subnets=dict(type='int', aliases=['subnet']), volumes=dict(type='int'), - volumes_types=dict(type='dict', default={}) ) module_kwargs = dict( supports_check_mode=True ) - def _get_volume_quotas(self, project): - return self.conn.get_volume_quotas(project) - - def _get_network_quotas(self, project): - return self.conn.get_network_quotas(project) - - def _get_compute_quotas(self, project): - return self.conn.get_compute_quotas(project) + # Some attributes in quota resources don't exist in the api anymore, mostly + # compute quotas that were simply network proxies. This map allows marking + # them to be skipped. + exclusion_map = { + 'compute': { + # 'fixed_ips', # Available until Nova API version 2.35 + 'floating_ips', # Available until Nova API version 2.35 + 'name', + 'networks', # Available until Nova API version 2.35 + 'security_group_rules', # Available until Nova API version 2.35 + 'security_groups', # Available until Nova API version 2.35 + # 'injected_file_content_bytes', # Available until + # 'injected_file_path_bytes', # Nova API + # 'injected_files', # version 2.56 + }, + 'network': {'name'}, + 'volume': {'name'}, + } def _get_quotas(self, project): quota = {} - try: - quota['volume'] = self._get_volume_quotas(project) - except Exception: - self.warn("No public endpoint for volumev2 service was found. Ignoring volume quotas.") - - try: - quota['network'] = self._get_network_quotas(project) - except Exception: - self.warn("No public endpoint for network service was found. Ignoring network quotas.") - - quota['compute'] = self._get_compute_quotas(project) - - for quota_type in quota.keys(): - quota[quota_type] = self._scrub_results(quota[quota_type]) - - return quota + if self.conn.has_service('block-storage'): + quota['volume'] = self.conn.block_storage.get_quota_set(project) + else: + self.warn('Block storage service aka volume service is not' + ' supported by your cloud. Ignoring volume quotas.') - def _scrub_results(self, quota): - filter_attr = [ - 'HUMAN_ID', - 'NAME_ATTR', - 'human_id', - 'request_ids', - 'x_openstack_request_ids', - ] + if self.conn.has_service('network'): + quota['network'] = self.conn.network.get_quota(project.id) + else: + self.warn('Network service is not supported by your cloud.' + ' Ignoring network quotas.') - for attr in filter_attr: - if attr in quota: - del quota[attr] + quota['compute'] = self.conn.compute.get_quota_set(project.id) return quota - def _system_state_change_details(self, project_quota_output): - quota_change_request = {} - changes_required = False - - for quota_type in project_quota_output.keys(): - for quota_option in project_quota_output[quota_type].keys(): - if quota_option in self.params and self.params[quota_option] is not None: - if project_quota_output[quota_type][quota_option] != self.params[quota_option]: - changes_required = True + def _build_update(self, quotas): + changes = defaultdict(dict) - if quota_type not in quota_change_request: - quota_change_request[quota_type] = {} + for quota_type in quotas.keys(): + exclusions = self.exclusion_map[quota_type] + for attr in quotas[quota_type].keys(): + if attr in exclusions: + continue + if (attr in self.params and self.params[attr] is not None + and quotas[quota_type][attr] != self.params[attr]): + changes[quota_type][attr] = self.params[attr] - quota_change_request[quota_type][quota_option] = self.params[quota_option] - - return (changes_required, quota_change_request) + return changes def _system_state_change(self, project_quota_output): """ @@ -377,86 +446,54 @@ class QuotaModule(OpenStackModule): the desired quota settings set on the module params. """ - changes_required, quota_change_request = self._system_state_change_details( - project_quota_output - ) - - if changes_required: + if self.params['state'] == 'absent': return True - else: - return False - def run(self): - cloud_params = dict(self.params) - - # In order to handle the different volume types we update module params after. - dynamic_types = [ - 'gigabytes_types', - 'snapshots_types', - 'volumes_types', - ] + return bool(self._build_update(project_quota_output)) - for dynamic_type in dynamic_types: - for k, v in self.params[dynamic_type].items(): - self.params[k] = int(v) + def run(self): + project = self.conn.identity.find_project( + self.params['name'], ignore_missing=False) # Get current quota values - project_quota_output = self._get_quotas(cloud_params['name']) - changes_required = False - - if self.params['state'] == "absent": - # If a quota state is set to absent we should assume there will be changes. - # The default quota values are not accessible so we can not determine if - # no changes will occur or not. - if self.ansible.check_mode: - self.exit_json(changed=True) - - # Calling delete_network_quotas when a quota has not been set results - # in an error, according to the sdk docs it should return the - # current quota. - # The following error string is returned: - # network client call failed: Quota for tenant 69dd91d217e949f1a0b35a4b901741dc could not be found. - neutron_msg1 = "network client call failed: Quota for tenant" - neutron_msg2 = "could not be found" - - for quota_type in project_quota_output.keys(): - quota_call = getattr(self.conn, 'delete_%s_quotas' % (quota_type)) - try: - quota_call(cloud_params['name']) - except Exception as e: - error_msg = str(e) - if error_msg.find(neutron_msg1) > -1 and error_msg.find(neutron_msg2) > -1: - pass - else: - self.fail_json(msg=str(e), extra_data=e.extra_data) - - project_quota_output = self._get_quotas(cloud_params['name']) - changes_required = True - - elif self.params['state'] == "present": - if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change( - project_quota_output)) - - changes_required, quota_change_request = self._system_state_change_details( - project_quota_output - ) - - if changes_required: - for quota_type in quota_change_request.keys(): - quota_call = getattr(self.conn, 'set_%s_quotas' % (quota_type)) - quota_call(cloud_params['name'], **quota_change_request[quota_type]) - - # Get quota state post changes for validation - project_quota_update = self._get_quotas(cloud_params['name']) - - if project_quota_output == project_quota_update: - self.fail_json(msg='Could not apply quota update') - - project_quota_output = project_quota_update - - self.exit_json( - changed=changes_required, openstack_quotas=project_quota_output) + quotas = self._get_quotas(project) + + changed = False + + if self.ansible.check_mode: + self.exit_json(changed=self._system_state_change(quotas)) + + if self.params['state'] == 'absent': + # If a quota state is set to absent we should assume there will be + # changes. The default quota values are not accessible so we can + # not determine if no changes will occur or not. + changed = True + self.conn.compute.revert_quota_set(project) + if 'network' in quotas: + self.conn.network.delete_quota(project.id) + if 'volume' in quotas: + self.conn.block_storage.revert_quota_set(project) + + # Necessary since we can't tell what the default quotas are + quotas = self._get_quotas(project) + + elif self.params['state'] == 'present': + changes = self._build_update(quotas) + + if changes: + if 'volume' in changes: + self.conn.block_storage.update_quota_set( + quotas['volume'], **changes['volume']) + if 'compute' in changes: + self.conn.compute.update_quota_set( + quotas['compute'], **changes['compute']) + if 'network' in changes: + quotas['network'] = self.conn.network.update_quota( + project.id, **changes['network']) + changed = True + + quotas = {k: v.to_dict(computed=False) for k, v in quotas.items()} + self.exit_json(changed=changed, quotas=quotas) def main(): -- GitLab