api: Add ability to filter flavors by name
Change-Id: I0d51d29339d1380b93ccb1501e33891082f930ec Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
@@ -33,6 +33,7 @@ Request
|
||||
- minDisk: minDisk
|
||||
- minRam: minRam
|
||||
- is_public: flavor_is_public_query
|
||||
- name: flavor_name_query
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.101",
|
||||
"version": "2.102",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.101",
|
||||
"version": "2.102",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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).\
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user