Merge "API GET to return only minimal data"

This commit is contained in:
Jenkins 2013-09-25 19:17:34 +00:00 committed by Gerrit Code Review
commit 98670162c7
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
@ -570,6 +571,7 @@ Usage
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
@ -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,16 +67,20 @@ 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', chassis.uuid)
]
chassis.nodes = [link.Link.make_link('self', pecan.request.host_url,
if expand:
chassis.nodes = [link.Link.make_link('self',
pecan.request.host_url,
'chassis',
chassis.uuid + "/nodes"),
link.Link.make_link('bookmark',
@ -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,6 +236,7 @@ class Node(base.APIBase):
'nodes', node.uuid,
bookmark=True)
]
if expand:
node.ports = [link.Link.make_link('self', pecan.request.host_url,
'nodes', node.uuid + "/ports"),
link.Link.make_link('bookmark',
@ -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)
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)
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 = []