Expose volume & snapshot use_quota field

This patch adds a new REST API microversion to expose the new use_quota
DB field present in volumes and snapshots.

The field will be reported when listing and showing resources and can be
used when filtering as well.

The field is exposed in the REST API as `consumes_quota` to prevent
confusion for users and admins, since exposing it as `use_quota` may
give them the wrong impression that they can set it up for their own
purposes.

For users we say what is happening with the quota for that resource -it
consumes or doesn't consume quota-, whereas internally we express
instruction to the core code -whether to use quota or not-, hence the
difference in the naming.

APIImpact
DocImpact
Implements: blueprint temp-resources
Change-Id: I655a47fc75ddc11caf1defe984d9a66a9ad5a2e7
This commit is contained in:
Gorka Eguileor 2021-04-09 16:15:46 +02:00
parent 94dfad99c2
commit ec44fc8999
43 changed files with 733 additions and 60 deletions

View File

@ -214,6 +214,18 @@ detail:
in: query in: query
required: false required: false
type: boolean type: boolean
filter_consumes_quota:
description: |
Filters results by ``consumes_quota`` field. Resources that don't use
quotas are usually temporary internal resources created to perform an
operation. Default is to not filter by it. Filtering by this option may
not be always possible in a cloud, see
:ref:`List Resource Filters <resource-filters>` to determine whether this
filter is available in your cloud.
in: query
required: false
type: boolean
min_version: 3.65
filter_created_at: filter_created_at:
description: | description: |
Filters reuslts by a time that resources are created at with time Filters reuslts by a time that resources are created at with time
@ -841,6 +853,15 @@ consumer:
in: body in: body
required: false required: false
type: string type: string
consumes_quota:
description: |
Whether this resource consumes quota or not. Resources that not counted
for quota usage are usually temporary internal resources created to perform
an operation.
in: body
required: false
type: boolean
min_version: 3.65
container: container:
description: | description: |
The container name or null. The container name or null.

View File

@ -1,5 +1,7 @@
.. -*- rst -*- .. -*- rst -*-
.. _resource-filters:
Resource Filters (resource_filters) Resource Filters (resource_filters)
=================================== ===================================

View File

@ -0,0 +1,18 @@
{
"snapshot": {
"created_at": "2019-03-11T16:24:34.469003",
"description": "Daily backup",
"id": "b36476e5-d18b-47f9-ac69-4818cb43ee21",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "d291b81c-6e40-4525-8231-90aa1588121e",
"group_snapshot_id": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"consumes_quota": true
}
}

View File

@ -0,0 +1,20 @@
{
"snapshot": {
"created_at": "2019-03-12T04:42:00.809352",
"description": "Daily backup",
"id": "4a584cae-e4ce-429b-9154-d4c9eb8fda4c",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"os-extended-snapshot-attributes:progress": "0%",
"os-extended-snapshot-attributes:project_id": "89afd400-b646-4bbc-b12b-c0a4d63e5bd3",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "b72c48f1-64b7-4cd8-9745-b12e0be82d37",
"group_snapshot_id": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"consumes_quota": true
}
}

View File

@ -0,0 +1,18 @@
{
"snapshot": {
"created_at": "2019-03-12T04:53:53.426591",
"description": "This is yet, another snapshot.",
"id": "43666194-8e72-451a-b7bb-54fef763b2b8",
"metadata": {
"key": "v3"
},
"name": "snap-002",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "070c942d-9909-42e9-a467-7a781f150c58",
"group_snapshot_id": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"consumes_quota": true
}
}

View File

@ -0,0 +1,22 @@
{
"snapshots": [
{
"created_at": "2019-03-11T16:24:36.464445",
"description": "Daily backup",
"id": "d0083dc5-8795-4c1a-bc9c-74f70006c205",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"os-extended-snapshot-attributes:progress": "0%",
"os-extended-snapshot-attributes:project_id": "89afd400-b646-4bbc-b12b-c0a4d63e5bd3",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "7acd675e-4e06-4653-af9f-2ecd546342d6",
"group_snapshot_id": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"consumes_quota": true
}
]
}

View File

@ -21,8 +21,8 @@
], ],
"min_version": "3.0", "min_version": "3.0",
"status": "CURRENT", "status": "CURRENT",
"updated": "2021-05-30T00:00:00Z", "updated": "2021-08-25T00:00:00Z",
"version": "3.64" "version": "3.65"
} }
] ]
} }

View File

