From 088f09903b310fe66cb76f1c5bd62f1ac076dd53 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 17 Jun 2016 09:42:33 +0200 Subject: [PATCH] Add dbapi and objects functions to get a node by associated MAC addresses Adds a new dbapi call get_node_by_port_addresses and associated objects call Node.get_by_port_addresses. The logic is the same as in "lookup" agent passthru. Will be used for a new lookup endpoint. Change-Id: Ia5549fb16cd363f3492b9ca0400177c92a1aea19 Partial-Bug: #1570841 --- ironic/db/api.py | 9 ++++ ironic/db/sqlalchemy/api.py | 17 ++++++- ironic/objects/node.py | 16 ++++++- ironic/tests/unit/db/test_nodes.py | 54 +++++++++++++++++++++++ ironic/tests/unit/objects/test_node.py | 11 +++++ ironic/tests/unit/objects/test_objects.py | 2 +- 6 files changed, 106 insertions(+), 3 deletions(-) diff --git a/ironic/db/api.py b/ironic/db/api.py index f8ab5a6869..c0fd52c597 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -604,3 +604,12 @@ class Connection(object): :param tag: A tag string. :returns: True if the tag exists otherwise False. """ + + @abc.abstractmethod + def get_node_by_port_addresses(self, addresses): + """Find a node by any matching port address. + + :param addresses: list of port addresses (e.g. MACs). + :returns: Node object. + :raises: NodeNotFound if none or several nodes are found. + """ diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index 02bf4782c1..0ac46b9fb0 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -28,7 +28,7 @@ from oslo_log import log from oslo_utils import strutils from oslo_utils import timeutils from oslo_utils import uuidutils -from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound from sqlalchemy.orm import joinedload from sqlalchemy import sql @@ -843,3 +843,18 @@ class Connection(api.Connection): def node_tag_exists(self, node_id, tag): q = model_query(models.NodeTag).filter_by(node_id=node_id, tag=tag) return model_query(q.exists()).scalar() + + def get_node_by_port_addresses(self, addresses): + q = model_query(models.Node).distinct().join(models.Port) + q = q.filter(models.Port.address.in_(addresses)) + + try: + return q.one() + except NoResultFound: + raise exception.NodeNotFound( + _('Node with port addresses %s was not found') + % addresses) + except MultipleResultsFound: + raise exception.NodeNotFound( + _('Multiple nodes with port addresses %s were found') + % addresses) diff --git a/ironic/objects/node.py b/ironic/objects/node.py index 2e5049b7a1..aa4b851ae2 100644 --- a/ironic/objects/node.py +++ b/ironic/objects/node.py @@ -45,7 +45,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.13: Add touch_provisioning() # Version 1.14: Add _validate_property_values() and make create() # and save() validate the input of property values. - VERSION = '1.14' + # Version 1.15: Add get_by_port_addresses + VERSION = '1.15' dbapi = db_api.get_instance() @@ -364,3 +365,16 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat): def touch_provisioning(self, context=None): """Touch the database record to mark the provisioning as alive.""" self.dbapi.touch_node_provisioning(self.id) + + @classmethod + def get_by_port_addresses(cls, context, addresses): + """Get a node by associated port addresses. + + :param context: Security context. + :param addresses: A list of port addresses. + :raises: NodeNotFound if the node is not found. + :returns: a :class:`Node` object. + """ + db_node = cls.dbapi.get_node_by_port_addresses(addresses) + node = Node._from_db_object(cls(context), db_node) + return node diff --git a/ironic/tests/unit/db/test_nodes.py b/ironic/tests/unit/db/test_nodes.py index 33522a1e4c..3852f7bf6a 100644 --- a/ironic/tests/unit/db/test_nodes.py +++ b/ironic/tests/unit/db/test_nodes.py @@ -570,3 +570,57 @@ class DbNodeTestCase(base.DbTestCase): self.assertRaises( exception.NodeNotFound, self.dbapi.touch_node_provisioning, uuidutils.generate_uuid()) + + def test_get_node_by_port_addresses(self): + wrong_node = utils.create_test_node( + driver='driver-one', + uuid=uuidutils.generate_uuid()) + node = utils.create_test_node( + driver='driver-two', + uuid=uuidutils.generate_uuid()) + addresses = [] + for i in (1, 2, 3): + address = '52:54:00:cf:2d:4%s' % i + utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=node.id, address=address) + if i > 1: + addresses.append(address) + utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=wrong_node.id, + address='aa:bb:cc:dd:ee:ff') + + res = self.dbapi.get_node_by_port_addresses(addresses) + self.assertEqual(node.uuid, res.uuid) + + def test_get_node_by_port_addresses_not_found(self): + node = utils.create_test_node( + driver='driver', + uuid=uuidutils.generate_uuid()) + utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=node.id, + address='aa:bb:cc:dd:ee:ff') + + self.assertRaisesRegexp(exception.NodeNotFound, + 'was not found', + self.dbapi.get_node_by_port_addresses, + ['11:22:33:44:55:66']) + + def test_get_node_by_port_addresses_multiple_found(self): + node1 = utils.create_test_node( + driver='driver', + uuid=uuidutils.generate_uuid()) + node2 = utils.create_test_node( + driver='driver', + uuid=uuidutils.generate_uuid()) + addresses = ['52:54:00:cf:2d:4%s' % i for i in (1, 2)] + utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=node1.id, + address=addresses[0]) + utils.create_test_port(uuid=uuidutils.generate_uuid(), + node_id=node2.id, + address=addresses[1]) + + self.assertRaisesRegexp(exception.NodeNotFound, + 'Multiple nodes', + self.dbapi.get_node_by_port_addresses, + addresses) diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py index b7ef8073ab..8d972d1805 100644 --- a/ironic/tests/unit/objects/test_node.py +++ b/ironic/tests/unit/objects/test_node.py @@ -54,6 +54,17 @@ class TestNodeObject(base.DbTestCase): self.assertRaises(exception.InvalidIdentity, objects.Node.get, self.context, 'not-a-uuid') + def test_get_by_port_addresses(self): + with mock.patch.object(self.dbapi, 'get_node_by_port_addresses', + autospec=True) as mock_get_node: + mock_get_node.return_value = self.fake_node + + node = objects.Node.get_by_port_addresses(self.context, + ['aa:bb:cc:dd:ee:ff']) + + mock_get_node.assert_called_once_with(['aa:bb:cc:dd:ee:ff']) + self.assertEqual(self.context, node._context) + def test_save(self): uuid = self.fake_node['uuid'] with mock.patch.object(self.dbapi, 'get_node_by_uuid', diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 1564466c24..ae67537bd9 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -404,7 +404,7 @@ class TestObject(_LocalTest, _TestObject): # version bump. It is md5 hash of object fields and remotable methods. # The fingerprint values should only be changed if there is a version bump. expected_object_fingerprints = { - 'Node': '1.14-9ee8ab283b06398545880dfdedb49891', + 'Node': '1.15-9ee8ab283b06398545880dfdedb49891', 'MyObj': '1.5-4f5efe8f0fcaf182bbe1c7fe3ba858db', 'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905', 'Port': '1.5-a224755c3da5bc5cf1a14a11c0d00f3f',