Compute manager device tagging support

This patch allows the network manager(s) to set virtual device tags if
they are present during allocation.

Implements: blueprint virt-device-role-tagging
Co-authored-by: Vladik Romanovsky <vromanso@redhat.com>
Co-authored-by: Dan Smith <dansmith@redhat.com>
Change-Id: I8367f740d6d4ebaeb81bc74c6a82a8faf5cd8308
This commit is contained in:
Artom Lifshitz 2016-01-06 02:31:45 +00:00 committed by Dan Smith
parent ceaacaa24f
commit e2eb6659a4
11 changed files with 229 additions and 28 deletions

View File

@ -13,7 +13,7 @@
"disabled_reason": null,
"report_count": 1,
"forced_down": false,
"version": 13
"version": 14
}
},
"event_type": "service.update",

View File

@ -1704,6 +1704,24 @@ class ComputeManager(manager.Manager):
requested_networks, security_groups,
block_device_mapping, node, limits)
def _check_device_tagging(self, requested_networks, block_device_mapping):
tagging_requested = False
if requested_networks:
for net in requested_networks:
if 'tag' in net and net.tag is not None:
tagging_requested = True
break
if block_device_mapping and not tagging_requested:
for bdm in block_device_mapping:
if 'tag' in bdm and bdm.tag is not None:
tagging_requested = True
break
if (tagging_requested and
not self.driver.capabilities.get('supports_device_tagging')):
raise exception.BuildAbortException('Attempt to boot guest with '
'tagged devices on host that '
'does not support tagging.')
@hooks.add_hook('build_instance')
@wrap_exception()
@reverts_task_state
@ -1856,6 +1874,9 @@ class ComputeManager(manager.Manager):
image_name = image.get('name')
self._notify_about_instance_usage(context, instance, 'create.start',
extra_usage_info={'image_name': image_name})
self._check_device_tagging(requested_networks, block_device_mapping)
try:
rt = self._get_resource_tracker(node)
with rt.instance_claim(context, instance, limits):

View File

@ -407,7 +407,7 @@ class NetworkManager(manager.Manager):
try:
self._allocate_mac_addresses(admin_context, instance_uuid,
networks, macs)
networks, macs, requested_networks)
except Exception:
with excutils.save_and_reraise_exception():
# If we fail to allocate any one mac address, clean up all
@ -663,26 +663,38 @@ class NetworkManager(manager.Manager):
return subnets
def _allocate_mac_addresses(self, context, instance_uuid, networks, macs):
def _allocate_mac_addresses(self, context, instance_uuid, networks, macs,
requested_networks):
"""Generates mac addresses and creates vif rows in db for them."""
# make a copy we can mutate
if macs is not None:
available_macs = set(macs)
if requested_networks:
tags_by_network = {
network.network_id: network.tag if 'tag' in network else None
for network in requested_networks}
else:
tags_by_network = {}
for network in networks:
if macs is None:
self._add_virtual_interface(context, instance_uuid,
network['id'])
self._add_virtual_interface(
context, instance_uuid,
network['id'],
tag=tags_by_network.get(network['uuid']))
else:
try:
mac = available_macs.pop()
except KeyError:
raise exception.VirtualInterfaceCreateException()
self._add_virtual_interface(context, instance_uuid,
network['id'], mac)
self._add_virtual_interface(
context, instance_uuid,
network['id'], mac,
tag=tags_by_network.get(network['uuid']))
def _add_virtual_interface(self, context, instance_uuid, network_id,
mac=None):
mac=None, tag=None):
attempts = 1 if mac else CONF.create_unique_mac_address_attempts
for i in range(attempts):
try:
@ -691,6 +703,7 @@ class NetworkManager(manager.Manager):
vif.instance_uuid = instance_uuid
vif.network_id = network_id
vif.uuid = str(uuid.uuid4())
vif.tag = tag
vif.create()
return vif
except exception.VirtualInterfaceCreateException:

View File

