From 01374adc2fadb9bada6d14c0ba476d8b5b6b6e93 Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Wed, 30 Nov 2016 18:31:06 +0000 Subject: [PATCH] Add Virtual Network Interface RPC APIs This patch adds the RPC API interfaces for the virtual network interface API in order to abstract the task of assigning logical network interfaces to physical network interfaces. Since the OpenStack Newton release, Ironic provides an interface for pluggable network implementations. Different network implementations may want to handle how logical to physical network interface assignment happens. To do this the new API calls into new functions on the network implementation loaded for the specified node. This is part 2 of 3, and adds vif_attach, vif_detach and vif_list functions to the conductor manager and RPC API classes. Co-Authored-By: Vasyl Saienko (vsaienko@mirantis.com) Change-Id: I6c5a50016d12ad88b3c8175bc9b665e325e8df66 Partial-Bug: #1582188 --- ironic/conductor/manager.py | 83 ++++++++++++++- ironic/conductor/rpcapi.py | 52 +++++++++- ironic/tests/unit/conductor/test_manager.py | 108 ++++++++++++++++++++ ironic/tests/unit/conductor/test_rpcapi.py | 20 ++++ 4 files changed, 260 insertions(+), 3 deletions(-) diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index f9989c7d23..ed035485d1 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -83,7 +83,7 @@ class ConductorManager(base_manager.BaseConductorManager): """Ironic Conductor manager main class.""" # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. - RPC_API_VERSION = '1.37' + RPC_API_VERSION = '1.38' target = messaging.Target(version=RPC_API_VERSION) @@ -1777,7 +1777,6 @@ class ConductorManager(base_manager.BaseConductorManager): with task_manager.acquire(context, port_obj.node_id, purpose='port update') as task: node = task.node - # Only allow updating MAC addresses for active nodes if maintenance # mode is on. if ((node.provision_state == states.ACTIVE or node.instance_uuid) @@ -2324,6 +2323,86 @@ class ConductorManager(base_manager.BaseConductorManager): task.spawn_after(self._spawn_worker, task.driver.deploy.heartbeat, task, callback_url) + @METRICS.timer('ConductorManager.vif_list') + @messaging.expected_exceptions(exception.NetworkError, + exception.InvalidParameterValue) + def vif_list(self, context, node_id): + """List attached VIFs for a node + + :param context: request context. + :param node_id: node ID or UUID. + :returns: List of VIF dictionaries, each dictionary will have an + 'id' entry with the ID of the VIF. + :raises: NetworkError, if something goes wrong during list the VIFs. + :raises: InvalidParameterValue, if a parameter that's required for + VIF list is wrong/missing. + """ + LOG.debug("RPC vif_list called for the node %s", node_id) + with task_manager.acquire(context, node_id, + purpose='list vifs', + shared=True) as task: + task.driver.network.validate(task) + return task.driver.network.vif_list(task) + + @METRICS.timer('ConductorManager.vif_attach') + @messaging.expected_exceptions(exception.NodeLocked, + exception.NetworkError, + exception.VifAlreadyAttached, + exception.NoFreePhysicalPorts, + exception.InvalidParameterValue) + def vif_attach(self, context, node_id, vif_info): + """Attach a VIF to a node + + :param context: request context. + :param node_id: node ID or UUID. + :param vif_info: a dictionary representing VIF object. + It must have an 'id' key, whose value is a unique + identifier for that VIF. + :raises: VifAlreadyAttached, if VIF is already attached to node + :raises: NoFreePhysicalPorts, if no free physical ports left to attach + :raises: NodeLocked, if node has an exclusive lock held on it + :raises: NetworkError, if an error occurs during attaching the VIF. + :raises: InvalidParameterValue, if a parameter that's required for + VIF attach is wrong/missing. + """ + LOG.debug("RPC vif_attach called for the node %(node_id)s with " + "vif_info %(vif_info)s", {'node_id': node_id, + 'vif_info': vif_info}) + with task_manager.acquire(context, node_id, + purpose='attach vif') as task: + task.driver.network.validate(task) + task.driver.network.vif_attach(task, vif_info) + LOG.info(_LI("VIF %(vif_id)s successfully attached to node " + "%(node_id)s"), {'vif_id': vif_info['id'], + 'node_id': node_id}) + + @METRICS.timer('ConductorManager.vif_detach') + @messaging.expected_exceptions(exception.NodeLocked, + exception.NetworkError, + exception.VifNotAttached, + exception.InvalidParameterValue) + def vif_detach(self, context, node_id, vif_id): + """Detach a VIF from a node + + :param context: request context. + :param node_id: node ID or UUID. + :param vif_id: A VIF ID. + :raises: VifNotAttached, if VIF not attached to node + :raises: NodeLocked, if node has an exclusive lock held on it + :raises: NetworkError, if an error occurs during detaching the VIF. + :raises: InvalidParameterValue, if a parameter that's required for + VIF detach is wrong/missing. + """ + LOG.debug("RPC vif_detach called for the node %(node_id)s with " + "vif_id %(vif_id)s", {'node_id': node_id, 'vif_id': vif_id}) + with task_manager.acquire(context, node_id, + purpose='detach vif') as task: + task.driver.network.validate(task) + task.driver.network.vif_detach(task, vif_id) + LOG.info(_LI("VIF %(vif_id)s successfully detached from node " + "%(node_id)s"), {'vif_id': vif_id, + 'node_id': node_id}) + def _object_dispatch(self, target, method, context, args, kwargs): """Dispatch a call to an object method. diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py index b4267271f6..369806f1c3 100644 --- a/ironic/conductor/rpcapi.py +++ b/ironic/conductor/rpcapi.py @@ -84,11 +84,12 @@ class ConductorAPI(object): | 1.35 - Added destroy_volume_connector and update_volume_connector | 1.36 - Added create_node | 1.37 - Added destroy_volume_target and update_volume_target + | 1.38 - Added vif_attach, vif_detach, vif_list """ # NOTE(rloo): This must be in sync with manager.ConductorManager's. - RPC_API_VERSION = '1.37' + RPC_API_VERSION = '1.38' def __init__(self, topic=None): super(ConductorAPI, self).__init__() @@ -839,3 +840,52 @@ class ConductorAPI(object): cctxt = self.client.prepare(topic=topic or self.topic, version='1.37') return cctxt.call(context, 'update_volume_target', target=target) + + def vif_attach(self, context, node_id, vif_info, topic=None): + """Attach VIF to a node + + :param context: request context. + :param node_id: node ID or UUID. + :param vif_info: a dictionary representing VIF object. + It must have an 'id' key, whose value is a unique + identifier for that VIF. + :param topic: RPC topic. Defaults to self.topic. + :raises: NodeLocked, if node has an exclusive lock held on it + :raises: NetworkError, if an error occurs during attaching the VIF. + :raises: InvalidParameterValue, if a parameter that's required for + VIF attach is wrong/missing. + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.38') + return cctxt.call(context, 'vif_attach', node_id=node_id, + vif_info=vif_info) + + def vif_detach(self, context, node_id, vif_id, topic=None): + """Detach VIF from a node + + :param context: request context. + :param node_id: node ID or UUID. + :param vif_id: an ID of a VIF. + :param topic: RPC topic. Defaults to self.topic. + :raises: NodeLocked, if node has an exclusive lock held on it + :raises: NetworkError, if an error occurs during detaching the VIF. + :raises: InvalidParameterValue, if a parameter that's required for + VIF detach is wrong/missing. + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.38') + return cctxt.call(context, 'vif_detach', node_id=node_id, + vif_id=vif_id) + + def vif_list(self, context, node_id, topic=None): + """List attached VIFs for a node + + :param context: request context. + :param node_id: node ID or UUID. + :param topic: RPC topic. Defaults to self.topic. + :returns: List of VIF dictionaries, each dictionary will have an + 'id' entry with the ID of the VIF. + :raises: NetworkError, if an error occurs during listing the VIFs. + :raises: InvalidParameterValue, if a parameter that's required for + VIF list is wrong/missing. + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.38') + return cctxt.call(context, 'vif_list', node_id=node_id) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index a86e3d367d..fc73087c83 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -3322,6 +3322,114 @@ class UpdatePortTestCase(mgr_utils.ServiceSetUpMixin, exc.exc_info[0]) +@mgr_utils.mock_record_keepalive +@mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True) +class VifTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): + + def setUp(self): + super(VifTestCase, self).setUp() + self.vif = {'id': 'fake'} + + @mock.patch.object(n_flat.FlatNetwork, 'vif_list', autospec=True) + def test_vif_list(self, mock_list, mock_valid): + mock_list.return_value = ['VIF_ID'] + node = obj_utils.create_test_node(self.context, driver='fake') + data = self.service.vif_list(self.context, node.uuid) + mock_list.assert_called_once_with(mock.ANY, mock.ANY) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + self.assertEqual(mock_list.return_value, data) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_attach', autospec=True) + def test_vif_attach(self, mock_attach, mock_valid): + node = obj_utils.create_test_node(self.context, driver='fake') + self.service.vif_attach(self.context, node.uuid, self.vif) + mock_attach.assert_called_once_with(mock.ANY, mock.ANY, self.vif) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_attach', autospec=True) + def test_vif_attach_node_locked(self, mock_attach, mock_valid): + node = obj_utils.create_test_node(self.context, driver='fake', + reservation='fake-reserv') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_attach, + self.context, node.uuid, self.vif) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NodeLocked, exc.exc_info[0]) + self.assertFalse(mock_attach.called) + self.assertFalse(mock_valid.called) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_attach', autospec=True) + def test_vif_attach_raises_network_error(self, mock_attach, + mock_valid): + mock_attach.side_effect = exception.NetworkError("BOOM") + node = obj_utils.create_test_node(self.context, driver='fake') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_attach, + self.context, node.uuid, self.vif) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NetworkError, exc.exc_info[0]) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + mock_attach.assert_called_once_with(mock.ANY, mock.ANY, self.vif) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_attach', autpspec=True) + def test_vif_attach_validate_error(self, mock_attach, + mock_valid): + mock_valid.side_effect = exception.MissingParameterValue("BOOM") + node = obj_utils.create_test_node(self.context, driver='fake') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_attach, + self.context, node.uuid, self.vif) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.MissingParameterValue, exc.exc_info[0]) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + self.assertFalse(mock_attach.called) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_detach', autpspec=True) + def test_vif_detach(self, mock_detach, mock_valid): + node = obj_utils.create_test_node(self.context, driver='fake') + self.service.vif_detach(self.context, node.uuid, "interface") + mock_detach.assert_called_once_with(mock.ANY, "interface") + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_detach', autpspec=True) + def test_vif_detach_node_locked(self, mock_detach, mock_valid): + node = obj_utils.create_test_node(self.context, driver='fake', + reservation='fake-reserv') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_detach, + self.context, node.uuid, "interface") + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NodeLocked, exc.exc_info[0]) + self.assertFalse(mock_detach.called) + self.assertFalse(mock_valid.called) + + @mock.patch.object(n_flat.FlatNetwork, 'vif_detach', autpspec=True) + def test_vif_detach_raises_network_error(self, mock_detach, + mock_valid): + mock_detach.side_effect = exception.NetworkError("BOOM") + node = obj_utils.create_test_node(self.context, driver='fake') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_detach, + self.context, node.uuid, "interface") + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.NetworkError, exc.exc_info[0]) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + mock_detach.assert_called_once_with(mock.ANY, "interface") + + @mock.patch.object(n_flat.FlatNetwork, 'vif_detach', autpspec=True) + def test_vif_detach_validate_error(self, mock_detach, + mock_valid): + mock_valid.side_effect = exception.MissingParameterValue("BOOM") + node = obj_utils.create_test_node(self.context, driver='fake') + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.vif_detach, + self.context, node.uuid, "interface") + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.MissingParameterValue, exc.exc_info[0]) + mock_valid.assert_called_once_with(mock.ANY, mock.ANY) + self.assertFalse(mock_detach.called) + + @mgr_utils.mock_record_keepalive class UpdatePortgroupTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): diff --git a/ironic/tests/unit/conductor/test_rpcapi.py b/ironic/tests/unit/conductor/test_rpcapi.py index 9cb92d874f..fddfb8cc38 100644 --- a/ironic/tests/unit/conductor/test_rpcapi.py +++ b/ironic/tests/unit/conductor/test_rpcapi.py @@ -434,3 +434,23 @@ class RPCAPITestCase(base.DbTestCase): 'call', version='1.37', target=fake_volume_target) + + def test_vif_attach(self): + self._test_rpcapi('vif_attach', + 'call', + node_id='fake-node', + vif_info={"id": "vif"}, + version='1.38') + + def test_vif_detach(self): + self._test_rpcapi('vif_detach', + 'call', + node_id='fake-node', + vif_id="vif", + version='1.38') + + def test_vif_list(self): + self._test_rpcapi('vif_list', + 'call', + node_id='fake-node', + version='1.38')