Skip to content

Commit

Permalink
Implement trunk extension
Browse files Browse the repository at this point in the history
Implement the trunk service extension. A trunk can be created on any
port that is a in a direct binding hostgroup. Only one trunk per
hostgroup is possible. If the host is part of a metagroup, only VLANs
not used by the metagroup VLAN pool can be used as target, to not
interfere with VMs put on the host(s).

The trunk plugin modifies the subports by setting binding host, vnic
type and binding profile. The binding profile contains details
about the specific VLAN translation, but this is only informational, as
the "real" info will be fetched from the respective trunk/subport tables
in the DB.
  • Loading branch information
sebageek committed Oct 30, 2023
1 parent 3a08355 commit e4dd18c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 10 deletions.
10 changes: 10 additions & 0 deletions networking_ccloud/common/config/config_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,16 @@ def has_switches_as_member(self, drv_conf, switch_names):
return True
return False

def get_parent_metagroup(self, drv_conf):
"""Get metagroup for this Hostgroup, if it is part of a metagroup"""
if self.metagroup:
return None

for hg in drv_conf.hostgroups:
if any(host in hg.members for host in self.binding_hosts):
return hg
return None


class VRF(pydantic.BaseModel):
name: str
Expand Down
11 changes: 9 additions & 2 deletions networking_ccloud/db/db_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ def get_hosts_on_segments(self, context, segment_ids=None, network_ids=None, phy

hosts = net_hosts.setdefault(network_id, {})
if host not in hosts:
# FIXME: do we want to take the trunk segmentation id from the SubPort table
# or alternatively from the port's binding profile?
hosts[host] = dict(segment_id=segment_id, network_id=network_id, segmentation_id=segmentation_id,
physical_network=physnet, driver=driver, level=level,
trunk_segmentation_id=trunk_seg_id, is_bgw=False)
Expand Down Expand Up @@ -401,3 +399,12 @@ def get_subnetpool_details(self, context, subnetpool_ids):
result[snp_id]['cidrs'].append(cidr)

return result

@db_api.retry_if_session_inactive()
def get_subport_trunk_vlan_id(self, context, port_id):
query = context.session.query(trunk_models.SubPort.segmentation_id)
query = query.filter(trunk_models.SubPort.port_id == port_id)
subport = query.first()
if subport:
return subport.segmentation_id
return None
12 changes: 10 additions & 2 deletions networking_ccloud/ml2/mech_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from neutron_lib.plugins import directory
from neutron_lib.plugins.ml2 import api as ml2_api
from neutron_lib import rpc as n_rpc
from neutron_lib.services.trunk import constants as trunk_const
from oslo_config import cfg
from oslo_log import log as logging

Expand All @@ -33,6 +34,7 @@
from networking_ccloud.ml2.agent.common import messages as agent_msg
from networking_ccloud.ml2.driver_rpc_api import CCFabricDriverAPI
from networking_ccloud.ml2.plugin import FabricPlugin
from networking_ccloud.services.trunk.driver import CCTrunkDriver


LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -85,6 +87,7 @@ def initialize(self):
self._agents = {}

fabricoperations.register_api_extension()
self.trunk_driver = CCTrunkDriver.create()

LOG.info("CC-Fabric ml2 driver initialized")

Expand Down Expand Up @@ -235,11 +238,16 @@ def _bind_port_direct(self, context, binding_host, hg_config):
context.current['id'], config_physnet, context.segments_to_bind)
return

# FIXME: trunk ports
trunk_vlan = None
if context.current['device_owner'] == trunk_const.TRUNK_SUBPORT_OWNER:
if hg_config.direct_binding and not hg_config.role:
trunk_vlan = self.fabric_plugin.get_subport_trunk_vlan_id(context._plugin_context,
context.current['id'])

net_external = context.network.current[extnet_api.EXTERNAL]
self.handle_binding_host_changed(context._plugin_context, context.current['network_id'], binding_host,
hg_config, context.binding_levels[0][ml2_api.BOUND_SEGMENT], segment,
net_external=net_external)
net_external=net_external, trunk_vlan=trunk_vlan)

vif_details = {} # no vif-details needed yet
context.set_binding(segment['id'], cc_const.VIF_TYPE_CC_FABRIC, vif_details, nl_const.ACTIVE)
Expand Down
5 changes: 3 additions & 2 deletions networking_ccloud/ml2/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def allocate_and_configure_interconnects(self, context, network):
device_segment = self._plugin.type_manager.allocate_dynamic_segment(context, network_id,
segment_spec)
device_segment['is_bgw'] = device_type == cc_const.DEVICE_TYPE_BGW
device_segment['trunk_segmentation_id'] = None
self.add_segments_to_config(context, scul, {network_id: {device.host: device_segment}})

