Add filters to server list API

APIImpact
DocImpact

Closes-Bug: #1697390
Change-Id: I1fd41d0e1897d0187e49cb54c7d4539ce64a70ff
This commit is contained in:
Xinran 2017-05-27 10:21:49 +08:00
parent eca9e61187
commit 0563c48554
8 changed files with 212 additions and 29 deletions

View File

@ -73,6 +73,42 @@ fields:
in: query
required: false
type: array
fixed_ip_query:
description: |
Filters the server list result by fixed ip. Users can filter by prefix of ip address.
in: query
required: false
type: string
flavor_name_query:
description: |
Filters the server list by flavor's name.
in: query
required: false
type: string
flavor_query:
description: |
Filters the server list by flavor's UUID.
in: query
required: false
type: string
image_query:
description: |
Filters the server list by image's UUID.
in: query
required: false
type: string
server_name_query:
description: |
Filters the server list by name. Users can filter by prefix of server's name.
in: query
required: false
type: string
status_query:
description: |
Filters the server list by the server's status.
in: query
required: false
type: string
user_id:
description: |
Filters the response by a user, by ID.

View File

@ -143,6 +143,12 @@ Request
.. rest_parameters:: parameters.yaml
- name: server_name_query
- status: status_query
- flavor_uuid: flavor_query
- flavor_name: flavor_name_query
- image_uuid: image_query
- ip: fixed_ip_query
- all_tenants: all_tenants
- fields: fields
@ -169,7 +175,8 @@ List Servers Detailed
.. rest_method:: GET /servers/detail
Return a list of bare metal Servers with complete details.
Return a list of bare metal Servers with complete details. We can also apply
filters to show more precisely the servers.
Normal response codes: 200
@ -181,8 +188,15 @@ Request
.. rest_parameters:: parameters.yaml
- name: server_name_query
- status: status_query
- flavor_uuid: flavor_query
- flavor_name: flavor_name_query
- image_uuid: image_query
- ip: fixed_ip_query
- all_tenants: all_tenants
Response
--------

View File

