From c54e9ba5d4a65a5b2b3ad8a7ae1a58e5ac017e1a Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Fri, 9 Dec 2016 14:54:15 +0200 Subject: [PATCH 1/4] Add vlan aware VMs support With this patch ngs starts supporting attaching of trunk port to baremetal server. Only VLAN Neutron network is supported. There are two ways to configure trunk port: * C1: When segmentation details for trunk ports are inherited from Neutron network. VLAN translation support is not required. Added implementation for cisco, arista, OVS on Linux. * C2: When user set segmentation details for trunk port explicitly. Switch should support VLAN translation for this case. Implement only for OVS on Linux, experimental. NetmikoSwitch.plug_port_to_network() is deprecated. New bind_port() should be used instead. New switch config option: vlan_translation_supported was introduced. This option defines if switch support vlan translation which affect the way how trunk is configured. Change-Id: If084382f4c17438e5142f51f88d6e436f8b85082 (cherry picked from commit 1163a1f062dbf53d9eb5891effb8182fee6a0c21) (cherry picked from commit 7dfe3e2f16cc17f10f07ea389298aa95548a1127) --- networking_generic_switch/devices/__init__.py | 3 +- .../devices/netmiko_devices/__init__.py | 41 +++++++++ .../devices/netmiko_devices/arista.py | 12 +++ .../devices/netmiko_devices/cisco.py | 12 +++ .../devices/netmiko_devices/dell.py | 70 ++++++++++++++++ networking_generic_switch/exceptions.py | 5 ++ .../generic_switch_mech.py | 51 ++++++++++-- .../tests/unit/netmiko/test_arista_eos.py | 39 +++++++++ .../tests/unit/netmiko/test_cisco_ios.py | 39 +++++++++ .../tests/unit/netmiko/test_dell.py | 47 +++++++++++ .../tests/unit/netmiko/test_netmiko_base.py | 83 +++++++++++++++++++ .../tests/unit/test_devices.py | 8 ++ .../tests/unit/test_generic_switch_mech.py | 6 +- 13 files changed, 409 insertions(+), 7 deletions(-) diff --git a/networking_generic_switch/devices/__init__.py b/networking_generic_switch/devices/__init__.py index 7ad5b61e..93010e29 100644 --- a/networking_generic_switch/devices/__init__.py +++ b/networking_generic_switch/devices/__init__.py @@ -42,6 +42,7 @@ {'name': 'ngs_network_name_format', 'default': '{network_id}'}, # If false, ngs will not add and delete VLANs from switches {'name': 'ngs_manage_vlans', 'default': True}, + {'name': 'vlan_translation_supported', 'default': False} ] @@ -145,7 +146,7 @@ def del_network(self, segmentation_id, network_id): pass @abc.abstractmethod - def plug_port_to_network(self, port_id, segmentation_id): + def plug_port_to_network(self, port_id, segmentation_id, trunk_details=None, vtr=False): pass @abc.abstractmethod diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index 14bd989d..185e6ffb 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -89,6 +89,10 @@ class NetmikoSwitch(devices.GenericSwitchDevice): SAVE_CONFIGURATION = None + SET_NATIVE_VLAN = None + + ALLOW_NETWORK_ON_TRUNK = None + ERROR_MSG_PATTERNS = () """Sequence of error message patterns. @@ -251,6 +255,27 @@ def del_network(self, segmentation_id, network_id): network_name=network_name) return self.send_commands_to_device(cmds) + @check_output('plug port trunk') + def plug_port_to_network_trunk(self, port, segmentation_id, trunk_details=None, vtr=False): + cmd_set = [] + vts = self.ngs_config.get('vlan_translation_supported', False) + # NOTE(vsaienko) Always use vlan translation if it is supported. + if vts: + cmd_set.extend(self.get_trunk_port_cmds_vlan_translation( + port, segmentation_id, trunk_details)) + else: + if vtr: + msg = _("Cannot bind_port VLAN aware port as switch %s " + "doesn't support VLAN translation. " + "But it is required.") % self.config['ip'] + raise exc.GenericSwitchNotSupported(error=msg) + else: + cmd_set.extend( + self.get_trunk_port_cmds_no_vlan_translation( + port, segmentation_id, trunk_details)) + + self.send_commands_to_device(cmd_set) + @check_output('plug port') def plug_port_to_network(self, port, segmentation_id): cmds = [] @@ -384,3 +409,19 @@ def check_output(self, output, operation): raise exc.GenericSwitchNetmikoConfigError( config=device_utils.sanitise_config(self.config), error=msg) + + def get_trunk_port_cmds_no_vlan_translation(self, port_id, segmentation_id, trunk_details): + cmd_set = [] + cmd_set.extend( + self._format_commands(self.SET_NATIVE_VLAN, + port=port_id, + segmentation_id=segmentation_id)) + for sub_port in trunk_details.get('sub_ports'): + cmd_set.extend( + self._format_commands( + self.ALLOW_NETWORK_ON_TRUNK, port=port_id, + segmentation_id=sub_port['segmentation_id'])) + return cmd_set + + def get_trunk_port_cmds_vlan_translation(self, port_id, segmentation_id, trunk_details): + pass diff --git a/networking_generic_switch/devices/netmiko_devices/arista.py b/networking_generic_switch/devices/netmiko_devices/arista.py index a9500ac9..c32e338b 100644 --- a/networking_generic_switch/devices/netmiko_devices/arista.py +++ b/networking_generic_switch/devices/netmiko_devices/arista.py @@ -37,3 +37,15 @@ class AristaEos(netmiko_devices.NetmikoSwitch): 'no switchport mode trunk', 'switchport trunk allowed vlan none' ) + + SET_NATIVE_VLAN = ( + 'interface {port}', + 'switchport mode trunk', + 'switchport trunk native vlan {segmentation_id}', + 'switchport trunk allowed vlan add {segmentation_id}' + ) + + ALLOW_NETWORK_ON_TRUNK = ( + 'interface {port}', + 'switchport trunk allowed vlan add {segmentation_id}' + ) diff --git a/networking_generic_switch/devices/netmiko_devices/cisco.py b/networking_generic_switch/devices/netmiko_devices/cisco.py index d0a19768..7be992e0 100644 --- a/networking_generic_switch/devices/netmiko_devices/cisco.py +++ b/networking_generic_switch/devices/netmiko_devices/cisco.py @@ -37,3 +37,15 @@ class CiscoIos(netmiko_devices.NetmikoSwitch): 'no switchport mode trunk', 'switchport trunk allowed vlan none' ) + + SET_NATIVE_VLAN = ( + 'interface {port}', + 'switchport mode trunk', + 'switchport trunk native vlan {segmentation_id}', + 'switchport trunk allowed vlan add {segmentation_id}' + ) + + ALLOW_NETWORK_ON_TRUNK = ( + 'interface {port}', + 'switchport trunk allowed vlan add {segmentation_id}' + ) diff --git a/networking_generic_switch/devices/netmiko_devices/dell.py b/networking_generic_switch/devices/netmiko_devices/dell.py index d6ab435d..2c61a602 100644 --- a/networking_generic_switch/devices/netmiko_devices/dell.py +++ b/networking_generic_switch/devices/netmiko_devices/dell.py @@ -18,6 +18,76 @@ from networking_generic_switch import exceptions as exc +class DellOS10(netmiko_devices.NetmikoSwitch): + """Netmiko device driver for Dell PowerSwitch switches.""" + + ADD_NETWORK = ( + "interface vlan {segmentation_id}", + "exit", + ) + + DELETE_NETWORK = ( + "no interface vlan {segmentation_id}", + "exit", + ) + + PLUG_PORT_TO_NETWORK = ( + "interface {port}", + "switchport mode access", + "switchport access vlan {segmentation_id}", + "exit", + ) + + DELETE_PORT = ( + "interface {port}", + "no switchport access vlan", + "exit", + ) + + ADD_NETWORK_TO_TRUNK = ( + "interface {port}", + "switchport mode trunk", + "switchport trunk allowed vlan {segmentation_id}", + "exit", + ) + + REMOVE_NETWORK_FROM_TRUNK = ( + "interface {port}", + "no switchport trunk allowed vlan {segmentation_id}", + "exit", + ) + + ENABLE_PORT = ( + "interface {port}", + "no shutdown", + "exit", + ) + + DISABLE_PORT = ( + "interface {port}", + "shutdown", + "exit", + ) + + SET_NATIVE_VLAN = ( + 'interface {port}', + 'switchport mode trunk', + 'switchport access vlan {segmentation_id}', + ) + + ALLOW_NETWORK_ON_TRUNK = ( + 'interface {port}', + 'switchport trunk allowed vlan {segmentation_id}' + ) + + ERROR_MSG_PATTERNS = () + """Sequence of error message patterns. + + Sequence of re.RegexObject objects representing patterns to check for in + device output that indicate a failure to apply configuration. + """ + + class DellNos(netmiko_devices.NetmikoSwitch): """Netmiko device driver for Dell Force10 switches.""" diff --git a/networking_generic_switch/exceptions.py b/networking_generic_switch/exceptions.py index 441d4cd7..edadfb5d 100644 --- a/networking_generic_switch/exceptions.py +++ b/networking_generic_switch/exceptions.py @@ -49,3 +49,8 @@ class GenericSwitchNetmikoConnectError(GenericSwitchException): class GenericSwitchNetmikoConfigError(GenericSwitchException): message = _("Netmiko configuration error: %(config)s, error: %(error)s") + + +class GenericSwitchNotSupported(GenericSwitchException): + message = _("Requested feature is not supported by " + "networking-generic-switch. %(error)s") diff --git a/networking_generic_switch/generic_switch_mech.py b/networking_generic_switch/generic_switch_mech.py index 43365aa0..245164a4 100644 --- a/networking_generic_switch/generic_switch_mech.py +++ b/networking_generic_switch/generic_switch_mech.py @@ -18,11 +18,13 @@ from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from neutron_lib.plugins.ml2 import api +from neutron_lib.plugins import directory from oslo_log import log as logging from networking_generic_switch import config as gsw_conf from networking_generic_switch import devices from networking_generic_switch.devices import utils as device_utils +from networking_generic_switch import exceptions as ngs_exc LOG = logging.getLogger(__name__) @@ -390,6 +392,23 @@ def delete_port_postcommit(self, context): if self._is_port_bound(port): self._unplug_port_from_network(port, context.network.current) + def _is_vlan_translation_required(self, trunk_details): + """Check if vlan translation is required to configure specific trunk. + + :returns: True if vlan translation is required, False otherwise. + """ + core_plugin = directory.get_plugin() + for sub_port in trunk_details.get('sub_ports', {}): + sp_id = sub_port['port_id'] + sp_sid = sub_port['segmentation_id'] + sp_port = core_plugin.get_port(context._plugin_context, sp_id) + sp_net = core_plugin.get_network(context._plugin_context, + sp_port['network_id']) + if sp_sid != sp_net['provider:segmentation_id']: + return True + + return False + def bind_port(self, context): """Attempt to bind a port. @@ -442,7 +461,6 @@ def bind_port(self, context): # of the links should be processed. if not self._is_link_valid(port, network): return - is_802_3ad = self._is_802_3ad(port) for link in local_link_information: port_id = link.get('port_id') @@ -455,15 +473,38 @@ def bind_port(self, context): segments = context.segments_to_bind # If segmentation ID is None, set vlan 1 segmentation_id = segments[0].get('segmentation_id') or 1 + trunk_details = port.get('trunk_details', {}) LOG.debug("Putting port %(port_id)s on %(switch_info)s " "to vlan: %(segmentation_id)s", {'port_id': port_id, 'switch_info': switch_info, 'segmentation_id': segmentation_id}) # Move port to network - if is_802_3ad and hasattr(switch, 'plug_bond_to_network'): - switch.plug_bond_to_network(port_id, segmentation_id) - else: - switch.plug_port_to_network(port_id, segmentation_id) + # START + try: + if trunk_details: + vtr = self._is_vlan_translation_required(trunk_details) + switch.plug_port_to_network_trunk( + port_id, segmentation_id, trunk_details, vtr) + elif is_802_3ad and hasattr(switch, 'plug_bond_to_network'): + switch.plug_bond_to_network(port_id, segmentation_id) + else: + switch.plug_port_to_network( + port_id, segmentation_id) + except ngs_exc.GenericSwitchNotSupported as e: + LOG.warning("Operation is not supported by " + "networking-generic-switch. %(err)s)", + {'err': e}) + raise e + except Exception as e: + LOG.error("Failed to bind port %(port_id)s in " + "segment %(segment_id)s on device " + "%(device)s due to error %(err)s", + {'port_id': port['id'], + 'device': switch_info, + 'segment_id': segmentation_id, + 'err': e}) + raise e + # END LOG.info("Successfully bound port %(port_id)s in segment " "%(segment_id)s on device %(device)s", {'port_id': port['id'], 'device': switch_info, diff --git a/networking_generic_switch/tests/unit/netmiko/test_arista_eos.py b/networking_generic_switch/tests/unit/netmiko/test_arista_eos.py index 69984376..5e17a360 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_arista_eos.py +++ b/networking_generic_switch/tests/unit/netmiko/test_arista_eos.py @@ -14,6 +14,8 @@ from unittest import mock +from neutron.plugins.ml2 import driver_context + from networking_generic_switch.devices.netmiko_devices import arista from networking_generic_switch.tests.unit.netmiko import test_netmiko_base @@ -57,6 +59,43 @@ def test_delete_port(self, mock_exec): 'no switchport mode trunk', 'switchport trunk allowed vlan none']) + def test_get_trunk_port_cmds_no_vlan_translation(self): + mock_context = mock.create_autospec(driver_context.PortContext) + self.switch.ngs_config['vlan_translation_supported'] = False + trunk_details = {'trunk_id': 'aaa-bbb-ccc-ddd', + 'sub_ports': [{'segmentation_id': 130, + 'port_id': 'aaa-bbb-ccc-ddd', + 'segmentation_type': 'vlan', + 'mac_address': u'fa:16:3e:1c:c2:7e'}]} + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': trunk_details} + mock_context.network = mock.Mock() + mock_context.network.current = {'provider:segmentation_id': 123} + mock_context.segments_to_bind = [ + { + 'segmentation_id': 777, + 'id': 123 + } + ] + res = self.switch.get_trunk_port_cmds_no_vlan_translation( + '2222', 777, trunk_details) + self.assertEqual(['interface 2222', 'switchport mode trunk', + 'switchport trunk native vlan 777', + 'switchport trunk allowed vlan add 777', + 'interface 2222', + 'switchport trunk allowed vlan add 130'], + res) + def test__format_commands(self): cmd_set = self.switch._format_commands( arista.AristaEos.ADD_NETWORK, diff --git a/networking_generic_switch/tests/unit/netmiko/test_cisco_ios.py b/networking_generic_switch/tests/unit/netmiko/test_cisco_ios.py index 22705b66..0ffdb42f 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_cisco_ios.py +++ b/networking_generic_switch/tests/unit/netmiko/test_cisco_ios.py @@ -14,6 +14,8 @@ from unittest import mock +from neutron.plugins.ml2 import driver_context + from networking_generic_switch.devices.netmiko_devices import cisco from networking_generic_switch.tests.unit.netmiko import test_netmiko_base @@ -56,6 +58,43 @@ def test_delete_port(self, mock_exec): ['interface 3333', 'no switchport access vlan 33', 'no switchport mode trunk', 'switchport trunk allowed vlan none']) + def test_get_trunk_port_cmds_no_vlan_translation(self): + mock_context = mock.create_autospec(driver_context.PortContext) + self.switch.ngs_config['vlan_translation_supported'] = True + trunk_details = {'trunk_id': 'aaa-bbb-ccc-ddd', + 'sub_ports': [{'segmentation_id': 130, + 'port_id': 'aaa-bbb-ccc-ddd', + 'segmentation_type': 'vlan', + 'mac_address': u'fa:16:3e:1c:c2:7e'}]} + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': trunk_details} + mock_context.network = mock.Mock() + mock_context.network.current = {'provider:segmentation_id': 123} + mock_context.segments_to_bind = [ + { + 'segmentation_id': 777, + 'id': 123 + } + ] + res = self.switch.get_trunk_port_cmds_no_vlan_translation( + '2222', 777, trunk_details) + self.assertEqual(['interface 2222', 'switchport mode trunk', + 'switchport trunk native vlan 777', + 'switchport trunk allowed vlan add 777', + 'interface 2222', + 'switchport trunk allowed vlan add 130'], + res) + def test__format_commands(self): cmd_set = self.switch._format_commands( cisco.CiscoIos.ADD_NETWORK, diff --git a/networking_generic_switch/tests/unit/netmiko/test_dell.py b/networking_generic_switch/tests/unit/netmiko/test_dell.py index 81f47010..40641da1 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_dell.py +++ b/networking_generic_switch/tests/unit/netmiko/test_dell.py @@ -14,11 +14,58 @@ from unittest import mock +from neutron.plugins.ml2 import driver_context + from networking_generic_switch.devices.netmiko_devices import dell from networking_generic_switch import exceptions as exc from networking_generic_switch.tests.unit.netmiko import test_netmiko_base +class TestNetmikoDellOS10(test_netmiko_base.NetmikoSwitchTestBase): + + def _make_switch_device(self, extra_cfg={}): + device_cfg = {'device_type': 'netmiko_dell_os10'} + device_cfg.update(extra_cfg) + return dell.DellOS10(device_cfg) + + + def test_get_trunk_port_cmds_no_vlan_translation(self): + mock_context = mock.create_autospec(driver_context.PortContext) + self.switch.ngs_config['vlan_translation_supported'] = True + trunk_details = {'trunk_id': 'aaa-bbb-ccc-ddd', + 'sub_ports': [{'segmentation_id': 130, + 'port_id': 'aaa-bbb-ccc-ddd', + 'segmentation_type': 'vlan', + 'mac_address': u'fa:16:3e:1c:c2:7e'}]} + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': trunk_details} + mock_context.network = mock.Mock() + mock_context.network.current = {'provider:segmentation_id': 123} + mock_context.segments_to_bind = [ + { + 'segmentation_id': 777, + 'id': 123 + } + ] + res = self.switch.get_trunk_port_cmds_no_vlan_translation( + '2222', 777, trunk_details) + self.assertEqual(['interface 2222', 'switchport mode trunk', + 'switchport access vlan 777', + 'interface 2222', + 'switchport trunk allowed vlan 130'], + res) + + class TestNetmikoDellNos(test_netmiko_base.NetmikoSwitchTestBase): def _make_switch_device(self, extra_cfg={}): diff --git a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py index 40f6a776..de6a38fb 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py +++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py @@ -15,6 +15,8 @@ import re from unittest import mock +from neutron.plugins.ml2 import driver_context + import fixtures import netmiko import netmiko.base_connection @@ -387,3 +389,84 @@ def test_check_output_error(self): "fake op. Output: %s" % output) self.assertRaisesRegex(exc.GenericSwitchNetmikoConfigError, msg, self.switch.check_output, output, 'fake op') + + @mock.patch.object(netmiko_devices.netmiko, 'ConnectHandler') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'send_commands_to_device') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_no_vlan_translation') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_vlan_translation') + def test_bind_port_trunk_no_vts(self, t_mock, nt_mock, sctd_mock, + nm_mock): + mock_context = mock.create_autospec(driver_context.PortContext) + connect_mock = mock.Mock() + nm_mock.return_value = connect_mock + self.switch.ngs_config['vlan_translation_supported'] = False + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': {'sub_ports': + [{'segmentation_id': 123}]}} + trunk_details = {'sub_ports': [{'segmentation_id': 123}]} + self.switch.plug_port_to_network_trunk('2222', None, trunk_details, vtr=False) + nt_mock.assert_called_once_with('2222', None, {'sub_ports': [{'segmentation_id': 123}]}) + self.assertFalse(t_mock.called) + + @mock.patch.object(netmiko_devices.netmiko, 'ConnectHandler') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'send_commands_to_device') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_no_vlan_translation') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_vlan_translation') + def test_bind_port_trunk_vts(self, t_mock, nt_mock, sctd_mock, + nm_mock): + mock_context = mock.create_autospec(driver_context.PortContext) + connect_mock = mock.Mock() + nm_mock.return_value = connect_mock + self.switch.ngs_config['vlan_translation_supported'] = True + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': {'sub_ports': + [{'segmentation_id': 123}]}} + trunk_details = {'sub_ports': [{'segmentation_id': 123}]} + self.switch.plug_port_to_network_trunk('2222', None, trunk_details, vtr=False) + t_mock.assert_called_once_with('2222', None, {'sub_ports': [{'segmentation_id': 123}]}) + self.assertFalse(nt_mock.called) + + @mock.patch.object(netmiko_devices.netmiko, 'ConnectHandler') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'send_commands_to_device') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_no_vlan_translation') + @mock.patch.object(netmiko_devices.NetmikoSwitch, + 'get_trunk_port_cmds_vlan_translation') + def test_bind_port_trunk_no_vts_raise(self, t_mock, nt_mock, sctd_mock, + nm_mock): + connect_mock = mock.Mock() + mock_context = mock.create_autospec(driver_context.PortContext) + nm_mock.return_value = connect_mock + self.switch.ngs_config['vlan_translation_supported'] = False + trunk_details = {'sub_ports': [{'segmentation_id': 123}]} + self.assertRaises(exc.GenericSwitchNotSupported, + self.switch.plug_port_to_network_trunk, '2222', None, trunk_details, vtr=True) + self.assertFalse(t_mock.called) + self.assertFalse(nt_mock.called) diff --git a/networking_generic_switch/tests/unit/test_devices.py b/networking_generic_switch/tests/unit/test_devices.py index 1eb8d8d4..199aa22c 100644 --- a/networking_generic_switch/tests/unit/test_devices.py +++ b/networking_generic_switch/tests/unit/test_devices.py @@ -190,3 +190,11 @@ def test__get_network_name_both(self): device = FakeDevice(device_cfg) name = device._get_network_name('fake-id', 22) self.assertEqual('fake-id_net_22', name) + + def test_driver_load_config_override(self): + device_cfg = {"device_type": 'netmiko_ovs_linux', + "vlan_translation_supported": True} + device = devices.device_manager(device_cfg) + self.assertIsInstance(device, devices.GenericSwitchDevice) + self.assertNotIn('vlan_translation_support', device.config) + self.assertTrue(device.ngs_config['vlan_translation_supported']) diff --git a/networking_generic_switch/tests/unit/test_generic_switch_mech.py b/networking_generic_switch/tests/unit/test_generic_switch_mech.py index 6c3356c9..6b1ae9f5 100644 --- a/networking_generic_switch/tests/unit/test_generic_switch_mech.py +++ b/networking_generic_switch/tests/unit/test_generic_switch_mech.py @@ -19,6 +19,7 @@ from neutron.db import provisioning_blocks from neutron.plugins.ml2 import driver_context from neutron_lib.callbacks import resources +from neutron_lib.plugins import directory from networking_generic_switch import exceptions from networking_generic_switch import generic_switch_mech as gsm @@ -512,10 +513,13 @@ def test_update_port_postcommit_complete_provisioning(self, m_pc, m_list): resources.PORT, 'GENERICSWITCH') + @mock.patch.object(gsm.GenericSwitchDriver, + '_is_vlan_translation_required', return_value=False) @mock.patch.object(provisioning_blocks, 'provisioning_complete') def test_update_portgroup_postcommit_complete_provisioning(self, m_pc, - m_list): + m_list, + m_ivtr): driver = gsm.GenericSwitchDriver() driver.initialize() mock_context = mock.create_autospec(driver_context.PortContext) From 81e4b13f2969605e74b6ff0567445134d7653bcc Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 25 Feb 2022 18:41:10 +0000 Subject: [PATCH 2/4] fixup! Add vlan aware VMs support (cherry picked from commit 10b1fbb9f8d5ecfdea496d5c7b399605f75f9ff1) --- networking_generic_switch/devices/__init__.py | 6 ++- .../generic_switch_mech.py | 11 +--- .../tests/unit/test_generic_switch_mech.py | 50 +++++++++++++++++++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/networking_generic_switch/devices/__init__.py b/networking_generic_switch/devices/__init__.py index 93010e29..1201183e 100644 --- a/networking_generic_switch/devices/__init__.py +++ b/networking_generic_switch/devices/__init__.py @@ -146,7 +146,11 @@ def del_network(self, segmentation_id, network_id): pass @abc.abstractmethod - def plug_port_to_network(self, port_id, segmentation_id, trunk_details=None, vtr=False): + def plug_port_to_network_trunk(self, port_id, segmentation_id, trunk_details=None, vtr=False): + pass + + @abc.abstractmethod + def plug_port_to_network(self, port_id, segmentation_id): pass @abc.abstractmethod diff --git a/networking_generic_switch/generic_switch_mech.py b/networking_generic_switch/generic_switch_mech.py index 245164a4..74718c85 100644 --- a/networking_generic_switch/generic_switch_mech.py +++ b/networking_generic_switch/generic_switch_mech.py @@ -397,16 +397,7 @@ def _is_vlan_translation_required(self, trunk_details): :returns: True if vlan translation is required, False otherwise. """ - core_plugin = directory.get_plugin() - for sub_port in trunk_details.get('sub_ports', {}): - sp_id = sub_port['port_id'] - sp_sid = sub_port['segmentation_id'] - sp_port = core_plugin.get_port(context._plugin_context, sp_id) - sp_net = core_plugin.get_network(context._plugin_context, - sp_port['network_id']) - if sp_sid != sp_net['provider:segmentation_id']: - return True - + # FIXME: removed for simplicity return False def bind_port(self, context): diff --git a/networking_generic_switch/tests/unit/test_generic_switch_mech.py b/networking_generic_switch/tests/unit/test_generic_switch_mech.py index 6b1ae9f5..51798569 100644 --- a/networking_generic_switch/tests/unit/test_generic_switch_mech.py +++ b/networking_generic_switch/tests/unit/test_generic_switch_mech.py @@ -787,6 +787,56 @@ def test_bind_port(self, m_apc, m_list): resources.PORT, 'GENERICSWITCH') + @mock.patch.object(gsm.GenericSwitchDriver, + '_is_vlan_translation_required', return_value=False) + @mock.patch.object(provisioning_blocks, 'add_provisioning_component') + def test_bind_port_trunk(self, m_apc, m_list, m_vlan): + driver = gsm.GenericSwitchDriver() + driver.initialize() + mock_context = mock.create_autospec(driver_context.PortContext) + mock_context._plugin_context = mock.MagicMock() + trunk_details = {'trunk_id': 'aaa-bbb-ccc-ddd', + 'sub_ports': [{'segmentation_id': 130, + 'port_id': 'aaa-bbb-ccc-ddd', + 'segmentation_type': 'vlan', + 'mac_address': u'fa:16:3e:1c:c2:7e'}]} + mock_context.network.current = { + 'provider:physical_network': 'physnet1' + } + mock_context.current = {'binding:profile': + {'local_link_information': + [ + { + 'switch_info': 'foo', + 'port_id': '2222' + } + ] + }, + 'binding:vnic_type': 'baremetal', + 'id': 'aaaa-bbbb-cccc', + 'trunk_details': trunk_details} + mock_context.network = mock.Mock() + mock_context.network.current = { + 'provider:segmentation_id': 123, + 'provider:physical_network': 'physnet1' + } + mock_context.segments_to_bind = [ + { + 'segmentation_id': 777, + 'id': 123 + } + ] + + driver.bind_port(mock_context) + self.switch_mock.plug_port_to_network_trunk.assert_called_once_with( + '2222', 777, trunk_details, False) + mock_context.set_binding.assert_called_with(123, 'other', {}) + m_apc.assert_called_once_with(mock_context._plugin_context, + mock_context.current['id'], + resources.PORT, + 'GENERICSWITCH') + + @mock.patch.object(provisioning_blocks, 'add_provisioning_component') def test_bind_portgroup(self, m_apc, m_list): driver = gsm.GenericSwitchDriver() From e57a5c5e2aa23255fba828b69da8e8534f610c0f Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 25 Feb 2022 19:35:49 +0000 Subject: [PATCH 3/4] fixup! Add vlan aware VMs support (cherry picked from commit d9409564012f932bd9ee91f11c2b8f3a51213b33) --- networking_generic_switch/devices/__init__.py | 1 - networking_generic_switch/devices/netmiko_devices/dell.py | 2 ++ networking_generic_switch/tests/unit/netmiko/test_dell.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/networking_generic_switch/devices/__init__.py b/networking_generic_switch/devices/__init__.py index 1201183e..04f4ebf3 100644 --- a/networking_generic_switch/devices/__init__.py +++ b/networking_generic_switch/devices/__init__.py @@ -145,7 +145,6 @@ def add_network(self, segmentation_id, network_id): def del_network(self, segmentation_id, network_id): pass - @abc.abstractmethod def plug_port_to_network_trunk(self, port_id, segmentation_id, trunk_details=None, vtr=False): pass diff --git a/networking_generic_switch/devices/netmiko_devices/dell.py b/networking_generic_switch/devices/netmiko_devices/dell.py index 2c61a602..066b316f 100644 --- a/networking_generic_switch/devices/netmiko_devices/dell.py +++ b/networking_generic_switch/devices/netmiko_devices/dell.py @@ -71,6 +71,8 @@ class DellOS10(netmiko_devices.NetmikoSwitch): SET_NATIVE_VLAN = ( 'interface {port}', + # Clean all the old trunked vlans by switching to access mode first + 'switchport mode access', 'switchport mode trunk', 'switchport access vlan {segmentation_id}', ) diff --git a/networking_generic_switch/tests/unit/netmiko/test_dell.py b/networking_generic_switch/tests/unit/netmiko/test_dell.py index 40641da1..5c6e8558 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_dell.py +++ b/networking_generic_switch/tests/unit/netmiko/test_dell.py @@ -59,7 +59,8 @@ def test_get_trunk_port_cmds_no_vlan_translation(self): ] res = self.switch.get_trunk_port_cmds_no_vlan_translation( '2222', 777, trunk_details) - self.assertEqual(['interface 2222', 'switchport mode trunk', + self.assertEqual(['interface 2222', 'switchport mode access', + 'switchport mode trunk', 'switchport access vlan 777', 'interface 2222', 'switchport trunk allowed vlan 130'], From 9ad464f79835da339035c044b4fbdb72eb309e9a Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Wed, 8 Feb 2023 11:11:46 +0000 Subject: [PATCH 4/4] VLAN aware VMs: pep8 fix ups Change-Id: Ia8b2e2b8ea90830fbdbc60d8a3b64afe693224db --- networking_generic_switch/devices/__init__.py | 3 ++- .../devices/netmiko_devices/__init__.py | 16 +++++++++----- .../generic_switch_mech.py | 22 +++++++++---------- .../tests/unit/netmiko/test_dell.py | 1 - .../tests/unit/netmiko/test_netmiko_base.py | 19 +++++++++------- .../tests/unit/test_generic_switch_mech.py | 2 -- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/networking_generic_switch/devices/__init__.py b/networking_generic_switch/devices/__init__.py index 04f4ebf3..c08803cd 100644 --- a/networking_generic_switch/devices/__init__.py +++ b/networking_generic_switch/devices/__init__.py @@ -145,7 +145,8 @@ def add_network(self, segmentation_id, network_id): def del_network(self, segmentation_id, network_id): pass - def plug_port_to_network_trunk(self, port_id, segmentation_id, trunk_details=None, vtr=False): + def plug_port_to_network_trunk(self, port_id, segmentation_id, + trunk_details=None, vtr=False): pass @abc.abstractmethod diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index 185e6ffb..9bf70606 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -256,7 +256,8 @@ def del_network(self, segmentation_id, network_id): return self.send_commands_to_device(cmds) @check_output('plug port trunk') - def plug_port_to_network_trunk(self, port, segmentation_id, trunk_details=None, vtr=False): + def plug_port_to_network_trunk(self, port, segmentation_id, + trunk_details=None, vtr=False): cmd_set = [] vts = self.ngs_config.get('vlan_translation_supported', False) # NOTE(vsaienko) Always use vlan translation if it is supported. @@ -265,9 +266,9 @@ def plug_port_to_network_trunk(self, port, segmentation_id, trunk_details=None, port, segmentation_id, trunk_details)) else: if vtr: - msg = _("Cannot bind_port VLAN aware port as switch %s " - "doesn't support VLAN translation. " - "But it is required.") % self.config['ip'] + msg = ("Cannot bind_port VLAN aware port as switch %s " + "doesn't support VLAN translation. " + "But it is required.") % self.config['ip'] raise exc.GenericSwitchNotSupported(error=msg) else: cmd_set.extend( @@ -410,7 +411,9 @@ def check_output(self, output, operation): config=device_utils.sanitise_config(self.config), error=msg) - def get_trunk_port_cmds_no_vlan_translation(self, port_id, segmentation_id, trunk_details): + def get_trunk_port_cmds_no_vlan_translation(self, port_id, + segmentation_id, + trunk_details): cmd_set = [] cmd_set.extend( self._format_commands(self.SET_NATIVE_VLAN, @@ -423,5 +426,6 @@ def get_trunk_port_cmds_no_vlan_translation(self, port_id, segmentation_id, trun segmentation_id=sub_port['segmentation_id'])) return cmd_set - def get_trunk_port_cmds_vlan_translation(self, port_id, segmentation_id, trunk_details): + def get_trunk_port_cmds_vlan_translation(self, port_id, segmentation_id, + trunk_details): pass diff --git a/networking_generic_switch/generic_switch_mech.py b/networking_generic_switch/generic_switch_mech.py index 74718c85..caf9ac46 100644 --- a/networking_generic_switch/generic_switch_mech.py +++ b/networking_generic_switch/generic_switch_mech.py @@ -18,7 +18,6 @@ from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from neutron_lib.plugins.ml2 import api -from neutron_lib.plugins import directory from oslo_log import log as logging from networking_generic_switch import config as gsw_conf @@ -476,25 +475,26 @@ def bind_port(self, context): vtr = self._is_vlan_translation_required(trunk_details) switch.plug_port_to_network_trunk( port_id, segmentation_id, trunk_details, vtr) - elif is_802_3ad and hasattr(switch, 'plug_bond_to_network'): + elif (is_802_3ad + and hasattr(switch, 'plug_bond_to_network')): switch.plug_bond_to_network(port_id, segmentation_id) else: switch.plug_port_to_network( port_id, segmentation_id) except ngs_exc.GenericSwitchNotSupported as e: LOG.warning("Operation is not supported by " - "networking-generic-switch. %(err)s)", + "networking-generic-switch. %(err)s)", {'err': e}) raise e except Exception as e: - LOG.error("Failed to bind port %(port_id)s in " - "segment %(segment_id)s on device " - "%(device)s due to error %(err)s", - {'port_id': port['id'], - 'device': switch_info, - 'segment_id': segmentation_id, - 'err': e}) - raise e + LOG.error("Failed to bind port %(port_id)s in " + "segment %(segment_id)s on device " + "%(device)s due to error %(err)s", + {'port_id': port['id'], + 'device': switch_info, + 'segment_id': segmentation_id, + 'err': e}) + raise e # END LOG.info("Successfully bound port %(port_id)s in segment " "%(segment_id)s on device %(device)s", diff --git a/networking_generic_switch/tests/unit/netmiko/test_dell.py b/networking_generic_switch/tests/unit/netmiko/test_dell.py index 5c6e8558..69cd32c1 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_dell.py +++ b/networking_generic_switch/tests/unit/netmiko/test_dell.py @@ -28,7 +28,6 @@ def _make_switch_device(self, extra_cfg={}): device_cfg.update(extra_cfg) return dell.DellOS10(device_cfg) - def test_get_trunk_port_cmds_no_vlan_translation(self): mock_context = mock.create_autospec(driver_context.PortContext) self.switch.ngs_config['vlan_translation_supported'] = True diff --git a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py index de6a38fb..078441b2 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py +++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py @@ -15,11 +15,10 @@ import re from unittest import mock -from neutron.plugins.ml2 import driver_context - import fixtures import netmiko import netmiko.base_connection +from neutron.plugins.ml2 import driver_context from oslo_config import fixture as config_fixture import paramiko import tenacity @@ -417,8 +416,10 @@ def test_bind_port_trunk_no_vts(self, t_mock, nt_mock, sctd_mock, 'trunk_details': {'sub_ports': [{'segmentation_id': 123}]}} trunk_details = {'sub_ports': [{'segmentation_id': 123}]} - self.switch.plug_port_to_network_trunk('2222', None, trunk_details, vtr=False) - nt_mock.assert_called_once_with('2222', None, {'sub_ports': [{'segmentation_id': 123}]}) + self.switch.plug_port_to_network_trunk('2222', None, trunk_details, + vtr=False) + nt_mock.assert_called_once_with( + '2222', None, {'sub_ports': [{'segmentation_id': 123}]}) self.assertFalse(t_mock.called) @mock.patch.object(netmiko_devices.netmiko, 'ConnectHandler') @@ -448,8 +449,10 @@ def test_bind_port_trunk_vts(self, t_mock, nt_mock, sctd_mock, 'trunk_details': {'sub_ports': [{'segmentation_id': 123}]}} trunk_details = {'sub_ports': [{'segmentation_id': 123}]} - self.switch.plug_port_to_network_trunk('2222', None, trunk_details, vtr=False) - t_mock.assert_called_once_with('2222', None, {'sub_ports': [{'segmentation_id': 123}]}) + self.switch.plug_port_to_network_trunk('2222', None, + trunk_details, vtr=False) + t_mock.assert_called_once_with( + '2222', None, {'sub_ports': [{'segmentation_id': 123}]}) self.assertFalse(nt_mock.called) @mock.patch.object(netmiko_devices.netmiko, 'ConnectHandler') @@ -462,11 +465,11 @@ def test_bind_port_trunk_vts(self, t_mock, nt_mock, sctd_mock, def test_bind_port_trunk_no_vts_raise(self, t_mock, nt_mock, sctd_mock, nm_mock): connect_mock = mock.Mock() - mock_context = mock.create_autospec(driver_context.PortContext) nm_mock.return_value = connect_mock self.switch.ngs_config['vlan_translation_supported'] = False trunk_details = {'sub_ports': [{'segmentation_id': 123}]} self.assertRaises(exc.GenericSwitchNotSupported, - self.switch.plug_port_to_network_trunk, '2222', None, trunk_details, vtr=True) + self.switch.plug_port_to_network_trunk, '2222', None, + trunk_details, vtr=True) self.assertFalse(t_mock.called) self.assertFalse(nt_mock.called) diff --git a/networking_generic_switch/tests/unit/test_generic_switch_mech.py b/networking_generic_switch/tests/unit/test_generic_switch_mech.py index 51798569..ebbdebd9 100644 --- a/networking_generic_switch/tests/unit/test_generic_switch_mech.py +++ b/networking_generic_switch/tests/unit/test_generic_switch_mech.py @@ -19,7 +19,6 @@ from neutron.db import provisioning_blocks from neutron.plugins.ml2 import driver_context from neutron_lib.callbacks import resources -from neutron_lib.plugins import directory from networking_generic_switch import exceptions from networking_generic_switch import generic_switch_mech as gsm @@ -836,7 +835,6 @@ def test_bind_port_trunk(self, m_apc, m_list, m_vlan): resources.PORT, 'GENERICSWITCH') - @mock.patch.object(provisioning_blocks, 'add_provisioning_component') def test_bind_portgroup(self, m_apc, m_list): driver = gsm.GenericSwitchDriver()