summaryrefslogtreecommitdiffstats
path: root/ansible_collections/openstack/cloud/plugins/modules/quota.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:35 +0000
commit7fec0b69a082aaeec72fee0612766aa42f6b1b4d (patch)
treeefb569b86ca4da888717f5433e757145fa322e08 /ansible_collections/openstack/cloud/plugins/modules/quota.py
parentReleasing progress-linux version 7.7.0+dfsg-3~progress7.99u1. (diff)
downloadansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.tar.xz
ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.zip
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/openstack/cloud/plugins/modules/quota.py')
-rw-r--r--ansible_collections/openstack/cloud/plugins/modules/quota.py628
1 files changed, 331 insertions, 297 deletions
diff --git a/ansible_collections/openstack/cloud/plugins/modules/quota.py b/ansible_collections/openstack/cloud/plugins/modules/quota.py
index 0d6a4f04c..572d1d7fb 100644
--- a/ansible_collections/openstack/cloud/plugins/modules/quota.py
+++ b/ansible_collections/openstack/cloud/plugins/modules/quota.py
@@ -1,4 +1,6 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
# Copyright (c) 2016 Pason System Corporation
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@@ -12,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
@@ -33,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
@@ -111,112 +116,185 @@ 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"
-
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,
@@ -226,146 +304,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(
+ backup_gigabytes=dict(type='int'),
+ backups=dict(type='int'),
+ cores=dict(type='int'),
+ fixed_ips=dict(type='int'),
+ floating_ips=dict(
+ type='int', aliases=['floatingip', 'compute_floating_ips',
+ 'network_floating_ips']),
+ gigabytes=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'),
+ instances=dict(type='int'),
+ key_pairs=dict(type='int', no_log=False),
+ load_balancers=dict(type='int', aliases=['loadbalancer']),
+ metadata_items=dict(type='int'),
name=dict(required=True),
+ networks=dict(type='int', aliases=['network']),
+ per_volume_gigabytes=dict(type='int'),
+ pools=dict(type='int', aliases=['pool']),
+ ports=dict(type='int', aliases=['port']),
+ ram=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'),
state=dict(default='present', choices=['absent', 'present']),
- backup_gigabytes=dict(required=False, type='int', default=None),
- backups=dict(required=False, type='int', default=None),
- cores=dict(required=False, type='int', default=None),
- fixed_ips=dict(required=False, type='int', default=None),
- floating_ips=dict(required=False, type='int', default=None, aliases=['compute_floating_ips']),
- floatingip=dict(required=False, type='int', default=None, aliases=['network_floating_ips']),
- gigabytes=dict(required=False, type='int', default=None),
- gigabytes_types=dict(required=False, type='dict', default={}),
- injected_file_size=dict(required=False, type='int', default=None),
- injected_files=dict(required=False, type='int', default=None),
- injected_path_size=dict(required=False, type='int', default=None),
- instances=dict(required=False, type='int', default=None),
- key_pairs=dict(required=False, type='int', default=None, no_log=False),
- loadbalancer=dict(required=False, type='int', default=None),
- metadata_items=dict(required=False, type='int', default=None),
- network=dict(required=False, type='int', default=None),
- per_volume_gigabytes=dict(required=False, type='int', default=None),
- pool=dict(required=False, type='int', default=None),
- port=dict(required=False, type='int', default=None),
- project=dict(required=False, type='int', default=None),
- properties=dict(required=False, type='int', default=None),
- ram=dict(required=False, type='int', default=None),
- rbac_policy=dict(required=False, type='int', default=None),
- router=dict(required=False, type='int', default=None),
- security_group_rule=dict(required=False, type='int', default=None),
- security_group=dict(required=False, type='int', default=None),
- server_group_members=dict(required=False, type='int', default=None),
- server_groups=dict(required=False, type='int', default=None),
- snapshots=dict(required=False, type='int', default=None),
- snapshots_types=dict(required=False, type='dict', default={}),
- subnet=dict(required=False, type='int', default=None),
- subnetpool=dict(required=False, type='int', default=None),
- volumes=dict(required=False, type='int', default=None),
- volumes_types=dict(required=False, type='dict', default={})
+ subnet_pools=dict(type='int', aliases=['subnetpool']),
+ subnets=dict(type='int', aliases=['subnet']),
+ volumes=dict(type='int'),
)
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):
"""
@@ -375,86 +441,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():