Support to query volume filter by updated_at/created_at
Support users can query resources by specifying the time that resources are created at or updated at with time comparison operators: "gt/gte/eq/neq/lt/lte", and cinder will return all which match the condition. The date and time stamp format is ISO 8601: CCYY-MM-DDThh:mm:ss±hh:mm. The ±hh:mm value, if included, returns the time zone as an offset from UTC. Add two filters updated_at and created_at in querying API. "volumes/detail?created_at=gt:2016-01-01T01:00:00,lt:2016-12-31T01:00:00" Change-Id: I1f43c37c2266e43146637beadc027ccf6dec017e Partial-Implements: blueprint support-to-query-cinder-resources-filter-by-time-comparison-operators Co-Authored-By: wangxiyuan <wangxiyuan@huawei.com>
This commit is contained in:
parent
fade80a0ee
commit
7e98d14a57
@ -208,6 +208,26 @@ detail:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
filter_created_at:
|
||||||
|
description: |
|
||||||
|
Filters reuslts by a time that resources are created at with time
|
||||||
|
comparison operators: gt/gte/eq/neq/lt/lte.
|
||||||
|
The date and time stamp format is ISO 8601: CCYY-MM-DDThh:mm:ss±hh:mm.
|
||||||
|
The ±hh:mm value, if included, returns the time zone as an offset from UTC.
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 3.60
|
||||||
|
filter_updated_at:
|
||||||
|
description: |
|
||||||
|
Filters reuslts by a time that resources are updated at with time
|
||||||
|
comaprison operators: gt/gte/eq/neq/lt/lte.
|
||||||
|
The date and time stamp format is ISO 8601: CCYY-MM-DDThh:mm:ss±hh:mm.
|
||||||
|
The ±hh:mm value, if included, returns the time zone as an offset from UTC.
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 3.60
|
||||||
fix_allocated_quotas:
|
fix_allocated_quotas:
|
||||||
description: |
|
description: |
|
||||||
Whether to fix all the non-leaf projects' ``allocation``
|
Whether to fix all the non-leaf projects' ``allocation``
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2018-07-17T00:00:00Z",
|
"updated": "2018-07-17T00:00:00Z",
|
||||||
"version": "3.59"
|
"version": "3.60"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2018-07-17T00:00:00Z",
|
"updated": "2018-07-17T00:00:00Z",
|
||||||
"version": "3.59"
|
"version": "3.60"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -94,6 +94,8 @@ Request
|
|||||||
- offset: offset
|
- offset: offset
|
||||||
- marker: marker
|
- marker: marker
|
||||||
- with_count: with_count
|
- with_count: with_count
|
||||||
|
- created_at: filter_created_at
|
||||||
|
- updated_at: filter_updated_at
|
||||||
|
|
||||||
|
|
||||||
Response Parameters
|
Response Parameters
|
||||||
@ -286,6 +288,8 @@ Request
|
|||||||
- offset: offset
|
- offset: offset
|
||||||
- marker: marker
|
- marker: marker
|
||||||
- with_count: with_count
|
- with_count: with_count
|
||||||
|
- created_at: filter_created_at
|
||||||
|
- updated_at: filter_updated_at
|
||||||
|
|
||||||
|
|
||||||
Response Parameters
|
Response Parameters
|
||||||
|
@ -385,6 +385,14 @@ def get_enabled_resource_filters(resource=None):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_comparsion_operators():
|
||||||
|
"""Get list of time comparsion operators.
|
||||||
|
|
||||||
|
This method returns list which contains the allowed comparsion operators.
|
||||||
|
"""
|
||||||
|
return ["gt", "gte", "eq", "neq", "lt", "lte"]
|
||||||
|
|
||||||
|
|
||||||
def convert_filter_attributes(filters, resource):
|
def convert_filter_attributes(filters, resource):
|
||||||
for key in filters.copy().keys():
|
for key in filters.copy().keys():
|
||||||
if resource in ['volume', 'backup',
|
if resource in ['volume', 'backup',
|
||||||
|
@ -159,6 +159,8 @@ GROUP_GROUPSNAPSHOT_PROJECT_ID = '3.58'
|
|||||||
|
|
||||||
SUPPORT_TRANSFER_PAGINATION = '3.59'
|
SUPPORT_TRANSFER_PAGINATION = '3.59'
|
||||||
|
|
||||||
|
VOLUME_TIME_COMPARISON_FILTER = '3.60'
|
||||||
|
|
||||||
|
|
||||||
def get_mv_header(version):
|
def get_mv_header(version):
|
||||||
"""Gets a formatted HTTP microversion header.
|
"""Gets a formatted HTTP microversion header.
|
||||||
|
@ -135,6 +135,10 @@ REST_API_VERSION_HISTORY = """
|
|||||||
detail, list group snapshots with detail, show group detail and
|
detail, list group snapshots with detail, show group detail and
|
||||||
show group snapshot detail APIs.
|
show group snapshot detail APIs.
|
||||||
* 3.59 - Support volume transfer pagination.
|
* 3.59 - Support volume transfer pagination.
|
||||||
|
* 3.60 - Support filtering on the "updated_at" and "created_at" fields with
|
||||||
|
time comparison operators for the volume summary list
|
||||||
|
("GET /v3/{project_id}/volumes") and volume detail list
|
||||||
|
("GET /v3/{project_id}/volumes/detail") requests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
@ -142,7 +146,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v2 endpoints will still work
|
# Explicitly using /v2 endpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.59"
|
_MAX_API_VERSION = "3.60"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
UPDATED = "2018-07-17T00:00:00Z"
|
UPDATED = "2018-07-17T00:00:00Z"
|
||||||
|
|
||||||
|
@ -463,3 +463,7 @@ detail APIs.
|
|||||||
---------------------------------
|
---------------------------------
|
||||||
Support volume transfer pagination.
|
Support volume transfer pagination.
|
||||||
|
|
||||||
|
3.60
|
||||||
|
----
|
||||||
|
Add 'created_at' and 'updated_at' to support users can list/detail volumes by
|
||||||
|
specifying the time comparison operators along with created_at or updated_at.
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_log import versionutils
|
from oslo_log import versionutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
import six
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
import webob
|
import webob
|
||||||
@ -90,10 +91,44 @@ class VolumeController(volumes_v2.VolumeController):
|
|||||||
if req_version.matches(None, mv.BACKUP_UPDATE):
|
if req_version.matches(None, mv.BACKUP_UPDATE):
|
||||||
filters.pop('group_id', None)
|
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,
|
||||||
self._get_volume_filter_options())
|
self._get_volume_filter_options())
|
||||||
|
|
||||||
|
def _handle_time_comparison_filters(self, filters):
|
||||||
|
for time_comparison_filter in ['created_at', 'updated_at']:
|
||||||
|
if time_comparison_filter in filters:
|
||||||
|
time_filter_dict = {}
|
||||||
|
comparison_units = filters[time_comparison_filter].split(',')
|
||||||
|
operators = common.get_time_comparsion_operators()
|
||||||
|
for comparison_unit in comparison_units:
|
||||||
|
try:
|
||||||
|
operator_and_time = comparison_unit.split(":")
|
||||||
|
comparison_operator = operator_and_time[0]
|
||||||
|
time = ''
|
||||||
|
for time_str in operator_and_time[1:-1]:
|
||||||
|
time += time_str + ":"
|
||||||
|
time += operator_and_time[-1]
|
||||||
|
if comparison_operator not in operators:
|
||||||
|
msg = _(
|
||||||
|
'Invalid %s operator') % comparison_operator
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
except IndexError:
|
||||||
|
msg = _('Invalid %s value') % time_comparison_filter
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
try:
|
||||||
|
parsed_time = timeutils.parse_isotime(time)
|
||||||
|
except ValueError:
|
||||||
|
msg = _('Invalid %s value') % time
|
||||||
|
raise exc.HTTPBadRequest(explanation=msg)
|
||||||
|
time_filter_dict[comparison_operator] = parsed_time
|
||||||
|
|
||||||
|
filters[time_comparison_filter] = time_filter_dict
|
||||||
|
|
||||||
def _get_volumes(self, req, is_detail):
|
def _get_volumes(self, req, is_detail):
|
||||||
"""Returns a list of volumes, transformed through view builder."""
|
"""Returns a list of volumes, transformed through view builder."""
|
||||||
|
|
||||||
@ -121,6 +156,8 @@ 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')
|
||||||
|
|
||||||
|
self._handle_time_comparison_filters(filters)
|
||||||
|
|
||||||
strict = req.api_version_request.matches(
|
strict = req.api_version_request.matches(
|
||||||
mv.VOLUME_LIST_BOOTABLE, None)
|
mv.VOLUME_LIST_BOOTABLE, None)
|
||||||
self.volume_api.check_volume_filters(filters, strict)
|
self.volume_api.check_volume_filters(filters, strict)
|
||||||
|
@ -413,6 +413,27 @@ def _filter_host(field, value, match_level=None):
|
|||||||
return or_(*conditions)
|
return or_(*conditions)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_time_comparison(field, time_filter_dict):
|
||||||
|
"""Generate a filter condition for time comparison operators"""
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
for operator in time_filter_dict:
|
||||||
|
filter_time = timeutils.normalize_time(time_filter_dict[operator])
|
||||||
|
if operator == 'gt':
|
||||||
|
conditions.append(field.op('>')(filter_time))
|
||||||
|
elif operator == 'gte':
|
||||||
|
conditions.append(field.op('>=')(filter_time))
|
||||||
|
if operator == 'eq':
|
||||||
|
conditions.append(field.op('=')(filter_time))
|
||||||
|
elif operator == 'neq':
|
||||||
|
conditions.append(field.op('!=')(filter_time))
|
||||||
|
if operator == 'lt':
|
||||||
|
conditions.append(field.op('<')(filter_time))
|
||||||
|
elif operator == 'lte':
|
||||||
|
conditions.append(field.op('<=')(filter_time))
|
||||||
|
return or_(*conditions)
|
||||||
|
|
||||||
|
|
||||||
def _clustered_bool_field_filter(query, field_name, filter_value):
|
def _clustered_bool_field_filter(query, field_name, filter_value):
|
||||||
# Now that we have clusters, a service is disabled/frozen if the service
|
# Now that we have clusters, a service is disabled/frozen if the service
|
||||||
# doesn't belong to a cluster or if it belongs to a cluster and the cluster
|
# doesn't belong to a cluster or if it belongs to a cluster and the cluster
|
||||||
@ -2433,6 +2454,19 @@ def _process_volume_filters(query, filters):
|
|||||||
query = query.filter(_filter_host(models.Volume.cluster_name,
|
query = query.filter(_filter_host(models.Volume.cluster_name,
|
||||||
cluster_name))
|
cluster_name))
|
||||||
|
|
||||||
|
for time_comparison_filter in ['created_at', 'updated_at']:
|
||||||
|
if filters.get(time_comparison_filter, None):
|
||||||
|
time_filter_dict = filters.pop(time_comparison_filter)
|
||||||
|
try:
|
||||||
|
time_filter_attr = getattr(models.Volume,
|
||||||
|
time_comparison_filter)
|
||||||
|
query = query.filter(_filter_time_comparison(time_filter_attr,
|
||||||
|
time_filter_dict))
|
||||||
|
except AttributeError:
|
||||||
|
LOG.debug("%s column could not be found.",
|
||||||
|
time_comparison_filter)
|
||||||
|
return None
|
||||||
|
|
||||||
# Apply exact match filters for everything else, ensure that the
|
# Apply exact match filters for everything else, ensure that the
|
||||||
# filter value exists on the model
|
# filter value exists on the model
|
||||||
for key in filters.keys():
|
for key in filters.keys():
|
||||||
|
@ -477,7 +477,7 @@ 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"]},
|
"size", "created_at", "updated_at"]},
|
||||||
{'resource': 'backup',
|
{'resource': 'backup',
|
||||||
'expected': ["name", "status", "volume_id"]},
|
'expected': ["name", "status", "volume_id"]},
|
||||||
{'resource': 'snapshot',
|
{'resource': 'snapshot',
|
||||||
|
@ -20,6 +20,7 @@ import fixtures
|
|||||||
import iso8601
|
import iso8601
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
@ -120,18 +121,27 @@ class VolumeApiTest(test.TestCase):
|
|||||||
self._reset_filter_file()
|
self._reset_filter_file()
|
||||||
|
|
||||||
def _create_volume_with_glance_metadata(self):
|
def _create_volume_with_glance_metadata(self):
|
||||||
|
basetime = timeutils.utcnow()
|
||||||
|
td = datetime.timedelta(minutes=1)
|
||||||
|
|
||||||
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1',
|
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1',
|
||||||
|
'created_at': basetime - 3 * td,
|
||||||
|
'updated_at': basetime - 2 * td,
|
||||||
'project_id':
|
'project_id':
|
||||||
self.ctxt.project_id,
|
self.ctxt.project_id,
|
||||||
'volume_type_id':
|
'volume_type_id':
|
||||||
fake.VOLUME_TYPE_ID})
|
fake.VOLUME_TYPE_ID,
|
||||||
|
'id': fake.VOLUME_ID})
|
||||||
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'image_name',
|
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'image_name',
|
||||||
'imageTestOne')
|
'imageTestOne')
|
||||||
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2',
|
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2',
|
||||||
|
'created_at': basetime - td,
|
||||||
|
'updated_at': basetime,
|
||||||
'project_id':
|
'project_id':
|
||||||
self.ctxt.project_id,
|
self.ctxt.project_id,
|
||||||
'volume_type_id':
|
'volume_type_id':
|
||||||
fake.VOLUME_TYPE_ID})
|
fake.VOLUME_TYPE_ID,
|
||||||
|
'id': fake.VOLUME2_ID})
|
||||||
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'image_name',
|
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'image_name',
|
||||||
'imageTestTwo')
|
'imageTestTwo')
|
||||||
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'disk_format',
|
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'disk_format',
|
||||||
@ -984,3 +994,80 @@ class VolumeApiTest(test.TestCase):
|
|||||||
self.assertEqual('host1', attachments[0]['host_name'])
|
self.assertEqual('host1', attachments[0]['host_name'])
|
||||||
self.assertEqual('na', attachments[0]['device'])
|
self.assertEqual('na', attachments[0]['device'])
|
||||||
self.assertEqual(att_time, attachments[0]['attached_at'])
|
self.assertEqual(att_time, attachments[0]['attached_at'])
|
||||||
|
|
||||||
|
@ddt.data(('created_at=gt:', 0), ('created_at=lt:', 2))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_volume_index_filter_by_created_at_with_gt_and_lt(self, change,
|
||||||
|
expect_result):
|
||||||
|
self._create_volume_with_glance_metadata()
|
||||||
|
change_time = timeutils.utcnow() + datetime.timedelta(minutes=1)
|
||||||
|
req = fakes.HTTPRequest.blank(("/v3/volumes?%s%s") %
|
||||||
|
(change, change_time))
|
||||||
|
req.environ['cinder.context'] = self.ctxt
|
||||||
|
req.headers = mv.get_mv_header(mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
req.api_version_request = mv.get_api_version(
|
||||||
|
mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
volumes = res_dict['volumes']
|
||||||
|
self.assertEqual(expect_result, len(volumes))
|
||||||
|
|
||||||
|
@ddt.data(('updated_at=gt:', 0), ('updated_at=lt:', 1))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_vol_filter_by_updated_at_with_gt_and_lt(self, change, result):
|
||||||
|
vols = self._create_volume_with_glance_metadata()
|
||||||
|
change_time = vols[1].updated_at
|
||||||
|
req = fakes.HTTPRequest.blank(("/v3/volumes?%s%s") %
|
||||||
|
(change, change_time))
|
||||||
|
req.environ['cinder.context'] = self.ctxt
|
||||||
|
req.headers = mv.get_mv_header(mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
req.api_version_request = mv.get_api_version(
|
||||||
|
mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
volumes = res_dict['volumes']
|
||||||
|
self.assertEqual(result, len(volumes))
|
||||||
|
|
||||||
|
@ddt.data(('updated_at=eq:', 1, fake.VOLUME2_ID),
|
||||||
|
('updated_at=neq:', 1, fake.VOLUME_ID))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_vol_filter_by_updated_at_with_eq_and_neq(self, change, result,
|
||||||
|
expected_volume_id):
|
||||||
|
vols = self._create_volume_with_glance_metadata()
|
||||||
|
change_time = vols[1].updated_at
|
||||||
|
req = fakes.HTTPRequest.blank(("/v3/volumes?%s%s") %
|
||||||
|
(change, change_time))
|
||||||
|
req.environ['cinder.context'] = self.ctxt
|
||||||
|
req.headers = mv.get_mv_header(mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
req.api_version_request = mv.get_api_version(
|
||||||
|
mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
volumes = res_dict['volumes']
|
||||||
|
self.assertEqual(result, len(volumes))
|
||||||
|
self.assertEqual(expected_volume_id, volumes[0]['id'])
|
||||||
|
|
||||||
|
@ddt.data('created_at', 'updated_at')
|
||||||
|
def test_volume_filter_by_time_with_invaild_time(self, change):
|
||||||
|
self._create_volume_with_glance_metadata()
|
||||||
|
change_time = '123'
|
||||||
|
req = fakes.HTTPRequest.blank(("/v3/volumes?%s=%s") %
|
||||||
|
(change, change_time))
|
||||||
|
req.environ['cinder.context'] = self.ctxt
|
||||||
|
req.headers = mv.get_mv_header(mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
req.api_version_request = mv.get_api_version(
|
||||||
|
mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req)
|
||||||
|
|
||||||
|
def test_volume_index_filter_by_time_with_lte_and_gte(self):
|
||||||
|
vols = self._create_volume_with_glance_metadata()
|
||||||
|
change_since = vols[1].updated_at
|
||||||
|
change_before = timeutils.utcnow() + datetime.timedelta(minutes=1)
|
||||||
|
req = fakes.HTTPRequest.blank(("/v3/volumes?updated_at=lte:%s&"
|
||||||
|
"updated_at=gte:%s") %
|
||||||
|
(change_before, change_since))
|
||||||
|
req.environ['cinder.context'] = self.ctxt
|
||||||
|
req.headers = mv.get_mv_header(mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
req.api_version_request = mv.get_api_version(
|
||||||
|
mv.VOLUME_TIME_COMPARISON_FILTER)
|
||||||
|
res_dict = self.controller.index(req)
|
||||||
|
volumes = res_dict['volumes']
|
||||||
|
self.assertEqual(1, len(volumes))
|
||||||
|
self.assertEqual(vols[1].id, volumes[0]['id'])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"volume": ["name", "status", "metadata",
|
"volume": ["name", "status", "metadata",
|
||||||
"bootable", "migration_status", "availability_zone",
|
"bootable", "migration_status", "availability_zone",
|
||||||
"group_id", "size"],
|
"group_id", "size", "created_at", "updated_at"],
|
||||||
"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"],
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Beginning with microversion 3.60, users may apply time comparison filters
|
||||||
|
to the volume summary list and volume detail list requests by using the
|
||||||
|
"created_at" or "updated_at" fields. Time must be expressed in ISO
|
||||||
|
8601 format. See the 'Block Storage API v3 Reference
|
||||||
|
<https://docs.openstack.org/api-ref/block-storage/v3/index.html>'_ for
|
||||||
|
details.
|
Loading…
x
Reference in New Issue
Block a user