API GET to return only minimal data

Requests to list top-level resources like nodes, chassis or ports will
now return only a subset of it's attributes, a subresource called /detail
could be used to get the full details of the resource. This changes
is supposed to improve performance and UX, also, others OpenStack APIs
already do it the same way so it's also about being consistent between
other APIs.

Change-Id: Ida45febf60e44d50e506f3680ab371e1027010c4
Closes-Bug: #1227431
This commit is contained in:
Lucas Alvares Gomes 2013-09-23 17:30:59 +01:00
parent 59c2862d65
commit 3dd85586b6
9 changed files with 338 additions and 117 deletions

View File

@ -434,8 +434,9 @@ Usage
======= ============= ==========
Verb Path Response
======= ============= ==========
GET /nodes List nodes.
GET /nodes/<id> Retrieve a specific node.
GET /nodes List nodes
GET /nodes/detail Lists all details for all nodes
GET /nodes/<id> Retrieve a specific node
POST /nodes Create a new node
PATCH /nodes/<id> Update a node
DELETE /nodes/<id> Delete node and all associated ports
@ -566,16 +567,17 @@ Chassis
Usage
^^^^^^
======= ============= ==========
Verb Path Response
======= ============= ==========
GET /chassis List chassis
GET /chassis/<id> Retrieve a specific chassis
POST /chassis Create a new chassis
PATCH /chassis/<id> Update a chassis
DELETE /chassis/<id> Delete chassis and remove all associations between
nodes
======= ============= ==========
======= ============= ==========
Verb Path Response
======= ============= ==========
GET /chassis List chassis
GET /chassis/detail Lists all details for all chassis
GET /chassis/<id> Retrieve a specific chassis
POST /chassis Create a new chassis
PATCH /chassis/<id> Update a chassis
DELETE /chassis/<id> Delete chassis and remove all associations between
nodes
======= ============= ==========
Fields
@ -635,6 +637,7 @@ Usage
Verb Path Response
======= ============= ==========
GET /ports List ports
GET /ports/detail Lists all details for all ports
GET /ports/<id> Retrieve a specific port
POST /ports Create a new port
PATCH /ports/<id> Update a port

View File

@ -28,5 +28,12 @@ class APIBase(wtypes.Base):
getattr(self, k) != wsme.Unset)
@classmethod
def from_rpc_object(cls, m):
return cls(**m.as_dict())
def from_rpc_object(cls, m, fields=None):
"""Convert a RPC object to an API object."""
obj_dict = m.as_dict()
# Unset non-required fields so they do not appear
# in the message body
obj_dict.update(dict((k, wsme.Unset)
for k in obj_dict.keys()
if fields and k not in fields))
return cls(**obj_dict)

View File

