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
8 changes: 8 additions & 0 deletions networking_generic_switch/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,11 @@ def plug_port_to_network(self, port_id, segmentation_id):
@abc.abstractmethod
def delete_port(self, port_id, segmentation_id):
pass

def plug_bond_to_network(self, bond_id, segmentation_id):
# Fall back to interface method.
return self.plug_port_to_network(bond_id, segmentation_id)

def unplug_bond_from_network(self, bond_id, segmentation_id):
# Fall back to interface method.
return self.delete_port(bond_id, segmentation_id)
49 changes: 49 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ class NetmikoSwitch(devices.GenericSwitchDevice):

DELETE_PORT = None

PLUG_BOND_TO_NETWORK = None

UNPLUG_BOND_FROM_NETWORK = None

ADD_NETWORK_TO_TRUNK = None

REMOVE_NETWORK_FROM_TRUNK = None
Expand All @@ -79,6 +83,10 @@ class NetmikoSwitch(devices.GenericSwitchDevice):

DISABLE_PORT = None

ENABLE_BOND = None

DISABLE_BOND = None

SAVE_CONFIGURATION = None

ERROR_MSG_PATTERNS = ()
Expand Down Expand Up @@ -284,6 +292,47 @@ def delete_port(self, port, segmentation_id):
cmds += self._format_commands(self.DISABLE_PORT, port=port)
return self.send_commands_to_device(cmds)

@check_output('plug bond')
def plug_bond_to_network(self, bond, segmentation_id):
cmds = []
if self._disable_inactive_ports() and self.ENABLE_BOND:
cmds += self._format_commands(self.ENABLE_BOND, bond=bond)
ngs_port_default_vlan = self._get_port_default_vlan()
if ngs_port_default_vlan:
cmds += self._format_commands(
self.UNPLUG_BOND_FROM_NETWORK,
bond=bond,
segmentation_id=ngs_port_default_vlan)
cmds += self._format_commands(
self.PLUG_BOND_TO_NETWORK,
bond=bond,
segmentation_id=segmentation_id)
return self.send_commands_to_device(cmds)

@check_output('unplug bond')
def unplug_bond_from_network(self, bond, segmentation_id):
cmds = self._format_commands(self.UNPLUG_BOND_FROM_NETWORK,
bond=bond,
segmentation_id=segmentation_id)
ngs_port_default_vlan = self._get_port_default_vlan()
if ngs_port_default_vlan:
# NOTE(mgoddard): Pass network_id and segmentation_id for drivers
# not yet using network_name.
network_name = self._get_network_name(ngs_port_default_vlan,
ngs_port_default_vlan)
cmds += self._format_commands(
self.ADD_NETWORK,
segmentation_id=ngs_port_default_vlan,
network_id=ngs_port_default_vlan,
network_name=network_name)
cmds += self._format_commands(
self.PLUG_BOND_TO_NETWORK,
bond=bond,
segmentation_id=ngs_port_default_vlan)
if self._disable_inactive_ports() and self.DISABLE_BOND:
cmds += self._format_commands(self.DISABLE_BOND, bond=bond)
return self.send_commands_to_device(cmds)

def send_config_set(self, net_connect, cmd_set):
"""Send a set of configuration lines to the device.

Expand Down
16 changes: 16 additions & 0 deletions networking_generic_switch/devices/netmiko_devices/cumulus.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class Cumulus(netmiko_devices.NetmikoSwitch):
'net del interface {port} bridge access {segmentation_id}',
]

PLUG_BOND_TO_NETWORK = [
'net add bond {bond} bridge access {segmentation_id}',
]

UNPLUG_BOND_FROM_NETWORK = [
'net del bond {bond} bridge access {segmentation_id}',
]

ENABLE_PORT = [
'net del interface {port} link down',
]
Expand All @@ -59,6 +67,14 @@ class Cumulus(netmiko_devices.NetmikoSwitch):
'net add interface {port} link down',
]

ENABLE_BOND = [
'net del bond {bond} link down',
]

DISABLE_BOND = [
'net add bond {bond} link down',
]

SAVE_CONFIGURATION = [
'net commit',
]
Expand Down
190 changes: 129 additions & 61 deletions networking_generic_switch/generic_switch_mech.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,14 @@ def update_port_postcommit(self, context):
'local_link_information')
if not local_link_information:
return
switch_info = local_link_information[0].get('switch_info')
switch_id = local_link_information[0].get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)
if not switch:
return
for link in local_link_information:
switch_info = link.get('switch_info')
switch_id = link.get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)
if not switch:
return
provisioning_blocks.provisioning_complete(
context._plugin_context, port['id'], resources.PORT,
GENERIC_SWITCH_ENTITY)
Expand Down Expand Up @@ -432,44 +433,90 @@ def bind_port(self, context):
"""

