Skip to content

Commit c3868f0

Browse files
apdibbomarkgoddard
authored andcommitted
Cumulus NVUE support
Add support for managing Cumulus Linux 5 through NVUE Co-Authored-By: Michal Nasiadka <[email protected]> Change-Id: I2fd5ea08eab1125d857f7faeff5c0bbef80ab322 (cherry picked from commit 7d07a7dd39005dc7e980c6ede8f0a23896457822)
1 parent 2feadb0 commit c3868f0

File tree

6 files changed

+256
-1
lines changed

6 files changed

+256
-1
lines changed

doc/source/configuration.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ for a Cumulus Linux device::
184184
secret = secret
185185
ngs_mac_address = <switch mac address>
186186

187+
for a Cumulus NVUE Linux device::
188+
189+
[genericswitch:hostname-for-cumulus]
190+
device_type = netmiko_cumulus_nvue
191+
ip = <switch mgmt_ip address>
192+
username = admin
193+
password = password
194+
secret = secret
195+
ngs_mac_address = <switch mac address>
196+
187197
for the Nokia SRL series device::
188198

189199
[genericswitch:sw-hostname]

doc/source/supported-devices.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The following devices are supported by this plugin:
1111
* Cisco IOS switches
1212
* Cisco NX-OS switches (Nexus)
1313
* Cumulus Linux (via NCLU)
14+
* Cumulus Linux (via NVUE)
1415
* Dell Force10
1516
* Dell OS10
1617
* Dell PowerConnect

