diff --git a/nova/network/model.py b/nova/network/model.py index adfa0440c6b..1daa49a57e9 100644 --- a/nova/network/model.py +++ b/nova/network/model.py @@ -45,6 +45,7 @@ VIF_TYPE_BINDING_FAILED = 'binding_failed' VIF_TYPE_VIF = 'vif' VIF_TYPE_UNBOUND = 'unbound' +VIF_TYPE_TRUNK_SUBPORT = 'trunk-subport' # Constants for dictionary keys in the 'vif_details' field in the VIF @@ -411,7 +412,7 @@ def __init__(self, id=None, address=None, network=None, type=None, qbh_params=None, qbg_params=None, active=False, vnic_type=VNIC_TYPE_NORMAL, profile=None, preserve_on_delete=False, delegate_create=False, - **kwargs): + trunk_vifs=None, **kwargs): super(VIF, self).__init__() self['id'] = id @@ -429,6 +430,7 @@ def __init__(self, id=None, address=None, network=None, type=None, self['profile'] = profile self['preserve_on_delete'] = preserve_on_delete self['delegate_create'] = delegate_create + self['trunk_vifs'] = trunk_vifs or [] self._set_meta(kwargs) @@ -436,7 +438,8 @@ def __eq__(self, other): keys = ['id', 'address', 'network', 'vnic_type', 'type', 'profile', 'details', 'devname', 'ovs_interfaceid', 'qbh_params', 'qbg_params', - 'active', 'preserve_on_delete', 'delegate_create'] + 'active', 'preserve_on_delete', 'delegate_create', + 'trunk_vifs'] return all(self[k] == other[k] for k in keys) def __ne__(self, other): @@ -507,9 +510,16 @@ def get_physical_network(self): phy_network = self['details'].get(VIF_DETAILS_PHYSICAL_NETWORK) return phy_network + def add_trunk_vif(self, vif): + if any(vif['id'] == _vif['id'] for _vif in self['trunk_vifs']): + return + self['trunk_vifs'].append(vif) + @classmethod def hydrate(cls, vif): vif = cls(**vif) + vif['trunk_vifs'] = [VIF.hydrate(trunk_vif) for trunk_vif + in vif.get('trunk_vifs', [])] vif['network'] = Network.hydrate(vif['network']) return vif diff --git a/nova/network/neutron.py b/nova/network/neutron.py index fb01975fe35..7b0f71f2722 100644 --- a/nova/network/neutron.py +++ b/nova/network/neutron.py @@ -3385,13 +3385,17 @@ def _build_vif_model(self, context, client, current_neutron_port, preserve_on_delete = (current_neutron_port['id'] in preexisting_port_ids) - return network_model.VIF( + vif_type = current_neutron_port.get('binding:vif_type') + if current_neutron_port.get('device_owner') == 'trunk:subport': + vif_type = network_model.VIF_TYPE_TRUNK_SUBPORT + + vif = network_model.VIF( id=current_neutron_port['id'], address=current_neutron_port['mac_address'], network=network, vnic_type=current_neutron_port.get('binding:vnic_type', network_model.VNIC_TYPE_NORMAL), - type=current_neutron_port.get('binding:vif_type'), + type=vif_type, profile=get_binding_profile(current_neutron_port), details=current_neutron_port.get('binding:vif_details'), ovs_interfaceid=ovs_interfaceid, @@ -3400,6 +3404,8 @@ def _build_vif_model(self, context, client, current_neutron_port, preserve_on_delete=preserve_on_delete, delegate_create=True, ) + self._populate_trunk_info(context, client, vif, current_neutron_port) + return vif def _log_error_if_vnic_type_changed( self, port_id, old_vnic_type, new_vnic_type, instance @@ -3420,6 +3426,33 @@ def _log_error_if_vnic_type_changed( instance=instance ) + def _populate_trunk_info(self, context, client, vif, current_neutron_port): + sub_ports = current_neutron_port.get('trunk_details', {}).get( + 'sub_ports') or [] + if not sub_ports: + return + + port_ids = [sp['port_id'] for sp in sub_ports] + ports = client.list_ports(id=port_ids).get('ports', []) + ports_by_id = {p['id']: p for p in ports} + + net_ids = list({p['network_id'] for p in ports}) + networks = client.list_networks(id=net_ids).get('networks', []) + + seg_by_port = {sp['port_id']: sp.get('segmentation_id') + for sp in sub_ports} + + for port_id in port_ids: + port = ports_by_id.get(port_id) + if not port: + continue + subport_vif = self._build_vif_model( + context, client, port, networks, [port_id]) + subport_profile = dict(subport_vif['profile'] or {}) + subport_profile['tag'] = seg_by_port.get(port_id) + subport_vif['profile'] = subport_profile + vif.add_trunk_vif(subport_vif) + def _build_network_info_model(self, context, instance, networks=None, port_ids=None, admin_client=None, preexisting_port_ids=None, diff --git a/nova/tests/unit/network/test_network_info.py b/nova/tests/unit/network/test_network_info.py index 1c604975b02..842f8b583a3 100644 --- a/nova/tests/unit/network/test_network_info.py +++ b/nova/tests/unit/network/test_network_info.py @@ -464,6 +464,20 @@ def test_hydrate_vif_with_type(self): self.assertEqual(fake_network_cache_model.new_network(), vif['network']) + def test_hydrate_vif_with_trunk_vifs(self): + subport_vif = fake_network_cache_model.new_vif( + {'id': 'subport1', 'type': 'trunk-subport', + 'profile': {'tag': 1049}}) + parent_vif = fake_network_cache_model.new_vif( + {'id': 'parent1', 'trunk_vifs': [subport_vif]}) + + hydrated = model.VIF.hydrate(parent_vif) + + self.assertEqual(1, len(hydrated['trunk_vifs'])) + self.assertIsInstance(hydrated['trunk_vifs'][0], model.VIF) + self.assertEqual('subport1', hydrated['trunk_vifs'][0]['id']) + self.assertEqual(1049, hydrated['trunk_vifs'][0]['profile']['tag']) + class NetworkInfoTests(test.NoDBTestCase): def test_create_model(self): @@ -1081,6 +1095,31 @@ def test_get_network_metadata_json_ipv6_addr_mode_stateful(self): def test_get_network_metadata_json_ipv6_addr_mode_stateless(self): self._test_get_network_metadata_json_ipv6_addr_mode('dhcpv6-stateless') + def test_get_network_metadata_json_trunks(self): + subport_vif = fake_network_cache_model.new_vif( + {'type': 'trunk-subport', 'devname': 'tapsubport1', + 'id': 'subport1', + 'profile': {'tag': 1049}}) + parent_vif = fake_network_cache_model.new_vif( + {'type': 'ovs', 'devname': 'tapparent1', + 'id': 'parent1', + 'trunk_vifs': [subport_vif]}) + + netinfo = model.NetworkInfo([parent_vif]) + + net_metadata = netutils.get_network_metadata(netinfo) + + self.assertIn({ + 'id': 'tapsubport1', + 'vif_id': 'subport1', + 'type': 'vlan', + 'mtu': None, + 'ethernet_mac_address': 'aa:aa:aa:aa:aa:aa', + 'vlan_link': 'tapparent1', + 'vlan_id': 1049, + 'vlan_mac_address': 'aa:aa:aa:aa:aa:aa'}, + net_metadata['links']) + def test__get_nets(self): expected_net = { 'id': 'network0', diff --git a/nova/tests/unit/network/test_neutron.py b/nova/tests/unit/network/test_neutron.py index dba93521460..e4d86ef1b7e 100644 --- a/nova/tests/unit/network/test_neutron.py +++ b/nova/tests/unit/network/test_neutron.py @@ -3331,6 +3331,126 @@ def test_build_network_info_model(self, mock_get_client, mock_get_physnet.assert_has_calls([ mock.call(self.context, mocked_client, 'net-id')] * 6) + @mock.patch.object(neutronapi.API, '_get_physnet_tunneled_info', + return_value=(None, False)) + @mock.patch.object(neutronapi.API, '_get_preexisting_port_ids', + return_value=['port5']) + @mock.patch.object(neutronapi.API, '_get_subnets_from_port', + return_value=[model.Subnet(cidr='1.0.0.0/8')]) + @mock.patch.object(neutronapi.API, '_get_floating_ips_by_fixed_and_port', + return_value=[{'floating_ip_address': '10.0.0.1'}]) + @mock.patch.object(neutronapi, 'get_client') + def test_build_network_info_model_trunk( + self, mock_get_client, mock_get_floating, mock_get_subnets, + mock_get_preexisting, mock_get_physnet): + mocked_client = mock.create_autospec(client.Client) + mock_get_client.return_value = mocked_client + fake_inst = objects.Instance() + fake_inst.project_id = uuids.fake + fake_inst.uuid = uuids.instance + fake_inst.info_cache = objects.InstanceInfoCache() + fake_inst.info_cache.network_info = model.NetworkInfo() + fake_ports = [ + {'id': 'port1', + 'network_id': 'net-id', + 'admin_state_up': True, + 'status': 'ACTIVE', + 'tenant_id': uuids.fake, + 'fixed_ips': [{'ip_address': '1.1.1.1'}], + 'mac_address': 'de:ad:be:ef:00:05', + 'binding:vif_type': model.VIF_TYPE_802_QBH, + 'binding:vnic_type': model.VNIC_TYPE_MACVTAP, + constants.BINDING_PROFILE: {'pci_vendor_info': '1137:0047', + 'pci_slot': '0000:0a:00.2', + 'physical_network': 'physnet1'}, + 'binding:vif_details': {model.VIF_DETAILS_PROFILEID: 'pfid'}, + 'trunk_details': {'sub_ports': [{ + 'segmentation_id': 1049, + 'segmentation_type': 'vlan', + 'port_id': 'subport1'} + ]}, + }, + ] + fake_subport = { + 'id': 'subport1', + 'network_id': 'net-id2', + 'admin_state_up': True, + 'status': 'ACTIVE', + 'fixed_ips': [{'ip_address': '1.1.2.1'}], + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'binding:vif_type': model.VIF_TYPE_BRIDGE, + 'binding:vnic_type': model.VNIC_TYPE_NORMAL, + 'binding:vif_details': {}, + 'tenant_id': uuids.fake, + } + fake_nets = [ + {'id': 'net-id', + 'name': 'foo', + 'tenant_id': uuids.fake, + } + ] + fake_subport_net = { + 'id': 'net-id2', + 'name': 'subport-net', + 'tenant_id': uuids.fake, + } + + def list_ports(**kwargs): + if kwargs.get('id') == ['subport1']: + return {'ports': [fake_subport]} + return {'ports': fake_ports} + + mocked_client.list_ports.side_effect = list_ports + mocked_client.list_networks.return_value = { + 'networks': [fake_subport_net]} + + fake_inst.info_cache = objects.InstanceInfoCache.new( + self.context, uuids.instance) + fake_inst.info_cache.network_info = model.NetworkInfo.hydrate([]) + + nw_infos = self.api._build_network_info_model( + self.context, fake_inst, + fake_nets, + [fake_ports[0]['id']], + preexisting_port_ids=[]) + + mocked_client.list_ports.assert_any_call( + tenant_id=uuids.fake, device_id=uuids.instance) + mocked_client.list_ports.assert_any_call(id=['subport1']) + mocked_client.list_networks.assert_called_once_with(id=['net-id2']) + self.assertIn('trunk_vifs', nw_infos[0]) + self.assertEqual(1, len(nw_infos[0]['trunk_vifs'])) + subport_vif = nw_infos[0]['trunk_vifs'][0] + self.assertEqual('subport1', subport_vif['id']) + self.assertEqual(1049, subport_vif['profile']['tag']) + + @mock.patch.object(neutronapi.API, '_build_vif_model') + def test_populate_trunk_info_injects_tag(self, mock_build_vif): + parent_vif = model.VIF(id='parent-port-id') + subport_vif = model.VIF(id='subport-port-id', profile={'foo': 'bar'}) + mock_build_vif.return_value = subport_vif + + mocked_client = mock.create_autospec(client.Client) + mocked_client.list_ports.return_value = {'ports': [ + {'id': 'subport-port-id', 'network_id': 'net-id'}]} + mocked_client.list_networks.return_value = { + 'networks': [{'id': 'net-id'}]} + + current_neutron_port = { + 'trunk_details': {'sub_ports': [{ + 'port_id': 'subport-port-id', + 'segmentation_id': 1049, + 'segmentation_type': 'vlan'}]}} + + self.api._populate_trunk_info( + self.context, mocked_client, parent_vif, current_neutron_port) + + self.assertEqual(1, len(parent_vif['trunk_vifs'])) + child = parent_vif['trunk_vifs'][0] + self.assertEqual('subport-port-id', child['id']) + self.assertEqual(1049, child['profile']['tag']) + self.assertEqual('bar', child['profile']['foo']) + @mock.patch.object(neutronapi, 'get_client') @mock.patch('nova.network.neutron.API._nw_info_get_subnets') @mock.patch('nova.network.neutron.API._nw_info_get_ips') diff --git a/nova/tests/unit/test_metadata.py b/nova/tests/unit/test_metadata.py index 9369a923757..8a6611af3d8 100644 --- a/nova/tests/unit/test_metadata.py +++ b/nova/tests/unit/test_metadata.py @@ -50,6 +50,7 @@ from nova.tests.unit.api.openstack import fakes from nova.tests.unit import fake_block_device from nova.tests.unit import fake_network +from nova.tests.unit import fake_network_cache_model from nova.tests.unit import fake_requests from nova import utils from nova.virt import netutils @@ -1068,6 +1069,38 @@ def test_network_data_response(self): for k, v in nw_data.items(): self.assertEqual(nw[k], v) + def test_network_data_response_with_trunk_subport(self): + inst = self.instance.obj_clone() + + subport_vif = fake_network_cache_model.new_vif( + {'type': 'trunk-subport', 'devname': 'tapsubport', + 'id': 'subport-id', 'address': 'aa:aa:aa:aa:aa:bb', + 'profile': {'tag': 1049}}) + parent_vif = fake_network_cache_model.new_vif( + {'type': 'ovs', 'devname': 'tapparent', + 'id': 'parent-id', + 'trunk_vifs': [subport_vif]}) + nw_info = network_model.NetworkInfo([parent_vif]) + + mdinst = fake_InstanceMetadata(self, inst, network_info=nw_info) + + nwpath = "/openstack/2015-10-15/network_data.json" + nw = jsonutils.loads(mdinst.lookup(nwpath)) + + vlan_links = [link for link in nw['links'] + if link.get('type') == 'vlan'] + self.assertEqual(1, len(vlan_links)) + self.assertEqual({ + 'id': 'tapsubport', + 'vif_id': 'subport-id', + 'type': 'vlan', + 'mtu': None, + 'ethernet_mac_address': 'aa:aa:aa:aa:aa:bb', + 'vlan_link': 'tapparent', + 'vlan_id': 1049, + 'vlan_mac_address': 'aa:aa:aa:aa:aa:bb'}, + vlan_links[0]) + class MetadataHandlerTestCase(test.TestCase): """Test that metadata is returning proper values.""" diff --git a/nova/virt/netutils.py b/nova/virt/netutils.py index 2bc78134a17..962d913f6db 100644 --- a/nova/virt/netutils.py +++ b/nova/virt/netutils.py @@ -186,7 +186,13 @@ def get_network_metadata(network_info): ifc_num = -1 net_num = -1 + vifs_with_parent = [] for vif in network_info: + vifs_with_parent.append((vif, None)) + for subport in vif['trunk_vifs']: + vifs_with_parent.append((subport, vif)) + + for vif, parent_vif in vifs_with_parent: if not vif.get('network') or not vif['network'].get('subnets'): continue @@ -202,7 +208,7 @@ def get_network_metadata(network_info): # Get the VIF or physical NIC data if subnet_v4 or subnet_v6: - link = _get_eth_link(vif, ifc_num) + link = _get_eth_link(vif, ifc_num, parent_vif) links.append(link) # Add IPv4 and IPv6 networks if they exist @@ -240,7 +246,7 @@ def get_ec2_ip_info(network_info): return ip_info -def _get_eth_link(vif, ifc_num): +def _get_eth_link(vif, ifc_num, parent_vif=None): """Get a VIF or physical NIC representation. :param vif: Neutron VIF @@ -256,6 +262,8 @@ def _get_eth_link(vif, ifc_num): # Use 'phy' for physical links. Ethernet can be confusing if vif.get('type') in model.LEGACY_EXPOSED_VIF_TYPES: nic_type = vif.get('type') + elif vif.get('type') == model.VIF_TYPE_TRUNK_SUBPORT: + nic_type = 'vlan' else: nic_type = 'phy' @@ -266,6 +274,12 @@ def _get_eth_link(vif, ifc_num): 'mtu': _get_link_mtu(vif), 'ethernet_mac_address': vif.get('address'), } + + if nic_type == 'vlan': + link['vlan_link'] = parent_vif['devname'] + link['vlan_id'] = vif['profile']['tag'] + link['vlan_mac_address'] = vif['address'] + return link