Deployable V2 API implementation

This patch aims at implementing basic functions of v2/deployable API.
It contains:
1. Implement deployable list/show API based on v1 deployable API.
2. Remove 'patch' method, because Cyborg V2 API provides other
API resource(ARQ) to let caller to do binding, not based on deployable.
3. Remove 'delete' method, because deployable can not actually be
deleted, it is reported by each driver on the node.
4. Add related UT.

As for further implementation like program API, it will be done in
another patch.

Change-Id: I4fd12a7d34594a63bd80bdc188ddc977d0a094e7
Task: 2007427
This commit is contained in:
Xinran Wang 2020-03-13 04:58:39 +00:00
parent 5e69508e97
commit a9ca2c5ece
6 changed files with 248 additions and 5 deletions

View File

@ -33,7 +33,7 @@ class FilterType(wtypes.UserType):
_supported_fields = wtypes.Enum(wtypes.text, 'parent_uuid', 'root_uuid',
'board', 'availability', 'interface_type',
'instance_uuid', 'limit', 'marker',
'sort_key', 'sort_dir')
'sort_key', 'sort_dir', 'name')
field = wsme.wsattr(_supported_fields, mandatory=True)
value = wsme.wsattr(wtypes.text, mandatory=True)

View File

@ -23,6 +23,7 @@ from cyborg.api.controllers import base
from cyborg.api.controllers import link
from cyborg.api.controllers.v2 import api_version_request
from cyborg.api.controllers.v2 import arqs
from cyborg.api.controllers.v2 import deployables
from cyborg.api.controllers.v2 import device_profiles
from cyborg.api.controllers.v2 import devices
from cyborg.api import expose
@ -66,6 +67,7 @@ class Controller(rest.RestController):
device_profiles = device_profiles.DeviceProfilesController()
accelerator_requests = arqs.ARQsController()
devices = devices.DevicesController()
deployables = deployables.DeployablesController()
@expose.expose(V2)
def get(self):

View File

