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

View File

@ -28,5 +28,12 @@ class APIBase(wtypes.Base):
getattr(self, k) != wsme.Unset) getattr(self, k) != wsme.Unset)
@classmethod @classmethod
def from_rpc_object(cls, m): def from_rpc_object(cls, m, fields=None):
return cls(**m.as_dict()) """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)) setattr(self, k, kwargs.get(k))
@classmethod @classmethod
def convert_with_links(cls, rpc_chassis): def convert_with_links(cls, rpc_chassis, expand=True):
chassis = Chassis.from_rpc_object(rpc_chassis) fields = ['uuid', 'description'] if not expand else None
chassis.links = [link.Link.make_link('self', pecan.request.host_url, chassis = Chassis.from_rpc_object(rpc_chassis, fields)
chassis.links = [link.Link.make_link('self',
pecan.request.host_url,
'chassis', chassis.uuid), 'chassis', chassis.uuid),
link.Link.make_link('bookmark', link.Link.make_link('bookmark',
pecan.request.host_url, pecan.request.host_url,
'chassis', chassis.uuid, '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)
] ]
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 return chassis
@ -98,38 +102,62 @@ class ChassisCollection(collection.Collection):
self._type = 'chassis' self._type = 'chassis'
@classmethod @classmethod
def convert_with_links(cls, chassis, limit, **kwargs): def convert_with_links(cls, chassis, limit, url=None,
expand=False, **kwargs):
collection = ChassisCollection() collection = ChassisCollection()
collection.chassis = [Chassis.convert_with_links(ch) for ch in chassis] collection.chassis = [Chassis.convert_with_links(ch, expand)
collection.next = collection.get_next(limit, **kwargs) for ch in chassis]
url = url or None
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection return collection
class ChassisController(rest.RestController): class ChassisController(rest.RestController):
"""REST controller for Chassis.""" """REST controller for Chassis."""
nodes = node.NodesController(from_chassis=True)
"Expose nodes as a sub-element of chassis"
_custom_actions = { _custom_actions = {
'nodes': ['GET'], 'detail': ['GET'],
} }
@wsme_pecan.wsexpose(ChassisCollection, int, unicode, unicode, unicode) def _get_chassis(self, marker, limit, sort_key, sort_dir):
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of chassis."""
limit = utils.validate_limit(limit) limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir) sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None marker_obj = None
if marker: if marker:
marker_obj = objects.Chassis.get_by_uuid(pecan.request.context, marker_obj = objects.Chassis.get_by_uuid(pecan.request.context,
marker) marker)
chassis = pecan.request.dbapi.get_chassis_list(limit, marker_obj, chassis = pecan.request.dbapi.get_chassis_list(limit, marker_obj,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) 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, return ChassisCollection.convert_with_links(chassis, limit,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) 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) @wsme_pecan.wsexpose(Chassis, unicode)
def get_one(self, uuid): def get_one(self, uuid):
"""Retrieve information about the given chassis.""" """Retrieve information about the given chassis."""
@ -183,28 +211,3 @@ class ChassisController(rest.RestController):
def delete(self, uuid): def delete(self, uuid):
"""Delete a chassis.""" """Delete a chassis."""
pecan.request.dbapi.destroy_chassis(uuid) 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)) setattr(self, k, kwargs.get(k))
@classmethod @classmethod
def convert_with_links(cls, rpc_node): def convert_with_links(cls, rpc_node, expand=True):
node = Node.from_rpc_object(rpc_node) 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, node.links = [link.Link.make_link('self', pecan.request.host_url,
'nodes', node.uuid), 'nodes', node.uuid),
link.Link.make_link('bookmark', link.Link.make_link('bookmark',
@ -232,13 +236,14 @@ class Node(base.APIBase):
'nodes', node.uuid, 'nodes', node.uuid,
bookmark=True) bookmark=True)
] ]
node.ports = [link.Link.make_link('self', pecan.request.host_url, if expand:
'nodes', node.uuid + "/ports"), node.ports = [link.Link.make_link('self', pecan.request.host_url,
link.Link.make_link('bookmark', 'nodes', node.uuid + "/ports"),
pecan.request.host_url, link.Link.make_link('bookmark',
'nodes', node.uuid + "/ports", pecan.request.host_url,
bookmark=True) 'nodes', node.uuid + "/ports",
] bookmark=True)
]
return node return node
@ -252,10 +257,11 @@ class NodeCollection(collection.Collection):
self._type = 'nodes' self._type = 'nodes'
@classmethod @classmethod
def convert_with_links(cls, nodes, limit, **kwargs): def convert_with_links(cls, nodes, limit, url=None,
expand=False, **kwargs):
collection = NodeCollection() collection = NodeCollection()
collection.nodes = [Node.convert_with_links(n) for n in nodes] collection.nodes = [Node.convert_with_links(n, expand) for n in nodes]
collection.next = collection.get_next(limit, **kwargs) collection.next = collection.get_next(limit, url=url, **kwargs)
return collection return collection
@ -292,13 +298,21 @@ class NodesController(rest.RestController):
vendor_passthru = NodeVendorPassthruController() vendor_passthru = NodeVendorPassthruController()
"A resource used for vendors to expose a custom functionality in the API" "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 = { _custom_actions = {
'ports': ['GET'], 'detail': ['GET'],
} }
@wsme_pecan.wsexpose(NodeCollection, int, unicode, unicode, unicode) def __init__(self, from_chassis=False):
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'): self._from_chassis = from_chassis
"""Retrieve a list of nodes."""
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) limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir) 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_obj = objects.Node.get_by_uuid(pecan.request.context,
marker) marker)
nodes = pecan.request.dbapi.get_node_list(limit, marker_obj, if chassis_id:
sort_key=sort_key, nodes = pecan.request.dbapi.get_nodes_by_chassis(chassis_id, limit,
sort_dir=sort_dir) 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, return NodeCollection.convert_with_links(nodes, limit,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) 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) @wsme_pecan.wsexpose(Node, unicode)
def get_one(self, uuid): def get_one(self, uuid):
"""Retrieve information about the given node.""" """Retrieve information about the given node."""
if self._from_chassis:
raise exception.OperationNotPermitted
rpc_node = objects.Node.get_by_uuid(pecan.request.context, uuid) rpc_node = objects.Node.get_by_uuid(pecan.request.context, uuid)
return Node.convert_with_links(rpc_node) return Node.convert_with_links(rpc_node)
@wsme_pecan.wsexpose(Node, body=Node) @wsme_pecan.wsexpose(Node, body=Node)
def post(self, node): def post(self, node):
"""Create a new node.""" """Create a new node."""
if self._from_chassis:
raise exception.OperationNotPermitted
try: try:
new_node = pecan.request.dbapi.create_node(node.as_dict()) new_node = pecan.request.dbapi.create_node(node.as_dict())
except Exception as e: except Exception as e:
@ -336,6 +388,9 @@ class NodesController(rest.RestController):
TODO(deva): add exception handling TODO(deva): add exception handling
""" """
if self._from_chassis:
raise exception.OperationNotPermitted
node = objects.Node.get_by_uuid(pecan.request.context, uuid) node = objects.Node.get_by_uuid(pecan.request.context, uuid)
node_dict = node.as_dict() node_dict = node.as_dict()
@ -407,29 +462,7 @@ class NodesController(rest.RestController):
TODO(deva): don't allow deletion of an associated node. TODO(deva): don't allow deletion of an associated node.
""" """
if self._from_chassis:
raise exception.OperationNotPermitted
pecan.request.dbapi.destroy_node(node_id) 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)) setattr(self, k, kwargs.get(k))
@classmethod @classmethod
def convert_with_links(cls, rpc_port): def convert_with_links(cls, rpc_port, expand=True):
port = Port.from_rpc_object(rpc_port) 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, port.links = [link.Link.make_link('self', pecan.request.host_url,
'ports', port.uuid), 'ports', port.uuid),
link.Link.make_link('bookmark', link.Link.make_link('bookmark',
@ -82,19 +83,29 @@ class PortCollection(collection.Collection):
self._type = 'ports' self._type = 'ports'
@classmethod @classmethod
def convert_with_links(cls, ports, limit, **kwargs): def convert_with_links(cls, ports, limit, url=None,
expand=False, **kwargs):
collection = PortCollection() collection = PortCollection()
collection.ports = [Port.convert_with_links(p) for p in ports] collection.ports = [Port.convert_with_links(p, expand) for p in ports]
collection.next = collection.get_next(limit, **kwargs) collection.next = collection.get_next(limit, url=url, **kwargs)
return collection return collection
class PortsController(rest.RestController): class PortsController(rest.RestController):
"""REST controller for Ports.""" """REST controller for Ports."""
@wsme_pecan.wsexpose(PortCollection, int, unicode, unicode, unicode) _custom_actions = {
def get_all(self, limit=None, marker=None, sort_key='id', sort_dir='asc'): 'detail': ['GET'],
"""Retrieve a list of ports.""" }
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) limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir) 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_obj = objects.Port.get_by_uuid(pecan.request.context,
marker) marker)
ports = pecan.request.dbapi.get_port_list(limit, marker_obj, if node_id:
sort_key=sort_key, ports = pecan.request.dbapi.get_ports_by_node(node_id, limit,
sort_dir=sort_dir) 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, return PortCollection.convert_with_links(ports, limit,
sort_key=sort_key, sort_key=sort_key,
sort_dir=sort_dir) 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) @wsme_pecan.wsexpose(Port, unicode)
def get_one(self, uuid): def get_one(self, uuid):
"""Retrieve information about the given port.""" """Retrieve information about the given port."""
if self._from_nodes:
raise exception.OperationNotPermitted
rpc_port = objects.Port.get_by_uuid(pecan.request.context, uuid) rpc_port = objects.Port.get_by_uuid(pecan.request.context, uuid)
return Port.convert_with_links(rpc_port) return Port.convert_with_links(rpc_port)
@wsme_pecan.wsexpose(Port, body=Port) @wsme_pecan.wsexpose(Port, body=Port)
def post(self, port): def post(self, port):
"""Create a new port.""" """Create a new port."""
if self._from_nodes:
raise exception.OperationNotPermitted
try: try:
new_port = pecan.request.dbapi.create_port(port.as_dict()) new_port = pecan.request.dbapi.create_port(port.as_dict())
except exception.IronicException as e: except exception.IronicException as e:
@ -129,6 +178,9 @@ class PortsController(rest.RestController):
@wsme_pecan.wsexpose(Port, unicode, body=[unicode]) @wsme_pecan.wsexpose(Port, unicode, body=[unicode])
def patch(self, uuid, patch): def patch(self, uuid, patch):
"""Update an existing port.""" """Update an existing port."""
if self._from_nodes:
raise exception.OperationNotPermitted
port = objects.Port.get_by_uuid(pecan.request.context, uuid) port = objects.Port.get_by_uuid(pecan.request.context, uuid)
port_dict = port.as_dict() port_dict = port.as_dict()
@ -161,4 +213,7 @@ class PortsController(rest.RestController):
@wsme_pecan.wsexpose(None, unicode, status_code=204) @wsme_pecan.wsexpose(None, unicode, status_code=204)
def delete(self, port_id): def delete(self, port_id):
"""Delete a port.""" """Delete a port."""
if self._from_nodes:
raise exception.OperationNotPermitted
pecan.request.dbapi.destroy_port(port_id) 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.") message = _("Policy doesn't allow %(action)s to be performed.")
class OperationNotPermitted(NotAuthorized):
message = _("Operation not permitted.")
class Invalid(IronicException): class Invalid(IronicException):
message = _("Unacceptable parameters.") message = _("Unacceptable parameters.")
code = 400 code = 400

