From 185169e380aecac29fbe15f29412c957c71e96e5 Mon Sep 17 00:00:00 2001 From: Li Liu Date: Fri, 29 Jun 2018 22:41:46 -0400 Subject: [PATCH] Added rest API for FPGA programming Here is an example on how to call this api curl -s -X PATCH -H "X-Auth-Token: $OS_TOKEN" -H "Content-Type: application/json"\ -d '[{ "path": "/program", "op": "replace", "value": [{ "image_uuid": "9a17439a-85d0-4c53-a3d3-0f68a2eac896" }] }]'\ http://{api_ip}:6666/v1/accelerators/deployables/{pf_uuid}/program Change-Id: Iab6150f39be9ccb34f1fc86d6942b1b7c48a4348 --- cyborg/api/controllers/base.py | 29 ++++++++++++ cyborg/api/controllers/v1/accelerators.py | 5 +- cyborg/api/controllers/v1/deployables.py | 31 +++++++++++- cyborg/common/policy.py | 20 +++++++- cyborg/objects/deployable.py | 25 +++++++--- .../api/controllers/v1/test_fpga_program.py | 47 +++++++++++++++++++ cyborg/tests/unit/fake_deployable.py | 7 +-- cyborg/tests/unit/objects/test_deployable.py | 43 +++-------------- 8 files changed, 156 insertions(+), 51 deletions(-) create mode 100644 cyborg/tests/unit/api/controllers/v1/test_fpga_program.py diff --git a/cyborg/api/controllers/base.py b/cyborg/api/controllers/base.py index 9131d3a3..f029203e 100644 --- a/cyborg/api/controllers/base.py +++ b/cyborg/api/controllers/base.py @@ -15,8 +15,10 @@ import datetime +import pecan import wsme from wsme import types as wtypes +from pecan import rest class APIBase(wtypes.Base): @@ -31,3 +33,30 @@ class APIBase(wtypes.Base): return dict((k, getattr(self, k)) for k in self.fields if hasattr(self, k) and getattr(self, k) != wsme.Unset) + + +class CyborgController(rest.RestController): + + def _handle_patch(self, method, remainder, request=None): + """Routes ``PATCH`` _custom_actions.""" + # route to a patch_all or get if no additional parts are available + if not remainder or remainder == ['']: + controller = self._find_controller('patch_all', 'patch') + if controller: + return controller, [] + pecan.abort(404) + + controller = getattr(self, remainder[0], None) + if controller and not inspect.ismethod(controller): + return pecan.routing.lookup_controller(controller, remainder[1:]) + # route to custom_action + match = self._handle_custom_action(method, remainder, request) + if match: + return match + + # finally, check for the regular patch_one/patch requests + controller = self._find_controller('patch_one', 'patch') + if controller: + return controller, remainder + + pecan.abort(405) diff --git a/cyborg/api/controllers/v1/accelerators.py b/cyborg/api/controllers/v1/accelerators.py index 45f24326..7405bfe0 100644 --- a/cyborg/api/controllers/v1/accelerators.py +++ b/cyborg/api/controllers/v1/accelerators.py @@ -23,6 +23,7 @@ from cyborg.api.controllers import base from cyborg.api.controllers import link 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 @@ -118,7 +119,7 @@ class AcceleratorPatchType(types.JsonPatchType): '/product_id', '/remotable'] -class AcceleratorsControllerBase(rest.RestController): +class AcceleratorsControllerBase(base.CyborgController): _resource = None @@ -130,6 +131,8 @@ class AcceleratorsControllerBase(rest.RestController): class AcceleratorsController(AcceleratorsControllerBase): """REST controller for Accelerators.""" + deployables = deployables.DeployablesController() + @policy.authorize_wsgi("cyborg:accelerator", "create", False) @expose.expose(Accelerator, body=types.jsontype, status_code=http_client.CREATED) diff --git a/cyborg/api/controllers/v1/deployables.py b/cyborg/api/controllers/v1/deployables.py index afb5e2cc..8ecc507f 100644 --- a/cyborg/api/controllers/v1/deployables.py +++ b/cyborg/api/controllers/v1/deployables.py @@ -18,6 +18,7 @@ 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 @@ -79,6 +80,9 @@ class Deployable(base.APIBase): availability = wtypes.text """The availability of the deployable""" + attributes_list = wtypes.text + """The json list of attributes of the deployable""" + links = wsme.wsattr([link.Link], readonly=True) """A list containing a self link""" @@ -98,6 +102,13 @@ class Deployable(base.APIBase): link.Link.make_link('bookmark', url, 'deployables', api_dep.uuid, bookmark=True) ] + query = {"deployable_id": obj_dep.id} + attr_get_list = objects.Attribute.get_by_filter(pecan.request.context, + query) + attributes_list = [] + for exist_attr in attr_get_list: + attributes_list.append({exist_attr.key: exist_attr.value}) + api_dep.attributes_list = json.dumps(attributes_list) return api_dep @@ -125,9 +136,27 @@ class DeployablePatchType(types.JsonPatchType): return defaults + ['/address', '/host', '/type'] -class DeployablesController(rest.RestController): +class DeployablesController(base.CyborgController): """REST controller for Deployables.""" + _custom_actions = {'program': ['PATCH']} + + @policy.authorize_wsgi("cyborg:deployable", "program", False) + @expose.expose(Deployable, types.uuid, body=[DeployablePatchType]) + def program(self, uuid, program_info): + """Program a new deployable(FPGA). + + :param uuid: The uuid of the target deployable. + :param program_info: JSON string containing what to program. + """ + + image_uuid = program_info[0]['value'][0]['image_uuid'] + obj_dep = objects.Deployable.get(pecan.request.context, uuid) + # Set attribute of the new bitstream/image information + obj_dep.add_attribute(pecan.request.context, 'image_uuid', image_uuid) + # TODO (Li Liu) Trigger the program api in Agnet. + return Deployable.convert_with_links(obj_dep) + @policy.authorize_wsgi("cyborg:deployable", "create", False) @expose.expose(Deployable, body=types.jsontype, status_code=http_client.CREATED) diff --git a/cyborg/common/policy.py b/cyborg/common/policy.py index da3bd0d1..c8fb084b 100644 --- a/cyborg/common/policy.py +++ b/cyborg/common/policy.py @@ -104,11 +104,29 @@ deployable_policies = [ policy.RuleDefault('cyborg:deployable:update', 'rule:admin_api', description='Update deployable records'), + policy.RuleDefault('cyborg:deployable:program', + 'rule:allow', + description='Program deployable(FPGA) records'), +] + +fpga_policies = [ + policy.RuleDefault('cyborg:fpga:get_one', + 'rule:allow', + description='Show fpga detail'), + policy.RuleDefault('cyborg:fpga:get_all', + 'rule:allow', + description='Retrieve all fpga records'), + policy.RuleDefault('cyborg:fpga:update', + 'rule:allow', + description='Update fpga records'), ] def list_policies(): - return default_policies + accelerator_policies + deployable_policies + return default_policies \ + + accelerator_policies \ + + deployable_policies \ + + fpga_policies @lockutils.synchronized('policy_enforcer', 'cyborg-') diff --git a/cyborg/objects/deployable.py b/cyborg/objects/deployable.py index bf7dbed6..0d6ea27f 100644 --- a/cyborg/objects/deployable.py +++ b/cyborg/objects/deployable.py @@ -73,7 +73,7 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat): raise exception.ObjectActionError(action='create', reason='uuid is required') - if self.parent_uuid is None: + if not hasattr(self, 'parent_uuid') or self.parent_uuid is None: self.root_uuid = self.uuid else: self.root_uuid = self._get_parent_root_uuid() @@ -126,6 +126,10 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat): updates = self.obj_get_changes() db_dep = self.dbapi.deployable_update(context, self.uuid, updates) self._from_db_object(self, db_dep) + query = {"deployable_id": self.id} + attr_get_list = Attribute.get_by_filter(context, + query) + self.attributes_list = attr_get_list def destroy(self, context): """Delete a Deployable from the DB.""" @@ -133,19 +137,28 @@ class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat): self.dbapi.deployable_delete(context, self.uuid) self.obj_reset_changes() - def add_attribute(self, attribute): + def add_attribute(self, context, key, value): """add a attribute object to the attribute_list. If the attribute already exists, it will update the value, otherwise, the vf will be appended to the list """ for exist_attr in self.attributes_list: - if base.obj_equal_prims(attribute, exist_attr): + if key == exist_attr.key: LOG.warning("The attribute already exists") + if value != exist_attr.value: + exist_attr.value = attribute.value + exist_attr.save(context) return None - attribute.deployable_id = self.id - attribute_copy = copy.deepcopy(attribute) - self.attributes_list.append(attribute_copy) + # The attribute does not exist yet. Create it. + attr_vals = { + 'deployable_id': self.id, + 'key': key, + 'value': value + } + attr = Attribute(context, **attr_vals) + attr.create(context) + self.attributes_list.append(attr) def delete_attribute(self, context, attribute): """remove an attribute from the attributes_list diff --git a/cyborg/tests/unit/api/controllers/v1/test_fpga_program.py b/cyborg/tests/unit/api/controllers/v1/test_fpga_program.py new file mode 100644 index 00000000..2e0db881 --- /dev/null +++ b/cyborg/tests/unit/api/controllers/v1/test_fpga_program.py @@ -0,0 +1,47 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# 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 TestFPGAProgramController(v1_test.APITestV1): + + def setUp(self): + super(TestFPGAProgramController, self).setUp() + self.headers = self.gen_headers(self.context) + self.deployable_uuids = ['0acbf8d6-e02a-4394-aae3-57557d209498'] + + @mock.patch('cyborg.objects.Deployable.get') + def test_program(self, mock_get_dep): + 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 + body = [{"image_uuid": "9a17439a-85d0-4c53-a3d3-0f68a2eac896"}] + response = self.\ + patch_json('/accelerators/deployables/%s/program' % dep_uuid, + [{'path': '/program', 'value': body, + 'op': 'replace'}], + headers=self.headers) + self.assertEqual(http_client.OK, response.status_code) + data = response.json_body + self.assertEqual(dep_uuid, data['uuid']) diff --git a/cyborg/tests/unit/fake_deployable.py b/cyborg/tests/unit/fake_deployable.py index 1525bf4d..3352783a 100644 --- a/cyborg/tests/unit/fake_deployable.py +++ b/cyborg/tests/unit/fake_deployable.py @@ -62,10 +62,7 @@ def fake_db_deployable(**updates): def fake_deployable_obj(context, obj_dpl_class=None, **updates): if obj_dpl_class is None: obj_dpl_class = objects.Deployable - expected_attrs = updates.pop('expected_attrs', None) - deploy = obj_dpl_class._from_db_object(context, - obj_dpl_class(), - fake_db_deployable(**updates), - expected_attrs=expected_attrs) + deploy = obj_dpl_class._from_db_object(obj_dpl_class(), + fake_db_deployable(**updates)) deploy.obj_reset_changes() return deploy diff --git a/cyborg/tests/unit/objects/test_deployable.py b/cyborg/tests/unit/objects/test_deployable.py index 89ee308f..a51c323d 100644 --- a/cyborg/tests/unit/objects/test_deployable.py +++ b/cyborg/tests/unit/objects/test_deployable.py @@ -161,17 +161,12 @@ class _TestDeployableObject(DbTestCase): dpl_get = objects.Deployable.get(self.context, dpl.uuid) db_attr = self.fake_attribute - attr = objects.Attribute(context=self.context, - **db_attr) - attr.deployable_id = dpl_get.id - attr.create(self.context) - dpl.add_attribute(attr) + dpl.add_attribute(self.context, db_attr['key'], db_attr['value']) dpl.save(self.context) dpl_get = objects.Deployable.get(self.context, dpl.uuid) self.assertEqual(len(dpl_get.attributes_list), 1) - self.assertEqual(dpl_get.attributes_list[0].id, attr.id) def test_delete_attribute(self): db_acc = self.fake_accelerator @@ -187,21 +182,13 @@ class _TestDeployableObject(DbTestCase): dpl.create(self.context) dpl_get = objects.Deployable.get(self.context, dpl.uuid) db_attr = self.fake_attribute - attr = objects.Attribute(context=self.context, - **db_attr) - attr.deployable_id = dpl_get.id - attr.create(self.context) - dpl_get.add_attribute(attr) + dpl_get.add_attribute(self.context, db_attr['key'], db_attr['value']) dpl_get.save(self.context) dpl_get = objects.Deployable.get(self.context, dpl_get.uuid) self.assertEqual(len(dpl_get.attributes_list), 1) - self.assertEqual(dpl_get.attributes_list[0].id, attr.id) dpl_get.delete_attribute(self.context, dpl_get.attributes_list[0]) self.assertEqual(len(dpl_get.attributes_list), 0) - self.assertRaises(exception.AttributeNotFound, - objects.Attribute.get, self.context, - attr.uuid) def test_get_by_filter_with_attributes(self): db_acc = self.fake_accelerator @@ -225,44 +212,26 @@ class _TestDeployableObject(DbTestCase): dpl2_get = objects.Deployable.get(self.context, dpl2.uuid) db_attr = self.fake_attribute - attr = objects.Attribute(context=self.context, - **db_attr) - attr.deployable_id = dpl_get.id - attr.create(self.context) db_attr2 = self.fake_attribute2 - attr2 = objects.Attribute(context=self.context, - **db_attr2) - attr2.deployable_id = dpl2_get.id - attr2.create(self.context) db_attr3 = self.fake_attribute3 - attr3 = objects.Attribute(context=self.context, - **db_attr3) - attr3.deployable_id = dpl2_get.id - attr3.create(self.context) - dpl.add_attribute(attr) + dpl.add_attribute(self.context, 'attr_key', 'attr_val') dpl.save(self.context) - dpl2.add_attribute(attr2) + dpl2.add_attribute(self.context, 'test_key', 'test_val') dpl2.save(self.context) - dpl2.add_attribute(attr3) + dpl2.add_attribute(self.context, 'test_key3', 'test_val3') dpl2.save(self.context) query = {"attr_key": "attr_val"} dpl_get_list = objects.Deployable.get_by_filter(self.context, query) - self.assertEqual(len(dpl_get_list), 2) + self.assertEqual(len(dpl_get_list), 1) self.assertEqual(dpl_get_list[0].uuid, dpl.uuid) - attr2.set_key_value_pair("test_key", "test_val") - attr2.save(self.context) - - attr3.set_key_value_pair("test_key3", "test_val3") - attr3.save(self.context) - query = {"test_key": "test_val"} dpl_get_list = objects.Deployable.get_by_filter(self.context, query) self.assertEqual(len(dpl_get_list), 1)