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..0682535e 100644 --- a/networking_generic_switch/devices/netmiko_devices/cumulus.py +++ b/networking_generic_switch/devices/netmiko_devices/cumulus.py @@ -87,3 +87,94 @@ 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 ' + '{segmentation_id}', + ] + + PLUG_BOND_TO_NETWORK = [ + 'nv set interface bond {bond} bridge domain br_default access ' + '{segmentation_id}', + ] + + UNPLUG_BOND_FROM_NETWORK = [ + 'nv unset interface bond {bond} bridge domain br_default access ' + '{segmentation_id}', + ] + + ENABLE_PORT = [ + 'nv set interface {port} link state up', + ] + + DISABLE_PORT = [ + 'nv set interface {port} link state down', + ] + + ENABLE_BOND = [ + 'nv set interface bond {bond} link state up', + ] + + DISABLE_BOND = [ + 'nv set interface bond {bond} 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'), + ] + + 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') + net_connect.enable() + return net_connect.send_config_set(config_commands=cmd_set, + cmd_verify=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..18f8c683 --- /dev/null +++ b/networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py @@ -0,0 +1,168 @@ +# 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 123', + '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 33', + '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 33']) + + @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 bond 3333 link state up', + 'nv unset interface bond 3333 bridge domain br_default ' + 'access 123', + 'nv set interface bond 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 bond 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 bond 3333 bridge domain br_default ' + 'access 33', + 'nv set bridge domain br_default vlan 123', + 'nv set interface bond 3333 bridge domain br_default ' + 'access 123', + 'nv set interface bond 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 bond 3333 bridge domain br_default ' + 'access 33']) + + 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..cd9ba077 --- /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 cumulus + based switch devices via NVUE. diff --git a/setup.cfg b/setup.cfg index 2328905f..2aac3765 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