PONSIM: PON simulator with real dataplane handling
This was needed because neither CPQD nor OVS can handle
both zero-tagged packets and 802.1ad (QinQ).
- extensive unittest proves ponsim functional correctness
(for the common use-cases needed in the PON scenario)
- integrated with frameio and coupled with a rather
simple gRPC NBI, ponsim can be operated from Voltha
just like a real PON system
- posim_olt/_onu adapters added to Voltha to work on
ponsim
- CLI can be used to preprovision and activate a PONSIM
instance (e.g., preprovision_olt -t ponsim_olt -H localhost:50060)
- Some of olt-oftest:olt-complex testcases can be run on
the ponsim device (in vagrant/Ubuntu environment),
but there are some remaining issues to work out:
- barrier calls in OF do not guaranty that the flow
is already installed on the device. This is a generic
issue, not just for ponsim.
- the whole test framework is inconsistent about zero-
tagged vs. untagged frames at the ONUs, while ponsim
is rather pedantica and does exactly what was defined
in the flows.
Change-Id: I0dd564c932416ae1566935492134cb5b08113bdc
diff --git a/ponsim/ponsim.py b/ponsim/ponsim.py
new file mode 100644
index 0000000..c855e39
--- /dev/null
+++ b/ponsim/ponsim.py
@@ -0,0 +1,236 @@
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""
+Simple PON Simulator which would not be needed if openvswitch could do
+802.1ad (QinQ), which it cannot (the reason is beyond me), or if CPQD could
+handle 0-tagged packets (no comment).
+"""
+import structlog
+from scapy.layers.inet import IP, UDP
+from scapy.layers.l2 import Ether, Dot1Q
+from scapy.packet import Packet
+
+from common.frameio.frameio import hexify
+from voltha.protos import third_party
+from voltha.core.flow_decomposer import *
+_ = third_party
+
+
+def ipv4int2str(ipv4int):
+ return '{}.{}.{}.{}'.format(
+ (ipv4int >> 24) & 0xff,
+ (ipv4int >> 16) & 0xff,
+ (ipv4int >> 8) & 0xff,
+ ipv4int & 0xff
+ )
+
+
+class SimDevice(object):
+
+ def __init__(self, name, logical_port_no):
+ self.name = name
+ self.logical_port_no = logical_port_no
+ self.links = dict()
+ self.flows = list()
+ self.log = structlog.get_logger(name=name,
+ logical_port_no=logical_port_no)
+
+ def link(self, port, egress_fun):
+ self.links.setdefault(port, []).append(egress_fun)
+
+ def ingress(self, port, frame):
+ self.log.debug('ingress', ingress_port=port)
+ outcome = self.process_frame(port, frame)
+ if outcome is not None:
+ egress_port, egress_frame = outcome
+ forwarded = 0
+ links = self.links.get(egress_port)
+ if links is not None:
+ for fun in links:
+ forwarded += 1
+ self.log.debug('forwarding', egress_port=egress_port)
+ fun(egress_port, egress_frame)
+ if not forwarded:
+ self.log.debug('no-one-to-forward-to', egress_port=egress_port)
+ else:
+ self.log.debug('dropped')
+
+ def install_flows(self, flows):
+ # store flows in precedence order so we can roll down on frame arrival
+ self.flows = sorted(flows, key=lambda fm: fm.priority, reverse=True)
+
+ def process_frame(self, ingress_port, ingress_frame):
+ for flow in self.flows:
+ if self.is_match(flow, ingress_port, ingress_frame):
+ egress_port, egress_frame = self.process_actions(
+ flow, ingress_frame)
+ return egress_port, egress_frame
+ return None
+
+ @staticmethod
+ def is_match(flow, ingress_port, frame):
+
+ def get_non_shim_ether_type(f):
+ if f.haslayer(Dot1Q):
+ f = f.getlayer(Dot1Q)
+ return f.type
+
+ def get_vlan_pcp(f):
+ if f.haslayer(Dot1Q):
+ return f.getlayer(Dot1Q).prio
+
+ def get_ip_proto(f):
+ if f.haslayer(IP):
+ return f.getlayer(IP).proto
+
+ def get_ipv4_dst(f):
+ if f.haslayer(IP):
+ return f.getlayer(IP).dst
+
+ def get_udp_dst(f):
+ if f.haslayer(UDP):
+ return f.getlayer(UDP).dport
+
+ for field in get_ofb_fields(flow):
+
+ if field.type == IN_PORT:
+ if field.port != ingress_port:
+ return False
+
+ elif field.type == ETH_TYPE:
+ if field.eth_type != get_non_shim_ether_type(frame):
+ return False
+
+ elif field.type == IP_PROTO:
+ if field.ip_proto != get_ip_proto(frame):
+ return False
+
+ elif field.type == VLAN_VID:
+ expected_vlan = field.vlan_vid
+ tagged = frame.haslayer(Dot1Q)
+ if bool(expected_vlan & 4096) != bool(tagged):
+ return False
+ if tagged:
+ actual_vid = frame.getlayer(Dot1Q).vlan
+ if actual_vid != expected_vlan & 4095:
+ return False
+
+ elif field.type == VLAN_PCP:
+ if field.vlan_pcp != get_vlan_pcp(frame):
+ return False
+
+ elif field.type == IPV4_DST:
+ if ipv4int2str(field.ipv4_dst) != get_ipv4_dst(frame):
+ return False
+
+ elif field.type == UDP_DST:
+ if field.udsp_dst != get_udp_dst(frame):
+ return False
+
+ else:
+ raise NotImplementedError('field.type=%d' % field.type)
+
+ return True
+
+ @staticmethod
+ def process_actions(flow, frame):
+ egress_port = None
+ for action in get_actions(flow):
+
+ if action.type == OUTPUT:
+ egress_port = action.output.port
+
+ elif action.type == POP_VLAN:
+ if frame.haslayer(Dot1Q):
+ shim = frame.getlayer(Dot1Q)
+ frame = Ether(
+ src=frame.src,
+ dst=frame.dst,
+ type=shim.type) / shim.payload
+
+ elif action.type == PUSH_VLAN:
+ frame = (
+ Ether(src=frame.src, dst=frame.dst,
+ type=action.push.ethertype) /
+ Dot1Q(type=frame.type) /
+ frame.payload
+ )
+
+ elif action.type == SET_FIELD:
+ assert (action.set_field.field.oxm_class ==
+ ofp.OFPXMC_OPENFLOW_BASIC)
+ field = action.set_field.field.ofb_field
+
+ if field.type == VLAN_VID:
+ shim = frame.getlayer(Dot1Q)
+ shim.vlan = field.vlan_vid & 4095
+
+ elif field.type == VLAN_PCP:
+ shim = frame.getlayer(Dot1Q)
+ shim.prio = field.vlan_pcp
+
+ else:
+ raise NotImplementedError('set_field.field.type=%d'
+ % field.type)
+
+ else:
+ raise NotImplementedError('action.type=%d' % action.type)
+
+ return egress_port, frame
+
+
+class PonSim(object):
+
+ def __init__(self, onus, egress_fun):
+ self.egress_fun = egress_fun
+
+ # Create OLT and hook NNI port up for egress
+ self.olt = SimDevice('olt', 0)
+ self.olt.link(2, lambda _, frame: self.egress_fun(0, frame))
+ self.devices = dict()
+ self.devices[0] = self.olt
+
+ # Create ONUs of the requested number and hook them up with OLT
+ # and with egress fun
+ def mk_egress_fun(port_no):
+ return lambda _, frame: self.egress_fun(port_no, frame)
+
+ def mk_onu_ingress(onu):
+ return lambda _, frame: onu.ingress(1, frame)
+
+ for i in range(onus):
+ port_no = 128 + i
+ onu = SimDevice('onu%d' % i, port_no)
+ onu.link(1, lambda _, frame: self.olt.ingress(1, frame))
+ onu.link(2, mk_egress_fun(port_no))
+ self.olt.link(1, mk_onu_ingress(onu))
+ self.devices[port_no] = onu
+
+ def get_ports(self):
+ return sorted(self.devices.keys())
+
+ def olt_install_flows(self, flows):
+ self.olt.install_flows(flows)
+
+ def onu_install_flows(self, onu_port, flows):
+ self.devices[onu_port].install_flows(flows)
+
+ def ingress(self, port, frame):
+ if not isinstance(frame, Packet):
+ frame = Ether(frame)
+ self.devices[port].ingress(2, frame)
+