Plumbing for tagged nic attachment

This patch adds support to the compute manager and Neutron API for
tagged attach for network interfaces.

Change-Id: I3ca96e6d791d609db8bf4368a3c71f880de8e7ad
Implements: blueprint virt-device-tagged-attach-detach
This commit is contained in:
Artom Lifshitz 2016-12-29 07:16:59 +00:00
parent 68c95c9290
commit 0f3e85e136
15 changed files with 166 additions and 22 deletions

View File

@ -13,7 +13,7 @@
"disabled_reason": null,
"report_count": 1,
"forced_down": false,
"version": 19,
"version": 20,
"availability_zone": null,
"uuid": "fa69c544-906b-4a6a-a9c6-c1f7a8078c73"
}

View File

@ -134,7 +134,8 @@ class InterfaceAttachmentController(wsgi.Controller):
exception.NoMoreFixedIps,
exception.PortNotUsable,
exception.AttachInterfaceNotSupported,
exception.SecurityGroupCannotBeApplied) as e:
exception.SecurityGroupCannotBeApplied,
exception.TaggedAttachmentNotSupported) as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
except (exception.InstanceIsLocked,
exception.FixedIpAlreadyInUse,

View File

@ -3922,11 +3922,11 @@ class API(base.Base):
vm_states.STOPPED],
task_state=[None])
def attach_interface(self, context, instance, network_id, port_id,
requested_ip):
requested_ip, tag=None):
"""Use hotplug to add an network adapter to an instance."""
return self.compute_rpcapi.attach_interface(context,
instance=instance, network_id=network_id, port_id=port_id,
requested_ip=requested_ip)
requested_ip=requested_ip, tag=tag)
@check_instance_lock
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED,

View File

@ -481,7 +481,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.Manager):
"""Manages the running instances from creation to destruction."""
target = messaging.Target(version='4.15')
target = messaging.Target(version='4.16')
# How long to wait in seconds before re-issuing a shutdown
# signal to an instance during power off. The overall
@ -5151,15 +5151,19 @@ class ComputeManager(manager.Manager):
@wrap_exception()
@wrap_instance_fault
def attach_interface(self, context, instance, network_id, port_id,
requested_ip):
requested_ip, tag=None):
"""Use hotplug to add an network adapter to an instance."""
if not self.driver.capabilities['supports_attach_interface']:
raise exception.AttachInterfaceNotSupported(
instance_uuid=instance.uuid)
if (tag and not
self.driver.capabilities.get('supports_tagged_attach_interface',
False)):
raise exception.NetworkInterfaceTaggedAttachNotSupported()
bind_host_id = self.driver.network_binding_host_id(context, instance)
network_info = self.network_api.allocate_port_for_instance(
context, instance, port_id, network_id, requested_ip,
bind_host_id=bind_host_id)
bind_host_id=bind_host_id, tag=tag)
if len(network_info) != 1:
LOG.error('allocate_port_for_instance returned %(ports)s '
'ports', {'ports': len(network_info)})

View File

