Skip to content

Commit

Permalink
Merge pull request #91 from stackhpc/2024.1-cherrypick
Browse files Browse the repository at this point in the history
Apply 2024.1 backports from 2023.1
  • Loading branch information
markgoddard authored Jul 5, 2024
2 parents 4d97285 + c3868f0 commit 9f36fd1
Show file tree
Hide file tree
Showing 19 changed files with 694 additions and 6 deletions.
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
5 changes: 5 additions & 0 deletions networking_generic_switch/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/arista.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
)
12 changes: 12 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/cisco.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
76 changes: 76 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,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:<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',
]

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)
13 changes: 13 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/dell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions networking_generic_switch/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
38 changes: 34 additions & 4 deletions networking_generic_switch/generic_switch_mech.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions networking_generic_switch/tests/unit/netmiko/test_arista_eos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 9f36fd1

Please sign in to comment.