if device_type == cc_const.DEVICE_TYPE_TRANSIT:
Expand Down Expand Up @@ -173,11 +174,11 @@ def add_segments_to_config(self, context, scul, net_segments):
LOG.error("Got a port binding for binding host %s in network %s, which was not found in config",
binding_host, network_id)
continue
# FIXME: handle trunk_vlans
trunk_vlan = segment_1['trunk_segmentation_id']
# FIXME: exclude_hosts
# FIXME: direct binding hosts? are they included?
gateways = net_gateways.get(network_id)
scul.add_binding_host_to_config(hg_config, network_id, vni, vlan,
scul.add_binding_host_to_config(hg_config, network_id, vni, vlan, trunk_vlan,
gateways=gateways, is_bgw=segment_1['is_bgw'])
if gateways:
l3_net_switch_map.setdefault(network_id, set()).update(hg_config.get_switch_names(self.drv_conf))
Expand Down
25 changes: 25 additions & 0 deletions networking_ccloud/tests/unit/common/test_driver_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,31 @@ def test_duplicate_vrf_id(self):
self.assertRaisesRegex(ValueError, "VRF id 2 is duplicated on VRF SWITCH-ME",
cfix.make_global_config, cfix.make_azs(['monster-az-a']), vrfs=vrfs)

def test_get_metagroup_for_child_hostgroup(self):
# should work
gc = cfix.make_global_config(availability_zones=cfix.make_azs(["qa-de-1a", "qa-de-1b"]))
sg1 = cfix.make_switchgroup("seagull", availability_zone="qa-de-1a")
sg2 = cfix.make_switchgroup("crow", availability_zone="qa-de-1a")
hg_seagulls = cfix.make_metagroup("seagull")
hg_crows = cfix.make_hostgroups("crow")
drv_conf = config.DriverConfig(global_config=gc, switchgroups=[sg1, sg2], hostgroups=hg_seagulls + hg_crows)

# metagroups have no parent
parent_hg = drv_conf.get_hostgroup_by_host("nova-compute-seagull")
self.assertIsNotNone(parent_hg)
self.assertIsNone(parent_hg.get_parent_metagroup(drv_conf))

# children are part of their metagroup
child_hg = drv_conf.get_hostgroup_by_host("node002-seagull")
self.assertIsNotNone(child_hg)
self.assertEqual(parent_hg, child_hg.get_parent_metagroup(drv_conf))

# crow is not part of any metagroup
crow_hg = drv_conf.get_hostgroup_by_host("node003-crow")
self.assertIsNotNone(crow_hg)
self.assertTrue(crow_hg.direct_binding)
self.assertIsNone(crow_hg.get_parent_metagroup(drv_conf))


class TestDriverConfig(base.TestCase):
def setUp(self):
Expand Down
14 changes: 14 additions & 0 deletions networking_ccloud/tests/unit/db/test_db_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ def test_get_subnetpool_details(self):
}},
self._db.get_subnetpool_details(ctx, [self._subnetpool_reg['id'], self._subnetpool_az['id']]))

def test_get_subport_trunk_vlan_id(self):
ctx = context.get_admin_context()
with self.port() as trunkport, self.port() as subport:
self.assertIsNone(self._db.get_subport_trunk_vlan_id(ctx, subport['port']['id']))

with ctx.session.begin():
subport = trunk_models.SubPort(port_id=subport['port']['id'], segmentation_type='vlan',
segmentation_id=1000)
trunk = trunk_models.Trunk(name='random-trunk', port_id=trunkport['port']['id'], sub_ports=[subport])
ctx.session.add(trunk)

self.assertEqual(1000, self._db.get_subport_trunk_vlan_id(ctx, subport['port']['id']))
self.assertIsNone(self._db.get_subport_trunk_vlan_id(ctx, trunkport['port']['id']))


class TestNetworkInterconnectAllocation(test_segment.SegmentTestCase, base.PortBindingHelper, base.TestCase):
def setUp(self):
Expand Down
76 changes: 72 additions & 4 deletions networking_ccloud/tests/unit/ml2/test_mech_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from neutron.db import segments_db
from neutron.plugins.ml2 import driver_context
from neutron.plugins.ml2 import models as ml2_models
from neutron.services.trunk import models as trunk_models
from neutron.services.trunk import plugin as trunk_plugin
from neutron.tests.common import helpers as neutron_test_helpers
from neutron.tests.unit.plugins.ml2 import test_plugin

Expand All @@ -33,6 +35,7 @@
from neutron_lib import context
from neutron_lib import exceptions as nl_exc
from neutron_lib.plugins import directory
from neutron_lib.services.trunk import constants as trunk_const
from oslo_config import cfg

from networking_ccloud.common.config import _override_driver_config
Expand All @@ -55,19 +58,25 @@ def _register_azs(self):
self.agent2 = neutron_test_helpers.register_dhcp_agent(host='network-agent-b-1', az='qa-de-1b')
self.agent3 = neutron_test_helpers.register_dhcp_agent(host='network-agent-c-1', az='qa-de-1c')

