Skip to content

Commit 9f36fd1

Browse files
authored
Merge pull request #91 from stackhpc/2024.1-cherrypick
Apply 2024.1 backports from 2023.1
2 parents 4d97285 + c3868f0 commit 9f36fd1

19 files changed

+694
-6
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/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
{'name': 'ngs_network_name_format', 'default': '{network_id}'},
4747
# If false, ngs will not add and delete VLANs from switches
4848
{'name': 'ngs_manage_vlans', 'default': True},
49+
{'name': 'vlan_translation_supported', 'default': False},
4950
# If False, ngs will skip saving configuration on devices
5051
{'name': 'ngs_save_configuration', 'default': True},
5152
# When true try to batch up in flight switch requests
@@ -187,6 +188,10 @@ def add_network(self, segmentation_id, network_id):
187188
def del_network(self, segmentation_id, network_id):
188189
pass
189190

191+
def plug_port_to_network_trunk(self, port_id, segmentation_id,
192+
trunk_details=None, vtr=False):
193+
pass
194+
190195
@abc.abstractmethod
191196
def plug_port_to_network(self, port_id, segmentation_id):
192197
pass

networking_generic_switch/devices/netmiko_devices/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ class NetmikoSwitch(devices.GenericSwitchDevice):
9090

9191
SAVE_CONFIGURATION = None
9292

93+
SET_NATIVE_VLAN = None
94+
95+
ALLOW_NETWORK_ON_TRUNK = None
96+
9397
ERROR_MSG_PATTERNS = ()
9498
"""Sequence of error message patterns.
9599
@@ -276,6 +280,28 @@ def del_network(self, segmentation_id, network_id):
276280
network_name=network_name)
277281
return self.send_commands_to_device(cmds)
278282

283+
@check_output('plug port trunk')
284+
def plug_port_to_network_trunk(self, port, segmentation_id,
285+
trunk_details=None, vtr=False):
286+
cmd_set = []
287+
vts = self.ngs_config.get('vlan_translation_supported', False)
288+
# NOTE(vsaienko) Always use vlan translation if it is supported.
289+
if vts:
290+
cmd_set.extend(self.get_trunk_port_cmds_vlan_translation(
291+
port, segmentation_id, trunk_details))
292+
else:
293+
if vtr:
294+
msg = ("Cannot bind_port VLAN aware port as switch %s "
295+
"doesn't support VLAN translation. "
296+
"But it is required.") % self.config['ip']
297+
raise exc.GenericSwitchNotSupported(error=msg)
298+
else:
299+
cmd_set.extend(
300+
self.get_trunk_port_cmds_no_vlan_translation(
301+
port, segmentation_id, trunk_details))
302+
303+
self.send_commands_to_device(cmd_set)
304+
279305
@check_output('plug port')
280306
def plug_port_to_network(self, port, segmentation_id):
281307
cmds = []
@@ -417,3 +443,22 @@ def check_output(self, output, operation):
417443
raise exc.GenericSwitchNetmikoConfigError(
418444
config=device_utils.sanitise_config(self.config),
419445
error=msg)
446+
447+
def get_trunk_port_cmds_no_vlan_translation(self, port_id,
448+
segmentation_id,
449+
trunk_details):
450+
cmd_set = []
451+
cmd_set.extend(
452+
self._format_commands(self.SET_NATIVE_VLAN,
453+
port=port_id,
454+
segmentation_id=segmentation_id))
455+
for sub_port in trunk_details.get('sub_ports'):
456+
cmd_set.extend(
457+
self._format_commands(
458+
self.ALLOW_NETWORK_ON_TRUNK, port=port_id,
459+
segmentation_id=sub_port['segmentation_id']))
460+
return cmd_set
461+
462+
def get_trunk_port_cmds_vlan_translation(self, port_id, segmentation_id,
463+
trunk_details):
464+
pass

networking_generic_switch/devices/netmiko_devices/arista.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ class AristaEos(netmiko_devices.NetmikoSwitch):
3737
'no switchport mode trunk',
3838
'switchport trunk allowed vlan none'
3939
)
40+
41+
SET_NATIVE_VLAN = (
42+
'interface {port}',
43+
'switchport mode trunk',
44+
'switchport trunk native vlan {segmentation_id}',
45+
'switchport trunk allowed vlan add {segmentation_id}'
46+
)
47+
48+
ALLOW_NETWORK_ON_TRUNK = (
49+
'interface {port}',
50+
'switchport trunk allowed vlan add {segmentation_id}'
51+
)

networking_generic_switch/devices/netmiko_devices/cisco.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ class CiscoIos(netmiko_devices.NetmikoSwitch):
3939
'switchport trunk allowed vlan none'
4040
)
4141

42+
SET_NATIVE_VLAN = (
43+
'interface {port}',
44+
'switchport mode trunk',
45+
'switchport trunk native vlan {segmentation_id}',
46+
'switchport trunk allowed vlan add {segmentation_id}'
47+
)
48+
49+
ALLOW_NETWORK_ON_TRUNK = (
50+
'interface {port}',
51+
'switchport trunk allowed vlan add {segmentation_id}'
52+
)
53+
4254

4355
class CiscoNxOS(netmiko_devices.NetmikoSwitch):
4456
"""Netmiko device driver for Cisco Nexus switches running NX-OS."""

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)

networking_generic_switch/devices/netmiko_devices/dell.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ class DellOS10(netmiko_devices.NetmikoSwitch):
7070
"exit",
7171
)
7272

73+
SET_NATIVE_VLAN = (
74+
'interface {port}',
75+
# Clean all the old trunked vlans by switching to access mode first
76+
'switchport mode access',
77+
'switchport mode trunk',
78+
'switchport access vlan {segmentation_id}',
79+
)
80+
81+
ALLOW_NETWORK_ON_TRUNK = (
82+
'interface {port}',
83+
'switchport trunk allowed vlan {segmentation_id}'
84+
)
85+
7386
ERROR_MSG_PATTERNS = ()
7487
"""Sequence of error message patterns.
7588