View File

@ -32,6 +32,23 @@ class TestListChassis(base.FunctionalTest):
chassis = self.dbapi.create_chassis(ndict) chassis = self.dbapi.create_chassis(ndict)
data = self.get_json('/chassis') data = self.get_json('/chassis')
self.assertEqual(chassis['uuid'], data['chassis'][0]["uuid"]) 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): def test_many(self):
ch_list = [] ch_list = []
@ -93,6 +110,15 @@ class TestListChassis(base.FunctionalTest):
self.assertEqual(len(data['nodes']), 1) self.assertEqual(len(data['nodes']), 1)
self.assertIn('next', data.keys()) 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): class TestPatch(base.FunctionalTest):
@ -203,6 +229,13 @@ class TestPatch(base.FunctionalTest):
expected = {"foo1": "bar1", "foo2": "bar2"} expected = {"foo1": "bar1", "foo2": "bar2"}
self.assertEqual(result['extra'], expected) 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): class TestPost(base.FunctionalTest):
@ -221,6 +254,14 @@ class TestPost(base.FunctionalTest):
result['chassis'][0]['description']) result['chassis'][0]['description'])
self.assertTrue(uuidutils.is_uuid_like(result['chassis'][0]['uuid'])) 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): class TestDelete(base.FunctionalTest):
@ -251,3 +292,10 @@ class TestDelete(base.FunctionalTest):
self.assertEqual(response.status_int, 404) self.assertEqual(response.status_int, 404)
self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.content_type, 'application/json')
self.assertTrue(response.json['error_message']) 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) node = self.dbapi.create_node(ndict)
data = self.get_json('/nodes') data = self.get_json('/nodes')
self.assertEqual(node['uuid'], data['nodes'][0]["uuid"]) 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): def test_many(self):
nodes = [] nodes = []
@ -99,6 +122,15 @@ class TestListNodes(base.FunctionalTest):
self.assertEqual(len(data['ports']), 1) self.assertEqual(len(data['ports']), 1)
self.assertIn('next', data.keys()) 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): def test_state(self):
ndict = dbutils.get_test_node() ndict = dbutils.get_test_node()
self.dbapi.create_node(ndict) self.dbapi.create_node(ndict)
@ -239,6 +271,12 @@ class TestPatch(base.FunctionalTest):
[{'path': '/extra/foo', 'value': 'bar', [{'path': '/extra/foo', 'value': 'bar',
'op': 'add'}]) '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): class TestPost(base.FunctionalTest):
@ -271,6 +309,14 @@ class TestPost(base.FunctionalTest):
'/nodes/%s/vendor_passthru' % ndict['uuid'], '/nodes/%s/vendor_passthru' % ndict['uuid'],
{'foo': 'bar'}) {'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): class TestDelete(base.FunctionalTest):
@ -284,6 +330,13 @@ class TestDelete(base.FunctionalTest):
self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.content_type, 'application/json')
self.assertTrue(response.json['error_message']) 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): class TestPut(base.FunctionalTest):

View File

@ -32,6 +32,21 @@ class TestListPorts(base.FunctionalTest):
port = self.dbapi.create_port(ndict) port = self.dbapi.create_port(ndict)
data = self.get_json('/ports') data = self.get_json('/ports')
self.assertEqual(port['uuid'], data['ports'][0]["uuid"]) 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): def test_many(self):
ports = [] ports = []