networking_generic_switch/devices/netmiko_devices/cumulus.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,79 @@ class Cumulus(netmiko_devices.NetmikoSwitch):
8787
re.compile(r'command not found'),
8888
re.compile(r'is not a physical interface on this switch'),
8989
]
90+
91+
92+
class CumulusNVUE(netmiko_devices.NetmikoSwitch):
93+
"""Built for Cumulus 5.x
94+
95+
Note for this switch you want config like this,
96+
where secret is the password needed for sudo su:
97+
98+
[genericswitch:<hostname>]
99+
device_type = netmiko_cumulus
100+
ip = <ip>
101+
username = <username>
102+
password = <password>
103+
secret = <password for sudo>
104+
ngs_physical_networks = physnet1
105+
ngs_max_connections = 1
106+
ngs_port_default_vlan = 123
107+
ngs_disable_inactive_ports = False
108+
"""
109+
NETMIKO_DEVICE_TYPE = "linux"
110+
111+
ADD_NETWORK = [
112+
'nv set bridge domain br_default vlan {segmentation_id}',
113+
]
114+
115+
DELETE_NETWORK = [
116+
'nv unset bridge domain br_default vlan {segmentation_id}',
117+
]
118+
119+
PLUG_PORT_TO_NETWORK = [
120+
'nv set interface {port} bridge domain br_default access '
121+
'{segmentation_id}',
122+
]
123+
124+
DELETE_PORT = [
125+
'nv unset interface {port} bridge domain br_default access',
126+
]
127+
128+
ENABLE_PORT = [
129+
'nv set interface {port} link state up',
130+
]
131+
132+
DISABLE_PORT = [
133+
'nv set interface {port} link state down',
134+
]
135+
136+
SAVE_CONFIGURATION = [
137+
'nv config save',
138+
]
139+
140+
ERROR_MSG_PATTERNS = [
141+
# Its tempting to add this error message, but as only one
142+
# bridge-access is allowed, we ignore that error for now:
143+
# re.compile(r'configuration does not have "bridge-access')
144+
re.compile(r'Invalid config'),
145+
re.compile(r'Config invalid at'),
146+
re.compile(r'ERROR: Command not found.'),
147+
re.compile(r'command not found'),
148+
re.compile(r'is not a physical interface on this switch'),
149+
re.compile(r'Error: Invalid parameter'),
150+
]
151+
152+
def send_config_set(self, net_connect, cmd_set):
153+
"""Send a set of configuration lines to the device.
154+
155+
:param net_connect: a netmiko connection object.
156+
:param cmd_set: a list of configuration lines to send.
157+
:returns: The output of the configuration commands.
158+
"""
159+
cmd_set.append('nv config apply --assume-yes')
160+
net_connect.enable()
161+
# NOTE: Do not exit config mode because save needs elevated
162+
# privileges
163+
return net_connect.send_config_set(config_commands=cmd_set,
164+
cmd_verify=False,
165+
exit_config_mode=False)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Copyright 2024 UKRI STFC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from unittest import mock
16+
17+
from networking_generic_switch.devices.netmiko_devices import cumulus
18+
from networking_generic_switch import exceptions as exc
19+
from networking_generic_switch.tests.unit.netmiko import test_netmiko_base
20+
21+
22+
class TestNetmikoCumulusNVUE(test_netmiko_base.NetmikoSwitchTestBase):
23+
24+
def _make_switch_device(self, extra_cfg={}):
25+
device_cfg = {
26+
'device_type': 'netmiko_cumulus_nvue',
27+
'ngs_port_default_vlan': '123',
28+
'ngs_disable_inactive_ports': 'True',
29+
}
30+
device_cfg.update(extra_cfg)
31+
return cumulus.CumulusNVUE(device_cfg)
32+
33+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
34+
'NetmikoSwitch.send_commands_to_device',
35+
return_value="")
36+
def test_add_network(self, mock_exec):
37+
self.switch.add_network(3333, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
38+
mock_exec.assert_called_with(
39+
['nv set bridge domain br_default vlan 3333'])
40+
41+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
42+
'NetmikoSwitch.send_commands_to_device',
43+
return_value="")
44+
def test_delete_network(self, mock_exec):
45+
self.switch.del_network(3333, '0ae071f5-5be9-43e4-80ea-e41fefe85b21')
46+
mock_exec.assert_called_with(
47+
['nv unset bridge domain br_default vlan 3333'])
48+
49+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
50+
'NetmikoSwitch.send_commands_to_device',
51+
return_value="")
52+
def test_plug_port_to_network(self, mock_exec):
53+
self.switch.plug_port_to_network(3333, 33)
54+
mock_exec.assert_called_with(
55+
['nv set interface 3333 link state up',
56+
'nv unset interface 3333 bridge domain br_default access',
57+
'nv set interface 3333 bridge domain br_default access 33'])
58+
59+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
60+
'NetmikoSwitch.send_commands_to_device')
61+
def test_plug_port_to_network_fails(self, mock_exec):
62+
mock_exec.return_value = (
63+
'ERROR: Command not found.\n\nasdf'
64+
)
65+
self.assertRaises(exc.GenericSwitchNetmikoConfigError,
66+
self.switch.plug_port_to_network, 3333, 33)
67+
68+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
69+
'NetmikoSwitch.send_commands_to_device')
70+
def test_plug_port_to_network_fails_bad_port(self, mock_exec):
71+
mock_exec.return_value = (
72+
'ERROR: asd123 is not a physical interface on this switch.'
73+
'\n\nasdf'
74+
)
75+
self.assertRaises(exc.GenericSwitchNetmikoConfigError,
76+
self.switch.plug_port_to_network, 3333, 33)
77+
78+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
79+
'NetmikoSwitch.send_commands_to_device',
80+
return_value="")
81+
def test_plug_port_simple(self, mock_exec):
82+
switch = self._make_switch_device({
83+
'ngs_disable_inactive_ports': 'false',
84+
'ngs_port_default_vlan': '',
85+
})
86+
switch.plug_port_to_network(3333, 33)
87+
mock_exec.assert_called_with(
88+
['nv set interface 3333 bridge domain br_default access 33'])
89+
90+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
91+
'NetmikoSwitch.send_commands_to_device',
92+
return_value="")
93+
def test_delete_port(self, mock_exec):
94+
self.switch.delete_port(3333, 33)
95+
mock_exec.assert_called_with(
96+
['nv unset interface 3333 bridge domain br_default access',
97+
'nv set bridge domain br_default vlan 123',
98+
'nv set interface 3333 bridge domain br_default access 123',
99+
'nv set interface 3333 link state down'])
100+
101+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
102+
'NetmikoSwitch.send_commands_to_device',
103+
return_value="")
104+
def test_delete_port_simple(self, mock_exec):
105+
switch = self._make_switch_device({
106+
'ngs_disable_inactive_ports': 'false',
107+
'ngs_port_default_vlan': '',
108+
})
109+
switch.delete_port(3333, 33)
110+
mock_exec.assert_called_with(
111+
['nv unset interface 3333 bridge domain br_default access'])
112+
113+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
114+
'NetmikoSwitch.send_commands_to_device',
115+
return_value="")
116+
def test_plug_bond_to_network(self, mock_exec):
117+
self.switch.plug_bond_to_network(3333, 33)
118+
mock_exec.assert_called_with(
119+
['nv set interface 3333 link state up',
120+
'nv unset interface 3333 bridge domain br_default access',
121+
'nv set interface 3333 bridge domain br_default access 33'])
122+
123+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
124+
'NetmikoSwitch.send_commands_to_device',
125+
return_value="")
126+
def test_plug_bond_simple(self, mock_exec):
127+
switch = self._make_switch_device({
128+
'ngs_disable_inactive_ports': 'false',
129+
'ngs_port_default_vlan': '',
130+
})
131+
switch.plug_bond_to_network(3333, 33)
132+
mock_exec.assert_called_with(
133+
['nv set interface 3333 bridge domain br_default access 33'])
134+
135+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
136+
'NetmikoSwitch.send_commands_to_device',
137+
return_value="")
138+
def test_unplug_bond_from_network(self, mock_exec):
139+
self.switch.unplug_bond_from_network(3333, 33)
140+
mock_exec.assert_called_with(
141+
['nv unset interface 3333 bridge domain br_default access',
142+
'nv set bridge domain br_default vlan 123',
143+
'nv set interface 3333 bridge domain br_default access 123',
144+
'nv set interface 3333 link state down'])
145+
146+
@mock.patch('networking_generic_switch.devices.netmiko_devices.'
147+
'NetmikoSwitch.send_commands_to_device',
148+
return_value="")
149+
def test_unplug_bond_from_network_simple(self, mock_exec):
150+
switch = self._make_switch_device({
151+
'ngs_disable_inactive_ports': 'false',
152+
'ngs_port_default_vlan': '',
153+
})
154+
switch.unplug_bond_from_network(3333, 33)
155+
mock_exec.assert_called_with(
156+
['nv unset interface 3333 bridge domain br_default access'])
157+
158+
def test_save(self):
159+
mock_connect = mock.MagicMock()
160+
mock_connect.save_config.side_effect = NotImplementedError
161+
self.switch.save_configuration(mock_connect)
162+
mock_connect.send_command.assert_called_with('nv config save')
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
Adds a new device driver, ``netmiko_cumulus_nvue``, for managing NVIDIA
5+
Cumulus based switch devices via NVUE.

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ generic_switch.devices =
4747
netmiko_juniper = networking_generic_switch.devices.netmiko_devices.juniper:Juniper
4848
netmiko_mellanox_mlnxos = networking_generic_switch.devices.netmiko_devices.mellanox_mlnxos:MellanoxMlnxOS
4949
netmiko_cumulus = networking_generic_switch.devices.netmiko_devices.cumulus:Cumulus
50+
netmiko_cumulus_nvue = networking_generic_switch.devices.netmiko_devices.cumulus:CumulusNVUE
5051
netmiko_sonic = networking_generic_switch.devices.netmiko_devices.sonic:Sonic
5152
netmiko_nokia_srl = networking_generic_switch.devices.netmiko_devices.nokia:NokiaSRL
5253
netmiko_pluribus = networking_generic_switch.devices.netmiko_devices.pluribus:Pluribus
@@ -59,4 +60,4 @@ tempest.test_plugins =
5960
quiet-level = 4
6061
# Words to ignore:
6162
# cna: Intel CNA card
62-
ignore-words-list = cna
63+
ignore-words-list = cna

0 commit comments

Comments
 (0)