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')