port = context.current
network = context.network.current
binding_profile = port['binding:profile']
local_link_information = binding_profile.get('local_link_information')

if self._is_port_supported(port) and local_link_information:
switch_info = local_link_information[0].get('switch_info')
switch_id = local_link_information[0].get('switch_id')
# NOTE(jamesdenton): If any link of the port is invalid, none
# of the links should be processed.
if not self._is_link_valid(port, network):
return

is_802_3ad = self._is_802_3ad(port)
for link in local_link_information:
port_id = link.get('port_id')
switch_info = link.get('switch_info')
switch_id = link.get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)

segments = context.segments_to_bind
# If segmentation ID is None, set vlan 1
segmentation_id = segments[0].get('segmentation_id') or 1
LOG.debug("Putting port %(port_id)s on %(switch_info)s "
"to vlan: %(segmentation_id)s",
{'port_id': 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)
LOG.info("Successfully bound port %(port_id)s in segment "
"%(segment_id)s on device %(device)s",
{'port_id': port['id'], 'device': switch_info,
'segment_id': segmentation_id})

context.set_binding(segments[0][api.ID],
portbindings.VIF_TYPE_OTHER, {})
provisioning_blocks.add_provisioning_component(
context._plugin_context, port['id'], resources.PORT,
GENERIC_SWITCH_ENTITY)

def _is_link_valid(self, port, network):
"""Return whether a link references valid switch and physnet.

If the local link information refers to a switch that is not
known to NGS or the switch is not associated with the respective
physnet, the port will not be processed and no exception will
be raised.

:param port: The port to check
:param network: the network mapped to physnet
:returns: Whether the link refers to a configured switch and/or switch
is associated with physnet
"""

binding_profile = port['binding:profile']
local_link_information = binding_profile.get('local_link_information')

for link in local_link_information:
switch_info = link.get('switch_info')
switch_id = link.get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)
if not switch:
return
network = context.network.current
LOG.error("Cannot bind port %(port)s as device %(device)s "
"is not configured. Check baremetal port link "
"configuration.",
{'port': port['id'],
'device': switch_info})
return False

physnet = network['provider:physical_network']
switch_physnets = switch._get_physical_networks()