networking_generic_switch/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,8 @@ class GenericSwitchNetmikoConfigError(GenericSwitchException):
5353

5454
class GenericSwitchBatchError(GenericSwitchException):
5555
message = _("Batching error: %(device)s, error: %(error)s")
56+
57+
58+
class GenericSwitchNotSupported(GenericSwitchException):
59+
message = _("Requested feature is not supported by "
60+
"networking-generic-switch. %(error)s")

networking_generic_switch/generic_switch_mech.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from networking_generic_switch import config as gsw_conf
2525
from networking_generic_switch import devices
2626
from networking_generic_switch.devices import utils as device_utils
27+
from networking_generic_switch import exceptions as ngs_exc
2728

2829
LOG = logging.getLogger(__name__)
2930

@@ -371,15 +372,36 @@ def update_port_postcommit(self, context):
371372

372373
# If segmentation ID is None, set vlan 1
373374
segmentation_id = network.get('provider:segmentation_id') or 1
375+
trunk_details = port.get('trunk_details', {})
374376
LOG.debug("Putting switch port %(switch_port)s on "
375377
"%(switch_info)s in vlan %(segmentation_id)s",
376378
{'switch_port': port_id, 'switch_info': switch_info,
377379
'segmentation_id': segmentation_id})
378380
# Move port to network
379-
if is_802_3ad and hasattr(switch, 'plug_bond_to_network'):
380-
switch.plug_bond_to_network(port_id, segmentation_id)
381-
else:
382-
switch.plug_port_to_network(port_id, segmentation_id)
381+
try:
382+
if trunk_details:
383+
vtr = self._is_vlan_translation_required(trunk_details)
384+
switch.plug_port_to_network_trunk(
385+
port_id, segmentation_id, trunk_details, vtr)
386+
elif (is_802_3ad
387+
and hasattr(switch, 'plug_bond_to_network')):
388+
switch.plug_bond_to_network(port_id, segmentation_id)
389+
else:
390+
switch.plug_port_to_network(port_id, segmentation_id)
391+
except ngs_exc.GenericSwitchNotSupported as e:
392+
LOG.warning("Operation is not supported by "
393+
"networking-generic-switch. %(err)s)",
394+
{'err': e})
395+
raise e
396+
except Exception as e:
397+
LOG.error("Failed to plug port %(port_id)s in "
398+
"segment %(segment_id)s on device "
399+
"%(device)s due to error %(err)s",
400+
{'port_id': port['id'],
401+
'device': switch_info,
402+
'segment_id': segmentation_id,
403+
'err': e})
404+
raise e
383405
LOG.info("Successfully plugged port %(port_id)s in segment "
384406
"%(segment_id)s on device %(device)s",
385407
{'port_id': port['id'], 'device': switch_info,
@@ -424,6 +446,14 @@ def delete_port_postcommit(self, context):
424446
if self._is_port_bound(port):
425447
self._unplug_port_from_network(port, context.network.current)
426448

449+
def _is_vlan_translation_required(self, trunk_details):
450+
"""Check if vlan translation is required to configure specific trunk.
451+
452+
:returns: True if vlan translation is required, False otherwise.
453+
"""
454+
# FIXME: removed for simplicity
455+
return False
456+
427457
def bind_port(self, context):
428458
"""Attempt to bind a port.
429459

networking_generic_switch/tests/unit/netmiko/test_arista_eos.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from unittest import mock
1616

17+
from neutron.plugins.ml2 import driver_context
18+
1719
from networking_generic_switch.devices.netmiko_devices import arista
1820
from networking_generic_switch.tests.unit.netmiko import test_netmiko_base
1921

@@ -57,6 +59,43 @@ def test_delete_port(self, mock_exec):
5759
'no switchport mode trunk',
5860
'switchport trunk allowed vlan none'])
5961

62+
def test_get_trunk_port_cmds_no_vlan_translation(self):
63+
mock_context = mock.create_autospec(driver_context.PortContext)
64+
self.switch.ngs_config['vlan_translation_supported'] = False
65+
trunk_details = {'trunk_id': 'aaa-bbb-ccc-ddd',
66+
'sub_ports': [{'segmentation_id': 130,
67+
'port_id': 'aaa-bbb-ccc-ddd',
68+
'segmentation_type': 'vlan',
69+
'mac_address': u'fa:16:3e:1c:c2:7e'}]}
70+
mock_context.current = {'binding:profile':
71+
{'local_link_information':
72+
[
73+
{
74+
'switch_info': 'foo',
75+
'port_id': '2222'
76+
}
77+
]
78+
},
79+
'binding:vnic_type': 'baremetal',
80+
'id': 'aaaa-bbbb-cccc',
81+
'trunk_details': trunk_details}
82+
mock_context.network = mock.Mock()
83+
mock_context.network.current = {'provider:segmentation_id': 123}
84+
mock_context.segments_to_bind = [
85+
{
86+
'segmentation_id': 777,
87+
'id': 123
88+
}
89+
]
90+
res = self.switch.get_trunk_port_cmds_no_vlan_translation(
91+
'2222', 777, trunk_details)
92+
self.assertEqual(['interface 2222', 'switchport mode trunk',
93+
'switchport trunk native vlan 777',
94+
'switchport trunk allowed vlan add 777',
95+
'interface 2222',
96+
'switchport trunk allowed vlan add 130'],
97+
res)
98+
6099
def test__format_commands(self):
61100
cmd_set = self.switch._format_commands(
62101
arista.AristaEos.ADD_NETWORK,

0 commit comments

Comments
 (0)