[SDFAB-504] Add UP4 encap, decap test

Change-Id: Ic1cc30fd9c87d7c20e03f02171b62e92b7ff6e94
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/README.md b/TestON/tests/USECASE/SegmentRouting/UP4/README.md
new file mode 100644
index 0000000..06e23b2
--- /dev/null
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/README.md
@@ -0,0 +1,31 @@
+# UP4 System Tests
+
+Tests in this folder use UP4 ONOS APIs (P4Runtime) to simulate the attachment
+and detachment of UEs. Tests also verify upstream and downstream traffic by
+checking that GTP encapsulation and decapsulation is correctly performed by the
+UPF (implemented by the leaves switches). The testing topology a paired-leaves
+topology.
+
+# Requirements to run UP4 tests
+
+The UP4 test uses the P4RuntimeCliDriver. This driver requires
+the `p4runtime-shell` to be installed on the ONOS Bench machine To install it
+run `python3 -m pip install p4runtime-sh==0.0.2` (requires Python>3.7).
+
+The driver also requires an ipython config file. The file should contain the
+following lines:
+
+```
+c.InteractiveShell.color_info = False
+c.InteractiveShell.colors = 'NoColor'
+c.TerminalInteractiveShell.color_info = False
+c.TerminalInteractiveShell.colors = 'NoColor'
+```
+
+and can be placed in `<IPYTHONDIR>/profile_default/ipython_config.py`. A
+different location for the ipython config folder can be set via the `IPYTHONDIR`
+, in that case the `ipython_config.py` file can be placed
+in `<IPYTHONDIR>/profile_default`
+(more info at https://ipython.readthedocs.io/en/stable/development/config.html).
+
+
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params
new file mode 100644
index 0000000..113d33d
--- /dev/null
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.params
@@ -0,0 +1,87 @@
+<PARAMS>
+    <testcases>1</testcases>
+
+    <GRAPH>
+        <nodeCluster>pairedleaves</nodeCluster>
+        <builds>20</builds>
+        <jobName>UP4</jobName>
+        <branch>master</branch>
+    </GRAPH>
+
+    <persistent_setup>True</persistent_setup>
+
+    <kubernetes>
+        <appName>onos-classic</appName>
+        <namespace>tost</namespace>
+    </kubernetes>
+
+    <UP4>
+        <pdn_host>Compute1</pdn_host>
+        <enodeb_host>Compute3</enodeb_host>
+        <s1u_address>10.32.11.126</s1u_address>
+        <enb_address>10.32.11.194</enb_address>
+        <router_mac>00:00:0A:4C:1C:46</router_mac>
+        <ues>
+            <ue1>
+                <pfcp_session_id>100</pfcp_session_id>
+                <ue_address>10.240.0.1</ue_address>
+                <teid>100</teid>
+                <up_id>10</up_id>
+                <down_id>11</down_id>
+                <qfi></qfi>
+                <five_g>False</five_g>
+            </ue1>
+            <ue2>
+                <pfcp_session_id>100</pfcp_session_id>
+                <ue_address>10.240.0.2</ue_address>
+                <teid>200</teid>
+                <up_id>20</up_id>
+                <down_id>21</down_id>
+                <qfi></qfi>
+                <five_g>False</five_g>
+            </ue2>
+        </ues>
+    </UP4>
+
+    <TOPO>
+        <switchNum>2</switchNum>
+        <linkNum>2</linkNum>
+    </TOPO>
+
+    <ONOS_Logging>
+        <org.onosproject.p4runtime.ctl.client>DEBUG</org.onosproject.p4runtime.ctl.client>
+        <org.onosproject.p4runtime.ctl.client.writerequestimpl>TRACE</org.onosproject.p4runtime.ctl.client.writerequestimpl>
+        <org.onosproject.segmentrouting>DEBUG</org.onosproject.segmentrouting>
+        <org.onosproject.gnmi.ctl>TRACE</org.onosproject.gnmi.ctl>
+        <org.omecproject.up4>TRACE</org.omecproject.up4>
+    </ONOS_Logging>
+    <ONOS_Logging_Reset>
+        <org.onosproject.p4runtime.ctl.client>INFO</org.onosproject.p4runtime.ctl.client>
+        <org.onosproject.p4runtime.ctl.client.writerequestimpl>INFO</org.onosproject.p4runtime.ctl.client.writerequestimpl>
+        <org.onosproject.segmentrouting>DEBUG</org.onosproject.segmentrouting>
+        <org.onosproject.gnmi.ctl>INFO</org.onosproject.gnmi.ctl>
+        <org.omecproject.up4>INFO</org.omecproject.up4>
+    </ONOS_Logging_Reset>
+
+    <ENV>
+        <cellName>productionCell</cellName>
+        <cellApps>drivers,fpm,lldpprovider,hostprovider,netcfghostprovider,drivers.bmv2,org.opencord.fabric-tofino,pipelines.fabric,org.stratumproject.fabric-tna,drivers.barefoot,segmentrouting,t3,up4</cellApps>
+    </ENV>
+
+    <DEPENDENCY>
+        <useCommonConf>False</useCommonConf>
+        <useCommonTopo>True</useCommonTopo>
+        <useBmv2>True</useBmv2>
+        <bmv2SwitchType>stratum</bmv2SwitchType>
+        <switchPrefix></switchPrefix>
+        <stratumRoot>~/stratum</stratumRoot>
+        <topology>trellis_fabric.py</topology>
+        <lib></lib>
+    </DEPENDENCY>
+
+    <SCALE>
+        <size>3</size>
+        <max>3</max>
+    </SCALE>
+
+</PARAMS>
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py
new file mode 100644
index 0000000..a7f9804
--- /dev/null
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.py
@@ -0,0 +1,203 @@
+class UP4:
+
+    def __init__(self):
+        self.default = ''
+
+    # TODO: add test case that checks entries are being inserted and deleted from ONOS correclty
+    def CASE1(self, main):
+        """
+        Attach UE
+        Generate traffic from UE to PDN
+        Verify traffic received from PDN
+        Generate traffic from PDN to UE
+        Verify traffic received from UE
+        Detach UE
+        """
+        UE_PORT = 400
+        PDN_PORT = 800
+        GPDU_PORT = 2152
+        try:
+            from tests.USECASE.SegmentRouting.dependencies.up4libcli import \
+                Up4LibCli
+            from tests.USECASE.SegmentRouting.dependencies.Testcaselib import \
+                Testcaselib as run
+            from distutils.util import strtobool
+        except ImportError as e:
+            main.log.error("Import not found. Exiting the test")
+            main.log.error(e)
+            main.cleanAndExit()
+
+        # TODO: Move to a setup script
+        run.initTest(main)
+        main.log.info(main.Cluster.numCtrls)
+        main.Cluster.setRunningNode(3)
+        run.installOnos(main, skipPackage=True, cliSleep=5)
+
+        # Get the P4RT client connected to UP4 in the first available ONOS instance
+        up4Client = main.Cluster.active(0).p4rtUp4
+
+        s1u_address = main.params["UP4"]["s1u_address"]
+        enb_address = main.params["UP4"]["enb_address"]
+        router_mac = main.params["UP4"]["router_mac"]
+
+        pdn_host = getattr(main, main.params["UP4"]["pdn_host"])
+        pdn_interface = pdn_host.interfaces[0]
+
+        enodeb_host = getattr(main, main.params["UP4"]["enodeb_host"])
+        enodeb_interface = enodeb_host.interfaces[0]
+
+        emulated_ues = main.params["UP4"]['ues']
+        n_ues = len(emulated_ues)
+
+        main.step("Start scapy and p4rt client")
+        pdn_host.startScapy(ifaceName=pdn_interface["name"])
+        enodeb_host.startScapy(ifaceName=enodeb_interface["name"],
+                               enableGtp=True)
+        up4Client.startP4RtClient()
+
+        # TODO: move to library in dependencies
+        main.step("Attach UEs")
+        for ue in emulated_ues.values():
+            # Sanitize values coming from the params file
+            if "five_g" in ue:
+                ue["five_g"] = bool(strtobool(ue["five_g"]))
+            if "qfi" in ue and ue["qfi"] == "":
+                ue["qfi"] = None
+            Up4LibCli.attachUe(up4Client, s1u_address=s1u_address,
+                               enb_address=enb_address,
+                               **ue)
+
+        # ----------------- Test Upstream traffic (enb->pdn)
+        main.step("Test upstream traffic")
+        # Scapy filter needs to start before sending traffic
+        pkt_filter_upstream = ""
+        for ue in emulated_ues.values():
+            if "ue_address" in ue:
+                if len(pkt_filter_upstream) != 0:
+                    pkt_filter_upstream += " or "
+                pkt_filter_upstream += "src host " + ue["ue_address"]
+        pkt_filter_upstream = "ip and udp dst port %s and (%s) and dst host %s" % \
+                              (PDN_PORT, pkt_filter_upstream,
+                               pdn_interface["ips"][0])
+        main.log.info("Start listening on %s intf %s" %
+                      (main.params["UP4"]["pdn_host"], pdn_interface["name"]))
+        main.log.debug("BPF Filter Upstream: \n %s" % pkt_filter_upstream)
+        pdn_host.startFilter(ifaceName=pdn_interface["name"],
+                             sniffCount=n_ues,
+                             pktFilter=pkt_filter_upstream)
+
+        main.log.info("Sending %d packets from eNodeB host" % len(emulated_ues))
+        for ue in emulated_ues.values():
+            enodeb_host.buildEther()
+            enodeb_host.buildIP(src=enb_address, dst=s1u_address)
+            enodeb_host.buildUDP(ipVersion=4, dport=GPDU_PORT)
+            # FIXME: With newer scapy TEID becomes teid (required for Scapy 2.4.5)
+            enodeb_host.buildGTP(gtp_type=0xFF, TEID=int(ue["teid"]))
+            enodeb_host.buildIP(overGtp=True, src=ue["ue_address"],
+                                dst=pdn_interface["ips"][0])
+            enodeb_host.buildUDP(ipVersion=4, overGtp=True, sport=UE_PORT,
+                                 dport=PDN_PORT)
+
+            enodeb_host.sendPacket(iface=enodeb_interface["name"])
+
+        finished = pdn_host.checkFilter()
+        packets = ""
+        if finished:
+            packets = pdn_host.readPackets(detailed=True)
+            for p in packets.splitlines():
+                main.log.debug(p)
+            # We care only of the last line from readPackets
+            packets = packets.splitlines()[-1]
+        else:
+            kill = pdn_host.killFilter()
+            main.log.debug(kill)
+
+        fail = False
+        if len(emulated_ues) != packets.count('Ether'):
+            fail = True
+            msg = "Failed to capture packets in PDN. "
+        else:
+            msg = "Correctly captured packet in PDN. "
+        # We expect exactly 1 packet per UE
+        pktsFiltered = [packets.count("src=" + ue["ue_address"])
+                        for ue in emulated_ues.values()]
+        if pktsFiltered.count(1) != len(pktsFiltered):
+            fail = True
+            msg += "More than one packet per UE in downstream. "
+        else:
+            msg += "One packet per UE in upstream. "
+
+        utilities.assert_equal(
+            expect=False, actual=fail, onpass=msg, onfail=msg)
+
+        # --------------- Test Downstream traffic (pdn->enb)
+        main.step("Test downstream traffic")
+        pkt_filter_downstream = "ip and udp src port %d and udp dst port %d and dst host %s and src host %s" % (
+            GPDU_PORT, GPDU_PORT, enb_address, s1u_address)
+        main.log.info("Start listening on %s intf %s" % (
+            main.params["UP4"]["enodeb_host"], enodeb_interface["name"]))
+        main.log.debug("BPF Filter Downstream: \n %s" % pkt_filter_downstream)
+        enodeb_host.startFilter(ifaceName=enodeb_interface["name"],
+                                sniffCount=len(emulated_ues),
+                                pktFilter=pkt_filter_downstream)
+
+        main.log.info("Sending %d packets from PDN host" % len(emulated_ues))
+        for ue in emulated_ues.values():
+            # From PDN we have to set dest MAC, otherwise scapy will do ARP
+            # request for the UE IP address.
+            pdn_host.buildEther(dst=router_mac)
+            pdn_host.buildIP(src=pdn_interface["ips"][0],
+                             dst=ue["ue_address"])
+            pdn_host.buildUDP(ipVersion=4, sport=PDN_PORT, dport=UE_PORT)
+            pdn_host.sendPacket(iface=pdn_interface["name"])
+
+        finished = enodeb_host.checkFilter()
+        packets = ""
+        if finished:
+            packets = enodeb_host.readPackets(detailed=True)
+            for p in packets.splitlines():
+                main.log.debug(p)
+            # We care only of the last line from readPackets
+            packets = packets.splitlines()[-1]
+        else:
+            kill = enodeb_host.killFilter()
+            main.log.debug(kill)
+
+        # The BPF filter might capture non-GTP packets because we can't filter
+        # GTP header in BPF. For this reason, check that the captured packets
+        # are from the expected tunnels.
+        # TODO: check inner UDP and IP fields as well
+        # FIXME: with newer scapy TEID becomes teid (required for Scapy 2.4.5)
+        pktsFiltered = [packets.count("TEID=" + hex(int(ue["teid"])) + "L ")
+                        for ue in emulated_ues.values()]
+
+        fail = False
+        if len(emulated_ues) != sum(pktsFiltered):
+            fail = True
+            msg = "Failed to capture packets in eNodeB. "
+        else:
+            msg = "Correctly captured packets in eNodeB. "
+        # We expect exactly 1 packet per UE
+        if pktsFiltered.count(1) != len(pktsFiltered):
+            fail = True
+            msg += "More than one packet per GTP TEID in downstream. "
+        else:
+            msg += "One packet per GTP TEID in downstream. "
+
+        utilities.assert_equal(
+            expect=False, actual=fail, onpass=msg, onfail=msg)
+
+        # Detach UEs
+        main.step("Detach UEs")
+        for ue in emulated_ues.values():
+            # No need to sanitize values, already sanitized during attachment
+            Up4LibCli.detachUe(up4Client, s1u_address=s1u_address,
+                               enb_address=enb_address,
+                               **ue)
+
+        # Teardown
+        main.step("Stop scapy and p4rt client")
+        enodeb_host.stopScapy()
+        pdn_host.stopScapy()
+        up4Client.stopP4RtClient()
+        run.cleanup(main)
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/UP4.topo b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.topo
new file mode 100644
index 0000000..5dac34a
--- /dev/null
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/UP4.topo
@@ -0,0 +1,100 @@
+<TOPOLOGY>
+    <COMPONENT>
+        <ONOScell>
+            <host>localhost</host>  # ONOS "bench" machine
+            <user>jenkins</user>
+            <password></password>
+            <type>OnosClusterDriver</type>
+            <connect_order>50</connect_order>
+            <jump_host></jump_host>
+            <home>~/onos</home>   # defines where onos home is on the build machine. Defaults to "~/onos/" if empty.
+            <COMPONENTS>
+                <kubeConfig>~/.kube/dev-pairedleaves-tucson</kubeConfig>  # If set, will attempt to use this file for setting up port-forwarding
+                <useDocker>True</useDocker>  # Whether to use docker for ONOS nodes
+                <docker_prompt>\$</docker_prompt>
+                <cluster_name></cluster_name>  # Used as a prefix for cluster components. Defaults to 'ONOS'
+                <diff_clihost>True</diff_clihost> # if it has different host other than localhost for CLI. True or empty. OC# will be used if True.
+                <karaf_username>karaf</karaf_username>
+                <karaf_password>karaf</karaf_password>
+                <web_user>karaf</web_user>
+                <web_pass>karaf</web_pass>
+                <karafPrompt_username>karaf</karafPrompt_username>
+                <rest_port></rest_port>
+                <prompt></prompt>  # TODO: we technically need a few of these, one per component
+                <onos_home>~/onos/</onos_home>  # defines where onos home is on the target cell machine. Defaults to entry in "home" if empty.
+                <nodes> 3 </nodes>  # number of nodes in the cluster
+                <up4_port>51001</up4_port> # Port where the UP4 P4Runtime server is listening
+            </COMPONENTS>
+        </ONOScell>
+
+        <Compute1>
+            <host>10.76.28.74</host>
+            <user>jenkins</user>
+            <password></password>
+            <type>HostDriver</type>
+            <connect_order>6</connect_order>
+            <jump_host></jump_host>
+            <COMPONENTS>
+                <mac></mac>
+                <inband>false</inband>
+                <dhcp>True</dhcp>
+                <ip>10.32.11.2</ip>
+                <shortName>h1</shortName>
+                <port1></port1>
+                <link1></link1>
+                <ifaceName>pairbond</ifaceName>
+                <scapy_path>/usr/bin/scapy</scapy_path>
+                <routes>
+                    <route1>
+                        <network></network>
+                        <netmask></netmask>
+                        <gw></gw>
+                        <interface></interface>
+                    </route1>
+                </routes>
+                <sudo_required>true</sudo_required>
+            </COMPONENTS>
+        </Compute1>
+
+        <Compute3>
+            <host>10.76.28.68</host>
+            <user>jenkins</user>
+            <password></password>
+            <type>HostDriver</type>
+            <connect_order>6</connect_order>
+            <jump_host></jump_host>
+            <COMPONENTS>
+                <mac></mac>
+                <inband>false</inband>
+                <dhcp>True</dhcp>
+                <ip>10.32.11.194</ip>
+                <shortName>h3</shortName>
+                <port1></port1>
+                <link1></link1>
+                <ifaceName>eno2</ifaceName>
+                <scapy_path>/usr/bin/scapy</scapy_path>
+                <routes>
+                    <route1>
+                        <network></network>
+                        <netmask></netmask>
+                        <gw></gw>
+                        <interface></interface>
+                    </route1>
+                </routes>
+                <sudo_required>true</sudo_required>
+            </COMPONENTS>
+        </Compute3>
+
+    <!--  This component is not needed, but required to use the Testcaselib  -->
+        <NetworkBench>
+            <host>10.76.28.66</host>
+            <user>jenkins</user>
+            <password></password>
+            <type>NetworkDriver</type>
+            <connect_order>1</connect_order>
+            <COMPONENTS>
+            </COMPONENTS>
+        </NetworkBench>
+
+    </COMPONENT>
+</TOPOLOGY>
diff --git a/TestON/tests/USECASE/SegmentRouting/UP4/__init__.py b/TestON/tests/USECASE/SegmentRouting/UP4/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/TestON/tests/USECASE/SegmentRouting/UP4/__init__.py