@ -21,8 +21,8 @@
], ],
"min_version": "3.0", "min_version": "3.0",
"status": "CURRENT", "status": "CURRENT",
"updated": "2021-05-30T00:00:00Z", "updated": "2021-08-25T00:00:00Z",
"version": "3.64" "version": "3.65"
} }
] ]
} }

View File

@ -0,0 +1,41 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "2018-11-28T06:21:12.715987",
"description": null,
"encrypted": false,
"id": "2b955850-f177-45f7-9f49-ecb2c256d161",
"links": [
{
"href": "http://127.0.0.1:33951/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/2b955850-f177-45f7-9f49-ecb2c256d161",
"rel": "self"
},
{
"href": "http://127.0.0.1:33951/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/2b955850-f177-45f7-9f49-ecb2c256d161",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"volume_type": "__DEFAULT__",
"group_id": null,
"provider_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "5fed9d7c-401d-46e2-8e80-f30c70cb7e1d",
"consumes_quota": true
}
}

View File

@ -0,0 +1,45 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "2018-11-29T06:50:07.770785",
"description": null,
"encrypted": false,
"id": "f7223234-1afc-4d19-bfa3-d19deb6235ef",
"links": [
{
"href": "http://127.0.0.1:45839/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/f7223234-1afc-4d19-bfa3-d19deb6235ef",
"rel": "self"
},
{
"href": "http://127.0.0.1:45839/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/f7223234-1afc-4d19-bfa3-d19deb6235ef",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"os-vol-host-attr:host": null,
"os-vol-mig-status-attr:migstat": null,
"os-vol-mig-status-attr:name_id": null,
"os-vol-tenant-attr:tenant_id": "89afd400-b646-4bbc-b12b-c0a4d63e5bd3",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"volume_type": "__DEFAULT__",
"provider_id": null,
"group_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "5fed9d7c-401d-46e2-8e80-f30c70cb7e1d",
"consumes_quota": true
}
}

View File

@ -0,0 +1,43 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "2018-11-29T06:59:23.679903",
"description": "This is yet, another volume.",
"encrypted": false,
"id": "8b2459d1-0059-4e14-a89f-dfa73a452af6",
"links": [
{
"href": "http://127.0.0.1:41467/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/8b2459d1-0059-4e14-a89f-dfa73a452af6",
"rel": "self"
},
{
"href": "http://127.0.0.1:41467/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/8b2459d1-0059-4e14-a89f-dfa73a452af6",
"rel": "bookmark"
}
],
"metadata": {
"name": "metadata0"
},
"migration_status": null,
"multiattach": false,
"name": "vol-003",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"volume_type": "__DEFAULT__",
"group_id": null,
"provider_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "5fed9d7c-401d-46e2-8e80-f30c70cb7e1d",
"consumes_quota": true
}
}

View File

