Updated P4 tutorial code/instructions for onos-1.13

Change-Id: Iccaa4114dbe2f408506ef5cf5e5cf7d3e22fb62e
diff --git a/apps/p4-tutorial/README.md b/apps/p4-tutorial/README.md
new file mode 100644
index 0000000..9373890
--- /dev/null
+++ b/apps/p4-tutorial/README.md
@@ -0,0 +1,123 @@
+# ONOS+P4 Tutorial
+
+This directory contains source code and instructions to run the ONOS+P4
+tutorial exercises. Goal of the exercises is to learn how to use ONOS to control
+P4-capable devices via P4Runtime, and how to write ONOS apps to control custom
+data plane capabilities implemented in P4.
+
+For help, please write to the mailing list
+[brigade-p4@onosproject.org](mailto:brigade-p4@onosproject.org) or check the
+[mailing list archives](https://groups.google.com/a/onosproject.org/forum/#!forum/brigade-p4).
+
+## Tutorial VM
+
+To complete the exercises, you will need to download and run the following VM
+(in .ova format):
+
+<http://onlab.vicci.org/onos/onos-p4-tutorial.ova>
+
+To run the VM you can use any modern virtualization system, although we
+recommend using VirtualBox. To download VirtualBox and import the VM use the
+following links:
+
+* <https://www.virtualbox.org/wiki/Downloads>
+* <https://docs.oracle.com/cd/E26217_01/E26796/html/qs-import-vm.html>
+
+For more information on the content of the VM and minimum system requirements,
+[click here](/tools/dev/p4vm/README.md).
+
+### VM credentials
+
+The VM comes with one user with sudo privileges. Use these credentials to log in:
+
+* Username: `sdn`
+* Password: `rocks`
+
+## Overview
+
+### mytunnel.p4
+
+These exercises are based on a simple P4 program called
+[mytunnel.p4](./pipeconf/src/main/resources/mytunnel.p4) designed for this
+tutorial.
+
+To start, have a look a the P4 program. Even if this is the first time you
+see P4 code, the program has been commented to provide an understanding of the
+pipeline behavior to anyone with basic programming and networking background.
+While checking the P4 program, try answering the following questions:
+
+* Which protocol headers are being extracted from each packet?
+* How can the parser distinguish a packet with MyTunnel encapsulation from one
+    without?
+* How many match+action tables are defined in the P4 program?
+* What is the first table in the pipeline applied to every packet?
+* Which headers can be matched on table `t_l2_fwd`?
+* Which type of match is applied to `t_l2_fwd`? E.g. exact match, ternary, or
+    longest-prefix match?
+* Which actions can be executed on matched packets?
+* Which action can be used to send a packet to the controller?
+* What happens if a matching entry is not found in table `t_l2_fwd`? What's the
+    next table applied to the packet?
+
+### MyTunnel Pipeconf
+
+The `mytunnel.p4` program is provided to ONOS as part of a "pipeconf".
+
+The main class used to implement the pipeconf is
+[PipeconfFactory.java](./pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipeconfFactory.java).
+This class is declared as an OSGi runtime component which is "activated" once
+the pipeconf app is loaded in ONOS. The main purpose of this class is to
+instantiate the Pipeconf object and register that with the corresponding service
+in ONOS. This is where we associate ONOS driver behaviors with the pipeconf, and
+also define the necessary pipeconf extensions to be able to deploy the P4
+program to a device.
+
+This pipeconf contains:
+
+* [mytunnel.json](/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.json):
+The JSON configuration used to execute the P4 program on BMv2. This is an output
+of the P4 compiler for BMv2.
+
+* [mytunnel.p4info](/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.p4info):
+P4Info file obtained from the P4 compiler.
+
+* [PipelineInterpreterImpl.java](./pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java):
+Implementation of the `PipelineInterpreter` ONOS driver behavior. The main
+purpose of this class is to provide a mapping between ONOS constructs and P4
+program-specific ones, for example methods to map ONOS well-known header fields
+and packet forwarding/manipulation actions to those defined in the P4 program.
+For a more detailed explanation of each method, check the
+[PipelineInterpreter interface](./core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java).
+
+* [PortStatisticsDiscoveryImpl.java](./pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java):
+Implementation of the `PortStatisticsDiscovery` ONOS driver behavior. As the
+name suggests, this behavior is used to report statistics on the switch ports to
+ONOS, e.g. number of packets/bytes received and transmitted for each port. This
+implementation works by reading the value of two P4 counters defined in
+`mytunnel.p4`, `tx_port_counter` and `rx_port_counter`.
+
+### MyTunnel App
+
+This app is used to provide connectivity between each pair of hosts via the
+MyTunnel protocol, a non-standard tunneling protocol created for this exercise.
+The implementation of this app can be found
+[here](./mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java),
+and it will be discussed in more details on Exercise 2.
+
+## Tutorial exercises
+
+### Exercise 1
+
+[Click here to go to this exercise instructions](./exercise-1.md)
+
+This exercise shows how to start ONOS and Mininet with BMv2, it also
+demonstrates connectivity between hosts using the pipeline-agnostic app
+Reactive Forwarding, in combination with other well known ONOS services such as
+Proxy ARP, Host Location Provider, and LLDP Link Discovery.
+
+### Exercise 2
+
+[Click here to go to this exercise instructions](./exercise-2.md)
+
+Similar to exercise 1, but here connectivity between hosts is demonstrated using
+pipeline-specific app "MyTunnel".
diff --git a/apps/p4-tutorial/exercise-1.md b/apps/p4-tutorial/exercise-1.md
new file mode 100644
index 0000000..631ea1f
--- /dev/null
+++ b/apps/p4-tutorial/exercise-1.md
@@ -0,0 +1,372 @@
+# ONOS+P4 Tutorial: Exercise 1
+
+The goal of this exercise is to introduce P4 and P4Runtime support in ONOS,
+along with the tools to practically experiment with it. In this exercise we will
+see how the ONOS "pipeconf" mechanism allow one to re-use existing ONOS apps to
+provide  basic forwarding capabilities in a pipeline-agnostic manner, i.e.
+independently of the P4 program.
+
+To run this exercise you will need multiple terminal windows (or tabs) to
+operate with the CLI of Mininet, ONOS, and BMv2. We use the following convention
+to distinguish between commands of different CLIs:
+
+* Commands starting with `$` are intended to be executed in the Ubuntu terminal
+    prompt;
+* `onos>` for commands in the ONOS CLI;
+* `mininet>` for the Mininet CLI;
+* `RuntimeCmd:` for the BMv2 CLI.
+
+## Exercise steps
+
+1. On terminal window 1, **start ONOS with a small subset of the apps**
+by executing the following command:
+
+    ```
+    $ cd $ONOS_ROOT
+    $ ONOS_APPS=proxyarp,hostprovider,lldpprovider ok clean
+    ```
+
+    The `$ONOS_ROOT` environment variable points to the root ONOS directory. The
+    `ok` command is an alias to run ONOS locally in your dev machine. Please
+    note that if this the first time you run ONOS on this machine, or if you
+    haven't built ONOS before, it can take some time (5-10 minutes depending on
+    your Internet speed).
+
+    Once ONOS has started you should see log messages being print on the screen.
+
+2. On terminal window 2, **activate the BMv2 driver and tutorial pipeconf** via
+    the ONOS CLI.
+
+    1. Use the following command to **access the ONOS CLI**:
+
+        ```
+        $ onos localhost
+        ```
+
+        You should now see the ONOS CLI command prompt. For a list of possible
+        commands that you can use here, type:
+
+        ```
+        onos> help onos
+        ```
+
+    2. Enter the following command to **activate the BMv2 driver**:
+
+        ```
+        onos> app activate org.onosproject.drivers.bmv2
+        ```
+
+        You should see the following message on the ONOS log:
+
+        ```
+        Application org.onosproject.drivers.bmv2 has been activated
+        ```
+
+    3. Enter the following command to **activate the pipeconf**:
+
+        ```
+        onos> app activate org.onosproject.p4tutorial.pipeconf
+        ```
+
+        You should see the following messages on the log:
+
+        ```
+        New pipeconf registered: p4-tutorial-pipeconf
+        Application org.onosproject.p4tutorial.pipeconf has been activated
+        ```
+
+        Please note the specific name used for this pipeconf `p4-tutorial-pipeconf`. We
+        will later use this name to tell ONOS to deploy that specific P4 program
+        to the switches.
+
+    4. To **verify that you have activated all the required apps**, run the
+        following command:
+
+        ```
+        onos> apps -a -s
+        ```
+
+        Make sure you see the following list of apps displayed:
+
+        ```
+        org.onosproject.generaldeviceprovider ... General Device Provider
+        org.onosproject.drivers               ... Default Drivers
+        org.onosproject.proxyarp              ... Proxy ARP/NDP
+        org.onosproject.lldpprovider          ... LLDP Link Provider
+        org.onosproject.protocols.grpc        ... gRPC Protocol Subsystem
+        org.onosproject.protocols.p4runtime   ... P4Runtime Protocol Subsystem
+        org.onosproject.p4runtime             ... P4Runtime Provider
+        org.onosproject.drivers.p4runtime     ... P4Runtime Drivers
+        org.onosproject.hostprovider          ... Host Location Provider
+        org.onosproject.drivers.bmv2          ... BMv2 Drivers
+        org.onosproject.p4tutorial.pipeconf   ... P4 Tutorial Pipeconf
+        ```
+
+    5. (optional) **Change flow rule polling interval**. Run the following
+        command in the ONOS CLI:
+
+        ```
+        onos> cfg set org.onosproject.net.flow.impl.FlowRuleManager fallbackFlowPollFrequency 5
+        ```
+
+        This command tells ONOS to check the state of flow rules on switches
+        every 5 seconds (default is 30). This is used to obtain more often flow
+        rules stats such as byte/packet counters. It helps also resolving more
+        quickly issues where some flow rules are installed in the ONOS store but
+        not on the device (which can often happen when emulating a large number
+        of devices in the same VM).
+
+3. On terminal window 3, **run Mininet to set up a topology of BMv2 devices**.
+
+    1. To **run Mininet**, use the following command:
+
+        ```
+        $ sudo -E mn --custom $BMV2_MN_PY --switch onosbmv2,pipeconf=p4-tutorial-pipeconf --controller remote,ip=127.0.0.1
+        ```
+
+        The `--custom` argument tells Mininet to use the `bmv2.py` custom script
+        to execute the BMv2 switch. The environment variable `$BMV2_MN_PY`
+        points to the exact location of the script (you can use the command
+        `echo $BMV2_MN_PY` to find out the location).
+
+        The `--switch` argument specifies the kind of switch instance we want to
+        run inside Mininet. In this case we are running a version of BMv2 that
+        also produces some configuration files used by ONOS to discover the
+        device (see steps below), hence the name `onosbmv2`. The `pipeconf`
+        sub-argument is used to tell ONOS which pipeconf to deploy on all
+        devices.
+
+        The `--controller` argument specifies the address of the controller,
+        ONOS in this case, which is running on the same machine where we are
+        executing Mininet.
+
+    2. A set of **files are generated in the `/tmp` folder as part of this
+        startup process**, to view them (on a separate terminal window):
+
+        ```
+        $ ls /tmp
+        ```
+
+    3. You will **find ONOS netcfg JSON files in this folder** for each BMv2
+        switch, open this file up, for example:
+
+        ```
+        $ cat /tmp/bmv2-s1-netcfg.json
+        ```
+
+        It contains the configuration for (1) the gRPC server and port used by the
+        BMv2 switch process for the P4Runtime service, (2) the ID of pipeconf to
+        deploy on the device, (3) switch ports details, and other
+        driver-specific information.
+
+        **This file is pushed to ONOS automatically by Mininet when executing
+        the switch instance**. If everything went as expected, you should see
+        the ONOS log populating with messages like:
+
+        ```
+        Connecting to device device:bmv2:s1 with driver bmv2
+        [...]
+        Setting pipeline config for device:bmv2:s1 to p4-tutorial-pipeconf...
+        [...]
+        Device device:bmv2:s1 connected
+        [...]
+        ```
+
+    4. **Check the BMv2 switch instance log**:
+
+        ```
+        $ less /tmp/bmv2-s1-log
+        ```
+
+        By scrolling the BMv2 log, you should see all P4Runtime messages
+        received by the switch. These messages are sent by ONOS and are used to
+        install table entries and to read counters. You should also see many
+        `PACKET_IN` and `PACKET_OUT` messages corresponding to packet-in/out
+        processed by the switch and used for LLDP-based link discovery.
+
+        Table entry messages are generated by ONOS according to the flow rules
+        generated by each app and based on the P4Info associated with
+        the `p4-tutorial-pipeconf`.
+
+        If you prefer to watch the BMv2 log updating in real time, you can use
+        the following command to print on screen all new messages:
+
+        ```
+        $ bm-log s1
+        ```
+
+        This command will show the log of the BMv2 switch in Mininet with name
+        "s1".
+
+        If needed, you can run BMv2 with **debug logging** enabled by passing
+        the sub-argument `loglevel=debug` when starting Mininet. For example:
+
+        ```
+        $ sudo -E mn [...] --switch onosbmv2,loglevel=debug,pipeconf=p4-tutorial-pipeconf [...]
+        ```
+
+        Debug logging in BMv2 is useful to observe the life of a packet inside the
+        pipeline, e.g. showing the header fields extracted by the parser for a
+        specific packet, the tables used to process the packets, matching table
+        entries (if any), etc.
+
+    5. **Check the flow rules inserted by each app in ONOS**. In the
+        ONOS CLI type:
+
+        ```
+        onos> flows -s
+        ```
+
+        You should see 3 flow rules:
+
+        ```
+        deviceId=device:bmv2:s1, flowRuleCount=3
+            ADDED, bytes=0, packets=0, table=0, priority=40000, selector=[ETH_TYPE:arp], treatment=[immediate=[OUTPUT:CONTROLLER], clearDeferred]
+            ADDED, bytes=0, packets=0, table=0, priority=40000, selector=[ETH_TYPE:bddp], treatment=[immediate=[OUTPUT:CONTROLLER], clearDeferred]
+            ADDED, bytes=0, packets=0, table=0, priority=40000, selector=[ETH_TYPE:lldp], treatment=[immediate=[OUTPUT:CONTROLLER], clearDeferred]
+        ```
+
+        These flow rules are installed automatically for each device by the
+        Proxy ARP and LLDP Link Discovery apps. The first one is used to
+        intercept ARP requests (`selector=[ETH_TYPE:arp]`), which are sent to
+        the controller (`treatment=[immediate=[OUTPUT:CONTROLLER]`), who in turn
+        will reply with an ARP response or broadcast the requests to all hosts.
+        The other two flow rules are used to intercept LLDP and BBDP packets
+        (for the purpose of link discovery).
+
+        These flow rules appear to be installed on table "0". This is a logical
+        table number mapped by the pipeconf's interpreter to the P4 table named
+        `t_l2_fwd` in [mytunnel.p4 (line 191)](./pipeconf/src/main/resources/mytunnel.p4#L191).
+
+        This mapping is defined in
+        [PipelineInterpreterImpl.java (line 103)](./pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java#L103)
+
+    6. **Compare ONOS flow rules to the table entries installed on the BMv2
+        switch**.
+
+        We can **use the BMv2 CLI to dump all table entries currently
+            installed on the switch**. On a separate terminal window type:
+
+        ```
+         $ bm-cli s1
+        ```
+
+        This command will start the CLI for the BMv2 switch in Mininet with name
+        "s1".
+
+        On the BMv2 CLI prompt, type the following command:
+
+        ```
+        RuntimeCmd: table_dump c_ingress.t_l2_fwd
+        ```
+
+        You should see exactly 3 entries, each one corresponding to a flow rule
+        in ONOS. For example, the flow rule matching on ARP packets should look
+        like this in the BMv2 CLI:
+
+        ```
+        ...
+        **********
+        Dumping entry 0x2000002
+        Match key:
+        * standard_metadata.ingress_port: TERNARY   0000 &&& 0000
+        * ethernet.dst_addr             : TERNARY   000000000000 &&& 000000000000
+        * ethernet.src_addr             : TERNARY   000000000000 &&& 000000000000
+        * ethernet.ether_type           : TERNARY   0806 &&& ffff
+        Priority: 16737216
+        Action entry: c_ingress.send_to_cpu -
+        ...
+        ```
+
+        Note how the ONOS selector `[ETH_TYPE:arp]` has been translated to an
+        entry matching only the header field `ethernet.ether_type`, while the
+        bits of all other fields are set as "don't care" (mask is all zeros).
+        While the ONOS treatment `[OUTPUT:CONTROLLER]` has been translated to
+        the action `c_ingress.send_to_cpu`.
+
+        **Important:** The BMv2 CLI is a powerful tool to debug the state of a
+        BMv2 switch. Type `help` to show a list of possible commands. This CLI
+        provides also auto-completion when pressing the `tab` key.
+
+4. It is finally time to **test connectivity between the hosts** of our Mininet
+    network.
+
+    1. On the Mininet prompt, **start a ping between host1 and host2**:
+
+        ```
+        mininet> h1 ping h2
+        ```
+
+        The **ping should NOT work**, and the reason is that we did not activate
+        yet any ONOS app providing connectivity between hosts.
+
+    2. While leaving the ping running on Mininet, **activate the Reactive
+        Forwarding app using the ONOS CLI**:
+
+        ```
+        onos> app activate org.onosproject.fwd
+        ```
+
+        Once activated, you should see the the ping working. Indeed, this
+        app installs the necessary flow rules to forward packets between
+        the two hosts.
+
+    3. Use steps 3.v and 3.vi to **check the new flow rules**.
+
+        You should see 3 new flow rules.
+
+        The Reactive Forwarding app works in the following way. It installs a
+        low priority flow rule to intercepts all IPv4 packets via a
+        `send_to_cpu` action (`[OUTPUT:CONTROLLER]` in ONOS). When a packet is
+        received by the control plane, the packet is processed by the app, which
+        in turn, by querying the Topology service and the Host Location service
+        is ONOS, computes the shortest path between the two hosts, and installs
+        higher priority flow rules on each hop to forward packets between the
+        two hosts (after having re-injected that packet in the network via a
+        packet-out).
+
+5. Congratulations, you completed the first exercise of the ONOS+P4 tutorial!
+
+    To kill ONOS, press `ctrl-c` in the ONOS log terminal window. To kill
+    Mininet, press `ctrl-d` in the Mininet CLI or type `exit`.
+
+## Bonus exercise
+
+As a bonus exercise, you can re-run Mininet with a larger topology to see how
+Exercise 1 works with a more complex topology.
+
+1. Rerun the steps in Exercise 1, replacing step 3.i. with the following:
+
+    ```
+    $ sudo -E mn --custom $BMV2_MN_PY --switch onosbmv2,pipeconf=p4-tutorial-pipeconf --topo tree,3 --controller remote,ip=127.0.0.1
+    ```
+
+    By using the argument `--topo` we are telling Mininet to emulate a Tree
+    topology with depth 3, i.e. with 7 switches, 6 links, and 8 hosts.
+
+    **Important:** due to the limited resources of the VM, when executing many
+    switches in Mininet, it might happen that some flow rules are not installed
+    correctly on the switch (showing state `PENDING_ADD` when using ONOS command
+    `flows`). In this case, ONOS provides an automatic reconciliation mechanism
+    that tries to re-install the failed entries. To force ONOS to perform this
+    process more often, **make sure to apply step 2.v before starting Mininet**.
+
+2. After you activate the Reactive Forwarding app as in step 4.ii.,
+    you can ping all hosts in the network using the following Mininet command:
+
+    ```
+    mininet> pingall
+    ```
+
+    If everything went well, ping should work for every host pair in the
+    network.
+
+3. You can visualize the topology using the ONOS web UI.
+
+    Open a browser from within the tutorial VM (e.g. Firefox) to
+    <http://127.0.0.1:8181/onos/ui/>. When asked, use the username `onos`
+    and password `rocks`. You should see a nice tree topology.
+
+    While here, feel free to interact with and discover the ONOS UI. For more
+    information on how to use the ONOS web UI please refer to this guide:
+    <https://wiki.onosproject.org/x/OYMg>
diff --git a/apps/p4-tutorial/exercise-2.md b/apps/p4-tutorial/exercise-2.md
new file mode 100644
index 0000000..a2a4b1c
--- /dev/null
+++ b/apps/p4-tutorial/exercise-2.md
@@ -0,0 +1,245 @@
+# Exercise 2 (ONOS+P4 Tutorial)
+
+The goal of this exercise is to demonstrate how ONOS apps can be used to
+control any P4-defined pipeline, even those implementing custom non-standard
+protocols.
+
+## Overview
+
+Similarly to exercise 1, in this example we want to provide connectivity between
+hosts of a network when using switches programmed with the `mytunnel.p4`
+program. Differently from exercise 1, forwarding between hosts will be provided
+by the MyTunnel app, instead of Reactive Forwarding. The MyTunnel app provides
+connectivity by programming the data plane to forward packets using the MyTunnel
+protocol.
+
+Before starting, we suggest to open the `onos/apps/p4-tutorial` directory in
+your editor of choice for an easier access to the different files of this
+exercise. For example, if using the Atom editor:
+
+```
+$ atom $ONOS_ROOT/apps/p4-tutorial/
+```
+
+## Protocol overview
+
+The MyTunnel protocol works by encapsulating IPv4 frames into a MyTunnel header
+defined as following:
+
+```
+header my_tunnel_t {
+    bit<16> proto_id; /* EtherType of the original
+                         unencapsulated Ethernet frame */
+    bit<32> tun_id;   /* Arbitrary tunnel identifier uniquelly
+                         representing the egress endpoint of the tunnel */
+}
+```
+
+A switch implementing the MyTunnel protocol can forward packets using three
+different forwarding behaviors.
+
+1. **Ingress**: for IPv4 packets received at a edge switch, i.e. the first node
+in the tunnel path, the MyTunnel header is applied with an arbitrary tunnel
+identifier decided by the control plane.
+
+2. **Transit**: for packets with the MyTunnel header processed by an
+intermediate node in the tunnel path. When operating in this mode, the switch
+simply forwards the packet by looking at the tunnel ID field.
+
+3. **Egress**: for packets with the MyTunnel header processed by the last node
+in the path, the switch removes the MyTunnel header before forwarding the packet
+to the output port.
+
+## MyTunnel pipeline overview
+
+The three forwarding behaviors described before can be achieved by inserting
+entries in two different tables of `mytunnel.p4`, namely `t_tunnel_ingress` and
+`t_tunnel_fwd`.
+
+* `t_tunnel_ingress`: this table is used to implement the ingress behavior. It
+matches on the IPv4 destination address (longest-prefix match), and provides
+the `my_tunnel_ingress` action, which encapsulates the packet in the MyTunnel
+header with a given tunnel ID (action parameter).
+
+* `t_tunnel_fwd`: this table is used to implement both the transit and egress
+behaviors. It matches on the tunnel ID, and allows two different actions,
+`set_out_port` and `my_tunnel_egress`. `set_out_port` is used to set the
+output port where the packet should be transmitted without further
+modifications. With `my_tunnel_egress`, the packet is stripped of the MyTunnel
+header before setting the output port.
+
+## MyTunnel app overview
+
+To begin, open
+[MyTunnelApp.java](./mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java)
+in your editor of choice, and familiarize with the app implementation.
+
+For example, if using the Atom editor:
+
+```
+$ atom $ONOS_ROOT/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java
+```
+
+The MyTunnel app works by registering an event listener with the ONOS Host
+Service (`class InternalHostListener` at line 308). This listener is used to
+notify the MyTunnel app every time a new host is discovered. Host discovery is
+performed by means of two ONOS core services: Host Location Provider and
+Proxy-ARP app. Each time an ARP request is received (via packet-in), ONOS learns
+the location of the sender of the ARP request, before generating an ARP reply or
+forwarding the requests to other hosts. When learning the location of a new
+host, ONOS informs all apps that have registered a listener with an `HOST_ADDED`
+event.
+
+Once an `HOST_ADDED` event is notified to the MyTunnel app, this creates two
+unidirectional tunnels between that host and any other host previously
+discovered. For each tunnel, the app computes the shortest path between the two
+hosts (method `provisionTunnel` at line 128), and for each switch in the path it
+installs flow rules for the `t_tunnel_ingress` table (method
+`insertTunnelIngressRule` at line 182), and/or the `t_tunnel_fwd` table (method
+`insertTunnelForwardRule` at line 219), depending on the position of the switch
+in the path, the app will install rule to perform the ingress, transit, or
+egress behaviors.
+
+## Exercise steps
+
+1. **Complete the implementation of the MyTunnel app**:
+
+    1. Open [MyTunnelApp.java](./mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java) in your editor of choice.
+
+    2. Look for the `insertTunnelForwardRule` method (line 219).
+
+    3. Complete the implementation of this method (There's a `TODO EXERCISE`
+    comment at line 251).
+
+        **Spoiler alert:** There is a reference solution in the same directory
+        as MyTunnelApp.java. Feel free to compare your implementation to the
+        reference one.
+
+2. **Start ONOS with and all the apps**.
+
+    1. On a first terminal window, start ONOS:
+
+        ```
+        $ cd $ONOS_ROOT
+        $ ONOS_APPS=proxyarp,hostprovider,lldpprovider ok clean
+        ```
+
+    2. On a second terminal window to **access the ONOS CLI**:
+
+        ```
+        $ onos localhost
+        ```
+
+    2. **Acticate the BMv2 drivers, pipeconf, and MyTunnel app**:
+
+        ```
+        onos> app activate org.onosproject.drivers.bmv2
+        onos> app activate org.onosproject.p4tutorial.pipeconf
+        onos> app activate org.onosproject.p4tutorial.mytunnel
+        ```
+
+        **Hint:** To avoid accessing the CLI to start all applications, you can
+        modify the value of the `ONOS_APPS` variable when starting ONOS. For
+        example:
+
+        ```
+        $ cd $ONOS_ROOT
+        $ ONOS_APPS=proxyarp,hostprovider,lldpprovider,drivers.bmv2,p4tutorial.pipeconf,p4tutorial.mytunnel ok clean
+        ```
+
+    3. **Check that all apps have been activated successfully**:
+
+        ```
+        onos> apps -s -a
+        ```
+
+        You should see an output like this:
+
+        ```
+        org.onosproject.hostprovider          ... Host Location Provider
+        org.onosproject.lldpprovider          ... LLDP Link Provider
+        org.onosproject.proxyarp              ... Proxy ARP/NDP
+        org.onosproject.drivers               ... Default Drivers
+        org.onosproject.protocols.grpc        ... gRPC Protocol Subsystem
+        org.onosproject.protocols.p4runtime   ... P4Runtime Protocol Subsystem
+        org.onosproject.p4runtime             ... P4Runtime Provider
+        org.onosproject.generaldeviceprovider ... General Device Provider
+        org.onosproject.drivers.p4runtime     ... P4Runtime Drivers
+        org.onosproject.p4tutorial.pipeconf   ... P4 Tutorial Pipeconf
+        org.onosproject.pipelines.basic       ... Basic Pipelines
+        org.onosproject.protocols.gnmi        ... gNMI Protocol Subsystem
+        org.onosproject.drivers.gnmi          ... gNMI Drivers
+        org.onosproject.drivers.bmv2          ... BMv2 Drivers
+        org.onosproject.p4tutorial.mytunnel   ... MyTunnel Demo App
+        ```
+
+    4. (optional) **Change flow rule polling interval**. Run the following
+    command in the ONOS CLI:
+
+        ```
+        onos> cfg set org.onosproject.net.flow.impl.FlowRuleManager fallbackFlowPollFrequency 5
+        ```
+
+3. **Run Mininet to set up a tree topology of BMv2 devices**, on a new terminal
+window type:
+
+    ```
+    $ sudo -E mn --custom $BMV2_MN_PY --switch onosbmv2,pipeconf=p4-tutorial-pipeconf --topo tree,3 --controller remote,ip=127.0.0.1
+    ```
+
+4. **Check that all devices, link, and hosts have been discovered correctly in ONOS**.
+
+    1. To check the devices, on the ONOS CLI, type:
+
+        ```
+        onos> devices -s
+        ```
+
+        The `-s` argument provides a more compact output.
+
+        You should see 7 devices in total. Please note the driver that has been
+        assigned to this device `bmv2:p4-tutorial-pipeconf`. It means that the
+        device is being controlled using the driver behaviors provided the BMv2
+        device driver (which uses P4Runtime) and the pipeconf.
+
+    2. Check the links:
+
+        ```
+        onos> links
+        ```
+
+        The `-s` argument provides a more compact output.
+
+        You should see 12 links (the topology has 6 bidirectional links in total).
+
+    3. Check the hosts:
+
+        ```
+        onos> hosts -s
+        ```
+
+        You should see 0 hosts, as we have not injected any ARP packet yet.
+
+5. **Ping hosts**, on the Mininet CLI, type:
+
+    ```
+    mininet> h1 ping h7
+    ```
+
+    If the implementation of MyTunnelApp.java has been completed correctly,
+    ping should work. If not, check the ONOS log for possible errors in the
+    MyTunnel app. As a last resort, please check the reference solution in
+    the same directory as MyTunnelApp.java and compare that to yours.
+
+6. **Look around**.
+
+    1. Repeat step 3.v and 3.vi from exercise one to check the
+flow rules in ONOS and on BMv2.
+
+    2. Check the hosts in ONOS:
+
+        ```
+        onos> hosts -s
+        ```
+
+        You should see 2 hosts, h1 and h7.
diff --git a/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java b/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java
index eb91243..5acc434 100644
--- a/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java
+++ b/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.java
@@ -55,7 +55,6 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -80,35 +79,7 @@
     private static final Logger log = getLogger(MyTunnelApp.class);
 
     //--------------------------------------------------------------------------
-    // P4 program entity names. Values derived from mytunnel.p4info
-    //--------------------------------------------------------------------------
-
-    // Match field IDs.
-    private static final PiMatchFieldId MF_ID_MY_TUNNEL_DST_ID =
-            PiMatchFieldId.of("hdr.my_tunnel.tun_id");
-    private static final PiMatchFieldId MF_ID_IP_DST =
-            PiMatchFieldId.of("hdr.ipv4.dst_addr");
-    // Table IDs.
-    private static final PiTableId TBL_ID_TUNNEL_FWD =
-            PiTableId.of("c_ingress.t_tunnel_fwd");
-    private static final PiTableId TBL_ID_TUNNEL_INGRESS =
-            PiTableId.of("c_ingress.t_tunnel_ingress");
-    // Action IDs.
-    private static final PiActionId ACT_ID_MY_TUNNEL_INGRESS =
-            PiActionId.of("c_ingress.my_tunnel_ingress");
-    private static final PiActionId ACT_ID_SET_OUT_PORT =
-            PiActionId.of("c_ingress.set_out_port");
-    private static final PiActionId ACT_ID_MY_TUNNEL_EGRESS =
-            PiActionId.of("c_ingress.my_tunnel_egress");
-
-    // Action params ID.
-    private static final PiActionParamId ACT_PARAM_ID_TUN_ID =
-            PiActionParamId.of("tun_id");
-    private static final PiActionParamId ACT_PARAM_ID_PORT =
-            PiActionParamId.of("port");
-
-    //--------------------------------------------------------------------------
-    // ONOS services needed by this application.
+    // ONOS core services needed by this application.
     //--------------------------------------------------------------------------
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -181,20 +152,20 @@
             // In ONOS discovered hosts can have multiple IP addresses.
             // Insert tunnel ingress rule for each IP address.
             // Next switches will forward based only on tunnel ID.
-            insertIngressRule(srcSwitch, dstIpAddr, tunId);
+            insertTunnelIngressRule(srcSwitch, dstIpAddr, tunId);
         }
 
-        // Insert tunnel forward rules on all switches in the path, excluded the
+        // Insert tunnel transit rules on all switches in the path, excluded the
         // last one.
         for (Link link : pathLinks) {
             DeviceId sw = link.src().deviceId();
             PortNumber port = link.src().port();
-            insertForwardRule(sw, port, tunId, false);
+            insertTunnelForwardRule(sw, port, tunId, false);
         }
 
         // Tunnel egress rule.
         PortNumber egressSwitchPort = dstHost.location().port();
-        insertForwardRule(dstSwitch, egressSwitchPort, tunId, true);
+        insertTunnelForwardRule(dstSwitch, egressSwitchPort, tunId, true);
 
         log.info("** Completed provisioning of tunnel {} (srcHost={} dstHost={})",
                  tunId, srcHost.id(), dstHost.id());
@@ -208,24 +179,31 @@
      * @param dstIpAddr IP address to forward inside the tunnel
      * @param tunId     tunnel ID
      */
-    private void insertIngressRule(DeviceId switchId,
-                                   IpAddress dstIpAddr,
-                                   int tunId) {
+    private void insertTunnelIngressRule(DeviceId switchId,
+                                         IpAddress dstIpAddr,
+                                         int tunId) {
 
-        log.info("Inserting INGRESS rule: switchId={}, dstIpAddr={}, tunId={}",
-                 switchId, dstIpAddr, tunId);
 
+        PiTableId tunnelIngressTableId = PiTableId.of("c_ingress.t_tunnel_ingress");
+
+        // Longest prefix match on IPv4 dest address.
+        PiMatchFieldId ipDestMatchFieldId = PiMatchFieldId.of("hdr.ipv4.dst_addr");
         PiCriterion match = PiCriterion.builder()
-                .matchLpm(MF_ID_IP_DST, dstIpAddr.toOctets(), 32)
+                .matchLpm(ipDestMatchFieldId, dstIpAddr.toOctets(), 32)
                 .build();
 
+        PiActionParam tunIdParam = new PiActionParam(PiActionParamId.of("tun_id"), tunId);
+
+        PiActionId ingressActionId = PiActionId.of("c_ingress.my_tunnel_ingress");
         PiAction action = PiAction.builder()
-                .withId(ACT_ID_MY_TUNNEL_INGRESS)
-                .withParameter(new PiActionParam(
-                        ACT_PARAM_ID_TUN_ID, copyFrom(tunId)))
+                .withId(ingressActionId)
+                .withParameter(tunIdParam)
                 .build();
 
-        insertPiFlowRule(switchId, TBL_ID_TUNNEL_INGRESS, match, action);
+        log.info("Inserting INGRESS rule on switch {}: table={}, match={}, action={}",
+                 switchId, tunnelIngressTableId, match, action);
+
+        insertPiFlowRule(switchId, tunnelIngressTableId, match, action);
     }
 
     /**
@@ -234,31 +212,59 @@
      *
      * @param switchId switch ID
      * @param outPort  output port where to forward tunneled packets
-     * @param tunId tunnel ID
+     * @param tunId    tunnel ID
      * @param isEgress if true, perform tunnel egress action, otherwise forward
      *                 packet as is to port
      */
-    private void insertForwardRule(DeviceId switchId,
-                                   PortNumber outPort,
-                                   int tunId,
-                                   boolean isEgress) {
+    private void insertTunnelForwardRule(DeviceId switchId,
+                                         PortNumber outPort,
+                                         int tunId,
+                                         boolean isEgress) {
 
-        log.info("Inserting {} rule: switchId={}, outPort={}, tunId={}",
-                 isEgress ? "EGRESS" : "FORWARD", switchId, outPort, tunId);
+        PiTableId tunnelForwardTableId = PiTableId.of("c_ingress.t_tunnel_fwd");
 
+        // Exact match on tun_id
+        PiMatchFieldId tunIdMatchFieldId = PiMatchFieldId.of("hdr.my_tunnel.tun_id");
         PiCriterion match = PiCriterion.builder()
-                .matchExact(MF_ID_MY_TUNNEL_DST_ID, tunId)
+                .matchExact(tunIdMatchFieldId, tunId)
                 .build();
 
-        PiActionId actionId = isEgress ? ACT_ID_MY_TUNNEL_EGRESS : ACT_ID_SET_OUT_PORT;
+        // Action depend on isEgress parameter.
+        // if true, perform tunnel egress action on the given outPort, otherwise
+        // simply forward packet as is (set_out_port action).
+        PiActionParamId portParamId = PiActionParamId.of("port");
+        PiActionParam portParam = new PiActionParam(portParamId, (short) outPort.toLong());
 
-        PiAction action = PiAction.builder()
-                .withId(actionId)
-                .withParameter(new PiActionParam(
-                        ACT_PARAM_ID_PORT, copyFrom((short) outPort.toLong())))
-                .build();
+        final PiAction action;
+        if (isEgress) {
+            // Tunnel egress action.
+            // Remove MyTunnel header and forward to outPort.
+            PiActionId egressActionId = PiActionId.of("c_ingress.my_tunnel_egress");
+            action = PiAction.builder()
+                    .withId(egressActionId)
+                    .withParameter(portParam)
+                    .build();
+        } else {
+            // Tunnel transit action.
+            // Forward the packet as is to outPort.
+            /*
+             * TODO EXERCISE: create action object for the transit case.
+             * Look at the t_tunnel_fwd table in the P4 program. Which of the 3
+             * actions can be used to simply set the output port? Get the full
+             * action name from the P4Info file, and use that when creating the
+             * PiActionId object. When creating the PiAction object, remember to
+             * add all action parameters as defined in the P4 program.
+             *
+             * Hint: the code will be similar to the case when isEgress = false.
+             */
+            action = null;
+        }
 
-        insertPiFlowRule(switchId, TBL_ID_TUNNEL_FWD, match, action);
+        log.info("Inserting {} rule on switch {}: table={}, match={}, action={}",
+                 isEgress ? "EGRESS" : "TRANSIT",
+                 switchId, tunnelForwardTableId, match, action);
+
+        insertPiFlowRule(switchId, tunnelForwardTableId, match, action);
     }
 
     /**
diff --git a/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.solution b/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.solution
new file mode 100644
index 0000000..ab28e52
--- /dev/null
+++ b/apps/p4-tutorial/mytunnel/src/main/java/org/onosproject/p4tutorial/mytunnel/MyTunnelApp.solution
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.p4tutorial.mytunnel;
+
+import com.google.common.collect.Lists;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.IpAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.criteria.PiCriterion;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.pi.model.PiActionId;
+import org.onosproject.net.pi.model.PiActionParamId;
+import org.onosproject.net.pi.model.PiMatchFieldId;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.topology.Topology;
+import org.onosproject.net.topology.TopologyService;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * MyTunnel application which provides forwarding between each pair of hosts via
+ * MyTunnel protocol as defined in mytunnel.p4.
+ * <p>
+ * The app works by listening for host events. Each time a new host is
+ * discovered, it provisions a tunnel between that host and all the others.
+ */
+@Component(immediate = true)
+public class MyTunnelApp {
+
+    private static final String APP_NAME = "org.onosproject.p4tutorial.mytunnel";
+
+    // Default priority used for flow rules installed by this app.
+    private static final int FLOW_RULE_PRIORITY = 100;
+
+    private final HostListener hostListener = new InternalHostListener();
+    private ApplicationId appId;
+    private AtomicInteger nextTunnelId = new AtomicInteger();
+
+    private static final Logger log = getLogger(MyTunnelApp.class);
+
+    //--------------------------------------------------------------------------
+    // ONOS services needed by this application.
+    //--------------------------------------------------------------------------
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private FlowRuleService flowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private TopologyService topologyService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private HostService hostService;
+
+    //--------------------------------------------------------------------------
+    //--------------------------------------------------------------------------
+
+    @Activate
+    public void activate() {
+        // Register app and event listeners.
+        log.info("Starting...");
+        appId = coreService.registerApplication(APP_NAME);
+        hostService.addListener(hostListener);
+        log.info("STARTED", appId.id());
+    }
+
+    @Deactivate
+    public void deactivate() {
+        // Remove listeners and clean-up flow rules.
+        log.info("Stopping...");
+        hostService.removeListener(hostListener);
+        flowRuleService.removeFlowRulesById(appId);
+        log.info("STOPPED");
+    }
+
+    /**
+     * Provisions a tunnel between the given source and destination host with
+     * the given tunnel ID. The tunnel is established using a randomly picked
+     * shortest path based on the given topology snapshot.
+     *
+     * @param tunId   tunnel ID
+     * @param srcHost tunnel source host
+     * @param dstHost tunnel destination host
+     * @param topo    topology snapshot
+     */
+    private void provisionTunnel(int tunId, Host srcHost, Host dstHost, Topology topo) {
+
+        // Get all shortest paths between switches connected to source and
+        // destination hosts.
+        DeviceId srcSwitch = srcHost.location().deviceId();
+        DeviceId dstSwitch = dstHost.location().deviceId();
+
+        List<Link> pathLinks;
+        if (srcSwitch.equals(dstSwitch)) {
+            // Source and dest hosts are connected to the same switch.
+            pathLinks = Collections.emptyList();
+        } else {
+            // Compute shortest path.
+            Set<Path> allPaths = topologyService.getPaths(topo, srcSwitch, dstSwitch);
+            if (allPaths.size() == 0) {
+                log.warn("No paths between {} and {}", srcHost.id(), dstHost.id());
+                return;
+            }
+            // If many shortest paths are available, pick a random one.
+            pathLinks = pickRandomPath(allPaths).links();
+        }
+
+        // Tunnel ingress rules.
+        for (IpAddress dstIpAddr : dstHost.ipAddresses()) {
+            // In ONOS discovered hosts can have multiple IP addresses.
+            // Insert tunnel ingress rule for each IP address.
+            // Next switches will forward based only on tunnel ID.
+            insertTunnelIngressRule(srcSwitch, dstIpAddr, tunId);
+        }
+
+        // Insert tunnel transit rules on all switches in the path, excluded the
+        // last one.
+        for (Link link : pathLinks) {
+            DeviceId sw = link.src().deviceId();
+            PortNumber port = link.src().port();
+            insertTunnelForwardRule(sw, port, tunId, false);
+        }
+
+        // Tunnel egress rule.
+        PortNumber egressSwitchPort = dstHost.location().port();
+        insertTunnelForwardRule(dstSwitch, egressSwitchPort, tunId, true);
+
+        log.info("** Completed provisioning of tunnel {} (srcHost={} dstHost={})",
+                 tunId, srcHost.id(), dstHost.id());
+    }
+
+    /**
+     * Generates and insert a flow rule to perform the tunnel INGRESS function
+     * for the given switch, destination IP address and tunnel ID.
+     *
+     * @param switchId  switch ID
+     * @param dstIpAddr IP address to forward inside the tunnel
+     * @param tunId     tunnel ID
+     */
+    private void insertTunnelIngressRule(DeviceId switchId,
+                                         IpAddress dstIpAddr,
+                                         int tunId) {
+
+
+        PiTableId tunnelIngressTableId = PiTableId.of("c_ingress.t_tunnel_ingress");
+
+        // Longest prefix match on IPv4 dest address.
+        PiMatchFieldId ipDestMatchFieldId = PiMatchFieldId.of("hdr.ipv4.dst_addr");
+        PiCriterion match = PiCriterion.builder()
+                .matchLpm(ipDestMatchFieldId, dstIpAddr.toOctets(), 32)
+                .build();
+
+        PiActionParam tunIdParam = new PiActionParam(PiActionParamId.of("tun_id"), tunId);
+
+        PiActionId ingressActionId = PiActionId.of("c_ingress.my_tunnel_ingress");
+        PiAction action = PiAction.builder()
+                .withId(ingressActionId)
+                .withParameter(tunIdParam)
+                .build();
+
+        log.info("Inserting INGRESS rule on switch {}: table={}, match={}, action={}",
+                 switchId, tunnelIngressTableId, match, action);
+
+        insertPiFlowRule(switchId, tunnelIngressTableId, match, action);
+    }
+
+    /**
+     * Generates and insert a flow rule to perform the tunnel FORWARD/EGRESS
+     * function for the given switch, output port address and tunnel ID.
+     *
+     * @param switchId switch ID
+     * @param outPort  output port where to forward tunneled packets
+     * @param tunId    tunnel ID
+     * @param isEgress if true, perform tunnel egress action, otherwise forward
+     *                 packet as is to port
+     */
+    private void insertTunnelForwardRule(DeviceId switchId,
+                                         PortNumber outPort,
+                                         int tunId,
+                                         boolean isEgress) {
+
+        PiTableId tunnelForwardTableId = PiTableId.of("c_ingress.t_tunnel_fwd");
+
+        // Exact match on tun_id
+        PiMatchFieldId tunIdMatchFieldId = PiMatchFieldId.of("hdr.my_tunnel.tun_id");
+        PiCriterion match = PiCriterion.builder()
+                .matchExact(tunIdMatchFieldId, tunId)
+                .build();
+
+        // Action depend on isEgress parameter.
+        // if true, perform tunnel egress action on the given outPort, otherwise
+        // simply forward packet as is (set_out_port action).
+        PiActionParamId portParamId = PiActionParamId.of("port");
+        PiActionParam portParam = new PiActionParam(portParamId, (short) outPort.toLong());
+
+        final PiAction action;
+        if (isEgress) {
+            // Tunnel egress action.
+            // Remove MyTunnel header and forward to outPort.
+            PiActionId egressActionId = PiActionId.of("c_ingress.my_tunnel_egress");
+            action = PiAction.builder()
+                    .withId(egressActionId)
+                    .withParameter(portParam)
+                    .build();
+        } else {
+            // Tunnel transit action.
+            // Forward the packet as is to outPort.
+            PiActionId egressActionId = PiActionId.of("c_ingress.set_out_port");
+            action = PiAction.builder()
+                    .withId(egressActionId)
+                    .withParameter(portParam)
+                    .build();
+        }
+
+        log.info("Inserting {} rule on switch {}: table={}, match={}, action={}",
+                 isEgress ? "EGRESS" : "TRANSIT",
+                 switchId, tunnelForwardTableId, match, action);
+
+        insertPiFlowRule(switchId, tunnelForwardTableId, match, action);
+    }
+
+    /**
+     * Inserts a flow rule in the system that using a PI criterion and action.
+     *
+     * @param switchId    switch ID
+     * @param tableId     table ID
+     * @param piCriterion PI criterion
+     * @param piAction    PI action
+     */
+    private void insertPiFlowRule(DeviceId switchId, PiTableId tableId,
+                                  PiCriterion piCriterion, PiAction piAction) {
+        FlowRule rule = DefaultFlowRule.builder()
+                .forDevice(switchId)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withPriority(FLOW_RULE_PRIORITY)
+                .makePermanent()
+                .withSelector(DefaultTrafficSelector.builder()
+                                      .matchPi(piCriterion).build())
+                .withTreatment(DefaultTrafficTreatment.builder()
+                                       .piTableAction(piAction).build())
+                .build();
+        flowRuleService.applyFlowRules(rule);
+    }
+
+    private int getNewTunnelId() {
+        return nextTunnelId.incrementAndGet();
+    }
+
+    private Path pickRandomPath(Set<Path> paths) {
+        int item = new Random().nextInt(paths.size());
+        List<Path> pathList = Lists.newArrayList(paths);
+        return pathList.get(item);
+    }
+
+    /**
+     * A listener of host events that provisions two tunnels for each pair of
+     * hosts when a new host is discovered.
+     */
+    private class InternalHostListener implements HostListener {
+
+        @Override
+        public void event(HostEvent event) {
+            if (event.type() != HostEvent.Type.HOST_ADDED) {
+                // Ignore other host events.
+                return;
+            }
+            synchronized (this) {
+                // Synchronizing here is an overkill, but safer for demo purposes.
+                Host host = event.subject();
+                Topology topo = topologyService.currentTopology();
+                for (Host otherHost : hostService.getHosts()) {
+                    if (!host.equals(otherHost)) {
+                        provisionTunnel(getNewTunnelId(), host, otherHost, topo);
+                        provisionTunnel(getNewTunnelId(), otherHost, host, topo);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
index 58afa10..77a6dc3 100644
--- a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
@@ -93,7 +93,7 @@
     private static final PiActionId ACT_ID_SEND_TO_CPU =
             PiActionId.of(C_INGRESS + DOT + "send_to_cpu");
     private static final PiActionId ACT_ID_SET_EGRESS_PORT =
-            PiActionId.of(C_INGRESS + DOT + "set_egress_port");
+            PiActionId.of(C_INGRESS + DOT + "set_out_port");
 
     private static final PiActionParamId ACT_PARAM_ID_PORT =
             PiActionParamId.of("port");
diff --git a/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.p4 b/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.p4
index 7d73489..36de752 100644
--- a/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.p4
+++ b/apps/p4-tutorial/pipeconf/src/main/resources/mytunnel.p4
@@ -14,6 +14,15 @@
  * limitations under the License.
  */
 
+ /*
+  * This program describes a pipeline implementing a very simple
+  * tunneling protocol called MyTunnel. The pipeline defines also table called
+  * t_l2_fwd that provides basic L2 forwarding capabilities and actions to
+  * send packets to the controller. This table is needed to provide
+  * compatibility with existing ONOS applications such as Proxy-ARP, LLDP Link
+  * Discovery and Reactive Forwarding.
+  */
+
 #include <core.p4>
 #include <v1model.p4>
 
@@ -92,6 +101,9 @@
                   inout metadata_t meta,
                   inout standard_metadata_t standard_metadata) {
 
+    // A P4 parser is described as a state machine, with initial state "start"
+    // and final one "accept". Each intermediate state can specify the next
+    // state by using a select statement over the header fields extracted.
     state start {
         transition select(standard_metadata.ingress_port) {
             CPU_PORT: parse_packet_out;
@@ -150,6 +162,8 @@
     }
 
     action set_out_port(port_t port) {
+        // Specifies the output port for this packet by setting the
+        // corresponding metadata.
         standard_metadata.egress_spec = port;
     }
 
@@ -170,6 +184,8 @@
         hdr.my_tunnel.setInvalid();
     }
 
+    // Table counter used to count packets and bytes matched by each entry of
+    // t_l2_fwd table.
     direct_counter(CounterType.packets_and_bytes) l2_fwd_counter;
 
     table t_l2_fwd {
@@ -180,9 +196,9 @@
             hdr.ethernet.ether_type         : ternary;
         }
         actions = {
-            set_out_port();
-            send_to_cpu();
-            _drop();
+            set_out_port;
+            send_to_cpu;
+            _drop;
             NoAction;
         }
         default_action = NoAction();
@@ -195,7 +211,7 @@
         }
         actions = {
             my_tunnel_ingress;
-            _drop();
+            _drop;
         }
         default_action = _drop();
     }
@@ -207,12 +223,13 @@
         actions = {
             set_out_port;
             my_tunnel_egress;
-            _drop();
+            _drop;
         }
         default_action = _drop();
     }
 
-    // Define processing applied by this control block.
+    // Defines the processing applied by this control block. You can see this as
+    // the main function applied to every packet received by the switch.
     apply {
         if (standard_metadata.ingress_port == CPU_PORT) {
             // Packet received from CPU_PORT, this is a packet-out sent by the
@@ -223,6 +240,7 @@
             hdr.packet_out.setInvalid();
         } else {
             // Packet received from data plane port.
+            // Applies table t_l2_fwd to the packet.
             if (t_l2_fwd.apply().hit) {
                 // Packet hit an entry in t_l2_fwd table. A forwarding action
                 // has already been taken. No need to apply other tables, exit