diff --git a/docs/api/curl.rst b/docs/api/curl.rst index 0c33dc21..84a5c1d2 100644 --- a/docs/api/curl.rst +++ b/docs/api/curl.rst @@ -9,6 +9,7 @@ Resources - `Flavor <#flavor>`_ - `ResourceClass <#resource_class>`_ - `DataCenter <#data_center>`_ +- `Node <#node>`_ Rack ---- @@ -325,9 +326,9 @@ delete :: - curl -X DELETE http://0.0.0.0:8585/v1/resource_classes/1`` `back to + curl -X DELETE http://0.0.0.0:8585/v1/resource_classes/1 -top <#index>`_ +`back to top <#index>`_ DataCenter ---------- @@ -343,3 +344,78 @@ Tuskar. curl -XPOST -H 'Content-Type:application/json' -H 'Accept: application/json' http://0.0.0.0:8585/v1/data_centers/ `back to top <#index>`_ + +Node +---- + +Get Collection +~~~~~~~~~~~~~~ + +:: + + curl http://0.0.0.0:8585/v1/nodes/ + +response +^^^^^^^^ + +:: + + [ + { + "nova_baremetal_node_id": "0e3ab3d3-bd85-40bd-b6a1-fae484040825", + "id": "1", + "links": [ + { + "href": "http://127.0.0.1:8585/v1/nodes/1", + "rel": "self" + } + ], + "rack": { + "id": 1, + "links": + [ + { + "href": "http://127.0.0.1:8585/v1/racks/1", + "rel": "self" + } + ] + } + } + ] + +Retrieve a single Node +~~~~~~~~~~~~~~~~~~~~~~ + +:: + + curl http://0.0.0.0:8585/v1/nodes/1 + +response +^^^^^^^^ + +:: + + { + "nova_baremetal_node_id": "0e3ab3d3-bd85-40bd-b6a1-fae484040825", + "id": "1", + "links": + [ + { + "href": "http://127.0.0.1:8585/v1/nodes/1", + "rel": "self" + } + ], + "rack": + { + "id": 1, + "links": + [ + { + "href": "http://127.0.0.1:8585/v1/racks/1", + "rel": "self" + } + ] + } + } + +`back to top <#index>`_ diff --git a/tuskar/api/controllers/v1/__init__.py b/tuskar/api/controllers/v1/__init__.py index 97f46fe2..1d602e0d 100644 --- a/tuskar/api/controllers/v1/__init__.py +++ b/tuskar/api/controllers/v1/__init__.py @@ -14,8 +14,10 @@ from tuskar.api.controllers.v1.controller import Controller from tuskar.api.controllers.v1.data_center import DataCenterController from tuskar.api.controllers.v1.flavor import FlavorsController +from tuskar.api.controllers.v1.node import NodesController from tuskar.api.controllers.v1.rack import RacksController from tuskar.api.controllers.v1.resource_class import ResourceClassesController __all__ = (Controller, DataCenterController, FlavorsController, - RacksController, ResourceClassesController) + RacksController, ResourceClassesController, + NodesController) diff --git a/tuskar/api/controllers/v1/controller.py b/tuskar/api/controllers/v1/controller.py index c1e4a2be..d0439d18 100644 --- a/tuskar/api/controllers/v1/controller.py +++ b/tuskar/api/controllers/v1/controller.py @@ -11,6 +11,7 @@ import pecan #from tuskar.openstack.common import log from tuskar.api.controllers.v1.data_center import DataCenterController +from tuskar.api.controllers.v1.node import NodesController from tuskar.api.controllers.v1.rack import RacksController from tuskar.api.controllers.v1.resource_class import ResourceClassesController @@ -21,6 +22,7 @@ class Controller(object): racks = RacksController() resource_classes = ResourceClassesController() data_centers = DataCenterController() + nodes = NodesController() @pecan.expose('json') def index(self): diff --git a/tuskar/api/controllers/v1/node.py b/tuskar/api/controllers/v1/node.py new file mode 100644 index 00000000..87026657 --- /dev/null +++ b/tuskar/api/controllers/v1/node.py @@ -0,0 +1,42 @@ +import pecan +from pecan import rest + +import wsmeext.pecan as wsme_pecan + +from tuskar.api.controllers.v1.types import Error +from tuskar.api.controllers.v1.types import Node +from tuskar.common import exception +from tuskar.openstack.common import log +from wsme import api + +LOG = log.getLogger(__name__) + + +class NodesController(rest.RestController): + """REST controller for Node.""" + + @wsme_pecan.wsexpose([Node]) + def get_all(self): + """Retrieve a list of all nodes.""" + result = [] + db_api = pecan.request.dbapi + + for node in db_api.get_nodes(None): + result.append(Node.convert(node)) + + return result + + @wsme_pecan.wsexpose(Node, unicode) + def get_one(self, node_id): + """Retrieve an instance of a Node.""" + db_api = pecan.request.dbapi + + try: + node = db_api.get_node(node_id) + except exception.TuskarException, e: + response = api.Response(None, + error=Error(faultcode=e.code, + faultstring=str(e)), + status_code=e.code) + return response + return Node.convert(node) diff --git a/tuskar/api/controllers/v1/types/node.py b/tuskar/api/controllers/v1/types/node.py index d52e82cf..dea625ce 100644 --- a/tuskar/api/controllers/v1/types/node.py +++ b/tuskar/api/controllers/v1/types/node.py @@ -12,15 +12,32 @@ # License for the specific language governing permissions and limitations # under the License. -#import wsme +import pecan from wsme import types as wtypes from tuskar.api.controllers.v1.types.base import Base from tuskar.api.controllers.v1.types.link import Link +from tuskar.api.controllers.v1.types.relation import Relation class Node(Base): """A Node representation.""" id = wtypes.text + # FIXME: We expose this as nova_baremetal_node_id, but are not yet changing + # the column name in the database, because this is a more involved change. + nova_baremetal_node_id = wtypes.text + rack = Relation links = [Link] + + @classmethod + def convert(self, node): + kwargs = node.as_dict() + links = [Link.build('self', pecan.request.host_url, 'nodes', + node.id)] + rack_link = [Link.build('self', pecan.request.host_url, + 'racks', node.rack_id)] + kwargs['rack'] = Relation(id=node.rack_id, links=rack_link) + kwargs['id'] = str(node.id) + kwargs['nova_baremetal_node_id'] = str(node.node_id) + return Node(links=links, **kwargs) diff --git a/tuskar/api/controllers/v1/types/rack.py b/tuskar/api/controllers/v1/types/rack.py index d656a271..5a84693c 100644 --- a/tuskar/api/controllers/v1/types/rack.py +++ b/tuskar/api/controllers/v1/types/rack.py @@ -60,9 +60,12 @@ class Rack(Base): unit=c.unit) for c in rack.capacities] - kwargs['nodes'] = [Node(id=n.node_id, + kwargs['nodes'] = [Node(id=str(n.id), + node_id=n.node_id, links=[ - Link.build_ironic_link('node', n.node_id) + Link.build('self', + pecan.request.host_url, + 'nodes', n.id) ]) for n in rack.nodes] diff --git a/tuskar/db/sqlalchemy/api.py b/tuskar/db/sqlalchemy/api.py index 3752a470..6a4b6400 100644 --- a/tuskar/db/sqlalchemy/api.py +++ b/tuskar/db/sqlalchemy/api.py @@ -507,3 +507,19 @@ class Connection(api.Connection): session.rollback() return False return True + + def get_nodes(self, columns): + session = get_session() + result = session.query(models.Node).options( + joinedload('rack')).all() + session.close() + return result + + def get_node(self, node_id): + session = get_session() + try: + result = session.query(models.Node).options( + joinedload('rack')).filter_by(id=node_id).one() + except NoResultFound: + raise exception.NodeNotFound(node=node_id) + return result diff --git a/tuskar/db/sqlalchemy/models.py b/tuskar/db/sqlalchemy/models.py index 27a74b54..1bcf9860 100644 --- a/tuskar/db/sqlalchemy/models.py +++ b/tuskar/db/sqlalchemy/models.py @@ -123,6 +123,7 @@ class Node(Base): id = Column(Integer, primary_key=True) rack_id = Column(Integer, ForeignKey('racks.id')) node_id = Column(String(length=64), unique=True) + rack = relationship("Rack") class Rack(Base): diff --git a/tuskar/tests/api/controllers/v1/test_nodes.py b/tuskar/tests/api/controllers/v1/test_nodes.py new file mode 100644 index 00000000..3fbab03c --- /dev/null +++ b/tuskar/tests/api/controllers/v1/test_nodes.py @@ -0,0 +1,62 @@ +from tuskar.api.controllers.v1.types import Capacity +from tuskar.api.controllers.v1.types import Chassis +from tuskar.api.controllers.v1.types import Node +from tuskar.api.controllers.v1.types import Rack +from tuskar.db.sqlalchemy import api as dbapi +from tuskar.tests.api import api + + +class TestNodes(api.FunctionalTest): + + test_node = None + db = dbapi.get_backend() + + def setUp(self): + """Create 'test_rack'.""" + + super(TestNodes, self).setUp() + # self.test_resource_class = None + self.test_node = Node(id='1', name='test_node', node_id='1') + self.test_rack = self.db.create_rack( + Rack(name='test-rack', + slots=1, + subnet='10.0.0.0/24', + location='nevada', + chassis=Chassis(id='123'), + capacities=[Capacity(name='cpu', value='10', + unit='count')], + nodes=[self.test_node] + )) + + def tearDown(self): + self.db.delete_rack(self.test_rack.id) + super(TestNodes, self).tearDown() + + def valid_node_json(self, node_json, test_node=None): + node = self.test_node if test_node is None else test_node + + self.assertEqual(node_json['nova_baremetal_node_id'], node.node_id) + self.assertEqual(node_json['id'], node.id) + + def test_it_returns_single_node(self): + response = self.get_json('/nodes/' + str(self.test_node.id), + expect_errors=True) + + self.assertEqual(response.status_int, 200) + self.assertEqual(response.content_type, 'application/json') + self.valid_node_json(response.json) + + # it should contain a rack + rack = response.json['rack'] + self.assertEqual(rack['id'], self.test_rack.id) + + def test_it_returns_node_list(self): + response = self.get_json('/nodes/', expect_errors=True) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.content_type, 'application/json') + + # It should consist solely of one node: + self.assertEqual(len(response.json), 1) + + # And that node should pass our JSON test: + self.valid_node_json(response.json[0])