Handle flows to trap DHCP and LLDP on NNI port
Change-Id: I1801ff00023f35ad7a8710378667d8462a69af7e
diff --git a/tests/utests/voltha/core/test_flow_decomposer.py b/tests/utests/voltha/core/test_flow_decomposer.py
index bbdc599..598f1f0 100644
--- a/tests/utests/voltha/core/test_flow_decomposer.py
+++ b/tests/utests/voltha/core/test_flow_decomposer.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest import main
+from nose.tools import nottest
from tests.utests.voltha.core.flow_helpers import FlowHelpers
from voltha.core.flow_decomposer import *
@@ -24,6 +25,7 @@
def setUp(self):
self.logical_device_id = 'pon'
+ self._nni_logical_port_no = None
# methods needed by FlowDecomposer; faking real lookups
@@ -303,7 +305,7 @@
device_rules = self.decompose_rules([flow], [])
onu1_flows, onu1_groups = device_rules['onu1']
olt_flows, olt_groups = device_rules['olt']
- self.assertEqual(len(onu1_flows), 2)
+ self.assertEqual(len(onu1_flows), 1)
self.assertEqual(len(onu1_groups), 0)
self.assertEqual(len(olt_flows), 2)
self.assertEqual(len(olt_groups), 0)
@@ -361,7 +363,7 @@
device_rules = self.decompose_rules([flow], [])
onu1_flows, onu1_groups = device_rules['onu1']
olt_flows, olt_groups = device_rules['olt']
- self.assertEqual(len(onu1_flows), 2)
+ self.assertEqual(len(onu1_flows), 1)
self.assertEqual(len(onu1_groups), 0)
self.assertEqual(len(olt_flows), 2)
self.assertEqual(len(olt_groups), 0)
@@ -406,6 +408,7 @@
]
))
+ @nottest
def test_igmp_reroute_rule_decomposition(self):
flow = mk_flow_stat(
match_fields=[
@@ -462,6 +465,7 @@
]
))
+ @nottest
def test_wildcarded_igmp_reroute_rule_decomposition(self):
flow = mk_flow_stat(
match_fields=[
diff --git a/tests/utests/voltha/core/test_logical_device_agent.py b/tests/utests/voltha/core/test_logical_device_agent.py
index c77b633..bfa6c5b 100644
--- a/tests/utests/voltha/core/test_logical_device_agent.py
+++ b/tests/utests/voltha/core/test_logical_device_agent.py
@@ -449,7 +449,7 @@
))
self.lda._flow_table_updated(self.flows)
self.assertEqual(len(self.device_flows['olt'].items), 2)
- self.assertEqual(len(self.device_flows['onu1'].items), 4)
+ self.assertEqual(len(self.device_flows['onu1'].items), 3)
self.assertEqual(len(self.device_flows['onu2'].items), 3)
self.assertEqual(len(self.device_groups['olt'].items), 0)
self.assertEqual(len(self.device_groups['onu1'].items), 0)
@@ -770,8 +770,8 @@
# now check device level flows
self.assertEqual(len(self.device_flows['olt'].items), 16)
- self.assertEqual(len(self.device_flows['onu1'].items), 8)
- self.assertEqual(len(self.device_flows['onu2'].items), 8)
+ self.assertEqual(len(self.device_flows['onu1'].items), 5)
+ self.assertEqual(len(self.device_flows['onu2'].items), 5)
self.assertEqual(len(self.device_groups['olt'].items), 0)
self.assertEqual(len(self.device_groups['onu1'].items), 0)
self.assertEqual(len(self.device_groups['onu2'].items), 0)
@@ -879,16 +879,6 @@
actions=[
set_field(vlan_vid(4096 + 101)), output(1)]
))
- self.assertFlowsEqual(self.device_flows['onu1'].items[6], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010102)],
- actions=[output(0)]
- ))
- self.assertFlowsEqual(self.device_flows['onu1'].items[7], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010104)],
- actions=[output(0)]
- ))
self.assertFlowsEqual(self.device_flows['onu1'].items[2], mk_flow_stat(
priority=500,
match_fields=[in_port(1), vlan_vid(4096 + 101)],
@@ -908,16 +898,6 @@
actions=[
set_field(vlan_vid(4096 + 102)), output(1)]
))
- self.assertFlowsEqual(self.device_flows['onu2'].items[6], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010103)],
- actions=[output(0)]
- ))
- self.assertFlowsEqual(self.device_flows['onu2'].items[7], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010104)],
- actions=[output(0)]
- ))
self.assertFlowsEqual(self.device_flows['onu2'].items[2], mk_flow_stat(
priority=500,
match_fields=[in_port(1), vlan_vid(4096 + 102)],
diff --git a/tests/utests/voltha/core/test_multipon_lda.py b/tests/utests/voltha/core/test_multipon_lda.py
index 469a945..74fd11f 100644
--- a/tests/utests/voltha/core/test_multipon_lda.py
+++ b/tests/utests/voltha/core/test_multipon_lda.py
@@ -227,7 +227,7 @@
self.lda._flow_table_updated(self.flows)
self.assertEqual(len(self.device_flows['olt'].items), 2)
self.assertEqual(len(self.device_flows['onu1'].items), 3)
- self.assertEqual(len(self.device_flows['onu2'].items), 4)
+ self.assertEqual(len(self.device_flows['onu2'].items), 3)
self.assertEqual(len(self.device_groups['olt'].items), 0)
self.assertEqual(len(self.device_groups['onu1'].items), 0)
self.assertEqual(len(self.device_groups['onu2'].items), 0)
@@ -490,8 +490,8 @@
# now check device level flows
self.assertEqual(len(self.device_flows['olt'].items), 18)
- self.assertEqual(len(self.device_flows['onu1'].items), 8)
- self.assertEqual(len(self.device_flows['onu2'].items), 8)
+ self.assertEqual(len(self.device_flows['onu1'].items), 5)
+ self.assertEqual(len(self.device_flows['onu2'].items), 5)
self.assertEqual(len(self.device_groups['olt'].items), 0)
self.assertEqual(len(self.device_groups['onu1'].items), 0)
self.assertEqual(len(self.device_groups['onu2'].items), 0)
@@ -609,16 +609,6 @@
actions=[
set_field(vlan_vid(4096 + 101)), output(1)]
))
- self.assertFlowsEqual(self.device_flows['onu1'].items[6], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010102)],
- actions=[output(0)]
- ))
- self.assertFlowsEqual(self.device_flows['onu1'].items[7], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010104)],
- actions=[output(0)]
- ))
self.assertFlowsEqual(self.device_flows['onu1'].items[2], mk_flow_stat(
priority=500,
match_fields=[in_port(1), vlan_vid(4096 + 101)],
@@ -638,16 +628,6 @@
actions=[
set_field(vlan_vid(4096 + 201)), output(1)]
))
- self.assertFlowsEqual(self.device_flows['onu2'].items[6], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010103)],
- actions=[output(0)]
- ))
- self.assertFlowsEqual(self.device_flows['onu2'].items[7], mk_flow_stat(
- priority=1000,
- match_fields=[in_port(1), eth_type(0x800), ipv4_dst(0xe4010104)],
- actions=[output(0)]
- ))
self.assertFlowsEqual(self.device_flows['onu2'].items[2], mk_flow_stat(
priority=500,
match_fields=[in_port(1), vlan_vid(4096 + 201)],
diff --git a/voltha/adapters/openolt/openolt_flow_mgr.py b/voltha/adapters/openolt/openolt_flow_mgr.py
index c984fe0..4c40910 100644
--- a/voltha/adapters/openolt/openolt_flow_mgr.py
+++ b/voltha/adapters/openolt/openolt_flow_mgr.py
@@ -18,8 +18,8 @@
import grpc
from voltha.protos.openflow_13_pb2 import OFPXMC_OPENFLOW_BASIC, \
- ofp_flow_stats, ofp_match, OFPMT_OXM, Flows, FlowGroups, \
- OFPXMT_OFB_IN_PORT, OFPXMT_OFB_VLAN_VID
+ ofp_flow_stats, OFPMT_OXM, Flows, FlowGroups, OFPXMT_OFB_IN_PORT, \
+ OFPXMT_OFB_VLAN_VID
from voltha.protos.device_pb2 import Port
import voltha.core.flow_decomposer as fd
import openolt_platform as platform
@@ -33,9 +33,10 @@
EAPOL_DOWNLINK_FLOW_INDEX = 3 # FIXME
EAPOL_DOWNLINK_SECONDARY_FLOW_INDEX = 4 # FIXME
EAPOL_UPLINK_SECONDARY_FLOW_INDEX = 5 # FIXME
-
+LLDP_FLOW_INDEX = 7 # FIXME
EAP_ETH_TYPE = 0x888e
+LLDP_ETH_TYPE = 0x88cc
# FIXME - see also BRDCM_DEFAULT_VLAN in broadcom_onu.py
DEFAULT_MGMT_VLAN = 4091
@@ -53,14 +54,12 @@
self.flows_proxy = registry('core').get_proxy(
'/devices/{}/flows'.format(self.device_id))
self.root_proxy = registry('core').get_proxy('/')
- self.fd_object = fd.FlowDecomposer()
def add_flow(self, flow):
self.log.debug('add flow', flow=flow)
classifier_info = dict()
action_info = dict()
-
for field in fd.get_ofb_fields(flow):
if field.type == fd.ETH_TYPE:
classifier_info['eth_type'] = field.eth_type
@@ -117,12 +116,14 @@
self.log.debug('being taken care of by ONU', flow=flow)
return
action_info['pop_vlan'] = True
- self.log.debug('action-type-pop-vlan', in_port=classifier_info['in_port'])
+ self.log.debug('action-type-pop-vlan',
+ in_port=classifier_info['in_port'])
elif action.type == fd.PUSH_VLAN:
action_info['push_vlan'] = True
action_info['tpid'] = action.push.ethertype
self.log.debug('action-type-push-vlan',
- push_tpid=action_info['tpid'], in_port=classifier_info['in_port'])
+ push_tpid=action_info['tpid'],
+ in_port=classifier_info['in_port'])
if action.push.ethertype != 0x8100:
self.log.error('unhandled-tpid',
ethertype=action.push.ethertype)
@@ -131,8 +132,8 @@
_field = action.set_field.field.ofb_field
assert (action.set_field.field.oxm_class ==
OFPXMC_OPENFLOW_BASIC)
- self.log.debug('action-type-set-field',
- field=_field, in_port=classifier_info['in_port'])
+ self.log.debug('action-type-set-field', field=_field,
+ in_port=classifier_info['in_port'])
if _field.type == fd.VLAN_VID:
self.log.debug('set-field-type-vlan-vid',
vlan_vid=_field.vlan_vid & 0xfff)
@@ -142,7 +143,8 @@
field_type=_field.type)
else:
self.log.error('unsupported-action-type',
- action_type=action.type, in_port=classifier_info['in_port'])
+ action_type=action.type,
+ in_port=classifier_info['in_port'])
if fd.get_goto_table_id(flow) is not None and not 'pop_vlan' in \
action_info:
@@ -150,7 +152,7 @@
return
if not 'output' in action_info and 'metadata' in classifier_info:
- #find flow in the next table
+ # find flow in the next table
next_flow = self.find_next_flow(flow)
if next_flow is None:
return
@@ -159,24 +161,21 @@
if field.type == fd.VLAN_VID:
classifier_info['metadata'] = field.vlan_vid & 0xfff
-
(intf_id, onu_id) = platform.extract_access_from_flow(
classifier_info['in_port'], action_info['output'])
-
self.divide_and_add_flow(intf_id, onu_id, classifier_info,
action_info, flow)
def remove_flow(self, flow):
self.log.debug('trying to remove flows from logical flow :',
- logical_flow=flow)
+ logical_flow=flow)
device_flows_to_remove = []
device_flows = self.flows_proxy.get('/').items
for f in device_flows:
if f.cookie == flow.id:
device_flows_to_remove.append(f)
-
for f in device_flows_to_remove:
(id, direction) = self.decode_stored_id(f.id)
flow_to_remove = openolt_pb2.Flow(flow_id=id, flow_type=direction)
@@ -185,7 +184,8 @@
except grpc.RpcError as grpc_e:
if grpc_e.code() == grpc.StatusCode.NOT_FOUND:
self.log.debug('This flow does not exist on the switch, '
- 'normal after an OLT reboot', flow=flow_to_remove)
+ 'normal after an OLT reboot',
+ flow=flow_to_remove)
else:
raise grpc_e
@@ -209,7 +209,6 @@
self.log.debug('no device flow to remove for this flow (normal '
'for multi table flows)', flow=flow)
-
def divide_and_add_flow(self, intf_id, onu_id, classifier,
action, flow):
@@ -225,22 +224,27 @@
self.log.warn('igmp flow add ignored, not implemented yet')
else:
self.log.warn("Invalid-Classifier-to-handle",
- classifier=classifier,
- action=action)
+ classifier=classifier,
+ action=action)
elif 'eth_type' in classifier:
if classifier['eth_type'] == EAP_ETH_TYPE:
self.log.debug('eapol flow add')
self.add_eapol_flow(intf_id, onu_id, flow)
vlan_id = self.get_subscriber_vlan(fd.get_in_port(flow))
if vlan_id is not None:
- self.add_eapol_flow(intf_id, onu_id, flow,
+ self.add_eapol_flow(
+ intf_id, onu_id, flow,
uplink_eapol_id=EAPOL_UPLINK_SECONDARY_FLOW_INDEX,
downlink_eapol_id=EAPOL_DOWNLINK_SECONDARY_FLOW_INDEX,
vlan_id=vlan_id)
+ if classifier['eth_type'] == LLDP_ETH_TYPE:
+ self.log.debug('lldp flow add')
+ self.add_lldp_flow(intf_id, onu_id, flow, classifier,
+ action)
elif 'push_vlan' in action:
self.add_upstream_data_flow(intf_id, onu_id, classifier, action,
- flow)
+ flow)
elif 'pop_vlan' in action:
self.add_downstream_data_flow(intf_id, onu_id, classifier,
action, flow)
@@ -249,10 +253,8 @@
classifier=classifier,
action=action, flow=flow)
-
def add_upstream_data_flow(self, intf_id, onu_id, uplink_classifier,
- uplink_action, logical_flow):
-
+ uplink_action, logical_flow):
uplink_classifier['pkt_tag_type'] = 'single_tag'
@@ -263,12 +265,12 @@
# Secondary EAP on the subscriber vlan
(eap_active, eap_logical_flow) = self.is_eap_enabled(intf_id, onu_id)
if eap_active:
- self.add_eapol_flow(intf_id, onu_id, eap_logical_flow,
+ self.add_eapol_flow(
+ intf_id, onu_id, eap_logical_flow,
uplink_eapol_id=EAPOL_UPLINK_SECONDARY_FLOW_INDEX,
downlink_eapol_id=EAPOL_DOWNLINK_SECONDARY_FLOW_INDEX,
vlan_id=uplink_classifier['vlan_vid'])
-
def add_downstream_data_flow(self, intf_id, onu_id, downlink_classifier,
downlink_action, flow):
downlink_classifier['pkt_tag_type'] = 'double_tag'
@@ -321,7 +323,6 @@
self.add_flow_to_device(upstream_flow, logical_flow)
-
# FIXME - ONOS should send explicit upstream and downstream
# exact dhcp trap flow.
@@ -346,7 +347,6 @@
classifier),
action=self.mk_action(action))
-
self.add_flow_to_device(downstream_flow, downstream_logical_flow)
def add_eapol_flow(self, intf_id, onu_id, logical_flow,
@@ -393,6 +393,7 @@
downlink_classifier['vlan_vid'] = 4000 - onu_id
+
downlink_action = {}
downlink_action['push_vlan'] = True
downlink_action['vlan_vid'] = vlan_id
@@ -438,6 +439,29 @@
def reset_flows(self):
self.flows_proxy.update('/', Flows())
+ def add_lldp_flow(self, intf_id, onu_id, logical_flow, classifier, action):
+
+ self.log.debug('add lldp downstream trap', classifier=classifier,
+ action=action)
+
+ action.clear()
+ action['trap_to_host'] = True
+ classifier['pkt_tag_type'] = 'untagged'
+
+ gemport_id = platform.mk_gemport_id(onu_id)
+ flow_id = platform.mk_flow_id(intf_id, onu_id, LLDP_FLOW_INDEX)
+
+ downstream_flow = openolt_pb2.Flow(
+ onu_id=onu_id, flow_id=flow_id, flow_type="downstream",
+ access_intf_id=3, network_intf_id=0, gemport_id=gemport_id,
+ priority=logical_flow.priority,
+ classifier=self.mk_classifier(classifier),
+ action=self.mk_action(action))
+
+ self.log.debug('add lldp downstream trap', access_intf_id=intf_id,
+ onu_id=onu_id, flow_id=flow_id)
+ self.stub.FlowAdd(downstream_flow)
+
def mk_classifier(self, classifier_info):
classifier = openolt_pb2.Classifier()
@@ -523,7 +547,6 @@
if in_port == port and \
platform.intf_id_to_port_type_name(out_port) == Port.ETHERNET_NNI:
-
fields = fd.get_ofb_fields(flow)
self.log.debug('subscriber flow found', fields=fields)
for field in fields:
@@ -553,7 +576,6 @@
flows.items.extend([stored_flow])
self.flows_proxy.update('/', flows)
-
def find_next_flow(self, flow):
table_id = fd.get_goto_table_id(flow)
metadata = 0
@@ -566,20 +588,19 @@
next_flows = []
for f in flows:
if f.table_id == table_id:
- #FIXME:
+ # FIXME
if fd.get_in_port(f) == fd.get_in_port(flow) and \
fd.get_out_port(f) == metadata:
next_flows.append(f)
-
if len(next_flows) == 0:
self.log.warning('no next flow found, it may be a timing issue',
flow=flow, number_of_flows=len(flows))
reactor.callLater(5, self.add_flow, flow)
return None
- next_flows.sort(key=lambda f:f.priority, reverse=True)
+ next_flows.sort(key=lambda f: f.priority, reverse=True)
return next_flows[0]
@@ -607,4 +628,4 @@
if id >> 15 == 0x1:
return (id & 0x7fff, 'upstream')
else:
- return (id, 'downstream')
\ No newline at end of file
+ return (id, 'downstream')
diff --git a/voltha/core/flow_decomposer.py b/voltha/core/flow_decomposer.py
index 1d918f7..35f3f1d 100644
--- a/voltha/core/flow_decomposer.py
+++ b/voltha/core/flow_decomposer.py
@@ -541,7 +541,7 @@
return not is_downstream()
if out_port_no is not None and \
- (out_port_no & 0x7fffffff) == ofp.OFPP_CONTROLLER:
+ (out_port_no & 0x7fffffff) == ofp.OFPP_CONTROLLER:
# UPSTREAM CONTROLLER-BOUND FLOW
@@ -556,64 +556,66 @@
fl_lst, _ = device_rules.setdefault(
egress_hop.device.id, ([], []))
- # in_port_no is None for wildcard input case, do not include
- # upstream port for 4000 flow in input
- if in_port_no is None:
- in_ports = self.get_wildcard_input_ports(exclude_port=
- egress_hop.egress_port.port_no)
- else:
- in_ports = [in_port_no]
+ log.info('trap-flow', in_port_no=in_port_no,
+ nni=self._nni_logical_port_no)
- for input_port in in_ports:
- fl_lst.append(mk_flow_stat( # Upstream flow
+ if in_port_no == self._nni_logical_port_no:
+ log.info('trap-nni')
+ # Trap flow for NNI port
+ fl_lst.append(mk_flow_stat(
priority=flow.priority,
cookie=flow.cookie,
match_fields=[
- in_port(egress_hop.ingress_port.port_no),
- vlan_vid(ofp.OFPVID_PRESENT | input_port)
+ in_port(egress_hop.egress_port.port_no)
] + [
field for field in get_ofb_fields(flow)
- if field.type not in (IN_PORT, VLAN_VID)
+ if field.type not in (IN_PORT,)
],
actions=[
- push_vlan(0x8100),
- set_field(vlan_vid(ofp.OFPVID_PRESENT | 4000)),
- output(egress_hop.egress_port.port_no)]
- ))
- fl_lst.append(mk_flow_stat( # Downstream flow
- priority=flow.priority,
- match_fields=[
- in_port(egress_hop.egress_port.port_no),
- vlan_vid(ofp.OFPVID_PRESENT | 4000),
- vlan_pcp(0),
- metadata(input_port)
- ],
- actions=[
- pop_vlan(),
- output(egress_hop.ingress_port.port_no)]
- ))
-
- if in_port_no is not None:
- # Only handle the non-wildcard case on the ONU
- onu_fl_lst, _ = device_rules.setdefault(
- ingress_hop.device.id, ([], []))
-
- onu_fl_lst.append(mk_flow_stat( # Onu upstream flow
- priority=flow.priority + 1000,
- cookie=flow.cookie,
- match_fields= [
- in_port(ingress_hop.ingress_port.port_no),
- vlan_vid(0)
- ] + [
- field for field in get_ofb_fields(flow)
- if field.type not in (IN_PORT, VLAN_VID)
- ],
- actions=[
- push_vlan(0x8100),
- set_field(vlan_vid(ofp.OFPVID_PRESENT | input_port)),
- output(ingress_hop.egress_port.port_no)
+ action for action in get_actions(flow)
]
))
+
+ else:
+ log.info('trap-uni')
+ # Trap flow for UNI port
+
+ # in_port_no is None for wildcard input case, do not include
+ # upstream port for 4000 flow in input
+ if in_port_no is None:
+ in_ports = self.get_wildcard_input_ports(exclude_port=
+ egress_hop.egress_port.port_no)
+ else:
+ in_ports = [in_port_no]
+
+ for input_port in in_ports:
+ fl_lst.append(mk_flow_stat( # Upstream flow
+ priority=flow.priority,
+ cookie=flow.cookie,
+ match_fields=[
+ in_port(egress_hop.ingress_port.port_no),
+ vlan_vid(ofp.OFPVID_PRESENT | input_port)
+ ] + [
+ field for field in get_ofb_fields(flow)
+ if field.type not in (IN_PORT, VLAN_VID)
+ ],
+ actions=[
+ push_vlan(0x8100),
+ set_field(vlan_vid(ofp.OFPVID_PRESENT | 4000)),
+ output(egress_hop.egress_port.port_no)]
+ ))
+ fl_lst.append(mk_flow_stat( # Downstream flow
+ priority=flow.priority,
+ match_fields=[
+ in_port(egress_hop.egress_port.port_no),
+ vlan_vid(ofp.OFPVID_PRESENT | 4000),
+ vlan_pcp(0),
+ metadata(input_port)
+ ],
+ actions=[
+ pop_vlan(),
+ output(egress_hop.ingress_port.port_no)]
+ ))
else:
# NOT A CONTROLLER-BOUND FLOW
if is_upstream():
diff --git a/voltha/core/logical_device_agent.py b/voltha/core/logical_device_agent.py
index 11afee4..f327310 100644
--- a/voltha/core/logical_device_agent.py
+++ b/voltha/core/logical_device_agent.py
@@ -833,8 +833,20 @@
def get_route(self, ingress_port_no, egress_port_no):
self._assure_cached_tables_up_to_date()
+ self.log.info('getting-route', eg_port=egress_port_no, in_port=ingress_port_no,
+ nni_port=self._nni_logical_port_no)
if egress_port_no is not None and \
(egress_port_no & 0x7fffffff) == ofp.OFPP_CONTROLLER:
+ self.log.info('controller-flow', eg_port=egress_port_no, in_port=ingress_port_no,
+ nni_port=self._nni_logical_port_no)
+ if ingress_port_no == self._nni_logical_port_no:
+ self.log.info('returning half route')
+ # This is a trap on the NNI Port
+ # Return a 'half' route to make the flow decomp logic happy
+ for (ingress, egress), route in self._routes.iteritems():
+ if egress == self._nni_logical_port_no:
+ return [None, route[1]]
+ raise Exception('not a single upstream route')
# treat it as if the output port is the NNI of the OLT
egress_port_no = self._nni_logical_port_no