@ -67,24 +67,28 @@ class Chassis(base.APIBase):
setattr(self, k, kwargs.get(k))
@classmethod
def convert_with_links(cls, rpc_chassis):
chassis = Chassis.from_rpc_object(rpc_chassis)
chassis.links = [link.Link.make_link('self', pecan.request.host_url,
def convert_with_links(cls, rpc_chassis, expand=True):
fields = ['uuid', 'description'] if not expand else None
chassis = Chassis.from_rpc_object(rpc_chassis, fields)
chassis.links = [link.Link.make_link('self',
pecan.request.host_url,
'chassis', chassis.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'chassis', chassis.uuid,
bookmark=True)
]
chassis.nodes = [link.Link.make_link('self', pecan.request.host_url,
'chassis',
chassis.uuid + "/nodes"),
link.Link.make_link('bookmark',
pecan.request.host_url,
'chassis',
chassis.uuid + "/nodes",
bookmark=True)
'chassis', chassis.uuid)
]
if expand:
chassis.nodes = [link.Link.make_link('self',
pecan.request.host_url,
'chassis',
chassis.uuid + "/nodes"),
link.Link.make_link('bookmark',
pecan.request.host_url,
'chassis',
chassis.uuid + "/nodes",
bookmark=True)
]
return chassis
@ -98,38 +102,62 @@ class ChassisCollection(collection.Collection):
self._type = 'chassis'
@classmethod
def convert_with_links(cls, chassis, limit, **kwargs):
def convert_with_links(cls, chassis, limit, url=None,
expand=False, **kwargs):
collection = ChassisCollection()
collection.chassis = [Chassis.convert_with_links(ch) for ch in chassis]
collection.next = collection.get_next(limit, **kwargs)
collection.chassis = [Chassis.convert_with_links(ch, expand)
for ch in chassis]
url = url or None
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
class ChassisController(rest.RestController):
"""REST controller for Chassis."""
nodes = node.NodesController(from_chassis=True)
"Expose nodes as a sub-element of chassis"
_custom_actions = {
'nodes': ['GET'],
'detail': ['GET'],
}
@wsme_pecan.wsexpose(ChassisCollection, int, unicode, unicode, unicode)
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of chassis."""
def _get_chassis(self, marker, limit, sort_key, sort_dir):
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Chassis.get_by_uuid(pecan.request.context,
marker)
chassis = pecan.request.dbapi.get_chassis_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return chassis
@wsme_pecan.wsexpose(ChassisCollection, unicode, int, unicode, unicode)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of chassis."""
chassis = self._get_chassis(marker, limit, sort_key, sort_dir)
return ChassisCollection.convert_with_links(chassis, limit,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(ChassisCollection, unicode, int, unicode, unicode)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of chassis with detail."""
# /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "chassis":
raise exception.HTTPNotFound
chassis = self._get_chassis(marker, limit, sort_key, sort_dir)
resource_url = '/'.join(['chassis', 'detail'])
return ChassisCollection.convert_with_links(chassis, limit,
url=resource_url,
expand=True,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(Chassis, unicode)
def get_one(self, uuid):
"""Retrieve information about the given chassis."""
@ -183,28 +211,3 @@ class ChassisController(rest.RestController):
def delete(self, uuid):
"""Delete a chassis."""
pecan.request.dbapi.destroy_chassis(uuid)
@wsme_pecan.wsexpose(node.NodeCollection, unicode, int, unicode,
unicode, unicode)
def nodes(self, chassis_uuid, limit=None, marker=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of nodes contained in the chassis."""
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Node.get_by_uuid(pecan.request.context,
marker)
nodes = pecan.request.dbapi.get_nodes_by_chassis(chassis_uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
collection = node.NodeCollection()
collection.nodes = [node.Node.convert_with_links(n) for n in nodes]
resource_url = '/'.join(['chassis', chassis_uuid, 'nodes'])
collection.next = collection.get_next(limit, url=resource_url,
sort_key=sort_key,
sort_dir=sort_dir)
return collection

View File

@ -223,8 +223,12 @@ class Node(base.APIBase):
setattr(self, k, kwargs.get(k))
@classmethod
def convert_with_links(cls, rpc_node):
node = Node.from_rpc_object(rpc_node)
def convert_with_links(cls, rpc_node, expand=True):
minimum_fields = ['uuid', 'power_state', 'target_power_state',
'provision_state', 'target_provision_state',
'instance_uuid']
fields = minimum_fields if not expand else None
node = Node.from_rpc_object(rpc_node, fields)
node.links = [link.Link.make_link('self', pecan.request.host_url,
'nodes', node.uuid),
link.Link.make_link('bookmark',
@ -232,13 +236,14 @@ class Node(base.APIBase):
'nodes', node.uuid,
bookmark=True)
]
node.ports = [link.Link.make_link('self', pecan.request.host_url,
'nodes', node.uuid + "/ports"),
link.Link.make_link('bookmark',
pecan.request.host_url,
'nodes', node.uuid + "/ports",
bookmark=True)
]
if expand:
node.ports = [link.Link.make_link('self', pecan.request.host_url,
'nodes', node.uuid + "/ports"),
link.Link.make_link('bookmark',
pecan.request.host_url,
'nodes', node.uuid + "/ports",
bookmark=True)
]
return node
@ -252,10 +257,11 @@ class NodeCollection(collection.Collection):
self._type = 'nodes'
@classmethod
def convert_with_links(cls, nodes, limit, **kwargs):
def convert_with_links(cls, nodes, limit, url=None,
expand=False, **kwargs):
collection = NodeCollection()
collection.nodes = [Node.convert_with_links(n) for n in nodes]
collection.next = collection.get_next(limit, **kwargs)
collection.nodes = [Node.convert_with_links(n, expand) for n in nodes]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@ -292,13 +298,21 @@ class NodesController(rest.RestController):
vendor_passthru = NodeVendorPassthruController()
"A resource used for vendors to expose a custom functionality in the API"
ports = port.PortsController(from_nodes=True)
"Expose ports as a sub-element of nodes"
_custom_actions = {
'ports': ['GET'],
'detail': ['GET'],
}
@wsme_pecan.wsexpose(NodeCollection, int, unicode, unicode, unicode)
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of nodes."""
def __init__(self, from_chassis=False):
self._from_chassis = from_chassis
def _get_nodes(self, chassis_id, marker, limit, sort_key, sort_dir):
if self._from_chassis and not chassis_id:
raise exception.InvalidParameterValue(_(
"Chassis id not specified."))
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
@ -307,22 +321,60 @@ class NodesController(rest.RestController):
marker_obj = objects.Node.get_by_uuid(pecan.request.context,
marker)
nodes = pecan.request.dbapi.get_node_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
if chassis_id:
nodes = pecan.request.dbapi.get_nodes_by_chassis(chassis_id, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
nodes = pecan.request.dbapi.get_node_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return nodes
@wsme_pecan.wsexpose(NodeCollection, unicode, unicode, int,
unicode, unicode)
def get_all(self, chassis_id=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of nodes."""
nodes = self._get_nodes(chassis_id, marker, limit, sort_key, sort_dir)
return NodeCollection.convert_with_links(nodes, limit,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(NodeCollection, unicode, unicode, int,
unicode, unicode)
def detail(self, chassis_id=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of nodes with detail."""
# /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "nodes":
raise exception.HTTPNotFound
nodes = self._get_nodes(chassis_id, marker, limit, sort_key, sort_dir)
resource_url = '/'.join(['nodes', 'detail'])
return NodeCollection.convert_with_links(nodes, limit,
url=resource_url,
expand=True,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(Node, unicode)
def get_one(self, uuid):
"""Retrieve information about the given node."""
if self._from_chassis:
raise exception.OperationNotPermitted
rpc_node = objects.Node.get_by_uuid(pecan.request.context, uuid)
return Node.convert_with_links(rpc_node)
@wsme_pecan.wsexpose(Node, body=Node)
def post(self, node):
"""Create a new node."""
if self._from_chassis:
raise exception.OperationNotPermitted
try:
new_node = pecan.request.dbapi.create_node(node.as_dict())
except Exception as e:
@ -336,6 +388,9 @@ class NodesController(rest.RestController):
TODO(deva): add exception handling
"""
if self._from_chassis:
raise exception.OperationNotPermitted
node = objects.Node.get_by_uuid(pecan.request.context, uuid)
node_dict = node.as_dict()
@ -407,29 +462,7 @@ class NodesController(rest.RestController):
TODO(deva): don't allow deletion of an associated node.
"""
if self._from_chassis:
raise exception.OperationNotPermitted
pecan.request.dbapi.destroy_node(node_id)
@wsme_pecan.wsexpose(port.PortCollection, unicode, int, unicode,
unicode, unicode)
def ports(self, node_uuid, limit=None, marker=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of ports on this node."""
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Port.get_by_uuid(pecan.request.context,
marker)
ports = pecan.request.dbapi.get_ports_by_node(node_uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
collection = port.PortCollection()
collection.ports = [port.Port.convert_with_links(n) for n in ports]
resource_url = '/'.join(['nodes', node_uuid, 'ports'])
collection.next = collection.get_next(limit, url=resource_url,
sort_key=sort_key,
sort_dir=sort_dir)
return collection

View File

@ -60,8 +60,9 @@ class Port(base.APIBase):
setattr(self, k, kwargs.get(k))
@classmethod
def convert_with_links(cls, rpc_port):
port = Port.from_rpc_object(rpc_port)
def convert_with_links(cls, rpc_port, expand=True):
fields = ['uuid', 'address'] if not expand else None
port = Port.from_rpc_object(rpc_port, fields)
port.links = [link.Link.make_link('self', pecan.request.host_url,
'ports', port.uuid),
link.Link.make_link('bookmark',
@ -82,19 +83,29 @@ class PortCollection(collection.Collection):
self._type = 'ports'
@classmethod
def convert_with_links(cls, ports, limit, **kwargs):
def convert_with_links(cls, ports, limit, url=None,
expand=False, **kwargs):
collection = PortCollection()
collection.ports = [Port.convert_with_links(p) for p in ports]
collection.next = collection.get_next(limit, **kwargs)
collection.ports = [Port.convert_with_links(p, expand) for p in ports]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
class PortsController(rest.RestController):
"""REST controller for Ports."""
@wsme_pecan.wsexpose(PortCollection, int, unicode, unicode, unicode)
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of ports."""
_custom_actions = {
'detail': ['GET'],
}
def __init__(self, from_nodes=False):
self._from_nodes = from_nodes
def _get_ports(self, node_id, marker, limit, sort_key, sort_dir):
if self._from_nodes and not node_id:
raise exception.InvalidParameterValue(_(
"Node id not specified."))
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
@ -103,22 +114,60 @@ class PortsController(rest.RestController):
marker_obj = objects.Port.get_by_uuid(pecan.request.context,
marker)
ports = pecan.request.dbapi.get_port_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
if node_id:
ports = pecan.request.dbapi.get_ports_by_node(node_id, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
ports = pecan.request.dbapi.get_port_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return ports
@wsme_pecan.wsexpose(PortCollection, unicode, unicode, int,
unicode, unicode)
def get_all(self, node_id=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of ports."""
ports = self._get_ports(node_id, marker, limit, sort_key, sort_dir)
return PortCollection.convert_with_links(ports, limit,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(PortCollection, unicode, unicode, int,
unicode, unicode)
def detail(self, node_id=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of ports."""
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "ports":
raise exception.HTTPNotFound
ports = self._get_ports(node_id, marker, limit, sort_key, sort_dir)
resource_url = '/'.join(['ports', 'detail'])
return PortCollection.convert_with_links(ports, limit,
url=resource_url,
expand=True,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(Port, unicode)
def get_one(self, uuid):
"""Retrieve information about the given port."""
if self._from_nodes:
raise exception.OperationNotPermitted
rpc_port = objects.Port.get_by_uuid(pecan.request.context, uuid)
return Port.convert_with_links(rpc_port)
@wsme_pecan.wsexpose(Port, body=Port)
def post(self, port):
"""Create a new port."""
if self._from_nodes:
raise exception.OperationNotPermitted
try:
new_port = pecan.request.dbapi.create_port(port.as_dict())
except exception.IronicException as e:
@ -129,6 +178,9 @@ class PortsController(rest.RestController):
@wsme_pecan.wsexpose(Port, unicode, body=[unicode])
def patch(self, uuid, patch):
"""Update an existing port."""
if self._from_nodes:
raise exception.OperationNotPermitted
port = objects.Port.get_by_uuid(pecan.request.context, uuid)
port_dict = port.as_dict()
@ -161,4 +213,7 @@ class PortsController(rest.RestController):
@wsme_pecan.wsexpose(None, unicode, status_code=204)
def delete(self, port_id):
"""Delete a port."""
if self._from_nodes:
raise exception.OperationNotPermitted
pecan.request.dbapi.destroy_port(port_id)

View File

@ -173,6 +173,10 @@ class PolicyNotAuthorized(NotAuthorized):
message = _("Policy doesn't allow %(action)s to be performed.")
class OperationNotPermitted(NotAuthorized):
message = _("Operation not permitted.")
class Invalid(IronicException):
message = _("Unacceptable parameters.")
code = 400

View File

@ -32,6 +32,23 @@ class TestListChassis(base.FunctionalTest):
chassis = self.dbapi.create_chassis(ndict)
data = self.get_json('/chassis')
self.assertEqual(chassis['uuid'], data['chassis'][0]["uuid"])
self.assertNotIn('extra', data['chassis'][0])
self.assertNotIn('nodes', data['chassis'][0])
def test_detail(self):
cdict = dbutils.get_test_chassis()
chassis = self.dbapi.create_chassis(cdict)
data = self.get_json('/chassis/detail')
self.assertEqual(chassis['uuid'], data['chassis'][0]["uuid"])
self.assertIn('extra', data['chassis'][0])
self.assertIn('nodes', data['chassis'][0])
def test_detail_against_single(self):
cdict = dbutils.get_test_chassis()
chassis = self.dbapi.create_chassis(cdict)
response = self.get_json('/chassis/%s/detail' % chassis['uuid'],
expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_many(self):
ch_list = []
@ -93,6 +110,15 @@ class TestListChassis(base.FunctionalTest):
self.assertEqual(len(data['nodes']), 1)
self.assertIn('next', data.keys())
def test_nodes_subresource_noid(self):
cdict = dbutils.get_test_chassis()
self.dbapi.create_chassis(cdict)
ndict = dbutils.get_test_node(chassis_id=cdict['id'])
self.dbapi.create_node(ndict)
# No chassis id specified
response = self.get_json('/chassis/nodes', expect_errors=True)
self.assertEqual(response.status_int, 400)
class TestPatch(base.FunctionalTest):
@ -203,6 +229,13 @@ class TestPatch(base.FunctionalTest):
expected = {"foo1": "bar1", "foo2": "bar2"}
self.assertEqual(result['extra'], expected)
def test_patch_nodes_subresource(self):
cdict = dbutils.get_test_chassis()
response = self.patch_json('/chassis/%s/nodes' % cdict['uuid'],
[{'path': '/extra/foo', 'value': 'bar',
'op': 'add'}], expect_errors=True)
self.assertEqual(response.status_int, 403)
class TestPost(base.FunctionalTest):
@ -221,6 +254,14 @@ class TestPost(base.FunctionalTest):
result['chassis'][0]['description'])
self.assertTrue(uuidutils.is_uuid_like(result['chassis'][0]['uuid']))
def test_post_nodes_subresource(self):
cdict = dbutils.get_test_chassis()
self.post_json('/chassis', cdict)
ndict = dbutils.get_test_node(chassis_id=cdict['id'])
response = self.post_json('/chassis/nodes', ndict,
expect_errors=True)
self.assertEqual(response.status_int, 403)
class TestDelete(base.FunctionalTest):
@ -251,3 +292,10 @@ class TestDelete(base.FunctionalTest):
self.assertEqual(response.status_int, 404)
self.assertEqual(response.content_type, 'application/json')
self.assertTrue(response.json['error_message'])
def test_delete_nodes_subresource(self):
cdict = dbutils.get_test_chassis()
self.post_json('/chassis', cdict)
response = self.delete('/chassis/%s/nodes' % cdict['uuid'],
expect_errors=True)
self.assertEqual(response.status_int, 403)

View File

@ -38,6 +38,29 @@ class TestListNodes(base.FunctionalTest):
node = self.dbapi.create_node(ndict)
data = self.get_json('/nodes')
self.assertEqual(node['uuid'], data['nodes'][0]["uuid"])
self.assertNotIn('driver', data['nodes'][0])
self.assertNotIn('driver_info', data['nodes'][0])
self.assertNotIn('extra', data['nodes'][0])
self.assertNotIn('properties', data['nodes'][0])
self.assertNotIn('chassis_id', data['nodes'][0])
def test_detail(self):
ndict = dbutils.get_test_node()
node = self.dbapi.create_node(ndict)
data = self.get_json('/nodes/detail')
self.assertEqual(node['uuid'], data['nodes'][0]["uuid"])
self.assertIn('driver', data['nodes'][0])
self.assertIn('driver_info', data['nodes'][0])
self.assertIn('extra', data['nodes'][0])
self.assertIn('properties', data['nodes'][0])
self.assertIn('chassis_id', data['nodes'][0])
def test_detail_against_single(self):
ndict = dbutils.get_test_node()
node = self.dbapi.create_node(ndict)
response = self.get_json('/nodes/%s/detail' % node['uuid'],
expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_many(self):
nodes = []
@ -99,6 +122,15 @@ class TestListNodes(base.FunctionalTest):
self.assertEqual(len(data['ports']), 1)
self.assertIn('next', data.keys())
def test_nodes_subresource_noid(self):
ndict = dbutils.get_test_node()
self.dbapi.create_node(ndict)
pdict = dbutils.get_test_port(node_id=ndict['id'])
self.dbapi.create_port(pdict)
# No node id specified
response = self.get_json('/nodes/ports', expect_errors=True)
self.assertEqual(response.status_int, 400)
def test_state(self):
ndict = dbutils.get_test_node()
self.dbapi.create_node(ndict)
@ -239,6 +271,12 @@ class TestPatch(base.FunctionalTest):
[{'path': '/extra/foo', 'value': 'bar',
'op': 'add'}])
def test_patch_ports_subresource(self):
response = self.patch_json('/nodes/%s/ports' % self.node['uuid'],
[{'path': '/extra/foo', 'value': 'bar',
'op': 'add'}], expect_errors=True)
self.assertEqual(response.status_int, 403)
class TestPost(base.FunctionalTest):
@ -271,6 +309,14 @@ class TestPost(base.FunctionalTest):
'/nodes/%s/vendor_passthru' % ndict['uuid'],
{'foo': 'bar'})
def test_post_ports_subresource(self):
ndict = dbutils.get_test_node()
self.post_json('/nodes', ndict)
pdict = dbutils.get_test_port()
response = self.post_json('/nodes/ports', pdict,
expect_errors=True)
self.assertEqual(response.status_int, 403)
class TestDelete(base.FunctionalTest):
@ -284,6 +330,13 @@ class TestDelete(base.FunctionalTest):
self.assertEqual(response.content_type, 'application/json')
self.assertTrue(response.json['error_message'])
def test_delete_ports_subresource(self):
ndict = dbutils.get_test_node()
self.post_json('/nodes', ndict)
response = self.delete('/nodes/%s/ports' % ndict['uuid'],
expect_errors=True)
self.assertEqual(response.status_int, 403)
class TestPut(base.FunctionalTest):

View File

@ -32,6 +32,21 @@ class TestListPorts(base.FunctionalTest):
port = self.dbapi.create_port(ndict)
data = self.get_json('/ports')
self.assertEqual(port['uuid'], data['ports'][0]["uuid"])
self.assertNotIn('extra', data['ports'][0])
def test_detail(self):
pdict = dbutils.get_test_port()
port = self.dbapi.create_port(pdict)
data = self.get_json('/ports/detail')
self.assertEqual(port['uuid'], data['ports'][0]["uuid"])
self.assertIn('extra', data['ports'][0])
def test_detail_against_single(self):
pdict = dbutils.get_test_port()
port = self.dbapi.create_port(pdict)
response = self.get_json('/ports/%s/detail' % port['uuid'],
expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_many(self):
ports = []