summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ironic/api/controllers/v1/chassis.py43
-rw-r--r--ironic/api/controllers/v1/link.py7
-rw-r--r--ironic/api/controllers/v1/node.py41
-rw-r--r--ironic/db/api.py18
-rw-r--r--ironic/db/sqlalchemy/api.py21
-rw-r--r--ironic/tests/api/test_chassis.py27
-rw-r--r--ironic/tests/api/test_nodes.py27
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):