@ -192,7 +192,7 @@ class API(base_api.NetworkAPI):
:param fixed_ip: Optional fixed IP to use from the given network.
:param security_group_ids: Optional list of security group IDs to
apply to the port.
:returns: ID of the created port.
:returns: The created port.
:raises PortLimitExceeded: If neutron fails with an OverQuota error.
:raises NoMoreFixedIps: If neutron fails with
IpAddressGenerationFailure error.
@ -220,7 +220,7 @@ class API(base_api.NetworkAPI):
LOG.debug('Successfully created port: %s', port_id,
instance=instance)
return port_id
return port
except neutron_client_exc.InvalidIpForNetworkClient:
LOG.warning(_LW('Neutron error: %(ip)s is not a valid IP address '
'for network %(network_id)s.'),
@ -629,6 +629,10 @@ class API(base_api.NetworkAPI):
ports_in_requested_order = []
nets_in_requested_order = []
for request in ordered_networks:
vifobj = objects.VirtualInterface(context)
vifobj.instance_uuid = instance.uuid
vifobj.tag = request.tag if 'tag' in request else None
# Network lookup for available network_id
network = None
for net in nets:
@ -665,23 +669,31 @@ class API(base_api.NetworkAPI):
bind_host_id=bind_host_id)
self._populate_pci_mac_address(instance,
request.pci_request_id, port_req_body)
self._populate_mac_address(
updated_mac = self._populate_mac_address(
instance, port_req_body, available_macs)
if dhcp_opts is not None:
port_req_body['port']['extra_dhcp_opts'] = dhcp_opts
if not request.port_id:
created_port_id = self._create_port(
created_port = self._create_port(
port_client, instance, request.network_id,
port_req_body, request.address,
security_group_ids)
created_port_id = created_port['id']
created_port_ids.append(created_port_id)
ports_in_requested_order.append(created_port_id)
vifobj.uuid = created_port_id
vifobj.address = created_port['mac_address']
else:
self._update_port(
port_client, instance, request.port_id, port_req_body)
preexisting_port_ids.append(request.port_id)
ports_in_requested_order.append(request.port_id)
vifobj.uuid = request.port_id
vifobj.address = (updated_mac or
ports[request.port_id]['mac_address'])
vifobj.create()
self._update_port_dns_name(context, instance, network,
ports_in_requested_order[-1],

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_utils import versionutils
from nova.objects import base as obj_base
from nova.objects import fields
from nova import utils
@ -31,14 +33,21 @@ class NetworkRequest(obj_base.NovaObject,
obj_base.NovaObjectDictCompat):
# Version 1.0: Initial version
# Version 1.1: Added pci_request_id
VERSION = '1.1'
# Version 1.2: Added tag field
VERSION = '1.2'
fields = {
'network_id': fields.StringField(nullable=True),
'address': fields.IPAddressField(nullable=True),
'port_id': fields.UUIDField(nullable=True),
'pci_request_id': fields.UUIDField(nullable=True),
'tag': fields.StringField(nullable=True),
}
def obj_make_compatible(self, primitive, target_version):
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 2) and 'tag' in primitive:
del primitive['tag']
def obj_load_attr(self, attr):
setattr(self, attr, None)

View File

@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter
SERVICE_VERSION = 13
SERVICE_VERSION = 14
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@ -81,6 +81,8 @@ SERVICE_VERSION_HISTORY = (
{'compute_rpc': '4.12'},
# Version 13: Compute RPC version 4.13
{'compute_rpc': '4.13'},
# Version 14: The compute manager supports setting device tags.
{'compute_rpc': '4.13'},
)

View File

@ -45,6 +45,7 @@ from nova import objects
from nova.objects import block_device as block_device_obj
from nova.objects import instance as instance_obj
from nova.objects import migrate_data as migrate_data_obj
from nova.objects import network_request as net_req_obj
from nova import test
from nova.tests import fixtures
from nova.tests.unit.api.openstack import fakes
@ -266,6 +267,87 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
instance.info_cache = None
self.compute._delete_instance(self.context, instance, [], quotas)
def test_check_device_tagging_no_tagging(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
instance_uuid=uuids.instance)])
net_req = net_req_obj.NetworkRequest(tag=None)
net_req_list = net_req_obj.NetworkRequestList(objects=[net_req])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=False):
self.compute._check_device_tagging(net_req_list, bdms)
def test_check_device_tagging_no_networks(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
instance_uuid=uuids.instance)])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=False):
self.compute._check_device_tagging(None, bdms)
def test_check_device_tagging_tagged_net_req_no_virt_support(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
instance_uuid=uuids.instance)])
net_req = net_req_obj.NetworkRequest(port_id='bar', tag='foo')
net_req_list = net_req_obj.NetworkRequestList(objects=[net_req])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=False):
self.assertRaises(exception.BuildAbortException,
self.compute._check_device_tagging,
net_req_list, bdms)
def test_check_device_tagging_tagged_bdm_no_driver_support(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
tag='foo',
instance_uuid=uuids.instance)])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=False):
self.assertRaises(exception.BuildAbortException,
self.compute._check_device_tagging,
None, bdms)
def test_check_device_tagging_tagged_bdm_no_driver_support_declared(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
tag='foo',
instance_uuid=uuids.instance)])
with mock.patch.dict(self.compute.driver.capabilities):
self.compute.driver.capabilities.pop('supports_device_tagging',
None)
self.assertRaises(exception.BuildAbortException,
self.compute._check_device_tagging,
None, bdms)
def test_check_device_tagging_tagged_bdm_with_driver_support(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
tag='foo',
instance_uuid=uuids.instance)])
net_req = net_req_obj.NetworkRequest(network_id='bar')
net_req_list = net_req_obj.NetworkRequestList(objects=[net_req])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=True):
self.compute._check_device_tagging(net_req_list, bdms)
def test_check_device_tagging_tagged_net_req_with_driver_support(self):
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(source_type='volume',
destination_type='volume',
instance_uuid=uuids.instance)])
net_req = net_req_obj.NetworkRequest(network_id='bar', tag='foo')
net_req_list = net_req_obj.NetworkRequestList(objects=[net_req])
with mock.patch.dict(self.compute.driver.capabilities,
supports_device_tagging=True):
self.compute._check_device_tagging(net_req_list, bdms)
@mock.patch.object(network_api.API, 'allocate_for_instance')
@mock.patch.object(objects.Instance, 'save')
@mock.patch.object(time, 'sleep')

