From 2feadb09afc94f16e892c71be06b6ce2221112d1 Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Fri, 9 Dec 2016 14:54:15 +0200 Subject: [PATCH 1/2] 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. Co-Authored-By: Will Szumski Co-Authored-By: Mark Goddard Co-Authored-By: Seunghun Lee Change-Id: If084382f4c17438e5142f51f88d6e436f8b85082 (cherry picked from commit 1163a1f062dbf53d9eb5891effb8182fee6a0c21) (cherry picked from commit 7dfe3e2f16cc17f10f07ea389298aa95548a1127) (cherry picked from commit c54e9ba5d4a65a5b2b3ad8a7ae1a58e5ac017e1a) (cherry picked from commit 701e93f85487b372d6a4dd732a242ef4bb0a4d93) (cherry picked from commit 0196f837529fd6510ba2746425ae29e844afef09) --- networking_generic_switch/devices/__init__.py | 5 + .../devices/netmiko_devices/__init__.py | 45 ++++++++ .../devices/netmiko_devices/arista.py | 12 +++ .../devices/netmiko_devices/cisco.py | 12 +++ .../devices/netmiko_devices/dell.py | 13 +++ networking_generic_switch/exceptions.py | 5 + .../generic_switch_mech.py | 38 ++++++- .../tests/unit/netmiko/test_arista_eos.py | 39 +++++++ .../tests/unit/netmiko/test_cisco_ios.py | 39 +++++++ .../tests/unit/netmiko/test_dell.py | 39 +++++++ .../tests/unit/netmiko/test_netmiko_base.py | 86 +++++++++++++++ .../tests/unit/test_devices.py | 8 ++ .../tests/unit/test_generic_switch_mech.py | 102 +++++++++++++++++- 13 files changed, 438 insertions(+), 5 deletions(-) diff --git a/networking_generic_switch/devices/__init__.py b/networking_generic_switch/devices/__init__.py index defe2038..87e60891 100644 --- a/networking_generic_switch/devices/__init__.py +++ b/networking_generic_switch/devices/__init__.py @@ -46,6 +46,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}, # If False, ngs will skip saving configuration on devices {'name': 'ngs_save_configuration', 'default': True}, # When true try to batch up in flight switch requests @@ -187,6 +188,10 @@ 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): + pass + @abc.abstractmethod def plug_port_to_network(self, port_id, segmentation_id): pass diff --git a/networking_generic_switch/devices/netmiko_devices/__init__.py b/networking_generic_switch/devices/netmiko_devices/__init__.py index a028b293..c697ce5a 100644 --- a/networking_generic_switch/devices/netmiko_devices/__init__.py +++ b/networking_generic_switch/devices/netmiko_devices/__init__.py @@ -90,6 +90,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. @@ -276,6 +280,28 @@ 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 = [] @@ -417,3 +443,22 @@ 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 4b919763..1587c6cd 100644 --- a/networking_generic_switch/devices/netmiko_devices/cisco.py +++ b/networking_generic_switch/devices/netmiko_devices/cisco.py @@ -39,6 +39,18 @@ class CiscoIos(netmiko_devices.NetmikoSwitch): '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}' + ) + class CiscoNxOS(netmiko_devices.NetmikoSwitch): """Netmiko device driver for Cisco Nexus switches running NX-OS.""" diff --git a/networking_generic_switch/devices/netmiko_devices/dell.py b/networking_generic_switch/devices/netmiko_devices/dell.py index a5c824df..e221c5f6 100644 --- a/networking_generic_switch/devices/netmiko_devices/dell.py +++ b/networking_generic_switch/devices/netmiko_devices/dell.py @@ -70,6 +70,19 @@ class DellOS10(netmiko_devices.NetmikoSwitch): "exit", ) + 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}', + ) + + ALLOW_NETWORK_ON_TRUNK = ( + 'interface {port}', + 'switchport trunk allowed vlan {segmentation_id}' + ) + ERROR_MSG_PATTERNS = () """Sequence of error message patterns. diff --git a/networking_generic_switch/exceptions.py b/networking_generic_switch/exceptions.py index 96b57e44..12d8c5f2 100644 --- a/networking_generic_switch/exceptions.py +++ b/networking_generic_switch/exceptions.py @@ -53,3 +53,8 @@ class GenericSwitchNetmikoConfigError(GenericSwitchException): class GenericSwitchBatchError(GenericSwitchException): message = _("Batching error: %(device)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 60719181..7272e269 100644 --- a/networking_generic_switch/generic_switch_mech.py +++ b/networking_generic_switch/generic_switch_mech.py @@ -24,6 +24,7 @@ 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__) @@ -371,15 +372,36 @@ def update_port_postcommit(self, context): # If segmentation ID is None, set vlan 1 segmentation_id = network.get('provider:segmentation_id') or 1 + trunk_details = port.get('trunk_details', {}) LOG.debug("Putting switch port %(switch_port)s on " "%(switch_info)s in vlan %(segmentation_id)s", {'switch_port': 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) + 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 plug 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.info("Successfully plugged port %(port_id)s in segment " "%(segment_id)s on device %(device)s", {'port_id': port['id'], 'device': switch_info, @@ -424,6 +446,14 @@ 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. + """ + # FIXME: removed for simplicity + return False + def bind_port(self, context): """Attempt to bind a port. 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 c274a9d9..d529c9e1 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_dell.py +++ b/networking_generic_switch/tests/unit/netmiko/test_dell.py @@ -14,6 +14,8 @@ 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 @@ -257,6 +259,43 @@ def test__format_commands(self): 'no switchport trunk allowed vlan 33', 'exit']) + 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 access', + 'switchport mode trunk', + 'switchport access vlan 777', + 'interface 2222', + 'switchport trunk allowed vlan 130'], + res) + class TestNetmikoDellPowerConnect(test_netmiko_base.NetmikoSwitchTestBase): 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 b34fa436..561f8f2c 100644 --- a/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py +++ b/networking_generic_switch/tests/unit/netmiko/test_netmiko_base.py @@ -18,6 +18,7 @@ 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 @@ -425,3 +426,88 @@ 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_plug_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_plug_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_plug_port_trunk_no_vts_raise(self, t_mock, nt_mock, sctd_mock, + nm_mock): + connect_mock = mock.Mock() + 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 7587837e..482dbd1f 100644 --- a/networking_generic_switch/tests/unit/test_devices.py +++ b/networking_generic_switch/tests/unit/test_devices.py @@ -218,3 +218,11 @@ def test__get_ssh_disabled_algorithms(self): "ciphers": ["blowfish-cbc", "3des-cbc"], } self.assertEqual(expected, algos) + + 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 d655ba8f..4f77b8ff 100644 --- a/networking_generic_switch/tests/unit/test_generic_switch_mech.py +++ b/networking_generic_switch/tests/unit/test_generic_switch_mech.py @@ -527,10 +527,63 @@ 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_port_postcommit_complete_provisioning_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', + 'binding:vif_type': 'other', + 'status': 'DOWN', + 'trunk_details': trunk_details} + mock_context.original = {'binding:profile': {}, + 'binding:vnic_type': 'baremetal', + 'id': '123', + 'binding:vif_type': 'unbound'} + mock_context.network = mock.Mock() + mock_context.network.current = { + 'provider:segmentation_id': 777, + 'provider:physical_network': 'physnet1' + } + driver.update_port_postcommit(mock_context) + self.switch_mock.plug_port_to_network_trunk.assert_called_once_with( + '2222', 777, trunk_details, False) + m_apc.assert_called_once_with(mock_context._plugin_context, + mock_context.current['id'], + 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) @@ -886,6 +939,53 @@ def test_bind_port(self, m_apc, m_list): 'GENERICSWITCH') self.switch_mock.plug_port_to_network.assert_not_called() + @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:physical_network': 'physnet1' + } + mock_context.segments_to_bind = [ + { + 'segmentation_id': 777, + 'id': 123 + } + ] + + driver.bind_port(mock_context) + 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') + self.switch_mock.plug_port_to_network_trunk.assert_not_called() + @mock.patch.object(provisioning_blocks, 'add_provisioning_component') def test_bind_portgroup(self, m_apc, m_list): driver = gsm.GenericSwitchDriver() From c3868f035fa2ff83d0456934f73a99a91bb922a3 Mon Sep 17 00:00:00 2001 From: Alexander Dibbo Date: Fri, 2 Feb 2024 14:07:07 +0000 Subject: [PATCH 2/2] Cumulus NVUE support Add support for managing Cumulus Linux 5 through NVUE Co-Authored-By: Michal Nasiadka Change-Id: I2fd5ea08eab1125d857f7faeff5c0bbef80ab322 (cherry picked from commit 7d07a7dd39005dc7e980c6ede8f0a23896457822) --- doc/source/configuration.rst | 10 ++ doc/source/supported-devices.rst | 1 + .../devices/netmiko_devices/cumulus.py | 76 ++++++++ .../tests/unit/netmiko/test_cumulus_nvue.py | 162 ++++++++++++++++++ ...cumulus-nvue-support-2207d67edc12e866.yaml | 5 + setup.cfg | 3 +- 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py create mode 100644 releasenotes/notes/add-cumulus-nvue-support-2207d67edc12e866.yaml diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index b86e3b1b..aea0e2b9 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -184,6 +184,16 @@ for a Cumulus Linux device:: secret = secret ngs_mac_address = +for a Cumulus NVUE Linux device:: + + [genericswitch:hostname-for-cumulus] + device_type = netmiko_cumulus_nvue + ip = + username = admin + password = password + secret = secret + ngs_mac_address = + for the Nokia SRL series device:: [genericswitch:sw-hostname] diff --git a/doc/source/supported-devices.rst b/doc/source/supported-devices.rst index 52b1f69e..950b0dc7 100644 --- a/doc/source/supported-devices.rst +++ b/doc/source/supported-devices.rst @@ -11,6 +11,7 @@ The following devices are supported by this plugin: * Cisco IOS switches * Cisco NX-OS switches (Nexus) * Cumulus Linux (via NCLU) +* Cumulus Linux (via NVUE) * Dell Force10 * Dell OS10 * Dell PowerConnect diff --git a/networking_generic_switch/devices/netmiko_devices/cumulus.py b/networking_generic_switch/devices/netmiko_devices/cumulus.py index 9095348e..5547f489 100644 --- a/networking_generic_switch/devices/netmiko_devices/cumulus.py +++ b/networking_generic_switch/devices/netmiko_devices/cumulus.py @@ -87,3 +87,79 @@ class Cumulus(netmiko_devices.NetmikoSwitch): re.compile(r'command not found'), re.compile(r'is not a physical interface on this switch'), ] + + +class CumulusNVUE(netmiko_devices.NetmikoSwitch): + """Built for Cumulus 5.x + + Note for this switch you want config like this, + where secret is the password needed for sudo su: + + [genericswitch:] + device_type = netmiko_cumulus + ip = + username = + password = + secret = + ngs_physical_networks = physnet1 + ngs_max_connections = 1 + ngs_port_default_vlan = 123 + ngs_disable_inactive_ports = False + """ + NETMIKO_DEVICE_TYPE = "linux" + + ADD_NETWORK = [ + 'nv set bridge domain br_default vlan {segmentation_id}', + ] + + DELETE_NETWORK = [ + 'nv unset bridge domain br_default vlan {segmentation_id}', + ] + + PLUG_PORT_TO_NETWORK = [ + 'nv set interface {port} bridge domain br_default access ' + '{segmentation_id}', + ] + + DELETE_PORT = [ + 'nv unset interface {port} bridge domain br_default access', + ] + + ENABLE_PORT = [ + 'nv set interface {port} link state up', + ] + + DISABLE_PORT = [ + 'nv set interface {port} link state down', + ] + + SAVE_CONFIGURATION = [ + 'nv config save', + ] + + ERROR_MSG_PATTERNS = [ + # Its tempting to add this error message, but as only one + # bridge-access is allowed, we ignore that error for now: + # re.compile(r'configuration does not have "bridge-access') + re.compile(r'Invalid config'), + re.compile(r'Config invalid at'), + re.compile(r'ERROR: Command not found.'), + re.compile(r'command not found'), + re.compile(r'is not a physical interface on this switch'), + re.compile(r'Error: Invalid parameter'), + ] + + def send_config_set(self, net_connect, cmd_set): + """Send a set of configuration lines to the device. + + :param net_connect: a netmiko connection object. + :param cmd_set: a list of configuration lines to send. + :returns: The output of the configuration commands. + """ + cmd_set.append('nv config apply --assume-yes') + net_connect.enable() + # NOTE: Do not exit config mode because save needs elevated + # privileges + return net_connect.send_config_set(config_commands=cmd_set, + cmd_verify=False, + exit_config_mode=False) diff --git a/networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py b/networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py new file mode 100644 index 00000000..6f7bb738 --- /dev/null +++ b/networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py @@ -0,0 +1,162 @@ +# Copyright 2024 UKRI STFC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from networking_generic_switch.devices.netmiko_devices import cumulus +from networking_generic_switch import exceptions as exc +from networking_generic_switch.tests.unit.netmiko import test_netmiko_base + + +class TestNetmikoCumulusNVUE(test_netmiko_base.NetmikoSwitchTestBase): + + def _make_switch_device(self, extra_cfg={}): + device_cfg = { + 'device_type': 'netmiko_cumulus_nvue', + 'ngs_port_default_vlan': '123', + 'ngs_disable_inactive_ports': 'True', + } + device_cfg.update(extra_cfg) + return cumulus.CumulusNVUE(device_cfg) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_add_network(self, mock_exec): + self.switch.add_network(3333, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') + mock_exec.assert_called_with( + ['nv set bridge domain br_default vlan 3333']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_delete_network(self, mock_exec): + self.switch.del_network(3333, '0ae071f5-5be9-43e4-80ea-e41fefe85b21') + mock_exec.assert_called_with( + ['nv unset bridge domain br_default vlan 3333']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_plug_port_to_network(self, mock_exec): + self.switch.plug_port_to_network(3333, 33) + mock_exec.assert_called_with( + ['nv set interface 3333 link state up', + 'nv unset interface 3333 bridge domain br_default access', + 'nv set interface 3333 bridge domain br_default access 33']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device') + def test_plug_port_to_network_fails(self, mock_exec): + mock_exec.return_value = ( + 'ERROR: Command not found.\n\nasdf' + ) + self.assertRaises(exc.GenericSwitchNetmikoConfigError, + self.switch.plug_port_to_network, 3333, 33) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device') + def test_plug_port_to_network_fails_bad_port(self, mock_exec): + mock_exec.return_value = ( + 'ERROR: asd123 is not a physical interface on this switch.' + '\n\nasdf' + ) + self.assertRaises(exc.GenericSwitchNetmikoConfigError, + self.switch.plug_port_to_network, 3333, 33) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_plug_port_simple(self, mock_exec): + switch = self._make_switch_device({ + 'ngs_disable_inactive_ports': 'false', + 'ngs_port_default_vlan': '', + }) + switch.plug_port_to_network(3333, 33) + mock_exec.assert_called_with( + ['nv set interface 3333 bridge domain br_default access 33']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_delete_port(self, mock_exec): + self.switch.delete_port(3333, 33) + mock_exec.assert_called_with( + ['nv unset interface 3333 bridge domain br_default access', + 'nv set bridge domain br_default vlan 123', + 'nv set interface 3333 bridge domain br_default access 123', + 'nv set interface 3333 link state down']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_delete_port_simple(self, mock_exec): + switch = self._make_switch_device({ + 'ngs_disable_inactive_ports': 'false', + 'ngs_port_default_vlan': '', + }) + switch.delete_port(3333, 33) + mock_exec.assert_called_with( + ['nv unset interface 3333 bridge domain br_default access']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_plug_bond_to_network(self, mock_exec): + self.switch.plug_bond_to_network(3333, 33) + mock_exec.assert_called_with( + ['nv set interface 3333 link state up', + 'nv unset interface 3333 bridge domain br_default access', + 'nv set interface 3333 bridge domain br_default access 33']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_plug_bond_simple(self, mock_exec): + switch = self._make_switch_device({ + 'ngs_disable_inactive_ports': 'false', + 'ngs_port_default_vlan': '', + }) + switch.plug_bond_to_network(3333, 33) + mock_exec.assert_called_with( + ['nv set interface 3333 bridge domain br_default access 33']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_unplug_bond_from_network(self, mock_exec): + self.switch.unplug_bond_from_network(3333, 33) + mock_exec.assert_called_with( + ['nv unset interface 3333 bridge domain br_default access', + 'nv set bridge domain br_default vlan 123', + 'nv set interface 3333 bridge domain br_default access 123', + 'nv set interface 3333 link state down']) + + @mock.patch('networking_generic_switch.devices.netmiko_devices.' + 'NetmikoSwitch.send_commands_to_device', + return_value="") + def test_unplug_bond_from_network_simple(self, mock_exec): + switch = self._make_switch_device({ + 'ngs_disable_inactive_ports': 'false', + 'ngs_port_default_vlan': '', + }) + switch.unplug_bond_from_network(3333, 33) + mock_exec.assert_called_with( + ['nv unset interface 3333 bridge domain br_default access']) + + def test_save(self): + mock_connect = mock.MagicMock() + mock_connect.save_config.side_effect = NotImplementedError + self.switch.save_configuration(mock_connect) + mock_connect.send_command.assert_called_with('nv config save') diff --git a/releasenotes/notes/add-cumulus-nvue-support-2207d67edc12e866.yaml b/releasenotes/notes/add-cumulus-nvue-support-2207d67edc12e866.yaml new file mode 100644 index 00000000..2e18a37b --- /dev/null +++ b/releasenotes/notes/add-cumulus-nvue-support-2207d67edc12e866.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds a new device driver, ``netmiko_cumulus_nvue``, for managing NVIDIA + Cumulus based switch devices via NVUE. diff --git a/setup.cfg b/setup.cfg index 01d6962c..0356c921 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ generic_switch.devices = netmiko_juniper = networking_generic_switch.devices.netmiko_devices.juniper:Juniper netmiko_mellanox_mlnxos = networking_generic_switch.devices.netmiko_devices.mellanox_mlnxos:MellanoxMlnxOS netmiko_cumulus = networking_generic_switch.devices.netmiko_devices.cumulus:Cumulus + netmiko_cumulus_nvue = networking_generic_switch.devices.netmiko_devices.cumulus:CumulusNVUE netmiko_sonic = networking_generic_switch.devices.netmiko_devices.sonic:Sonic netmiko_nokia_srl = networking_generic_switch.devices.netmiko_devices.nokia:NokiaSRL netmiko_pluribus = networking_generic_switch.devices.netmiko_devices.pluribus:Pluribus @@ -59,4 +60,4 @@ tempest.test_plugins = quiet-level = 4 # Words to ignore: # cna: Intel CNA card -ignore-words-list = cna \ No newline at end of file +ignore-words-list = cna