if switch_physnets and physnet not in switch_physnets:
LOG.error("Cannot bind port %(port)s as device %(device)s is "
"not on physical network %(physnet)",
{'port_id': port['id'], 'device': switch_info,
LOG.error("Cannot bind port %(port)s as device %(device)s "
"is not on physical network %(physnet)s. Check "
"baremetal port link configuration.",
{'port': port['id'], 'device': switch_info,
'physnet': physnet})
return
port_id = local_link_information[0].get('port_id')
segments = context.segments_to_bind
# If segmentation ID is None, set vlan 1
segmentation_id = segments[0].get('segmentation_id') or 1
provisioning_blocks.add_provisioning_component(
context._plugin_context, port['id'], resources.PORT,
GENERIC_SWITCH_ENTITY)
LOG.debug("Putting port %(port_id)s on %(switch_info)s to vlan: "
"%(segmentation_id)s",
{'port_id': port_id, 'switch_info': switch_info,
'segmentation_id': segmentation_id})
# Move port to network
switch.plug_port_to_network(port_id, segmentation_id)
LOG.info("Successfully bound port %(port_id)s in segment "
"%(segment_id)s on device %(device)s",
{'port_id': port['id'], 'device': switch_info,
'segment_id': segmentation_id})
context.set_binding(segments[0][api.ID],
portbindings.VIF_TYPE_OTHER, {})
return False
return True

@staticmethod
def _is_port_supported(port):
Expand Down Expand Up @@ -498,6 +545,21 @@ def _is_port_bound(port):
vif_type = port[portbindings.VIF_TYPE]
return vif_type == portbindings.VIF_TYPE_OTHER

@staticmethod
def _is_802_3ad(port):
"""Return whether a port is using 802.3ad link aggregation.

:param port: The port to check
:returns: Whether the port is a port group using 802.3ad link
aggregation.
"""
binding_profile = port['binding:profile']
local_group_information = binding_profile.get(
'local_group_information')
if not local_group_information:
return False
return local_group_information.get('bond_mode') in ['4', '802.3ad']

def _unplug_port_from_network(self, port, network):
"""Unplug a port from a network.

Expand All @@ -512,33 +574,39 @@ def _unplug_port_from_network(self, port, network):
local_link_information = binding_profile.get('local_link_information')
if not local_link_information:
return
switch_info = local_link_information[0].get('switch_info')
switch_id = local_link_information[0].get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)
if not switch:
return
port_id = local_link_information[0].get('port_id')
# If segmentation ID is None, set vlan 1
segmentation_id = network.get('provider:segmentation_id') or 1
LOG.debug("Unplugging port %(port)s on %(switch_info)s from vlan: "
"%(segmentation_id)s",
{'port': port_id, 'switch_info': switch_info,
'segmentation_id': segmentation_id})
try:
switch.delete_port(port_id, segmentation_id)
except Exception as e:
LOG.error("Failed to unplug port %(port_id)s "
"on device: %(switch)s from network %(net_id)s "
"reason: %(exc)s",
{'port_id': port['id'], 'net_id': network['id'],
'switch': switch_info, 'exc': e})
raise e
LOG.info('Port %(port_id)s has been unplugged from network '
'%(net_id)s on device %(device)s',
{'port_id': port['id'], 'net_id': network['id'],
'device': switch_info})

is_802_3ad = self._is_802_3ad(port)
for link in local_link_information:
switch_info = link.get('switch_info')
switch_id = link.get('switch_id')
switch = device_utils.get_switch_device(
self.switches, switch_info=switch_info,
ngs_mac_address=switch_id)
if not switch:
continue
port_id = link.get('port_id')
# If segmentation ID is None, set vlan 1
segmentation_id = network.get('provider:segmentation_id') or 1
LOG.debug("Unplugging port %(port)s on %(switch_info)s from vlan: "
"%(segmentation_id)s",
{'port': port_id, 'switch_info': switch_info,
'segmentation_id': segmentation_id})
try:
if is_802_3ad and hasattr(switch, 'unplug_bond_from_network'):
switch.unplug_bond_from_network(port_id, segmentation_id)
else:
switch.delete_port(port_id, segmentation_id)
except Exception as e:
LOG.error("Failed to unplug port %(port_id)s "
"on device: %(switch)s from network %(net_id)s "
"reason: %(exc)s",
{'port_id': port['id'], 'net_id': network['id'],
'switch': switch_info, 'exc': e})
raise e
LOG.info('Port %(port_id)s has been unplugged from network '
'%(net_id)s on device %(device)s',
{'port_id': port['id'], 'net_id': network['id'],
'device': switch_info})

def _get_devices_by_physnet(self, physnet):
"""Generator yielding switches on a particular physical network.
Expand Down
45 changes: 45 additions & 0 deletions networking_generic_switch/tests/unit/netmiko/test_cumulus.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,51 @@ def test_delete_port_simple(self, mock_exec):
mock_exec.assert_called_with(
['net del interface 3333 bridge 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(
['net del bond 3333 link down',
'net del bond 3333 bridge access 123',
'net add bond 3333 bridge 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(
['net add bond 3333 bridge 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(
['net del bond 3333 bridge access 33',
'net add vlan 123',
'net add bond 3333 bridge access 123',
'net add bond 3333 link 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(
['net del bond 3333 bridge access 33'])

def test_save(self):
mock_connect = mock.MagicMock()
mock_connect.save_config.side_effect = NotImplementedError
Expand Down
Loading