View File

@ -2717,15 +2717,25 @@ class AllocateTestCase(test.TestCase):
inst.uuid = FAKEUUID
inst.create()
networks = db.network_get_all(self.context)
reqnets = objects.NetworkRequestList(objects=[])
index = 0
project_id = self.user_context.project_id
for network in networks:
db.network_update(self.context, network['id'],
{'host': HOST})
project_id = self.user_context.project_id
{'host': HOST,
'project_id': project_id})
if index == 0:
reqnets.objects.append(objects.NetworkRequest(
network_id=network['uuid'],
tag='mynic'))
index += 1
nw_info = self.network.allocate_for_instance(self.user_context,
instance_id=inst['id'], instance_uuid=inst['uuid'],
host=inst['host'], vpn=None, rxtx_factor=3,
project_id=project_id, macs=None)
project_id=project_id, macs=None, requested_networks=reqnets)
self.assertEqual(1, len(nw_info))
vifs = objects.VirtualInterfaceList.get_all(self.context)
self.assertEqual(['mynic'], [vif.tag for vif in vifs])
fixed_ip = nw_info.fixed_ips()[0]['address']
self.assertTrue(netutils.is_valid_ipv4(fixed_ip))
self.network.deallocate_for_instance(self.context,

View File

@ -520,7 +520,9 @@ class TestNeutronv2Base(test.TestCase):
preexisting_port_ids = []
ports_in_requested_net_order = []
nets_in_requested_net_order = []
index = 0
for request in ordered_networks:
index += 1
port_req_body = {
'port': {
'device_id': self.instance.uuid,
@ -581,7 +583,8 @@ class TestNeutronv2Base(test.TestCase):
if has_portbinding:
port_req_body['port']['binding:host_id'] = (
self.instance.get('host'))
res_port = {'port': {'id': 'fake'}}
res_port = {'port': {'id': 'fake',
'mac_address': 'fakemac%i' % index}}
if kwargs.get('_break') == 'mac' + request.network_id:
self.mox.ReplayAll()
return api
@ -692,7 +695,17 @@ class TestNeutronv2Base(test.TestCase):
def _allocate_for_instance(self, net_idx=1, **kwargs):
api = self._stub_allocate_for_instance(net_idx, **kwargs)
return api.allocate_for_instance(self.context, self.instance, **kwargs)
self._vifs_created = []
def _new_vif(*args):
m = mock.MagicMock()
self._vifs_created.append(m)
return m
with mock.patch('nova.objects.VirtualInterface') as mock_vif:
mock_vif.side_effect = _new_vif
return api.allocate_for_instance(self.context, self.instance,
**kwargs)
class TestNeutronv2(TestNeutronv2Base):
@ -1018,14 +1031,23 @@ class TestNeutronv2(TestNeutronv2Base):
self._allocate_for_instance(net_idx=1,
requested_networks=requested_networks,
macs=set(['ab:cd:ef:01:23:45']))
self.assertEqual('ab:cd:ef:01:23:45', self._vifs_created[0].address)
def test_allocate_for_instance_accepts_only_portid(self):
# Make sure allocate_for_instance works when only a portid is provided
self._returned_nw_info = self.port_data1
result = self._allocate_for_instance(
requested_networks=objects.NetworkRequestList(
objects=[objects.NetworkRequest(port_id=uuids.portid_1)]))
objects=[objects.NetworkRequest(port_id=uuids.portid_1,
tag='test')]))
self.assertEqual(self.port_data1, result)
self.assertEqual(1, len(self._vifs_created))
self.assertEqual('test', self._vifs_created[0].tag)
self.assertEqual(self.instance.uuid,
self._vifs_created[0].instance_uuid)
self.assertEqual(uuids.portid_1, self._vifs_created[0].uuid)
self.assertEqual(self.port_data1[0]['mac_address'],
self._vifs_created[0].address)
@mock.patch('nova.network.neutronv2.api.API._unbind_ports')
def test_allocate_for_instance_not_enough_macs_via_ports(self,
@ -1096,8 +1118,18 @@ class TestNeutronv2(TestNeutronv2Base):
requested_networks = objects.NetworkRequestList(
objects=[objects.NetworkRequest(network_id=net['id'])
for net in (self.nets3[0], self.nets3[2], self.nets3[1])])
requested_networks[0].tag = 'foo'
self._allocate_for_instance(net_idx=2,
requested_networks=requested_networks)
self.assertEqual(3, len(self._vifs_created))
# NOTE(danms) nets3[2] is chosen above as one that won't validate,
# so we never actually run create() on the VIF.
vifs_really_created = [vif for vif in self._vifs_created
if vif.create.called]
self.assertEqual(2, len(vifs_really_created))
self.assertEqual([('foo', 'fakemac1'), (None, 'fakemac3')],
[(vif.tag, vif.address)
for vif in vifs_really_created])
def test_allocate_for_instance_with_requested_networks(self):
# specify only first and last network
@ -1200,7 +1232,8 @@ class TestNeutronv2(TestNeutronv2Base):
},
}
port_req_body['port'].update(binding_port_req_body['port'])
port = {'id': 'portid_' + network['id']}
port = {'id': 'portid_' + network['id'],
'mac_address': 'foo'}
api._populate_neutron_extension_values(self.context,
self.instance, None, binding_port_req_body, network=network,
@ -3642,7 +3675,9 @@ class TestNeutronv2WithMock(test.TestCase):
'_populate_neutron_extension_values')
@mock.patch('nova.network.neutronv2.api.API._get_available_networks')
@mock.patch('nova.network.neutronv2.api.get_client')
def test_allocate_for_instance_unbind(self, mock_ntrn,
@mock.patch('nova.objects.VirtualInterface')
def test_allocate_for_instance_unbind(self, mock_vif,
mock_ntrn,
mock_avail_nets,
mock_ext_vals,
mock_has_pbe,
@ -3656,6 +3691,7 @@ class TestNeutronv2WithMock(test.TestCase):
def show_port(port_id):
return {'port': {'network_id': 'net-1', 'id': port_id,
'mac_address': 'fakemac',
'tenant_id': 'proj-1'}}
mock_nc.show_port = show_port
@ -4491,8 +4527,9 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
@mock.patch.object(neutronapi.API, '_process_requested_networks')
@mock.patch.object(neutronapi.API, '_has_port_binding_extension')
@mock.patch.object(neutronapi, 'get_client')
@mock.patch('nova.objects.VirtualInterface')
def test_no_security_groups_requested(
self, mock_get_client, mock_has_port_binding_extension,
self, mock_vif, mock_get_client, mock_has_port_binding_extension,
mock_process_requested_networks, mock_get_available_networks,
mock_process_security_groups, mock_check_external_network_attach,
mock_populate_neutron_extension_values, mock_create_port,
@ -4520,6 +4557,7 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
mock_process_security_groups.return_value = []
api = neutronapi.API()
mock_create_port.return_value = {'id': 'foo', 'mac_address': 'bar'}
api.allocate_for_instance(
'context', instance, requested_networks=onets,
security_groups=secgroups)
@ -4550,8 +4588,9 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
@mock.patch.object(neutronapi.API, '_process_requested_networks')
@mock.patch.object(neutronapi.API, '_has_port_binding_extension')
@mock.patch.object(neutronapi, 'get_client')
@mock.patch('nova.objects.VirtualInterface')
def test_security_groups_requested(
self, mock_get_client, mock_has_port_binding_extension,
self, mock_vif, mock_get_client, mock_has_port_binding_extension,
mock_process_requested_networks, mock_get_available_networks,
mock_process_security_groups, mock_check_external_network_attach,
mock_populate_neutron_extension_values, mock_create_port,
@ -4581,6 +4620,7 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
'secgrp-uuid2']
api = neutronapi.API()
mock_create_port.return_value = {'id': 'foo', 'mac_address': 'bar'}
api.allocate_for_instance(
'context', instance, requested_networks=onets,
security_groups=secgroups)
@ -4611,8 +4651,9 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
@mock.patch.object(neutronapi.API, '_process_requested_networks')
@mock.patch.object(neutronapi.API, '_has_port_binding_extension')
@mock.patch.object(neutronapi, 'get_client')
@mock.patch('nova.objects.VirtualInterface')
def test_port_security_disabled_no_security_groups_requested(
self, mock_get_client, mock_has_port_binding_extension,
self, mock_vif, mock_get_client, mock_has_port_binding_extension,
mock_process_requested_networks, mock_get_available_networks,
mock_process_security_groups, mock_check_external_network_attach,
mock_populate_neutron_extension_values, mock_create_port,
@ -4640,6 +4681,7 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
mock_process_security_groups.return_value = []
api = neutronapi.API()
mock_create_port.return_value = {'id': 'foo', 'mac_address': 'bar'}
api.allocate_for_instance(
'context', instance, requested_networks=onets,
security_groups=secgroups)
@ -4670,8 +4712,9 @@ class TestNeutronPortSecurity(test.NoDBTestCase):
@mock.patch.object(neutronapi.API, '_process_requested_networks')
@mock.patch.object(neutronapi.API, '_has_port_binding_extension')
@mock.patch.object(neutronapi, 'get_client')
@mock.patch('nova.objects.VirtualInterface')
def test_port_security_disabled_and_security_groups_requested(
self, mock_get_client, mock_has_port_binding_extension,
self, mock_vif, mock_get_client, mock_has_port_binding_extension,
mock_process_requested_networks, mock_get_available_networks,
mock_process_security_groups, mock_check_external_network_attach,
mock_populate_neutron_extension_values, mock_create_port,
@ -4918,11 +4961,14 @@ class TestNeutronv2AutoAllocateNetwork(test.NoDBTestCase):
@mock.patch.object(self.api, '_populate_neutron_extension_values')
@mock.patch.object(self.api, '_populate_mac_address')
@mock.patch.object(self.api, '_create_port', spec=True,
return_value=uuids.port_id)
return_value={'id': uuids.port_id,
'mac_address': 'foo'})
@mock.patch.object(self.api, '_update_port_dns_name')
@mock.patch.object(self.api, 'get_instance_nw_info',
fake_get_instance_nw_info)
@mock.patch('nova.objects.VirtualInterface')
def do_test(self,
mock_vif,
update_port_dsn_name_mock,
create_port_mock,
populate_mac_addr_mock,

View File

@ -130,6 +130,12 @@ class _TestNetworkRequestObject(object):
network_id=network_request.NETWORK_ID_NONE)])
self.assertTrue(requests.no_allocate)
def test_obj_make_compatible_pre_1_2(self):
net_req = objects.NetworkRequest()
net_req.tag = 'foo'
primitive = net_req.obj_to_primitive(target_version='1.1')
self.assertNotIn('tag', primitive)
class TestNetworkRequestObject(test_objects._LocalTest,
_TestNetworkRequestObject):

View File

@ -1168,7 +1168,7 @@ object_data = {
'Network': '1.2-a977ab383aa462a479b2fae8211a5dde',
'NetworkInterfaceMetadata': '1.0-99a9574d086feb5ad45cd04a34855647',
'NetworkList': '1.2-69eca910d8fa035dfecd8ba10877ee59',
'NetworkRequest': '1.1-7a3e4ca2ce1e7b62d8400488f2f2b756',
'NetworkRequest': '1.2-af1ff2d986999fbb79377712794d82aa',
'NetworkRequestList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'PciDevice': '1.5-0d5abe5c91645b8469eb2a93fc53f932',
'PCIDeviceBus': '1.0-2b891cb77e42961044689f3dc2718995',