Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ for a Cumulus Linux device::
secret = secret
ngs_mac_address = <switch mac address>

for a Cumulus NVUE Linux device::

[genericswitch:hostname-for-cumulus]
device_type = netmiko_cumulus_nvue
ip = <switch mgmt_ip address>
username = admin
password = password
secret = secret
ngs_mac_address = <switch mac address>

for the Nokia SRL series device::

[genericswitch:sw-hostname]
Expand Down
1 change: 1 addition & 0 deletions doc/source/supported-devices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/cumulus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<hostname>]
device_type = netmiko_cumulus
ip = <ip>
username = <username>
password = <password>
secret = <password for sudo>
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)
168 changes: 168 additions & 0 deletions networking_generic_switch/tests/unit/netmiko/test_cumulus_nvue.py
Original file line number Diff line number Diff line change
@@ -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')
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
features:
- |
Adds a new device driver, ``netmiko_cumulus_nvue``, for managing cumulus
based switch devices via NVUE.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down