@ -325,6 +325,7 @@ class ComputeAPI(object):
the version because this method was unused before. The version
was bumped to signal the availability of the corrected RPC API
* 4.15 - Add tag argument to reserve_block_device_name()
* 4.16 - Add tag argument to attach_interface()
'''
VERSION_ALIASES = {
@ -420,13 +421,26 @@ class ComputeAPI(object):
instance=instance, network_id=network_id)
def attach_interface(self, ctxt, instance, network_id, port_id,
requested_ip):
version = '4.0'
cctxt = self.router.client(ctxt).prepare(
server=_compute_host(None, instance), version=version)
return cctxt.call(ctxt, 'attach_interface',
instance=instance, network_id=network_id,
port_id=port_id, requested_ip=requested_ip)
requested_ip, tag=None):
kw = {'instance': instance, 'network_id': network_id,
'port_id': port_id, 'requested_ip': requested_ip,
'tag': tag}
version = '4.16'
client = self.router.client(ctxt)
if not client.can_send_version(version):
if tag:
# NOTE(artom) Attach attempted with a device role tag, but
# we're pinned to less than 4.16 - ie, not all nodes have
# received the Pike code yet.
raise exception.TaggedAttachmentNotSupported()
else:
version = '4.0'
kw.pop('tag')
cctxt = client.prepare(server=_compute_host(None, instance),
version=version)
return cctxt.call(ctxt, 'attach_interface', **kw)
def attach_volume(self, ctxt, instance, bdm):
version = '4.0'

View File

@ -283,6 +283,11 @@ class VolumeTaggedAttachToShelvedNotSupported(TaggedAttachmentNotSupported):
"shelved-offloaded instances.")
class NetworkInterfaceTaggedAttachNotSupported(TaggedAttachmentNotSupported):
msg_fmt = _("Tagged network interface attachment is not supported for "
"this server instance.")
class InvalidKeypair(Invalid):
msg_fmt = _("Keypair data is invalid: %(reason)s")

View File

@ -301,7 +301,7 @@ class API(base_api.NetworkAPI):
# NOTE(danms): Here for neutron compatibility
def allocate_port_for_instance(self, context, instance, port_id,
network_id=None, requested_ip=None,
bind_host_id=None):
bind_host_id=None, tag=None):
raise NotImplementedError()
# NOTE(danms): Here for neutron compatibility

View File

@ -210,7 +210,7 @@ class NetworkAPI(base.Base):
def allocate_port_for_instance(self, context, instance, port_id,
network_id=None, requested_ip=None,
bind_host_id=None):
bind_host_id=None, tag=None):
"""Allocate port for instance."""
raise NotImplementedError()

View File

@ -1196,13 +1196,14 @@ class API(base_api.NetworkAPI):
def allocate_port_for_instance(self, context, instance, port_id,
network_id=None, requested_ip=None,
bind_host_id=None):
bind_host_id=None, tag=None):
"""Allocate a port for the instance."""
requested_networks = objects.NetworkRequestList(
objects=[objects.NetworkRequest(network_id=network_id,
address=requested_ip,
port_id=port_id,
pci_request_id=None)])
pci_request_id=None,
tag=tag)])
return self.allocate_for_instance(context, instance, vpn=False,
requested_networks=requested_networks,
bind_host_id=bind_host_id)

View File

@ -32,7 +32,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter
SERVICE_VERSION = 19
SERVICE_VERSION = 20
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@ -106,6 +106,8 @@ SERVICE_VERSION_HISTORY = (
{'compute_rpc': '4.14'},
# Version 19: Compute RPC version 4.15
{'compute_rpc': '4.15'},
# Version 20: Compute RPC version 4.16
{'compute_rpc': '4.16'},
)

View File

@ -231,6 +231,14 @@ class InterfaceAttachTestsV21(test.NoDBTestCase):
self.assertEqual(result['interfaceAttachment']['net_id'],
FAKE_NET_ID1)
@mock.patch.object(
compute_api.API, 'attach_interface',
side_effect=exception.NetworkInterfaceTaggedAttachNotSupported())
def test_interface_tagged_attach_not_supported(self, mock_attach):
body = {'interfaceAttachment': {'net_id': FAKE_NET_ID2}}
self.assertRaises(exc.HTTPBadRequest, self.attachments.create,
self.req, FAKE_UUID1, body=body)
def test_attach_interface_with_network_id(self):
self.stub_out('nova.compute.api.API.attach_interface',
fake_attach_interface)

View File

@ -9959,9 +9959,46 @@ class ComputeAPITestCase(BaseTestCase):
self.assertEqual(vif['id'], network_id)
mock_allocate.assert_called_once_with(
self.context, instance, port_id, network_id, req_ip,
bind_host_id='fake-host')
bind_host_id='fake-host', tag=None)
return nwinfo, port_id
def test_interface_tagged_attach(self):
new_type = flavors.get_flavor_by_flavor_id('4')
instance = objects.Instance(image_ref=uuids.image_instance,
system_metadata={},
flavor=new_type,
host='fake-host')
nwinfo = [fake_network_cache_model.new_vif()]
network_id = nwinfo[0]['network']['id']
port_id = nwinfo[0]['id']
req_ip = '1.2.3.4'
mock_allocate = mock.Mock(return_value=nwinfo)
self.compute.network_api.allocate_port_for_instance = mock_allocate
with mock.patch.dict(self.compute.driver.capabilities,
supports_attach_interface=True,
supports_tagged_attach_interface=True):
vif = self.compute.attach_interface(self.context,
instance,
network_id,
port_id,
req_ip, tag='foo')
self.assertEqual(vif['id'], network_id)
mock_allocate.assert_called_once_with(
self.context, instance, port_id, network_id, req_ip,
bind_host_id='fake-host', tag='foo')
return nwinfo, port_id
def test_tagged_attach_interface_raises(self):
instance = self._create_fake_instance_obj()
with mock.patch.dict(self.compute.driver.capabilities,
supports_attach_interface=True,
supports_tagged_attach_interface=False):
self.assertRaises(
exception.NetworkInterfaceTaggedAttachNotSupported,
self.compute.attach_interface, self.context, instance,
'fake-network-id', 'fake-port-id', 'fake-req-ip', tag='foo')
def test_attach_interface_failed(self):
new_type = flavors.get_flavor_by_flavor_id('4')
instance = objects.Instance(
@ -9993,7 +10030,8 @@ class ComputeAPITestCase(BaseTestCase):
instance, network_id, port_id, req_ip)
mock_allocate.assert_called_once_with(self.context, instance,
network_id, port_id, req_ip,
bind_host_id='fake-host')
bind_host_id='fake-host',
tag=None)
mock_deallocate.assert_called_once_with(self.context, instance,
port_id)

View File

@ -5229,6 +5229,15 @@ class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
self.assertItemsEqual(['default', uuids.secgroup_uuid],
security_groups)
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_interface')
def test_tagged_interface_attach(self, mock_attach):
instance = self._create_instance_obj()
self.compute_api.attach_interface(self.context, instance, None, None,
None, tag='foo')
mock_attach.assert_called_with(self.context, instance=instance,
network_id=None, port_id=None,
requested_ip=None, tag='foo')
class ComputeAPIAPICellUnitTestCase(_ComputeAPIUnitTestMixIn,
test.NoDBTestCase):

View File

@ -183,7 +183,59 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
def test_attach_interface(self):
self._test_compute_api('attach_interface', 'call',
instance=self.fake_instance_obj, network_id='id',
port_id='id2', version='4.0', requested_ip='192.168.1.50')
port_id='id2', version='4.16', requested_ip='192.168.1.50',
tag='foo')
def test_attach_interface_raises(self):
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
cctxt_mock = mock.Mock()
mock_client = mock.Mock()
rpcapi.router.client = mock.Mock()
rpcapi.router.client.return_value = mock_client
with test.nested(
mock.patch.object(mock_client, 'can_send_version',
return_value=False),
mock.patch.object(mock_client, 'prepare',
return_value=cctxt_mock)
) as (
can_send_mock, prepare_mock
):
self.assertRaises(exception.TaggedAttachmentNotSupported,
rpcapi.attach_interface, ctxt, instance,
'fake_network', 'fake_port', 'fake_requested_ip',
tag='foo')
can_send_mock.assert_called_once_with('4.16')
def test_attach_interface_downgrades_version(self):
ctxt = context.RequestContext('fake_user', 'fake_project')
instance = self.fake_instance_obj
rpcapi = compute_rpcapi.ComputeAPI()
call_mock = mock.Mock()
cctxt_mock = mock.Mock(call=call_mock)
mock_client = mock.Mock()
rpcapi.router.client = mock.Mock()
rpcapi.router.client.return_value = mock_client
with test.nested(
mock.patch.object(mock_client, 'can_send_version',
return_value=False),
mock.patch.object(mock_client, 'prepare',
return_value=cctxt_mock)
) as (
can_send_mock, prepare_mock
):
rpcapi.attach_interface(ctxt, instance, 'fake_network',
'fake_port', 'fake_requested_ip')
can_send_mock.assert_called_once_with('4.16')
prepare_mock.assert_called_once_with(server=instance['host'],
version='4.0')
call_mock.assert_called_once_with(ctxt, 'attach_interface',
instance=instance,
network_id='fake_network',
port_id='fake_port',
requested_ip='fake_requested_ip')
def test_attach_volume(self):
self._test_compute_api('attach_volume', 'cast',

View File

@ -4445,6 +4445,16 @@ class TestNeutronv2WithMock(test.TestCase):
"No specific network was requested and none are available for "
"project 'fake-project'.", six.text_type(ex))
@mock.patch.object(neutronapi.API, 'allocate_for_instance')
def test_allocate_port_for_instance_with_tag(self, mock_allocate):
instance = fake_instance.fake_instance_obj(self.context)
api = neutronapi.API()
api.allocate_port_for_instance(self.context, instance, None,
network_id=None, requested_ip=None,
bind_host_id=None, tag='foo')
req_nets_in_call = mock_allocate.call_args[1]['requested_networks']
self.assertEqual('foo', req_nets_in_call.objects[0].tag)
@mock.patch('nova.objects.network_request.utils')
@mock.patch('nova.network.neutronv2.api.LOG')
@mock.patch('nova.network.neutronv2.api.base_api')