@ -0,0 +1,47 @@
{
"volumes": [
{
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "2018-11-28T06:25:15.288987",
"description": null,
"encrypted": false,
"id": "cb49b381-9012-40cb-b8ee-80c19a4801b5",
"links": [
{
"href": "http://127.0.0.1:43543/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/cb49b381-9012-40cb-b8ee-80c19a4801b5",
"rel": "self"
},
{
"href": "http://127.0.0.1:43543/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/cb49b381-9012-40cb-b8ee-80c19a4801b5",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"os-vol-host-attr:host": null,
"os-vol-mig-status-attr:migstat": null,
"os-vol-mig-status-attr:name_id": null,
"os-vol-tenant-attr:tenant_id": "89afd400-b646-4bbc-b12b-c0a4d63e5bd3",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "c853ca26-e8ea-4797-8a52-ee124a013d0e",
"volume_type": "__DEFAULT__",
"volume_type_id": "5fed9d7c-401d-46e2-8e80-f30c70cb7e1d",
"provider_id": null,
"group_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"consumes_quota": true
}
]
}

View File

@ -67,6 +67,7 @@ Request
- offset: offset - offset: offset
- marker: marker - marker: marker
- with_count: with_count - with_count: with_count
- consumes_quota: filter_consumes_quota
Response Parameters Response Parameters
@ -89,11 +90,12 @@ Response Parameters
- updated_at: updated_at - updated_at: updated_at
- snapshots_links: links_snap - snapshots_links: links_snap
- group_snapshot_id: group_snapshot_id_3_14 - group_snapshot_id: group_snapshot_id_3_14
- consumes_quota: consumes_quota
Response Example (v3.41) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/snapshots/v3.41/snapshots-list-detailed-response.json .. literalinclude:: ./samples/snapshots/v3.65/snapshots-list-detailed-response.json
:language: javascript :language: javascript
@ -149,11 +151,12 @@ Response Parameters
- size: size - size: size
- updated_at: updated_at - updated_at: updated_at
- group_snapshot_id: group_snapshot_id_3_14 - group_snapshot_id: group_snapshot_id_3_14
- consumes_quota: consumes_quota
Response Example (v3.41) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/snapshots/v3.41/snapshot-create-response.json .. literalinclude:: ./samples/snapshots/v3.65/snapshot-create-response.json
:language: javascript :language: javascript
@ -189,6 +192,7 @@ Request
- limit: limit - limit: limit
- offset: offset - offset: offset
- marker: marker - marker: marker
- consumes_quota: filter_consumes_quota
- with_count: with_count - with_count: with_count
@ -388,11 +392,12 @@ Response Parameters
- metadata: metadata - metadata: metadata
- updated_at: updated_at - updated_at: updated_at
- group_snapshot_id: group_snapshot_id_3_14 - group_snapshot_id: group_snapshot_id_3_14
- consumes_quota: consumes_quota
Response Example (v3.41) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/snapshots/v3.41/snapshot-show-response.json .. literalinclude:: ./samples/snapshots/v3.65/snapshot-show-response.json
:language: javascript :language: javascript
@ -446,11 +451,12 @@ Response Parameters
- user_id: user_id_min - user_id: user_id_min
- metadata: metadata - metadata: metadata
- group_snapshot_id: group_snapshot_id_3_14 - group_snapshot_id: group_snapshot_id_3_14
- consumes_quota: consumes_quota
Response Example (v3.41) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/snapshots/v3.41/snapshot-update-response.json .. literalinclude:: ./samples/snapshots/v3.65/snapshot-update-response.json
:language: javascript :language: javascript

View File

@ -98,6 +98,7 @@ Request
- with_count: with_count - with_count: with_count
- created_at: filter_created_at - created_at: filter_created_at
- updated_at: filter_updated_at - updated_at: filter_updated_at
- consumes_quota: filter_consumes_quota
Response Parameters Response Parameters
@ -140,13 +141,14 @@ Response Parameters
- service_uuid: service_uuid - service_uuid: service_uuid
- shared_targets: shared_targets - shared_targets: shared_targets
- cluster_name: cluster_name - cluster_name: cluster_name
- consumes_quota: consumes_quota
- count: count - count: count
Response Example (v3.63) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/volumes/v3.63/volumes-list-detailed-response.json .. literalinclude:: ./samples/volumes/v3.65/volumes-list-detailed-response.json
:language: javascript :language: javascript
@ -258,11 +260,12 @@ Response Parameters
- service_uuid: service_uuid - service_uuid: service_uuid
- shared_targets: shared_targets - shared_targets: shared_targets
- cluster_name: cluster_name - cluster_name: cluster_name
- consumes_quota: consumes_quota
Response Example (v3.63) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/volumes/v3.63/volume-create-response.json .. literalinclude:: ./samples/volumes/v3.65/volume-create-response.json
:language: javascript :language: javascript
@ -301,6 +304,7 @@ Request
- marker: marker - marker: marker
- with_count: with_count - with_count: with_count
- created_at: filter_created_at - created_at: filter_created_at
- consumes_quota: filter_consumes_quota
- updated_at: filter_updated_at - updated_at: filter_updated_at
@ -394,12 +398,13 @@ Response Parameters
- cluster_name: cluster_name - cluster_name: cluster_name
- provider_id: provider_id - provider_id: provider_id
- group_id: group_id_optional - group_id: group_id_optional
- consumes_quota: consumes_quota
Response Example (v3.63) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/volumes/v3.63/volume-show-response.json .. literalinclude:: ./samples/volumes/v3.65/volume-show-response.json
:language: javascript :language: javascript
@ -473,12 +478,13 @@ Response Parameters
- service_uuid: service_uuid - service_uuid: service_uuid
- shared_targets: shared_targets - shared_targets: shared_targets
- cluster_name: cluster_name - cluster_name: cluster_name
- consumes_quota: consumes_quota
Response Example (v3.63) Response Example (v3.65)
------------------------ ------------------------
.. literalinclude:: ./samples/volumes/v3.63/volume-update-response.json .. literalinclude:: ./samples/volumes/v3.65/volume-update-response.json
:language: javascript :language: javascript

View File

@ -49,7 +49,8 @@ LOG = logging.getLogger(__name__)
_FILTERS_COLLECTION = None _FILTERS_COLLECTION = None
ATTRIBUTE_CONVERTERS = {'name~': 'display_name~', ATTRIBUTE_CONVERTERS = {'name~': 'display_name~',
'description~': 'display_description~'} 'description~': 'display_description~',
'consumes_quota': 'use_quota'}
METADATA_TYPES = enum.Enum('METADATA_TYPES', 'user image') METADATA_TYPES = enum.Enum('METADATA_TYPES', 'user image')

View File

@ -167,6 +167,8 @@ VOLUME_TYPE_ID_IN_VOLUME_DETAIL = '3.63'
ENCRYPTION_KEY_ID_IN_DETAILS = '3.64' ENCRYPTION_KEY_ID_IN_DETAILS = '3.64'
USE_QUOTA = '3.65'
def get_mv_header(version): def get_mv_header(version):
"""Gets a formatted HTTP microversion header. """Gets a formatted HTTP microversion header.

View File

@ -148,14 +148,17 @@ REST_API_VERSION_HISTORY = """
("GET /v3/{project_id}/volumes/detail") and volume-show ("GET /v3/{project_id}/volumes/detail") and volume-show
("GET /v3/{project_id}/volumes/{volume_id}") calls. ("GET /v3/{project_id}/volumes/{volume_id}") calls.
* 3.64 - Include 'encryption_key_id' in volume and backup details * 3.64 - Include 'encryption_key_id' in volume and backup details
* 3.65 - Include 'consumes_quota' in volume and snapshot details
- Accept 'consumes_quota' filter in volume and snapshot list
operation.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "3.0" _MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.64" _MAX_API_VERSION = "3.65"
UPDATED = "2021-05-30T00:00:00Z" UPDATED = "2021-08-25T00:00:00Z"
# NOTE(cyeoh): min and max versions declared as functions so we can # NOTE(cyeoh): min and max versions declared as functions so we can

View File

@ -53,16 +53,24 @@ class SnapshotsController(snapshots_v2.SnapshotsController):
LOG.debug('Could not evaluate value %s, assuming string', LOG.debug('Could not evaluate value %s, assuming string',
search_opts['metadata']) search_opts['metadata'])
if 'use_quota' in search_opts:
search_opts['use_quota'] = utils.get_bool_param('use_quota',
search_opts)
MV_ADDED_FILTERS = (
(mv.get_prior_version(mv.SNAPSHOT_LIST_METADATA_FILTER), 'metadata'),
# REST API receives consumes_quota, but process_general_filtering
# transforms it into use_quota
(mv.get_prior_version(mv.USE_QUOTA), 'use_quota'),
)
@common.process_general_filtering('snapshot') @common.process_general_filtering('snapshot')
def _process_snapshot_filtering(self, context=None, filters=None, def _process_snapshot_filtering(self, context=None, filters=None,
req_version=None): req_version=None):
"""Formats allowed filters""" """Formats allowed filters"""
for version, field in self.MV_ADDED_FILTERS:
# if the max version is less than SNAPSHOT_LIST_METADATA_FILTER if req_version.matches(None, version):
# metadata based filtering is not supported filters.pop(field, None)
if req_version.matches(
None, mv.get_prior_version(mv.SNAPSHOT_LIST_METADATA_FILTER)):
filters.pop('metadata', None)
# Filter out invalid options # Filter out invalid options
allowed_search_options = self._get_snapshot_filter_options() allowed_search_options = self._get_snapshot_filter_options()

View File

@ -27,9 +27,11 @@ class ViewBuilder(views_v2.ViewBuilder):
req_version = request.api_version_request req_version = request.api_version_request
# Add group_snapshot_id if min version is greater than or equal # Add group_snapshot_id if min version is greater than or equal
# to GROUP_SNAPSHOTS. # to GROUP_SNAPSHOTS.
snap = snapshot_ref['snapshot']
if req_version.matches(mv.GROUP_SNAPSHOTS, None): if req_version.matches(mv.GROUP_SNAPSHOTS, None):
snapshot_ref['snapshot']['group_snapshot_id'] = ( snap['group_snapshot_id'] = snapshot.get('group_snapshot_id')
snapshot.get('group_snapshot_id'))
if req_version.matches(mv.SNAPSHOT_LIST_USER_ID, None): if req_version.matches(mv.SNAPSHOT_LIST_USER_ID, None):
snapshot_ref['snapshot']['user_id'] = snapshot.get('user_id') snap['user_id'] = snapshot.get('user_id')
if req_version.matches(mv.USE_QUOTA):
snap['consumes_quota'] = snapshot.get('use_quota')
return snapshot_ref return snapshot_ref

View File

@ -77,6 +77,9 @@ class ViewBuilder(views_v2.ViewBuilder):
encryption_key_id != cinder_constants.FIXED_KEY_ID): encryption_key_id != cinder_constants.FIXED_KEY_ID):
volume_ref['volume']['encryption_key_id'] = encryption_key_id volume_ref['volume']['encryption_key_id'] = encryption_key_id
if req_version.matches(mv.USE_QUOTA):
volume_ref['volume']['consumes_quota'] = volume.get('use_quota')
return volume_ref return volume_ref
def _list_view(self, func, request, volumes, volume_count, def _list_view(self, func, request, volumes, volume_count,

View File

@ -80,18 +80,23 @@ class VolumeController(volumes_v2.VolumeController):
return webob.Response(status_int=HTTPStatus.ACCEPTED) return webob.Response(status_int=HTTPStatus.ACCEPTED)
MV_ADDED_FILTERS = (
(mv.get_prior_version(mv.VOLUME_LIST_GLANCE_METADATA),
'glance_metadata'),
(mv.get_prior_version(mv.VOLUME_LIST_GROUP), 'group_id'),
(mv.get_prior_version(mv.VOLUME_TIME_COMPARISON_FILTER), 'created_at'),
(mv.get_prior_version(mv.VOLUME_TIME_COMPARISON_FILTER), 'updated_at'),
# REST API receives consumes_quota, but process_general_filtering
# transforms it into use_quota
(mv.get_prior_version(mv.USE_QUOTA), 'use_quota'),
)
@common.process_general_filtering('volume') @common.process_general_filtering('volume')
def _process_volume_filtering(self, context=None, filters=None, def _process_volume_filtering(self, context=None, filters=None,
req_version=None): req_version=None):
if req_version.matches(None, mv.MESSAGES): for version, field in self.MV_ADDED_FILTERS:
filters.pop('glance_metadata', None) if req_version.matches(None, version):
filters.pop(field, None)
if req_version.matches(None, mv.BACKUP_UPDATE):
filters.pop('group_id', None)
if req_version.matches(None, mv.SUPPORT_TRANSFER_PAGINATION):
filters.pop('created_at', None)
filters.pop('updated_at', None)
api_utils.remove_invalid_filter_options( api_utils.remove_invalid_filter_options(
context, filters, context, filters,
@ -155,6 +160,9 @@ class VolumeController(volumes_v2.VolumeController):
if 'name' in filters: if 'name' in filters:
filters['display_name'] = filters.pop('name') filters['display_name'] = filters.pop('name')
if 'use_quota' in filters:
filters['use_quota'] = utils.get_bool_param('use_quota', filters)
self._handle_time_comparison_filters(filters) self._handle_time_comparison_filters(filters)
strict = req.api_version_request.matches( strict = req.api_version_request.matches(

View File

@ -0,0 +1,18 @@
{
"snapshot": {
"created_at": "%(strtime)s",
"description": "Daily backup",
"id": "%(uuid)s",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "%(uuid)s",
"group_snapshot_id": null,
"user_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,20 @@
{
"snapshot": {
"created_at": "%(strtime)s",
"description": "Daily backup",
"id": "%(uuid)s",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"os-extended-snapshot-attributes:progress": "0%",
"os-extended-snapshot-attributes:project_id": "%(uuid)s",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "%(uuid)s",
"group_snapshot_id": null,
"user_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,18 @@
{
"snapshot": {
"created_at": "%(strtime)s",
"description": "This is yet, another snapshot.",
"id": "%(uuid)s",
"metadata": {
"key": "v3"
},
"name": "snap-002",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "%(uuid)s",
"group_snapshot_id": null,
"user_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,22 @@
{
"snapshots": [
{
"created_at": "%(strtime)s",
"description": "Daily backup",
"id": "%(uuid)s",
"metadata": {
"key": "v3"
},
"name": "snap-001",
"os-extended-snapshot-attributes:progress": "0%",
"os-extended-snapshot-attributes:project_id": "%(uuid)s",
"size": 10,
"status": "creating",
"updated_at": null,
"volume_id": "%(uuid)s",
"group_snapshot_id": null,
"user_id": "%(uuid)s",
"consumes_quota": true
}
]
}

View File

@ -0,0 +1,41 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "%(strtime)s",
"description": null,
"encrypted": false,
"id": "%(uuid)s",
"links": [
{
"href": "%(host)s/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "%(uuid)s",
"volume_type": "__DEFAULT__",
"group_id": null,
"provider_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,45 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "%(strtime)s",
"description": null,
"encrypted": false,
"id": "%(uuid)s",
"links": [
{
"href": "%(host)s/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"os-vol-host-attr:host": null,
"os-vol-mig-status-attr:migstat": null,
"os-vol-mig-status-attr:name_id": null,
"os-vol-tenant-attr:tenant_id": "%(uuid)s",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "%(uuid)s",
"volume_type": "__DEFAULT__",
"provider_id": null,
"group_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,43 @@
{
"volume": {
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "%(strtime)s",
"description": "This is yet, another volume.",
"encrypted": false,
"id": "%(uuid)s",
"links": [
{
"href": "%(host)s/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {
"name": "metadata0"
},
"migration_status": null,
"multiattach": false,
"name": "vol-003",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "%(uuid)s",
"volume_type": "__DEFAULT__",
"group_id": null,
"provider_id": null,
"service_uuid": null,
"shared_targets": true,
"cluster_name": null,
"volume_type_id": "%(uuid)s",
"consumes_quota": true
}
}

View File

@ -0,0 +1,47 @@
{
"volumes": [
{
"attachments": [],
"availability_zone": "nova",
"bootable": "false",
"consistencygroup_id": null,
"created_at": "%(strtime)s",
"description": null,
"encrypted": false,
"id": "%(uuid)s",
"links": [
{
"href": "%(host)s/v3/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/89afd400-b646-4bbc-b12b-c0a4d63e5bd3/volumes/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {},
"migration_status": null,
"multiattach": false,
"name": null,
"os-vol-host-attr:host": null,
"os-vol-mig-status-attr:migstat": null,
"os-vol-mig-status-attr:name_id": null,
"os-vol-tenant-attr:tenant_id": "%(uuid)s",
"replication_status": null,
"size": 10,
"snapshot_id": null,
"source_volid": null,
"status": "creating",
"updated_at": null,
"user_id": "%(uuid)s",
"volume_type": "%(name)s",
"volume_type_id": "%(uuid)s",
"service_uuid": null,
"provider_id": null,
"group_id": null,
"shared_targets": true,
"cluster_name": null,
"consumes_quota": true
}
]
}

View File

@ -39,7 +39,8 @@ class SnapshotBaseTest(test_base.VolumesSampleBase):
@test_base.VolumesSampleBase.use_versions( @test_base.VolumesSampleBase.use_versions(
mv.BASE_VERSION, # 3.0 mv.BASE_VERSION, # 3.0
mv.GROUP_SNAPSHOTS, # 3.14 mv.GROUP_SNAPSHOTS, # 3.14
mv.SNAPSHOT_LIST_USER_ID) # 3.41 mv.SNAPSHOT_LIST_USER_ID, # 3.41
mv.USE_QUOTA) # 3.65
class SnapshotDetailTests(SnapshotBaseTest): class SnapshotDetailTests(SnapshotBaseTest):
"""Test snapshot details returned for operations with different MVs. """Test snapshot details returned for operations with different MVs.

View File

@ -22,7 +22,8 @@ from cinder.tests.functional import api_samples_test_base as test_base
mv.VOLUME_DETAIL_PROVIDER_ID, # 3.21 mv.VOLUME_DETAIL_PROVIDER_ID, # 3.21
mv.VOLUME_SHARED_TARGETS_AND_SERVICE_FIELDS, # 3.48 mv.VOLUME_SHARED_TARGETS_AND_SERVICE_FIELDS, # 3.48
mv.VOLUME_CLUSTER_NAME, # 3.61 mv.VOLUME_CLUSTER_NAME, # 3.61
mv.VOLUME_TYPE_ID_IN_VOLUME_DETAIL) # 3.63 mv.VOLUME_TYPE_ID_IN_VOLUME_DETAIL, # 3.63
mv.USE_QUOTA) # 3.65
class VolumeDetailTests(test_base.VolumesSampleBase): class VolumeDetailTests(test_base.VolumesSampleBase):
"""Test volume details returned for operations with different MVs. """Test volume details returned for operations with different MVs.

View File

@ -487,12 +487,13 @@ class GeneralFiltersTest(test.TestCase):
'expected': ["name", "status", "metadata", 'expected': ["name", "status", "metadata",
"bootable", "migration_status", "bootable", "migration_status",
"availability_zone", "group_id", "availability_zone", "group_id",
"size", "created_at", "updated_at"]}, "size", "created_at", "updated_at",
"consumes_quota"]},
{'resource': 'backup', {'resource': 'backup',
'expected': ["name", "status", "volume_id"]}, 'expected': ["name", "status", "volume_id"]},
{'resource': 'snapshot', {'resource': 'snapshot',
'expected': ["name", "status", "volume_id", "metadata", 'expected': ["name", "status", "volume_id", "metadata",
"availability_zone"]}, "availability_zone", "consumes_quota"]},
{'resource': 'group_snapshot', {'resource': 'group_snapshot',
'expected': ["name", "status", "group_id"]}, 'expected': ["name", "status", "group_id"]},
{'resource': 'attachment', {'resource': 'attachment',

View File

@ -115,6 +115,28 @@ class SnapshotApiTest(test.TestCase):
self.assertNotIn('group_snapshot_id', resp_dict['snapshot']) self.assertNotIn('group_snapshot_id', resp_dict['snapshot'])
self.assertNotIn('user_id', resp_dict['snapshot']) self.assertNotIn('user_id', resp_dict['snapshot'])
@ddt.data(
(True, True, mv.USE_QUOTA),
(True, False, mv.USE_QUOTA),
(False, True, mv.get_prior_version(mv.USE_QUOTA)),
(False, False, mv.get_prior_version(mv.USE_QUOTA)),
)
@ddt.unpack
def test_snapshot_show_with_use_quota(self, present, value, microversion):
volume = test_utils.create_volume(self.ctx, host='test_host1',
cluster_name='cluster1',
availability_zone='nova1')
snapshot = test_utils.create_snapshot(self.ctx, volume.id,
use_quota=value)
url = '/v3/snapshots?%s' % snapshot.id
req = fakes.HTTPRequest.blank(url, version=microversion)
res_dict = self.controller.show(req, snapshot.id)['snapshot']
if present:
self.assertIs(value, res_dict['consumes_quota'])
else:
self.assertNotIn('consumes_quota', res_dict)
def test_snapshot_show_invalid_id(self): def test_snapshot_show_invalid_id(self):
snapshot_id = INVALID_UUID snapshot_id = INVALID_UUID
req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id) req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id)
@ -133,25 +155,28 @@ class SnapshotApiTest(test.TestCase):
body = {"snapshot": snap} body = {"snapshot": snap}
self.controller.create(req, body=body) self.controller.create(req, body=body)
@ddt.data(('host', 'test_host1', True), ('cluster_name', 'cluster1', True), @ddt.data(('host', 'test_host1', True, mv.RESOURCE_FILTER),
('availability_zone', 'nova1', False)) ('cluster_name', 'cluster1', True, mv.RESOURCE_FILTER),
('availability_zone', 'nova1', False, mv.RESOURCE_FILTER),
('consumes_quota', 'true', False, mv.USE_QUOTA))
@ddt.unpack @ddt.unpack
def test_snapshot_list_with_filter(self, filter_name, filter_value, def test_snapshot_list_with_filter(self, filter_name, filter_value,
is_admin_user): is_admin_user, microversion):
volume1 = test_utils.create_volume(self.ctx, host='test_host1', volume1 = test_utils.create_volume(self.ctx, host='test_host1',
cluster_name='cluster1', cluster_name='cluster1',
availability_zone='nova1') availability_zone='nova1')
volume2 = test_utils.create_volume(self.ctx, host='test_host2', volume2 = test_utils.create_volume(self.ctx, host='test_host2',
cluster_name='cluster2', cluster_name='cluster2',
availability_zone='nova2') availability_zone='nova2')
snapshot1 = test_utils.create_snapshot(self.ctx, volume1.id) snapshot1 = test_utils.create_snapshot(self.ctx, volume1.id,
test_utils.create_snapshot(self.ctx, volume2.id) use_quota=True)
test_utils.create_snapshot(self.ctx, volume2.id, use_quota=False)
url = '/v3/snapshots?%s=%s' % (filter_name, filter_value) url = '/v3/snapshots?%s=%s' % (filter_name, filter_value)
# Generic filtering is introduced since '3,31' and we add # Generic filtering is introduced since '3,31' and we add
# 'availability_zone' support by using generic filtering. # 'availability_zone' support by using generic filtering.
req = fakes.HTTPRequest.blank(url, use_admin_context=is_admin_user, req = fakes.HTTPRequest.blank(url, use_admin_context=is_admin_user,
version=mv.RESOURCE_FILTER) version=microversion)
res_dict = self.controller.detail(req) res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['snapshots'])) self.assertEqual(1, len(res_dict['snapshots']))

View File

@ -343,6 +343,45 @@ class VolumeApiTest(test.TestCase):
volumes = res_dict['volumes'] volumes = res_dict['volumes']
self.assertEqual(2, len(volumes)) self.assertEqual(2, len(volumes))
@ddt.data(('true', 0), ('false', 1))
@ddt.unpack
def test_volume_list_with_quota_filter(self, use_quota, expected_index):
volumes = (test_utils.create_volume(self.ctxt, host='test_host1',
cluster_name='cluster1',
volume_type_id=None,
use_quota=True,
availability_zone='nova1'),
test_utils.create_volume(self.ctxt, host='test_host2',
cluster_name='cluster2',
volume_type_id=None,
use_quota=False,
availability_zone='nova2'))
req = fakes.HTTPRequest.blank(
'/v3/volumes?consumes_quota=%s' % use_quota, version=mv.USE_QUOTA)
res_dict = self.controller.detail(req)
self.assertEqual(1, len(res_dict['volumes']))
self.assertEqual(volumes[expected_index].id,
res_dict['volumes'][0]['id'])
def test_volume_list_without_quota_filter(self):
num_vols = 4
vol_ids = set()
# Half of the volumes will use quota, the other half won't
for i in range(num_vols):
vol = test_utils.create_volume(self.ctxt,
use_quota=bool(i % 2),
host='test_host',
cluster_name='cluster',
volume_type_id=None,
availability_zone='nova1')
vol_ids.add(vol.id)
req = fakes.HTTPRequest.blank('/v3/volumes', version=mv.USE_QUOTA)
res_dict = self.controller.detail(req)
res_vol_ids = {v['id'] for v in res_dict['volumes']}
self.assertEqual(num_vols, len(res_vol_ids))
self.assertEqual(vol_ids, res_vol_ids)
def _fake_volumes_summary_request(self, def _fake_volumes_summary_request(self,
version=mv.VOLUME_SUMMARY, version=mv.VOLUME_SUMMARY,
all_tenant=False, all_tenant=False,
@ -900,6 +939,27 @@ class VolumeApiTest(test.TestCase):
else: else:
self.assertNotIn('encryption_key_id', volume_details) self.assertNotIn('encryption_key_id', volume_details)
@ddt.data(
(True, True, mv.USE_QUOTA),
(True, False, mv.USE_QUOTA),
(False, True, mv.get_prior_version(mv.USE_QUOTA)),
(False, False, mv.get_prior_version(mv.USE_QUOTA)),
)
@ddt.unpack
def test_volume_show_with_use_quota(self, present, value, microversion):
volume = test_utils.create_volume(self.ctxt,
volume_type_id=None,
use_quota=value)
req = fakes.HTTPRequest.blank('/v3/volumes/%s' % volume.id,
version=microversion)
volume_details = self.controller.show(req, volume.id)['volume']
if present:
self.assertIs(value, volume_details['consumes_quota'])
else:
self.assertNotIn('consumes_quota', volume_details)
def _fake_create_volume(self, size=1): def _fake_create_volume(self, size=1):
vol = { vol = {
'display_name': 'fake_volume1', 'display_name': 'fake_volume1',

View File

@ -1,10 +1,11 @@
{ {
"volume": ["name", "status", "metadata", "volume": ["name", "status", "metadata",
"bootable", "migration_status", "availability_zone", "bootable", "migration_status", "availability_zone",
"group_id", "size", "created_at", "updated_at"], "group_id", "size", "created_at", "updated_at",
"consumes_quota"],
"backup": ["name", "status", "volume_id"], "backup": ["name", "status", "volume_id"],
"snapshot": ["name", "status", "volume_id", "metadata", "snapshot": ["name", "status", "volume_id", "metadata",
"availability_zone"], "availability_zone", "consumes_quota"],
"group": ["name"], "group": ["name"],
"group_snapshot": ["name", "status", "group_id"], "group_snapshot": ["name", "status", "group_id"],
"attachment": ["volume_id", "status", "instance_id", "attach_status"], "attachment": ["volume_id", "status", "instance_id", "attach_status"],

View File

@ -0,0 +1,14 @@
---
features:
- |
Starting with API microversion 3.65, a ``consumes_quota`` field
is included in the response body of volumes and snapshots to indicate
whether the volume is using quota or not.
Additionally, ``consumes_quota`` can be used as a listing filter for
volumes and snapshots. Its availability is controlled by its inclusion in
``etc/cinder/resource_filters.json``, where it is included by default. The
default listing behavior is not to use this filter.
Only temporary resources created internally by cinder will have the value
set to ``false``.