diff options
-rw-r--r-- | ironic/api/controllers/v1/chassis.py | 43 | ||||
-rw-r--r-- | ironic/api/controllers/v1/link.py | 7 | ||||
-rw-r--r-- | ironic/api/controllers/v1/node.py | 41 | ||||
-rw-r--r-- | ironic/db/api.py | 18 | ||||
-rw-r--r-- | ironic/db/sqlalchemy/api.py | 21 | ||||
-rw-r--r-- | ironic/tests/api/test_chassis.py | 27 | ||||
-rw-r--r-- | ironic/tests/api/test_nodes.py | 27 |
7 files changed, 172 insertions, 12 deletions
diff --git a/ironic/api/controllers/v1/chassis.py b/ironic/api/controllers/v1/chassis.py index 6d03f43f1..81635a8f9 100644 --- a/ironic/api/controllers/v1/chassis.py +++ b/ironic/api/controllers/v1/chassis.py @@ -28,6 +28,7 @@ from ironic import objects from ironic.api.controllers.v1 import base from ironic.api.controllers.v1 import collection from ironic.api.controllers.v1 import link +from ironic.api.controllers.v1 import node from ironic.api.controllers.v1 import utils from ironic.common import exception from ironic.openstack.common import log @@ -56,6 +57,9 @@ class Chassis(base.APIBase): links = [link.Link] "A list containing a self link and associated chassis links" + nodes = [link.Link] + "Links to the collection of nodes contained in this chassis" + def __init__(self, **kwargs): self.fields = objects.Chassis.fields.keys() for k in self.fields: @@ -71,6 +75,15 @@ class Chassis(base.APIBase): '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) + ] return chassis @@ -92,6 +105,10 @@ class ChassisCollection(collection.Collection): class ChassisController(rest.RestController): """REST controller for Chassis.""" + _custom_actions = { + 'nodes': ['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.""" @@ -148,3 +165,29 @@ 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.type = 'node' + collection.items = [node.Node.convert_with_links(n) for n in nodes] + resource_url = '/'.join(['chassis', chassis_uuid, 'nodes']) + collection.links = collection.make_links(limit, resource_url, + sort_key=sort_key, + sort_dir=sort_dir) + return collection diff --git a/ironic/api/controllers/v1/link.py b/ironic/api/controllers/v1/link.py index ed9e8a78b..f7cc04d33 100644 --- a/ironic/api/controllers/v1/link.py +++ b/ironic/api/controllers/v1/link.py @@ -32,6 +32,11 @@ class Link(base.APIBase): @classmethod def make_link(cls, rel_name, url, resource, resource_args, bookmark=False): - template = '%s/%s/%s' if bookmark else '%s/v1/%s/%s' + template = '%s/%s' if bookmark else '%s/v1/%s' + # FIXME(lucasagomes): I'm getting a 404 when doing a GET on + # a nested resource that the URL ends with a '/'. + # https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs + template += '%s' if resource_args.startswith('?') else '/%s' + return Link(href=(template) % (url, resource, resource_args), rel=rel_name) diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 01feb4cc2..d6a0006b2 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -27,6 +27,7 @@ from ironic import objects from ironic.api.controllers.v1 import base from ironic.api.controllers.v1 import collection from ironic.api.controllers.v1 import link +from ironic.api.controllers.v1 import port from ironic.api.controllers.v1 import utils from ironic.common import exception from ironic.openstack.common import log @@ -73,6 +74,9 @@ class Node(base.APIBase): links = [link.Link] "A list containing a self link and associated node links" + ports = [link.Link] + "Links to the collection of ports on this node" + def __init__(self, **kwargs): self.fields = objects.Node.fields.keys() for k in self.fields: @@ -88,6 +92,13 @@ 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) + ] return node @@ -109,6 +120,10 @@ class NodeCollection(collection.Collection): class NodesController(rest.RestController): """REST controller for Nodes.""" + _custom_actions = { + 'ports': ['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.""" @@ -195,3 +210,29 @@ class NodesController(rest.RestController): TODO(deva): don't allow deletion of an associated node. """ 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.type = 'port' + collection.items = [port.Port.convert_with_links(n) for n in ports] + resource_url = '/'.join(['nodes', node_uuid, 'ports']) + collection.links = collection.make_links(limit, resource_url, + sort_key=sort_key, + sort_dir=sort_dir) + return collection diff --git a/ironic/db/api.py b/ironic/db/api.py index 4d1dec9a6..ee30d2842 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -130,10 +130,17 @@ class Connection(object): """ @abc.abstractmethod - def get_nodes_by_chassis(self, chassis): + def get_nodes_by_chassis(self, chassis, limit=None, marker=None, + sort_key=None, sort_dir=None): """List all the nodes for a given chassis. :param chassis: The id or uuid of a chassis. + :param limit: Maximum number of nodes to return. + :param marker: the last item of the previous page; we returns the next + results after this value. + :param sort_key: Attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted + (asc, desc) :returns: A list of nodes. """ @@ -193,10 +200,17 @@ class Connection(object): """ @abc.abstractmethod - def get_ports_by_node(self, node): + def get_ports_by_node(self, node, limit=None, marker=None, + sort_key=None, sort_dir=None): """List all the ports for a given node. :param node: The id or uuid of a node. + :param limit: Maximum number of ports to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted + (asc, desc) :returns: A list of ports. """ diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index cf6549e73..8e18175e8 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -122,9 +122,10 @@ def _check_port_change_forbidden(port, session): raise exception.NodeLocked(node=node_id) -def _paginate_query(model, limit=None, marker=None, - sort_key=None, sort_dir=None): - query = model_query(model) +def _paginate_query(model, limit=None, marker=None, sort_key=None, + sort_dir=None, query=None): + if not query: + query = model_query(model) sort_keys = ['id'] if sort_key and sort_key not in sort_keys: sort_keys.insert(0, sort_key) @@ -150,11 +151,12 @@ class Connection(api.Connection): sort_key, sort_dir) @objects.objectify(objects.Node) - def get_nodes_by_chassis(self, chassis): + def get_nodes_by_chassis(self, chassis, limit=None, marker=None, + sort_key=None, sort_dir=None): query = model_query(models.Node) query = add_node_filter_by_chassis(query, chassis) - - return query.all() + return _paginate_query(models.Node, limit, marker, + sort_key, sort_dir, query) @objects.objectify(objects.Node) def get_associated_nodes(self): @@ -326,11 +328,12 @@ class Connection(api.Connection): sort_key, sort_dir) @objects.objectify(objects.Port) - def get_ports_by_node(self, node): + def get_ports_by_node(self, node, limit=None, marker=None, + sort_key=None, sort_dir=None): query = model_query(models.Port) query = add_port_filter_by_node(query, node) - - return query.all() + return _paginate_query(models.Port, limit, marker, + sort_key, sort_dir, query) @objects.objectify(objects.Port) def create_port(self, values): diff --git a/ironic/tests/api/test_chassis.py b/ironic/tests/api/test_chassis.py index 2745526aa..a792290c0 100644 --- a/ironic/tests/api/test_chassis.py +++ b/ironic/tests/api/test_chassis.py @@ -70,6 +70,33 @@ class TestListChassis(base.FunctionalTest): next_link = [l['href'] for l in data['links'] if l['rel'] == 'next'][0] self.assertIn(next_marker, next_link) + def test_nodes_subresource_link(self): + ndict = dbutils.get_test_chassis() + self.dbapi.create_chassis(ndict) + + data = self.get_json('/chassis/%s' % ndict['uuid']) + self.assertIn('nodes', data.keys()) + + def test_nodes_subresource(self): + cdict = dbutils.get_test_chassis() + self.dbapi.create_chassis(cdict) + + for id in xrange(2): + ndict = dbutils.get_test_node(id=id, chassis_id=cdict['id'], + uuid=uuidutils.generate_uuid()) + self.dbapi.create_node(ndict) + + data = self.get_json('/chassis/%s/nodes' % cdict['uuid']) + self.assertEqual(data['type'], 'node') + self.assertEqual(len(data['items']), 2) + self.assertEqual(len(data['links']), 0) + + # Test collection pagination + data = self.get_json('/chassis/%s/nodes?limit=1' % cdict['uuid']) + self.assertEqual(data['type'], 'node') + self.assertEqual(len(data['items']), 1) + self.assertEqual(len(data['links']), 1) + class TestPatch(base.FunctionalTest): diff --git a/ironic/tests/api/test_nodes.py b/ironic/tests/api/test_nodes.py index 24127995c..fb846e973 100644 --- a/ironic/tests/api/test_nodes.py +++ b/ironic/tests/api/test_nodes.py @@ -74,6 +74,33 @@ class TestListNodes(base.FunctionalTest): next_link = [l['href'] for l in data['links'] if l['rel'] == 'next'][0] self.assertIn(next_marker, next_link) + def test_ports_subresource_link(self): + ndict = dbutils.get_test_node() + self.dbapi.create_node(ndict) + + data = self.get_json('/nodes/%s' % ndict['uuid']) + self.assertIn('ports', data.keys()) + + def test_ports_subresource(self): + ndict = dbutils.get_test_node() + self.dbapi.create_node(ndict) + + for id in xrange(2): + pdict = dbutils.get_test_port(id=id, node_id=ndict['id'], + uuid=uuidutils.generate_uuid()) + self.dbapi.create_port(pdict) + + data = self.get_json('/nodes/%s/ports' % ndict['uuid']) + self.assertEqual(data['type'], 'port') + self.assertEqual(len(data['items']), 2) + self.assertEqual(len(data['links']), 0) + + # Test collection pagination + data = self.get_json('/nodes/%s/ports?limit=1' % ndict['uuid']) + self.assertEqual(data['type'], 'port') + self.assertEqual(len(data['items']), 1) + self.assertEqual(len(data['links']), 1) + class TestPatch(base.FunctionalTest): |