@ -0,0 +1,136 @@
# Copyright 2020 Intel 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 pecan
import wsme
from wsme import types as wtypes
from oslo_serialization import jsonutils
from cyborg.api.controllers import base
from cyborg.api.controllers import link
from cyborg.api.controllers import types
from cyborg.api import expose
from cyborg.common import policy
from cyborg import objects
class Deployable(base.APIBase):
"""API representation of a deployable.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
a deployable.
"""
uuid = types.uuid
"""The UUID of the deployable"""
parent_id = types.integer
"""The parent ID of the deployable"""
root_id = types.integer
"""The root ID of the deployable"""
name = wtypes.text
"""The name of the deployable"""
num_accelerators = types.integer
"""The number of accelerators of the deployable"""
device_id = types.integer
"""The device on which the deployable is located"""
attributes_list = wtypes.text
"""The json list of attributes of the deployable"""
rp_uuid = types.uuid
"""The uuid of resouce provider which represents this deployable"""
driver_name = wtypes.text
"""The driver name of this deployables"""
bitstream_id = wtypes.text
"""The id of bitstream which has been program in this deployable"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link"""
def __init__(self, **kwargs):
super(Deployable, self).__init__(**kwargs)
self.fields = []
for field in objects.Deployable.fields:
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
def convert_with_link(self, obj_dep):
api_dep = Deployable(**obj_dep.as_dict())
url = pecan.request.public_url
api_dep.links = [
link.Link.make_link('self', url, 'deployables', api_dep.uuid),
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 = jsonutils.dumps(attributes_list)
return api_dep
class DeployableCollection(Deployable):
"""API representation of a collection of deployables."""
deployables = [Deployable]
"""A list containing deployable objects"""
def convert_with_links(self, obj_deps):
collection = DeployableCollection()
collection.deployables = [
self.convert_with_link(obj_dep) for obj_dep in obj_deps]
return collection
class DeployablesController(base.CyborgController,
DeployableCollection):
"""REST controller for Deployables."""
@policy.authorize_wsgi("cyborg:deployable", "get_one")
@expose.expose(Deployable, types.uuid)
def get_one(self, uuid):
"""Retrieve information about the given deployable.
:param uuid: UUID of a deployable.
"""
obj_dep = objects.Deployable.get(pecan.request.context, uuid)
return self.convert_with_link(obj_dep)
@policy.authorize_wsgi("cyborg:deployable", "get_all")
@expose.expose(DeployableCollection, wtypes.ArrayType(types.FilterType))
def get_all(self, filters=None):
"""Retrieve a list of deployables.
:param filters: a filter of FilterType to get deployables list by
filter.
"""
filters_dict = {}
if filters:
for filter in filters:
filters_dict.update(filter.as_dict())
context = pecan.request.context
obj_deps = objects.Deployable.list(context, filters=filters_dict)
return self.convert_with_links(obj_deps)

View File

@ -115,6 +115,16 @@ device_policies = [
description='Retrieve all device records'),
]
deployable_policies = [
policy.RuleDefault('cyborg:deployable:get_one',
'rule:allow',
description='Show deployable detail'),
policy.RuleDefault('cyborg:deployable:get_all',
'rule:allow',
description='Retrieve all deployable records'),
]
fpga_policies = [
policy.RuleDefault('cyborg:fpga:get_one',
'rule:allow',
@ -133,7 +143,8 @@ def list_policies():
+ fpga_policies \
+ accelerator_request_policies \
+ device_profile_policies \
+ device_policies
+ device_policies \
+ deployable_policies
@lockutils.synchronized('policy_enforcer', 'cyborg-')

View File

@ -0,0 +1,97 @@
# Copyright 2020 Intel, 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 cyborg.tests.unit.api.controllers.v2 import base as v2_test
from cyborg.tests.unit import fake_deployable
class TestDeployablesController(v2_test.APITestV2):
DEPLOYABLE_URL = '/deployables'
def setUp(self):
super(TestDeployablesController, self).setUp()
self.headers = self.gen_headers(self.context)
self.fake_deployable = fake_deployable.fake_deployable_obj(
self.context)
def _validate_links(self, links, deployable_uuid):
has_self_link = False
for link in links:
if link['rel'] == 'self':
has_self_link = True
url = link['href']
components = url.split('/')
self.assertEqual(components[-1], deployable_uuid)
self.assertTrue(has_self_link)
def _validate_deployable(self, in_deployable, out_deployable):
for field in in_deployable.keys():
if field != 'id':
self.assertEqual(in_deployable[field], out_deployable[field])
# Check that the link is properly set up
self._validate_links(out_deployable['links'], in_deployable['uuid'])
@mock.patch('cyborg.objects.Deployable.get')
def test_get_one_by_uuid(self, mock_deployable):
in_deployable = self.fake_deployable
mock_deployable.return_value = in_deployable
uuid = in_deployable['uuid']
url = self.DEPLOYABLE_URL + '/%s'
out_deployable = self.get_json(url % uuid, headers=self.headers)
mock_deployable.assert_called_once()
self._validate_deployable(in_deployable, out_deployable)
@mock.patch('cyborg.objects.Deployable.list')
def test_get_all(self, mock_deployables):
mock_deployables.return_value = [self.fake_deployable]
data = self.get_json(self.DEPLOYABLE_URL, headers=self.headers)
out_deployable = data['deployables']
self.assertIsInstance(out_deployable, list)
for out_dev in out_deployable:
self.assertIsInstance(out_dev, dict)
self.assertTrue(len(out_deployable), 1)
self._validate_deployable(self.fake_deployable, out_deployable[0])
@mock.patch('cyborg.objects.Deployable.list')
def test_get_with_filters(self, mock_deployables):
mock_deployables.return_value = [self.fake_deployable]
# TODO(Xinran) Add API doc to explain the usage of filter.
# Add "?filters.field=limit&filters.value=1" in DEPLOYABLE_URL, in
# order to list the deployables with limited number which is 1.
data = self.get_json(
self.DEPLOYABLE_URL + "?filters.field=limit&filters.value=1",
headers=self.headers)
out_deployable = data['deployables']
mock_deployables.assert_called_once_with(mock.ANY,
filters={"limit": "1"})
self._validate_deployable(self.fake_deployable, out_deployable[0])
@mock.patch('cyborg.objects.Deployable.list')
def test_get_with_filters_not_match(self, mock_deployables):
# This will return null list because the fake deployable's name
# is "dp_name".
mock_deployables.return_value = []
data = self.get_json(
self.DEPLOYABLE_URL +
"?filters.field=name&filters.value=wrongname",
headers=self.headers)
out_deployable = data['deployables']
mock_deployables.assert_called_once_with(mock.ANY,
filters={"name": "wrongname"})
self.assertEqual(len(out_deployable), 0)

View File

@ -16,12 +16,10 @@ from oslo_utils import uuidutils
from cyborg import objects
from cyborg.objects import fields
import json
def fake_db_deployable(**updates):
root_uuid = uuidutils.generate_uuid()
bdf = {"domain": "0000", "bus": "00", "device": "01", "function": "1"}
db_deployable = {
'id': 1,
'uuid': root_uuid,
@ -33,7 +31,6 @@ def fake_db_deployable(**updates):
'driver_name': "fake-driver-name",
'rp_uuid': None,
'bitstream_id': None,
"cpid_info": json.dumps(bdf).encode('utf-8')
}
for name, field in objects.Deployable.fields.items():