diff --git a/cyborg/api/controllers/v1/__init__.py b/cyborg/api/controllers/v1/__init__.py index 20b13d72..6b3d2085 100644 --- a/cyborg/api/controllers/v1/__init__.py +++ b/cyborg/api/controllers/v1/__init__.py @@ -22,7 +22,6 @@ from wsme import types as wtypes from cyborg.api.controllers import base from cyborg.api.controllers import link from cyborg.api.controllers.v1 import accelerators -from cyborg.api.controllers.v1 import deployables from cyborg.api import expose @@ -52,7 +51,6 @@ class Controller(rest.RestController): """Version 1 API controller root""" accelerators = accelerators.AcceleratorsController() - deployables = deployables.DeployablesController() @expose.expose(V1) def get(self): diff --git a/cyborg/api/controllers/v1/accelerators.py b/cyborg/api/controllers/v1/accelerators.py index 7405bfe0..fddceff0 100644 --- a/cyborg/api/controllers/v1/accelerators.py +++ b/cyborg/api/controllers/v1/accelerators.py @@ -14,16 +14,15 @@ # under the License. import pecan -from pecan import rest from six.moves import http_client import wsme from wsme import types as wtypes from cyborg.api.controllers import base from cyborg.api.controllers import link +from cyborg.api.controllers.v1 import deployables from cyborg.api.controllers.v1 import types from cyborg.api.controllers.v1 import utils as api_utils -from cyborg.api.controllers.v1 import deployables from cyborg.api import expose from cyborg.common import exception from cyborg.common import policy @@ -77,6 +76,10 @@ class Accelerator(base.APIBase): def __init__(self, **kwargs): super(Accelerator, self).__init__(**kwargs) self.fields = [] + # NOTE(wangzhh): It not worked here. Because the response contain a + # white_list named _wsme_attributes. See wsme.types.list_attributes. + # Attribute which is not in the list will be ignored. + # We have no disscussion about it, so just left it here now. for field in objects.Accelerator.fields: self.fields.append(field) setattr(self, field, kwargs.get(field, wtypes.Unset)) diff --git a/cyborg/api/controllers/v1/deployables.py b/cyborg/api/controllers/v1/deployables.py index 8ecc507f..641c0203 100644 --- a/cyborg/api/controllers/v1/deployables.py +++ b/cyborg/api/controllers/v1/deployables.py @@ -13,12 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. +import json import pecan -from pecan import rest from six.moves import http_client import wsme from wsme import types as wtypes -import json from cyborg.api.controllers import base from cyborg.api.controllers import link @@ -186,23 +185,48 @@ class DeployablesController(base.CyborgController): @policy.authorize_wsgi("cyborg:deployable", "get_all") @expose.expose(DeployableCollection, int, types.uuid, wtypes.text, - wtypes.text, types.boolean) - def get_all(self): + wtypes.text, wtypes.ArrayType(types.FilterType)) + # TODO(wangzhh): Remove limit, marker, sort_key, sort_dir in next release. + # They are used to compatible with R release client. + def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc', + filters=None): """Retrieve a list of deployables.""" - obj_deps = objects.Deployable.list(pecan.request.context) + filters_dict = {} + self._generate_filters(limit, sort_key, sort_dir, filters_dict) + if filters: + for filter in filters: + filters_dict.update(filter.as_dict()) + context = pecan.request.context + if marker: + marker_obj = objects.Deployable.get(context, marker) + filters_dict["marker_obj"] = marker_obj + obj_deps = objects.Deployable.list(context, filters=filters_dict) return DeployableCollection.convert_with_links(obj_deps) + def _generate_filters(self, limit, sort_key, sort_dir, filters_dict): + """This method are used to compatible with R release client.""" + if limit: + filters_dict["limit"] = limit + if sort_key: + filters_dict["sort_key"] = sort_key + if sort_dir: + filters_dict["sort_dir"] = sort_dir + @policy.authorize_wsgi("cyborg:deployable", "update") @expose.expose(Deployable, types.uuid, body=[DeployablePatchType]) def patch(self, uuid, patch): """Update a deployable. + Usage: curl -X PATCH {ip}:{port}/v1/accelerators/deployables/ + {deployable_uuid} -d '[{"path":"/instance_uuid","value": + {instance_uuid}, "op":"replace"}]' -H "Content-type: + application/json" + :param uuid: UUID of a deployable. :param patch: a json PATCH document to apply to this deployable. """ context = pecan.request.context obj_dep = objects.Deployable.get(context, uuid) - try: api_dep = Deployable( **api_utils.apply_jsonpatch(obj_dep.as_dict(), patch)) diff --git a/cyborg/api/controllers/v1/types.py b/cyborg/api/controllers/v1/types.py index 01fc437f..fad93049 100644 --- a/cyborg/api/controllers/v1/types.py +++ b/cyborg/api/controllers/v1/types.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import inspect import json +import inspect from oslo_utils import strutils from oslo_utils import uuidutils @@ -25,6 +25,44 @@ from cyborg.common import exception from cyborg.common.i18n import _ +class FilterType(wtypes.UserType): + """Query filter.""" + name = 'filtertype' + basetype = wtypes.text + + _supported_fields = wtypes.Enum(wtypes.text, 'parent_uuid', 'root_uuid', + 'vender', 'host', 'board', 'availability', + 'assignable', 'interface_type', + 'instance_uuid', 'limit', 'marker', + 'sort_key', 'sort_dir') + + field = wsme.wsattr(_supported_fields, mandatory=True) + value = wsme.wsattr(wtypes.text, mandatory=True) + + def __repr__(self): + # for logging calls + return '' % (self.field, + self.value) + + @classmethod + def sample(cls): + return cls(field='interface_type', + value='pci') + + def as_dict(self): + d = dict() + d[getattr(self, 'field')] = getattr(self, 'value') + return d + + @staticmethod + def validate(filters): + for filter in filters: + if filter.field not in FilterType._supported_fields: + msg = _("'%s' is an unsupported field for querying.") + raise wsme.exc.ClientSideError(msg % filter.field) + return filters + + class UUIDType(wtypes.UserType): """A simple UUID type.""" diff --git a/cyborg/objects/deployable.py b/cyborg/objects/deployable.py index 0d6ea27f..5026b11f 100644 --- a/cyborg/objects/deployable.py +++ b/cyborg/objects/deployable.py @@ -109,10 +109,12 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat): return obj_dpl_list @classmethod - def list(cls, context): + def list(cls, context, filters={}): """Return a list of Deployable objects.""" - db_deps = cls.dbapi.deployable_list(context) - + if filters: + db_deps = cls.dbapi.deployable_get_by_filters(context, filters) + else: + db_deps = cls.dbapi.deployable_list(context) obj_dpl_list = cls._from_db_object_list(db_deps, context) for obj_dpl in obj_dpl_list: query = {"deployable_id": obj_dpl.id} diff --git a/cyborg/tests/unit/api/controllers/v1/test_accelerators.py b/cyborg/tests/unit/api/controllers/v1/test_accelerators.py index 76bd6c00..b1a1f0ff 100644 --- a/cyborg/tests/unit/api/controllers/v1/test_accelerators.py +++ b/cyborg/tests/unit/api/controllers/v1/test_accelerators.py @@ -18,6 +18,7 @@ import mock from oslo_utils import timeutils from six.moves import http_client +from cyborg.api.controllers.v1.accelerators import Accelerator from cyborg.conductor import rpcapi from cyborg.tests.unit.api.controllers.v1 import base as v1_test from cyborg.tests.unit.db import utils as db_utils @@ -80,19 +81,8 @@ class TestList(v1_test.APITestV1): data = self.get_json('/accelerators/%s' % self.acc.uuid, headers=self.headers) self.assertEqual(self.acc.uuid, data['uuid']) - self.assertIn('acc_capability', data) - self.assertIn('acc_type', data) - self.assertIn('created_at', data) - self.assertIn('description', data) - self.assertIn('device_type', data) - self.assertIn('links', data) - self.assertIn('name', data) - self.assertIn('product_id', data) - self.assertIn('project_id', data) - self.assertIn('remotable', data) - self.assertIn('updated_at', data) - self.assertIn('user_id', data) - self.assertIn('vendor_id', data) + for attr in Accelerator._wsme_attributes: + self.assertIn(attr.name, data) def test_get_all(self): data = self.get_json('/accelerators', headers=self.headers) diff --git a/cyborg/tests/unit/api/controllers/v1/test_deployables.py b/cyborg/tests/unit/api/controllers/v1/test_deployables.py new file mode 100644 index 00000000..cff8eeb1 --- /dev/null +++ b/cyborg/tests/unit/api/controllers/v1/test_deployables.py @@ -0,0 +1,76 @@ +# Copyright 2018 Lenovo, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from six.moves import http_client + +from cyborg.api.controllers.v1.deployables import Deployable +from cyborg.tests.unit.api.controllers.v1 import base as v1_test +from cyborg.tests.unit import fake_deployable + + +class TestDeployableController(v1_test.APITestV1): + + def setUp(self): + super(TestDeployableController, self).setUp() + self.headers = self.gen_headers(self.context) + self.deployable_uuids = ['10efe63d-dfea-4a37-ad94-4116fba50981'] + + @mock.patch('cyborg.objects.Deployable.get') + def test_get_one(self, mock_get_dep): + dep_uuid = self.deployable_uuids[0] + mock_get_dep.return_value = fake_deployable.\ + fake_deployable_obj(self.context, uuid=dep_uuid) + data = self.get_json('/accelerators/deployables/%s' % dep_uuid, + headers=self.headers) + self.assertEqual(dep_uuid, data['uuid']) + for attr in Deployable._wsme_attributes: + self.assertIn(attr.name, data) + mock_get_dep.assert_called_once_with(mock.ANY, dep_uuid) + + @mock.patch('cyborg.objects.Deployable.list') + def test_get_all(self, mock_list_dep): + fake_deps = [] + for uuid in self.deployable_uuids: + fake_dep = fake_deployable.fake_deployable_obj(self.context, + uuid=uuid) + fake_deps.append(fake_dep) + mock_list_dep.return_value = fake_deps + data = self.get_json('/accelerators/deployables', + headers=self.headers) + self.assertEqual(len(self.deployable_uuids), len(data['deployables'])) + mock_list_dep.assert_called_once() + + @mock.patch('cyborg.conductor.rpcapi.ConductorAPI.deployable_update') + @mock.patch('cyborg.objects.Deployable.get') + def test_patch(self, mock_get_dep, mock_deployable_update): + self.headers['X-Roles'] = 'admin' + self.headers['Content-Type'] = 'application/json' + dep_uuid = self.deployable_uuids[0] + fake_dep = fake_deployable.fake_deployable_obj(self.context, + uuid=dep_uuid) + mock_get_dep.return_value = fake_dep + instance_uuid = '10efe63d-dfea-4a37-ad94-4116fba50981' + fake_dep.instance_uuid = instance_uuid + mock_deployable_update.return_value = fake_dep + response = self.patch_json('/accelerators/deployables/%s' % dep_uuid, + [{'path': '/instance_uuid', + 'value': instance_uuid, + 'op': 'replace'}], + headers=self.headers) + self.assertEqual(http_client.OK, response.status_code) + data = response.json_body + self.assertEqual(instance_uuid, data['instance_uuid']) + mock_deployable_update.assert_called_once() diff --git a/tox.ini b/tox.ini index edd61093..0b1f5b11 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ skipsdist = True [testenv] usedevelop = True +whitelist_externals = rm install_command = {[testenv:common-constraints]install_command} setenv = VIRTUAL_ENV={envdir}