def _test_bind_port(self, fake_host, fake_segments=None, network=None, subnet=None, binding_levels=None):
def _test_bind_port(self, fake_host, fake_segments=None, network=None, subnet=None, binding_levels=None,
port_extra={}, port_created_cb=None):
if network is None:
with self.network() as network:
return self._test_bind_port(fake_host, fake_segments, network, binding_levels=binding_levels)
return self._test_bind_port(fake_host, fake_segments, network, binding_levels=binding_levels,
port_extra=port_extra, port_created_cb=port_created_cb)
if subnet is None:
with self.subnet(network=network) as subnet:
return self._test_bind_port(fake_host, fake_segments, network, subnet, binding_levels)
return self._test_bind_port(fake_host, fake_segments, network, subnet, binding_levels,
port_extra=port_extra, port_created_cb=port_created_cb)

with self.port(subnet=subnet) as port:
with self.port(subnet=subnet, network=network, **port_extra) as port:
port['port']['binding:host_id'] = fake_host
if fake_segments is None:
fake_segments = [self._vxlan_segment]

if port_created_cb:
port_created_cb(port=port, subnet=subnet, network=network)

with mock.patch('neutron.plugins.ml2.driver_context.PortContext.binding_levels',
new_callable=mock.PropertyMock) as bl_mock:
bindings = ml2_models.PortBinding()
Expand Down Expand Up @@ -169,6 +178,65 @@ def test_bind_port_direct_level_1_broken_segment(self):
mock_bhc.assert_not_called()
context.set_binding.assert_not_called()

def test_bind_port_trunking_direct_level_1(self):
ctx = context.get_admin_context()

fake_segments = [{'id': 'fake-segment-id', 'physical_network': 'seagull', 'segmentation_id': 42,
'network_type': 'vlan'}]
binding_levels = [{'driver': 'cc-fabric', 'bound_segment': self._vxlan_segment}]

with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu:
def _create_trunk(port, network, **kwargs):
with ctx.session.begin():
# FIXME: right port id
subport = trunk_models.SubPort(port_id=port['port']['id'], segmentation_type='vlan',
segmentation_id=1234)
trunk_port = self._make_port(net_id=network['network']['id'], fmt="json")
trunk = trunk_models.Trunk(name='random-trunk', port_id=trunk_port['port']['id'],
sub_ports=[subport])
ctx.session.add(trunk)
p_context = self._test_bind_port(fake_host='node001-seagull',
fake_segments=fake_segments, binding_levels=binding_levels,
port_extra={'device_owner': trunk_const.TRUNK_SUBPORT_OWNER},
port_created_cb=_create_trunk)
p_context.continue_binding.assert_not_called()
mock_acu.assert_called()
p_context.set_binding.assert_called()

# check config
swcfgs = mock_acu.call_args[0][1]
self.assertEqual(2, len(swcfgs))
for swcfg in swcfgs:
self.assertEqual(agent_msg.OperationEnum.add, swcfg.operation)
self.assertTrue(swcfg.switch_name.startswith("seagull-sw"))
self.assertEqual((23, 42), (swcfg.vxlan_maps[0].vni, swcfg.vxlan_maps[0].vlan))
self.assertIsNone(swcfg.ifaces[0].native_vlan)
self.assertEqual([42], swcfg.ifaces[0].trunk_vlans)
self.assertEqual(1, len(swcfg.ifaces[0].vlan_translations))
self.assertEqual({'inside': 42, 'outside': 1234}, swcfg.ifaces[0].vlan_translations[0].dict())

def test_bind_port_trunking_without_subport_direct_level_1(self):
fake_segments = [{'id': 'fake-segment-id', 'physical_network': 'seagull', 'segmentation_id': 42,
'network_type': 'vlan'}]
binding_levels = [{'driver': 'cc-fabric', 'bound_segment': self._vxlan_segment}]
with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu:
context = self._test_bind_port(fake_host='node001-seagull',
fake_segments=fake_segments, binding_levels=binding_levels,
port_extra={'device_owner': trunk_const.TRUNK_SUBPORT_OWNER})
context.continue_binding.assert_not_called()
mock_acu.assert_called()
context.set_binding.assert_called()

# check config
swcfgs = mock_acu.call_args[0][1]
self.assertEqual(2, len(swcfgs))
for swcfg in swcfgs:
self.assertEqual(agent_msg.OperationEnum.add, swcfg.operation)
self.assertTrue(swcfg.switch_name.startswith("seagull-sw"))
self.assertEqual((23, 42), (swcfg.vxlan_maps[0].vni, swcfg.vxlan_maps[0].vlan))
self.assertEqual(42, swcfg.ifaces[0].native_vlan)
self.assertEqual([42], swcfg.ifaces[0].trunk_vlans)

def test_bind_port_hpb(self):
# only one stage bound
with mock.patch.object(CCFabricSwitchAgentRPCClient, 'apply_config_update') as mock_acu:
Expand Down

0 comments on commit e4dd18c

Please sign in to comment.