Imply new cyborg deployable api

1. Imply deployable api as our discussion on 19th, Jul.
   https://etherpad.openstack.org/p/cyborg-rocky-development
2. Add filter to deployable.
3. Improve test_accelerator.
4. Add command "rm" to tox whitelist_externals.

Change-Id: I78154ef14a0ab3831a17ba93f44dfedef0d56df1
This commit is contained in:
wangzh21 2018-07-22 22:45:29 +08:00
parent c8ba137be6
commit 458e0b1162
8 changed files with 159 additions and 27 deletions

View File

@ -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):

View File

@ -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))

View File

@ -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))

View File

@ -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 '<Query %s %s>' % (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."""

View File

@ -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}

View File

@ -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)

View File

@ -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()

View File

@ -5,6 +5,7 @@ skipsdist = True
[testenv]
usedevelop = True
whitelist_externals = rm
install_command = {[testenv:common-constraints]install_command}
setenv =
VIRTUAL_ENV={envdir}