api: Add ability to filter flavors by name

Change-Id: I0d51d29339d1380b93ccb1501e33891082f930ec
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-01-30 11:06:53 +00:00
parent 889e3d83f6
commit e73a0bc84b
12 changed files with 142 additions and 13 deletions

View File

@@ -33,6 +33,7 @@ Request
- minDisk: minDisk
- minRam: minRam
- is_public: flavor_is_public_query
- name: flavor_name_query
Response
--------

View File

@@ -717,6 +717,18 @@ flavor_is_public_query:
``f``, ``false``, ``off``, ``n`` and ``no`` are treated as ``False``
(they are case-insensitive). If the value is ``None`` (case-insensitive)
both public and private flavors will be listed in a single request.
flavor_name_query:
description: |
Filters the response by a flavor name, as a string. You can use regular expressions
in the query. For example, the ``?name=bob`` regular expression returns both bob
and bobb. If you must match on only bob, you can use a regular expression that
matches the syntax of the underlying database server that is implemented for Compute,
such as MySQL or PostgreSQL.
format: regexp
in: query
required: false
type: string
min_version: 2.102
flavor_query:
description: |
Filters the response by a flavor, as a UUID. A flavor is a combination of memory,

View File

@@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.101",
"version": "2.102",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.101",
"version": "2.102",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@@ -281,6 +281,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.101 - Attaching a volume via
``POST /servers/{server_id}/os-volume_attachments`` returns HTTP
202 Accepted instead of HTTP 200 and a volumeAttachment response.
* 2.102 - Add support for filtering flavors by name.
"""
# The minimum and maximum versions of the API supported
@@ -289,7 +290,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = '2.1'
_MAX_API_VERSION = '2.101'
_MAX_API_VERSION = '2.102'
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@@ -137,7 +137,8 @@ class FlavorsController(wsgi.Controller):
@wsgi.expected_errors(400)
@validation.query_schema(schema.index_query, '2.0', '2.74')
@validation.query_schema(schema.index_query_275, '2.75')
@validation.query_schema(schema.index_query_v275, '2.75', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response, '2.0', '2.54')
@validation.response_body_schema(schema.index_response_v255, '2.55')
def index(self, req):
@@ -147,7 +148,8 @@ class FlavorsController(wsgi.Controller):
@wsgi.expected_errors(400)
@validation.query_schema(schema.index_query, '2.0', '2.74')
@validation.query_schema(schema.index_query_275, '2.75')
@validation.query_schema(schema.index_query_v275, '2.75', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.detail_response, '2.0', '2.54')
@validation.response_body_schema(schema.detail_response_v255, '2.55', '2.60') # noqa: E501
@validation.response_body_schema(schema.detail_response_v261, '2.61')
@@ -232,6 +234,9 @@ class FlavorsController(wsgi.Controller):
req.params['minDisk'])
raise webob.exc.HTTPBadRequest(explanation=msg)
if 'name' in req.params:
filters['name'] = req.params['name']
try:
limited_flavors = objects.FlavorList.get_all(
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir,

View File

@@ -136,8 +136,12 @@ index_query = {
'additionalProperties': True
}
index_query_275 = copy.deepcopy(index_query)
index_query_275['additionalProperties'] = False
index_query_v275 = copy.deepcopy(index_query)
index_query_v275['additionalProperties'] = False
index_query_v2102 = copy.deepcopy(index_query_v275)
index_query_v2102['properties']['name'] = parameter_types.multi_params(
{'type': 'string'})
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {

View File

@@ -605,15 +605,25 @@ def _flavor_get_all_from_db(context, inactive, filters, sort_key, sort_dir,
if 'min_memory_mb' in filters:
query = query.filter(
api_models.Flavors.memory_mb >= filters['min_memory_mb'])
api_models.Flavors.memory_mb >= filters['min_memory_mb'])
if 'min_root_gb' in filters:
query = query.filter(
api_models.Flavors.root_gb >= filters['min_root_gb'])
api_models.Flavors.root_gb >= filters['min_root_gb'])
if 'disabled' in filters:
query = query.filter(
api_models.Flavors.disabled == filters['disabled'])
api_models.Flavors.disabled == filters['disabled'])
if 'name' in filters:
# name can be a regex
safe_regex_filter, db_regexp_op = db_utils.get_regexp_ops(
CONF.database.connection)
query = query.filter(
api_models.Flavors.name.op(db_regexp_op)(
safe_regex_filter(filters['name'])
)
)
if 'is_public' in filters and filters['is_public'] is not None:
the_filter = [api_models.Flavors.is_public == filters['is_public']]
@@ -624,6 +634,7 @@ def _flavor_get_all_from_db(context, inactive, filters, sort_key, sort_dir,
query = query.filter(sa.or_(*the_filter))
else:
query = query.filter(the_filter[0])
marker_row = None
if marker is not None:
marker_row = Flavor._flavor_get_query_from_db(context).\

View File

@@ -321,7 +321,7 @@ class FlavorsTestV21(test.TestCase):
"href": "http://localhost/flavors/1",
}
]
}
}
]
if self.expect_description:
expected_flavors[0]['description'] = (
@@ -854,6 +854,82 @@ class FlavorsTestV275(FlavorsTestV261):
self.assertEqual(response_list['swap'], 0)
class FlavorsTestV2102(FlavorsTestV275):
microversion = '2.102'
def test_list_flavors_with_name_filter_old_version(self):
req = fakes.HTTPRequestV21.blank(
'/flavors?name=false', version='2.101')
self.assertRaises(
exception.ValidationError, self.controller.index, req)
def test_list_detail_flavors_with_name_filter_old_version(self):
req = fakes.HTTPRequestV21.blank(
'/flavors/detail?name=false', version='2.101')
self.assertRaises(
exception.ValidationError, self.controller.detail, req)
def test_list_flavors_with_name_filter(self):
req = fakes.HTTPRequestV21.blank(
'/flavors?name=2', version=self.microversion)
actual = self.controller.index(req)
expected = {
'flavors': [
{
'description': 'flavor 2 description',
'id': '2',
'links': [
{
'href': 'http://localhost/v2.1/flavors/2',
'rel': 'self',
},
{
'href': 'http://localhost/flavors/2',
'rel': 'bookmark',
},
],
'name': 'flavor 2',
},
],
}
self.assertEqual(expected, actual)
def test_list_detail_flavors_with_name_filter(self):
req = fakes.HTTPRequestV21.blank(
'/flavors/detail?name=2', version=self.microversion)
actual = self.controller.detail(req)
expected = {
'flavors': [
{
'OS-FLV-DISABLED:disabled': fakes.FLAVORS['2'].disabled,
'OS-FLV-EXT-DATA:ephemeral':
fakes.FLAVORS['2'].ephemeral_gb,
'description': fakes.FLAVORS['2'].description,
'disk': fakes.FLAVORS['2'].root_gb,
'extra_specs': {},
'id': '2',
'links': [
{
'href': 'http://localhost/v2.1/flavors/2',
'rel': 'self',
},
{
'href': 'http://localhost/flavors/2',
'rel': 'bookmark',
},
],
'name': fakes.FLAVORS['2'].name,
'os-flavor-access:is_public': True,
'ram': fakes.FLAVORS['2'].memory_mb,
'rxtx_factor': '',
'swap': fakes.FLAVORS['2'].swap,
'vcpus': fakes.FLAVORS['2'].vcpus,
},
],
}
self.assertEqual(expected, actual)
class DisabledFlavorsWithRealDBTestV21(test.TestCase):
"""Tests that disabled flavors should not be shown nor listed."""

View File

@@ -780,6 +780,12 @@ def stub_out_flavor_get_all(test):
elif reject_min('root_gb', 'min_root_gb'):
continue
# in reality our filtering is regex based, but Python's regex
# format differs from MySQL (which differs from PostgreSQL, etc.)
# so we do a simple substring search instead
if 'name' in filters and filters['name'] not in flavor.name:
continue
res.append(flavor)
res = sorted(res, key=lambda item: getattr(item, sort_key))

View File

@@ -390,10 +390,16 @@ class _TestFlavorList(object):
self.assertEqual(len(api_flavors), len(flavors))
def test_get_all_from_db_with_limit(self):
flavors = objects.FlavorList.get_all(self.context,
limit=1)
flavors = objects.FlavorList.get_all(self.context, limit=1)
self.assertEqual(1, len(flavors))
def test_get_all_from_db_with_filters(self):
flavors = objects.FlavorList.get_all(
self.context, filters={'name': 'tiny'})
# these flavors are created by the DefaultFlavorsFixture: there should
# be two: m1.tiny and m1.tiny.specs
self.assertEqual(2, len(flavors))
@mock.patch('nova.objects.flavor._flavor_get_all_from_db')
def test_get_all(self, mock_api_get):
_fake_flavor = dict(fake_flavor,

View File

@@ -0,0 +1,7 @@
---
features:
- |
The v2.102 microversion has been introduced. This allows users to search
flavors by name, e.g.::
GET /flavors?name=gpu