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
|
- minDisk: minDisk
|
||||||
- minRam: minRam
|
- minRam: minRam
|
||||||
- is_public: flavor_is_public_query
|
- is_public: flavor_is_public_query
|
||||||
|
- name: flavor_name_query
|
||||||
|
|
||||||
Response
|
Response
|
||||||
--------
|
--------
|
||||||
|
|||||||
@@ -717,6 +717,18 @@ flavor_is_public_query:
|
|||||||
``f``, ``false``, ``off``, ``n`` and ``no`` are treated as ``False``
|
``f``, ``false``, ``off``, ``n`` and ``no`` are treated as ``False``
|
||||||
(they are case-insensitive). If the value is ``None`` (case-insensitive)
|
(they are case-insensitive). If the value is ``None`` (case-insensitive)
|
||||||
both public and private flavors will be listed in a single request.
|
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:
|
flavor_query:
|
||||||
description: |
|
description: |
|
||||||
Filters the response by a flavor, as a UUID. A flavor is a combination of memory,
|
Filters the response by a flavor, as a UUID. A flavor is a combination of memory,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"version": "2.101",
|
"version": "2.102",
|
||||||
"min_version": "2.1",
|
"min_version": "2.1",
|
||||||
"updated": "2013-07-23T11:33:21Z"
|
"updated": "2013-07-23T11:33:21Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"version": "2.101",
|
"version": "2.102",
|
||||||
"min_version": "2.1",
|
"min_version": "2.1",
|
||||||
"updated": "2013-07-23T11:33:21Z"
|
"updated": "2013-07-23T11:33:21Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
|||||||
* 2.101 - Attaching a volume via
|
* 2.101 - Attaching a volume via
|
||||||
``POST /servers/{server_id}/os-volume_attachments`` returns HTTP
|
``POST /servers/{server_id}/os-volume_attachments`` returns HTTP
|
||||||
202 Accepted instead of HTTP 200 and a volumeAttachment response.
|
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
|
# 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
|
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||||
# support is fully merged. It does not affect the V2 API.
|
# support is fully merged. It does not affect the V2 API.
|
||||||
_MIN_API_VERSION = '2.1'
|
_MIN_API_VERSION = '2.1'
|
||||||
_MAX_API_VERSION = '2.101'
|
_MAX_API_VERSION = '2.102'
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
# Almost all proxy APIs which are related to network, images and baremetal
|
# Almost all proxy APIs which are related to network, images and baremetal
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ class FlavorsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.expected_errors(400)
|
@wsgi.expected_errors(400)
|
||||||
@validation.query_schema(schema.index_query, '2.0', '2.74')
|
@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, '2.0', '2.54')
|
||||||
@validation.response_body_schema(schema.index_response_v255, '2.55')
|
@validation.response_body_schema(schema.index_response_v255, '2.55')
|
||||||
def index(self, req):
|
def index(self, req):
|
||||||
@@ -147,7 +148,8 @@ class FlavorsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.expected_errors(400)
|
@wsgi.expected_errors(400)
|
||||||
@validation.query_schema(schema.index_query, '2.0', '2.74')
|
@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, '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_v255, '2.55', '2.60') # noqa: E501
|
||||||
@validation.response_body_schema(schema.detail_response_v261, '2.61')
|
@validation.response_body_schema(schema.detail_response_v261, '2.61')
|
||||||
@@ -232,6 +234,9 @@ class FlavorsController(wsgi.Controller):
|
|||||||
req.params['minDisk'])
|
req.params['minDisk'])
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
if 'name' in req.params:
|
||||||
|
filters['name'] = req.params['name']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
limited_flavors = objects.FlavorList.get_all(
|
limited_flavors = objects.FlavorList.get_all(
|
||||||
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir,
|
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir,
|
||||||
|
|||||||
@@ -136,8 +136,12 @@ index_query = {
|
|||||||
'additionalProperties': True
|
'additionalProperties': True
|
||||||
}
|
}
|
||||||
|
|
||||||
index_query_275 = copy.deepcopy(index_query)
|
index_query_v275 = copy.deepcopy(index_query)
|
||||||
index_query_275['additionalProperties'] = False
|
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
|
# TODO(stephenfin): Remove additionalProperties in a future API version
|
||||||
show_query = {
|
show_query = {
|
||||||
|
|||||||
+14
-3
@@ -605,15 +605,25 @@ def _flavor_get_all_from_db(context, inactive, filters, sort_key, sort_dir,
|
|||||||
|
|
||||||
if 'min_memory_mb' in filters:
|
if 'min_memory_mb' in filters:
|
||||||
query = query.filter(
|
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:
|
if 'min_root_gb' in filters:
|
||||||
query = query.filter(
|
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:
|
if 'disabled' in filters:
|
||||||
query = query.filter(
|
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:
|
if 'is_public' in filters and filters['is_public'] is not None:
|
||||||
the_filter = [api_models.Flavors.is_public == filters['is_public']]
|
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))
|
query = query.filter(sa.or_(*the_filter))
|
||||||
else:
|
else:
|
||||||
query = query.filter(the_filter[0])
|
query = query.filter(the_filter[0])
|
||||||
|
|
||||||
marker_row = None
|
marker_row = None
|
||||||
if marker is not None:
|
if marker is not None:
|
||||||
marker_row = Flavor._flavor_get_query_from_db(context).\
|
marker_row = Flavor._flavor_get_query_from_db(context).\
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class FlavorsTestV21(test.TestCase):
|
|||||||
"href": "http://localhost/flavors/1",
|
"href": "http://localhost/flavors/1",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if self.expect_description:
|
if self.expect_description:
|
||||||
expected_flavors[0]['description'] = (
|
expected_flavors[0]['description'] = (
|
||||||
@@ -854,6 +854,82 @@ class FlavorsTestV275(FlavorsTestV261):
|
|||||||
self.assertEqual(response_list['swap'], 0)
|
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):
|
class DisabledFlavorsWithRealDBTestV21(test.TestCase):
|
||||||
"""Tests that disabled flavors should not be shown nor listed."""
|
"""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'):
|
elif reject_min('root_gb', 'min_root_gb'):
|
||||||
continue
|
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.append(flavor)
|
||||||
|
|
||||||
res = sorted(res, key=lambda item: getattr(item, sort_key))
|
res = sorted(res, key=lambda item: getattr(item, sort_key))
|
||||||
|
|||||||
@@ -390,10 +390,16 @@ class _TestFlavorList(object):
|
|||||||
self.assertEqual(len(api_flavors), len(flavors))
|
self.assertEqual(len(api_flavors), len(flavors))
|
||||||
|
|
||||||
def test_get_all_from_db_with_limit(self):
|
def test_get_all_from_db_with_limit(self):
|
||||||
flavors = objects.FlavorList.get_all(self.context,
|
flavors = objects.FlavorList.get_all(self.context, limit=1)
|
||||||
limit=1)
|
|
||||||
self.assertEqual(1, len(flavors))
|
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')
|
@mock.patch('nova.objects.flavor._flavor_get_all_from_db')
|
||||||
def test_get_all(self, mock_api_get):
|
def test_get_all(self, mock_api_get):
|
||||||
_fake_flavor = dict(fake_flavor,
|
_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