diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 8b0c77ac..1747864f 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -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. diff --git a/api-ref/source/v1/servers.inc b/api-ref/source/v1/servers.inc index b1efa14a..493fbd17 100644 --- a/api-ref/source/v1/servers.inc +++ b/api-ref/source/v1/servers.inc @@ -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 -------- diff --git a/mogan/api/controllers/v1/servers.py b/mogan/api/controllers/v1/servers.py index 9cb7b43f..dc4accde 100644 --- a/mogan/api/controllers/v1/servers.py +++ b/mogan/api/controllers/v1/servers.py @@ -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, diff --git a/mogan/db/sqlalchemy/api.py b/mogan/db/sqlalchemy/api.py index 90b58436..77a22463 100644 --- a/mogan/db/sqlalchemy/api.py +++ b/mogan/db/sqlalchemy/api.py @@ -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(): diff --git a/mogan/objects/server.py b/mogan/objects/server.py index 7f27ac74..15b2293c 100644 --- a/mogan/objects/server.py +++ b/mogan/objects/server.py @@ -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 diff --git a/mogan/tests/functional/api/v1/test_servers.py b/mogan/tests/functional/api/v1/test_servers.py index 17d642f1..f677aaf4 100644 --- a/mogan/tests/functional/api/v1/test_servers.py +++ b/mogan/tests/functional/api/v1/test_servers.py @@ -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) diff --git a/mogan/tests/unit/db/test_servers.py b/mogan/tests/unit/db/test_servers.py index 2468bdaa..7ffed780 100644 --- a/mogan/tests/unit/db/test_servers.py +++ b/mogan/tests/unit/db/test_servers.py @@ -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): diff --git a/mogan/tests/unit/objects/test_server.py b/mogan/tests/unit/objects/test_server.py index 88d1983b..3ead2a13 100644 --- a/mogan/tests/unit/objects/test_server.py +++ b/mogan/tests/unit/objects/test_server.py @@ -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)