diff --git a/doc/source/dev/api-spec-v1.rst b/doc/source/dev/api-spec-v1.rst index fe27c26b7c..9db7de7011 100644 --- a/doc/source/dev/api-spec-v1.rst +++ b/doc/source/dev/api-spec-v1.rst @@ -434,8 +434,9 @@ Usage ======= ============= ========== Verb Path Response ======= ============= ========== -GET /nodes List nodes. -GET /nodes/ Retrieve a specific node. +GET /nodes List nodes +GET /nodes/detail Lists all details for all nodes +GET /nodes/ Retrieve a specific node POST /nodes Create a new node PATCH /nodes/ Update a node DELETE /nodes/ Delete node and all associated ports @@ -566,16 +567,17 @@ Chassis Usage ^^^^^^ -======= ============= ========== -Verb Path Response -======= ============= ========== -GET /chassis List chassis -GET /chassis/ Retrieve a specific chassis -POST /chassis Create a new chassis -PATCH /chassis/ Update a chassis -DELETE /chassis/ 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/ Retrieve a specific chassis +POST /chassis Create a new chassis +PATCH /chassis/ Update a chassis +DELETE /chassis/ 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/ Retrieve a specific port POST /ports Create a new port PATCH /ports/ Update a port diff --git a/ironic/api/controllers/v1/base.py b/ironic/api/controllers/v1/base.py index 6e62c8c39f..da572573b9 100644 --- a/ironic/api/controllers/v1/base.py +++ b/ironic/api/controllers/v1/base.py @@ -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) diff --git a/ironic/api/controllers/v1/chassis.py b/ironic/api/controllers/v1/chassis.py index 30eafea421..d189ab7fc3 100644 --- a/ironic/api/controllers/v1/chassis.py +++ b/ironic/api/controllers/v1/chassis.py @@ -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 diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 54c8a22e75..073d37a871 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -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 diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 807a827ebc..a3bb5c85c8 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -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) diff --git a/ironic/common/exception.py b/ironic/common/exception.py index b54a03ec78..10b74fe330 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -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 diff --git a/ironic/tests/api/test_chassis.py b/ironic/tests/api/test_chassis.py index 6a852c7d9d..603d5c632d 100644 --- a/ironic/tests/api/test_chassis.py +++ b/ironic/tests/api/test_chassis.py @@ -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) diff --git a/ironic/tests/api/test_nodes.py b/ironic/tests/api/test_nodes.py index 3372646487..d70c89a0ad 100644 --- a/ironic/tests/api/test_nodes.py +++ b/ironic/tests/api/test_nodes.py @@ -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): diff --git a/ironic/tests/api/test_ports.py b/ironic/tests/api/test_ports.py index 74dc76eec7..9d9af60381 100644 --- a/ironic/tests/api/test_ports.py +++ b/ironic/tests/api/test_ports.py @@ -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 = []