@ -41,6 +41,8 @@ from mogan.common import states
from mogan import network
from mogan import objects
import re
_DEFAULT_SERVER_RETURN_FIELDS = ('uuid', 'name', 'description',
'status', 'power_state')
@ -591,21 +593,62 @@ class ServerController(ServerControllerBase):
'detail': ['GET']
}
def _get_server_collection(self, fields=None, all_tenants=False):
def _get_server_collection(self, name=None, status=None,
flavor_uuid=None, flavor_name=None,
image_uuid=None, ip=None,
all_tenants=None, fields=None):
context = pecan.request.context
project_only = True
if context.is_admin and all_tenants:
project_only = False
filters = {}
if name:
filters['name'] = name
if status:
filters['status'] = status
if flavor_uuid:
filters['flavor_uuid'] = flavor_uuid
if flavor_name:
filters['flavor_name'] = flavor_name
if image_uuid:
filters['image_uuid'] = image_uuid
servers = objects.Server.list(pecan.request.context,
project_only=project_only)
project_only=project_only,
filters=filters)
if ip:
servers = self._ip_filter(servers, ip)
servers_data = [server.as_dict() for server in servers]
return ServerCollection.convert_with_links(servers_data,
fields=fields)
@expose.expose(ServerCollection, types.listtype, types.boolean)
def get_all(self, fields=None, all_tenants=None):
@staticmethod
def _ip_filter(servers, ip):
ip = ip.replace('.', '\.')
ip_obj = re.compile(ip)
def _match_server(server):
nw_info = server.nics
for vif in nw_info:
for fixed_ip in vif.fixed_ips:
address = fixed_ip.get('ip_address')
if not address:
continue
if ip_obj.match(address):
return True
return False
return filter(_match_server, servers)
@expose.expose(ServerCollection, wtypes.text, wtypes.text,
types.uuid, wtypes.text, types.uuid, wtypes.text,
types.listtype, types.boolean)
def get_all(self, name=None, status=None,
flavor_uuid=None, flavor_name=None, image_uuid=None, ip=None,
fields=None, all_tenants=None):
"""Retrieve a list of server.
:param fields: Optional, a list with a specified set of fields
@ -617,8 +660,11 @@ class ServerController(ServerControllerBase):
"""
if fields is None:
fields = _DEFAULT_SERVER_RETURN_FIELDS
return self._get_server_collection(fields=fields,
all_tenants=all_tenants)
return self._get_server_collection(name, status,
flavor_uuid, flavor_name,
image_uuid, ip,
all_tenants=all_tenants,
fields=fields)
@policy.authorize_wsgi("mogan:server", "get")
@expose.expose(Server, types.uuid, types.listtype)
@ -634,14 +680,23 @@ class ServerController(ServerControllerBase):
return Server.convert_with_links(server_data, fields=fields)
@expose.expose(ServerCollection, types.boolean)
def detail(self, all_tenants=None):
@expose.expose(ServerCollection, wtypes.text, wtypes.text,
types.uuid, wtypes.text, types.uuid, wtypes.text,
types.boolean)
def detail(self, name=None, status=None,
flavor_uuid=None, flavor_name=None, image_uuid=None, ip=None,
all_tenants=None):
"""Retrieve detail of a list of servers."""
# /detail should only work against collections
cdict = pecan.request.context.to_policy_values()
policy.authorize('mogan:server:get', cdict, cdict)
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "servers":
raise exception.NotFound()
return self._get_server_collection(all_tenants=all_tenants)
return self._get_server_collection(name, status,
flavor_uuid, flavor_name,
image_uuid, ip,
all_tenants=all_tenants)
@policy.authorize_wsgi("mogan:server", "create", False)
@expose.expose(Server, body=types.jsontype,

View File

@ -68,7 +68,6 @@ def model_query(context, model, *args, **kwargs):
project_id.
:type project_only: bool
"""
if kwargs.pop("project_only", False):
kwargs["project_id"] = context.tenant
@ -103,6 +102,25 @@ class Connection(api.Connection):
self.QUOTA_SYNC_FUNCTIONS = {'_sync_servers': self._sync_servers}
pass
def _add_servers_filters(self, context, query, filters):
if filters is None:
filters = []
if 'name' in filters:
name_start_with = filters['name']
query = query.filter(models.Server.name.like(
"%" + name_start_with + "%"))
if 'status' in filters:
query = query.filter_by(status=filters['status'])
if 'flavor_uuid' in filters or 'flavor_name' in filters:
if 'flavor_name' in filters:
flavor_query = model_query(context, models.Flavors).filter_by(
name=filters['flavor_name'])
filters['flavor_uuid'] = flavor_query.first().uuid
query = query.filter_by(flavor_uuid=filters['flavor_uuid'])
if 'image_uuid' in filters:
query = query.filter_by(image_uuid=filters['image_uuid'])
return query
def flavor_create(self, context, values):
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
@ -213,9 +231,11 @@ class Connection(api.Connection):
except NoResultFound:
raise exception.ServerNotFound(server=server_id)
def server_get_all(self, context, project_only):
return model_query(context, models.Server,
project_only=project_only)
def server_get_all(self, context, project_only, filters=None):
query = model_query(context, models.Server,
project_only=project_only)
query = self._add_servers_filters(context, query, filters)
return query.all()
def server_destroy(self, context, server_id):
with _session_for_write():

View File

@ -124,10 +124,11 @@ class Server(base.MoganObject, object_base.VersionedObjectDictCompat):
return data
@classmethod
def list(cls, context, project_only=False):
def list(cls, context, project_only=False, filters=None):
"""Return a list of Server objects."""
db_servers = cls.dbapi.server_get_all(context,
project_only=project_only)
project_only=project_only,
filters=filters)
return Server._from_db_object_list(db_servers, cls, context)
@classmethod

View File

@ -208,6 +208,32 @@ class TestServers(v1_test.APITestV1):
self.assertEqual('b8f82429-3a13-4ffe-9398-4d1abdc256a8',
resps[0]['image_uuid'])
# Test with filters
# Filter with server name
resps = self.get_json('/servers/detail?name=test_server_0',
headers=headers)['servers']
self.assertEqual(1, len(resps))
resps = self.get_json('/servers/detail?name=test',
headers=headers)['servers']
self.assertEqual(4, len(resps))
# Filter with server status
resps = self.get_json('/servers/detail?status=building',
headers=headers)['servers']
self.assertEqual(4, len(resps))
resps = self.get_json('/servers/detail?status=active',
headers=headers)['servers']
self.assertEqual(0, len(resps))
# Filter with flavor uuid
search_opt = "flavor_uuid=ff28b5a2-73e5-431c-b4b7-1b96b74bca7b"
resps = self.get_json('/servers/detail?' + search_opt,
headers=headers)['servers']
self.assertEqual(4, len(resps))
# Filter with image uuid
search_opt = "image_uuid=b8f82429-3a13-4ffe-9398-4d1abdc256a8"
resps = self.get_json('/servers/detail?' + search_opt,
headers=headers)['servers']
self.assertEqual(4, len(resps))
def test_server_delete(self):
self._prepare_server(4)
headers = self.gen_headers(self.context)

View File

@ -47,35 +47,66 @@ class DbServerTestCase(base.DbTestCase):
'12345678-9999-0000-aaaa-123456789012')
def test_server_get_all(self):
uuids_project_1 = []
uuids_project_2 = []
uuids_project_all = []
servers_project_1 = []
servers_project_2 = []
servers_project_all = []
for i in range(0, 3):
server = utils.create_test_server(
uuid=uuidutils.generate_uuid(),
project_id='project_1',
name=str(i))
uuids_project_1.append(six.text_type(server['uuid']))
servers_project_1.append(server)
for i in range(3, 5):
server = utils.create_test_server(
uuid=uuidutils.generate_uuid(),
project_id='project_2',
name=str(i))
uuids_project_2.append(six.text_type(server['uuid']))
uuids_project_all.extend(uuids_project_1)
uuids_project_all.extend(uuids_project_2)
servers_project_2.append(server)
servers_project_all.extend(servers_project_1)
servers_project_all.extend(servers_project_2)
# Set project_only to False
# get all servers from all projects
res = self.dbapi.server_get_all(self.context, project_only=False)
res_uuids = [r.uuid for r in res]
six.assertCountEqual(self, uuids_project_all, res_uuids)
for i, item in enumerate(res):
self.assertEqual(servers_project_all[i].uuid, item.uuid)
# Filter by server name test
res = self.dbapi.server_get_all(self.context, project_only=False,
filters={"name": "1"})
self.assertEqual(len(res), 1)
self.assertEqual(res[0].name, "1")
# Filter by server status test
res = self.dbapi.server_get_all(self.context, project_only=False,
filters={"status": "active"})
for i, item in enumerate(res):
self.assertEqual(item.status, "active")
# Filter by flavor_uuid test
res = self.dbapi.server_get_all(self.context, project_only=False,
filters={"flavor_uuid":
servers_project_all[0].
flavor_uuid})
for i, item in enumerate(res):
self.assertEqual(item.flavor_uuid,
servers_project_all[0].flavor_uuid)
# Filter by image_uuid test
res = self.dbapi.server_get_all(self.context, project_only=False,
filters={"image_uuid":
servers_project_all[0].
image_uuid})
for i, item in enumerate(res):
self.assertEqual(item.image_uuid,
servers_project_all[0].image_uuid)
# Set project_only to True
# get servers from current project (project_1)
self.context.tenant = 'project_1'
res = self.dbapi.server_get_all(self.context, project_only=True)
res_uuids = [r.uuid for r in res]
uuids_project_1 = [r.uuid for r in servers_project_1]
six.assertCountEqual(self, uuids_project_1, res_uuids)
# Set project_only to True
@ -83,6 +114,7 @@ class DbServerTestCase(base.DbTestCase):
self.context.tenant = 'project_2'
res = self.dbapi.server_get_all(self.context, project_only=True)
res_uuids = [r.uuid for r in res]
uuids_project_2 = [r.uuid for r in servers_project_2]
six.assertCountEqual(self, uuids_project_2, res_uuids)
def test_server_destroy(self):

View File

@ -45,12 +45,11 @@ class TestServerObject(base.DbTestCase):
with mock.patch.object(self.dbapi, 'server_get_all',
autospec=True) as mock_server_get_all:
mock_server_get_all.return_value = [self.fake_server]
project_only = False
servers = objects.Server.list(self.context, project_only)
filters = None
servers = objects.Server.list(self.context, project_only, filters)
mock_server_get_all.assert_called_once_with(
self.context, project_only)
self.context, project_only, filters)
self.assertIsInstance(servers[0], objects.Server)
self.assertEqual(self.context, servers[0]._context)