Skip to content
Merged
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
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