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
This commit is contained in:
Sam Betts 2016-11-30 18:31:06 +00:00 committed by Vasyl Saienko
parent 6de29dffbf
commit 01374adc2f
4 changed files with 260 additions and 3 deletions

View File

@ -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.

View File

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

View File

@ -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):

View File

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