ONOS-7000 P4 tutorial application and P4 program

Change-Id: Ia0a6befa6374a1950485c1fba0cfacb5ff4ce52c
diff --git a/apps/p4-tutorial/icmpdropper/BUCK b/apps/p4-tutorial/icmpdropper/BUCK
new file mode 100644
index 0000000..026827d
--- /dev/null
+++ b/apps/p4-tutorial/icmpdropper/BUCK
@@ -0,0 +1,25 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//apps/p4-tutorial/pipeconf:onos-apps-p4-tutorial-pipeconf',
+]
+
+osgi_jar (
+    deps = COMPILE_DEPS,
+)
+
+BUNDLES = [
+    '//apps/p4-tutorial/icmpdropper:onos-apps-p4-tutorial-icmpdropper',
+]
+
+onos_app (
+    app_name = 'org.onosproject.p4tutorial.icmpdropper',
+    title = 'ICMP Dropper (P4 Tutorial)',
+    category = 'Security',
+    url = 'http://onosproject.org',
+    description = 'Inhibits forwarding of ICMP packets for PI-enabled devices using the ' +
+                    'p4-tutorial-pipeconf.',
+    included_bundles = BUNDLES,
+    required_apps = [
+        'org.onosproject.p4tutorial.pipeconf',
+    ]
+)
diff --git a/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java
new file mode 100644
index 0000000..8914ee7
--- /dev/null
+++ b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java
@@ -0,0 +1,167 @@
+/*
+ * 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.icmpdropper;
+
+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.onosproject.app.ApplicationAdminService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+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.pi.model.PiPipelineProgrammable;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionId;
+import org.onosproject.net.pi.runtime.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiPipeconfService;
+import org.onosproject.net.pi.runtime.PiTableId;
+import org.onosproject.p4tutorial.pipeconf.PipeconfFactory;
+import org.slf4j.Logger;
+
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Simple application that drops all ICMP packets.
+ */
+@Component(immediate = true)
+public class IcmpDropper {
+
+    private static final Logger log = getLogger(IcmpDropper.class);
+
+    private static final String APP_NAME = "org.onosproject.p4tutorial.icmpdropper";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private FlowRuleService flowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private ApplicationAdminService appService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiPipeconfService piPipeconfService;
+
+    private final DeviceListener deviceListener = new InternalDeviceListener();
+
+    private ApplicationId appId;
+
+    @Activate
+    public void activate() {
+        log.info("Starting...");
+
+        appId = coreService.registerApplication(APP_NAME);
+        // Register listener for handling new devices.
+        deviceService.addListener(deviceListener);
+        // Install rules to existing devices.
+        deviceService.getDevices()
+                .forEach(device -> installDropRule(device.id()));
+
+        log.info("STARTED", appId.id());
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopping...");
+
+        deviceService.removeListener(deviceListener);
+        flowRuleService.removeFlowRulesById(appId);
+
+        log.info("STOPPED");
+    }
+
+    private boolean checkPipeconf(Device device) {
+        if (!device.is(PiPipelineProgrammable.class)) {
+            // Device is not PI-pipeline programmable. Ignore.
+            return false;
+        }
+        if (!piPipeconfService.ofDevice(device.id()).isPresent() ||
+                !piPipeconfService.ofDevice(device.id()).get().equals(PipeconfFactory.PIPECONF_ID)) {
+            log.warn("Device {} has pipeconf {} instead of {}, can't install flow rule for this device",
+                     device.id(), piPipeconfService.ofDevice(device.id()).get(), PipeconfFactory.PIPECONF_ID);
+            return false;
+        }
+
+        return true;
+    }
+
+    private void installDropRule(DeviceId deviceId) {
+        PiHeaderFieldId ipv4ProtoFieldId = PiHeaderFieldId.of("ipv4", "protocol");
+        PiActionId dropActionId = PiActionId.of("_drop");
+
+        PiCriterion piCriterion = PiCriterion.builder()
+                .matchExact(ipv4ProtoFieldId, (byte) 0x01)
+                .build();
+        PiAction dropAction = PiAction.builder()
+                .withId(dropActionId)
+                .build();
+
+        FlowRule flowRule = DefaultFlowRule.builder()
+                .forDevice(deviceId)
+                .forTable(PiTableId.of("ip_proto_filter_table"))
+                .fromApp(appId)
+                .makePermanent()
+                .withPriority(1000)
+                .withSelector(DefaultTrafficSelector.builder()
+                                      .matchPi(piCriterion)
+                                      .build())
+                .withTreatment(
+                        DefaultTrafficTreatment.builder()
+                                .piTableAction(dropAction)
+                                .build())
+                .build();
+
+        log.warn("Installing ICMP drop rule to {}", deviceId);
+
+        flowRuleService.applyFlowRules(flowRule);
+    }
+
+    /**
+     * A listener of device events that installs a rule to drop packet for each new device.
+     */
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            Device device = event.subject();
+            if (checkPipeconf(device)) {
+                installDropRule(device.id());
+            }
+        }
+
+        @Override
+        public boolean isRelevant(DeviceEvent event) {
+            // Reacts only to new devices.
+            return event.type() == DEVICE_ADDED;
+        }
+    }
+}
diff --git a/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/package-info.java b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/package-info.java
new file mode 100644
index 0000000..5ea1180
--- /dev/null
+++ b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+/**
+ * P4 tutorial application classes.
+ */
+package org.onosproject.p4tutorial.icmpdropper;
\ No newline at end of file
diff --git a/apps/p4-tutorial/pipeconf/BUCK b/apps/p4-tutorial/pipeconf/BUCK
new file mode 100644
index 0000000..038736f
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/BUCK
@@ -0,0 +1,29 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:minimal-json',
+    '//incubator/bmv2/model:onos-incubator-bmv2-model',
+    '//drivers/default:onos-drivers-default',
+    '//protocols/p4runtime/api:onos-protocols-p4runtime-api',
+]
+
+osgi_jar (
+    deps = COMPILE_DEPS,
+)
+
+BUNDLES = [
+    '//apps/p4-tutorial/pipeconf:onos-apps-p4-tutorial-pipeconf',
+    '//drivers/default:onos-drivers-default',
+    '//incubator/bmv2/model:onos-incubator-bmv2-model',
+]
+
+onos_app (
+    app_name = 'org.onosproject.p4tutorial.pipeconf',
+    title = 'P4 Tutorial Pipeconf',
+    category = 'Pipeconf',
+    url = 'http://onosproject.org',
+    description = 'Provides pipeconf for the ONOS-P4 Tutorial',
+    included_bundles = BUNDLES,
+    required_apps = [
+        'org.onosproject.drivers.p4runtime',
+    ]
+)
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipeconfFactory.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipeconfFactory.java
new file mode 100644
index 0000000..b284eaa
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipeconfFactory.java
@@ -0,0 +1,75 @@
+/*
+ * 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.pipeconf;
+
+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.onosproject.bmv2.model.Bmv2PipelineModelParser;
+import org.onosproject.driver.pipeline.DefaultSingleTablePipeline;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.device.PortStatisticsDiscovery;
+import org.onosproject.net.pi.model.DefaultPiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipeconfId;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.model.PiPipelineModel;
+import org.onosproject.net.pi.runtime.PiPipeconfService;
+
+import java.net.URL;
+
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.BMV2_JSON;
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
+
+/**
+ * Component that produces and registers a pipeconf when loaded.
+ */
+@Component(immediate = true)
+public final class PipeconfFactory {
+
+    public static final PiPipeconfId PIPECONF_ID = new PiPipeconfId("p4-tutorial-pipeconf");
+    private static final URL P4INFO_URL = PipeconfFactory.class.getResource("/main.p4info");
+    private static final URL BMV2_JSON_URL = PipeconfFactory.class.getResource("/main.json");
+    private static final PiPipelineModel PIPELINE_MODEL = Bmv2PipelineModelParser.parse(BMV2_JSON_URL);
+
+    private static final PiPipeconf PIPECONF = DefaultPiPipeconf.builder()
+            .withId(PIPECONF_ID)
+            .withPipelineModel(PIPELINE_MODEL)
+            .addBehaviour(PiPipelineInterpreter.class, PipelineInterpreterImpl.class)
+            .addBehaviour(PortStatisticsDiscovery.class, PortStatisticsDiscoveryImpl.class)
+            // Since main.p4 defines only 1 table, we re-use the existing single-table pipeliner.
+            .addBehaviour(Pipeliner.class, DefaultSingleTablePipeline.class)
+            .addExtension(P4_INFO_TEXT, P4INFO_URL)
+            .addExtension(BMV2_JSON, BMV2_JSON_URL)
+            .build();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiPipeconfService piPipeconfService;
+
+    @Activate
+    public void activate() {
+        // Registers the pipeconf at component activation.
+        piPipeconfService.register(PIPECONF);
+    }
+
+    @Deactivate
+    public void deactivate() {
+        piPipeconfService.remove(PIPECONF.id());
+    }
+}
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
new file mode 100644
index 0000000..c783236
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PipelineInterpreterImpl.java
@@ -0,0 +1,241 @@
+/*
+ * 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.pipeconf;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableList;
+import org.onlab.packet.Ethernet;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.packet.DefaultInboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionId;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiActionParamId;
+import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
+import org.onosproject.net.pi.runtime.PiPacketMetadataId;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.net.pi.runtime.PiTableId;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onlab.util.ImmutableByteSequence.fit;
+import static org.onosproject.net.PortNumber.CONTROLLER;
+import static org.onosproject.net.PortNumber.FLOOD;
+import static org.onosproject.net.flow.instructions.Instruction.Type.OUTPUT;
+import static org.onosproject.net.pi.runtime.PiPacketOperation.Type.PACKET_OUT;
+
+/**
+ * Implementation of a PI interpreter for the main.p4 program.
+ */
+public final class PipelineInterpreterImpl extends AbstractHandlerBehaviour implements PiPipelineInterpreter {
+
+    private static final String TABLE0 = "table0";
+    private static final String IP_PROTO_FILTER_TABLE = "ip_proto_filter_table";
+    private static final String SEND_TO_CPU = "send_to_cpu";
+    private static final String PORT = "port";
+    private static final String DROP = "_drop";
+    private static final String SET_EGRESS_PORT = "set_egress_port";
+    private static final String EGRESS_PORT = "egress_port";
+    private static final String INGRESS_PORT = "ingress_port";
+    private static final String ETHERNET = "ethernet";
+    private static final String STANDARD_METADATA = "standard_metadata";
+    private static final int PORT_FIELD_BITWIDTH = 9;
+
+    private static final PiHeaderFieldId INGRESS_PORT_ID = PiHeaderFieldId.of(STANDARD_METADATA, "ingress_port");
+    private static final PiHeaderFieldId ETH_DST_ID = PiHeaderFieldId.of(ETHERNET, "dst_addr");
+    private static final PiHeaderFieldId ETH_SRC_ID = PiHeaderFieldId.of(ETHERNET, "src_addr");
+    private static final PiHeaderFieldId ETH_TYPE_ID = PiHeaderFieldId.of(ETHERNET, "ether_type");
+    private static final PiTableId TABLE0_ID = PiTableId.of(TABLE0);
+    private static final PiTableId IP_PROTO_FILTER_TABLE_ID = PiTableId.of(IP_PROTO_FILTER_TABLE);
+
+    private static final BiMap<Integer, PiTableId> TABLE_MAP = ImmutableBiMap.of(
+            0, TABLE0_ID,
+            1, IP_PROTO_FILTER_TABLE_ID);
+
+    private static final BiMap<Criterion.Type, PiHeaderFieldId> CRITERION_MAP =
+            new ImmutableBiMap.Builder<Criterion.Type, PiHeaderFieldId>()
+                    .put(Criterion.Type.IN_PORT, INGRESS_PORT_ID)
+                    .put(Criterion.Type.ETH_DST, ETH_DST_ID)
+                    .put(Criterion.Type.ETH_SRC, ETH_SRC_ID)
+                    .put(Criterion.Type.ETH_TYPE, ETH_TYPE_ID)
+                    .build();
+
+    @Override
+    public PiAction mapTreatment(TrafficTreatment treatment, PiTableId piTableId)
+            throws PiInterpreterException {
+
+        if (treatment.allInstructions().size() == 0) {
+            // No instructions means drop for us.
+            return PiAction.builder()
+                    .withId(PiActionId.of(DROP))
+                    .build();
+        } else if (treatment.allInstructions().size() > 1) {
+            // We understand treatments with only 1 instruction.
+            throw new PiInterpreterException("Treatment has multiple instructions");
+        }
+
+        // Get the first and only instruction.
+        Instruction instruction = treatment.allInstructions().get(0);
+
+        switch (instruction.type()) {
+            case OUTPUT:
+                // We understand only instructions of type OUTPUT.
+                Instructions.OutputInstruction outInstruction = (Instructions.OutputInstruction) instruction;
+                PortNumber port = outInstruction.port();
+                if (!port.isLogical()) {
+                    return PiAction.builder()
+                            .withId(PiActionId.of(SET_EGRESS_PORT))
+                            .withParameter(new PiActionParam(PiActionParamId.of(PORT), copyFrom(port.toLong())))
+                            .build();
+                } else if (port.equals(CONTROLLER)) {
+                    return PiAction.builder()
+                            .withId(PiActionId.of(SEND_TO_CPU))
+                            .build();
+                } else {
+                    throw new PiInterpreterException(format("Output on logical port '%s' not supported", port));
+                }
+            default:
+                throw new PiInterpreterException(format("Instruction of type '%s' not supported", instruction.type()));
+        }
+    }
+
+    @Override
+    public Optional<PiCounterId> mapTableCounter(PiTableId piTableId) {
+        return Optional.empty();
+    }
+
+    @Override
+    public Collection<PiPacketOperation> mapOutboundPacket(OutboundPacket packet)
+            throws PiInterpreterException {
+
+        TrafficTreatment treatment = packet.treatment();
+
+        // We support only packet-out with OUTPUT instructions.
+        List<Instructions.OutputInstruction> outInstructions = treatment.allInstructions().stream()
+                .filter(i -> i.type().equals(OUTPUT))
+                .map(i -> (Instructions.OutputInstruction) i)
+                .collect(toList());
+
+        if (treatment.allInstructions().size() != outInstructions.size()) {
+            // There are other instructions that are not of type OUTPUT.
+            throw new PiInterpreterException("Treatment not supported: " + treatment);
+        }
+
+        ImmutableList.Builder<PiPacketOperation> builder = ImmutableList.builder();
+
+        for (Instructions.OutputInstruction outInst : outInstructions) {
+            if (outInst.port().isLogical() && !outInst.port().equals(FLOOD)) {
+                throw new PiInterpreterException(format("Output on logical port '%s' not supported", outInst.port()));
+            } else if (outInst.port().equals(FLOOD)) {
+                // Since main.p4 does not support flooding, we create a packet operation for each switch port.
+                DeviceService deviceService = handler().get(DeviceService.class);
+                for (Port port : deviceService.getPorts(packet.sendThrough())) {
+                    builder.add(createPiPacketOperation(packet.data(), port.number().toLong()));
+                }
+            } else {
+                builder.add(createPiPacketOperation(packet.data(), outInst.port().toLong()));
+            }
+        }
+        return builder.build();
+    }
+
+    @Override
+    public InboundPacket mapInboundPacket(DeviceId deviceId, PiPacketOperation packetIn)
+            throws PiInterpreterException {
+        // We assume that the packet is ethernet, which is fine since default.p4 can deparse only ethernet packets.
+        Ethernet ethPkt = new Ethernet();
+
+        ethPkt.deserialize(packetIn.data().asArray(), 0, packetIn.data().size());
+
+        // Returns the ingress port packet metadata.
+        Optional<PiPacketMetadata> packetMetadata = packetIn.metadatas().stream()
+                .filter(metadata -> metadata.id().name().equals(INGRESS_PORT))
+                .findFirst();
+
+        if (packetMetadata.isPresent()) {
+            ImmutableByteSequence portByteSequence = packetMetadata.get().value();
+            short s = portByteSequence.asReadOnlyBuffer().getShort();
+            ConnectPoint receivedFrom = new ConnectPoint(deviceId, PortNumber.portNumber(s));
+            return new DefaultInboundPacket(receivedFrom, ethPkt, packetIn.data().asReadOnlyBuffer());
+        } else {
+            throw new PiInterpreterException(format(
+                    "Missing metadata '%s' in packet-in received from '%s': %s", INGRESS_PORT, deviceId, packetIn));
+        }
+    }
+
+    private PiPacketOperation createPiPacketOperation(ByteBuffer data, long portNumber) throws PiInterpreterException {
+        PiPacketMetadata metadata = createPacketMetadata(portNumber);
+        return PiPacketOperation.builder()
+                .withType(PACKET_OUT)
+                .withData(copyFrom(data))
+                .withMetadatas(ImmutableList.of(metadata))
+                .build();
+    }
+
+    private PiPacketMetadata createPacketMetadata(long portNumber) throws PiInterpreterException {
+        try {
+            return PiPacketMetadata.builder()
+                    .withId(PiPacketMetadataId.of(EGRESS_PORT))
+                    .withValue(fit(copyFrom(portNumber), PORT_FIELD_BITWIDTH))
+                    .build();
+        } catch (ImmutableByteSequence.ByteSequenceTrimException e) {
+            throw new PiInterpreterException(format("Port number %d too big, %s", portNumber, e.getMessage()));
+        }
+    }
+
+    @Override
+    public Optional<PiHeaderFieldId> mapCriterionType(Criterion.Type type) {
+        return Optional.ofNullable(CRITERION_MAP.get(type));
+    }
+
+    @Override
+    public Optional<Criterion.Type> mapPiHeaderFieldId(PiHeaderFieldId headerFieldId) {
+        return Optional.ofNullable(CRITERION_MAP.inverse().get(headerFieldId));
+    }
+
+    @Override
+    public Optional<PiTableId> mapFlowRuleTableId(int flowRuleTableId) {
+        return Optional.ofNullable(TABLE_MAP.get(flowRuleTableId));
+    }
+
+    @Override
+    public Optional<Integer> mapPiTableId(PiTableId piTableId) {
+        return Optional.ofNullable(TABLE_MAP.inverse().get(piTableId));
+    }
+}
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java
new file mode 100644
index 0000000..7d715ee1
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/PortStatisticsDiscoveryImpl.java
@@ -0,0 +1,137 @@
+/*
+ * 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.pipeconf;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DefaultPortStatistics;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.PortStatistics;
+import org.onosproject.net.device.PortStatisticsDiscovery;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.net.pi.runtime.PiCounterId;
+import org.onosproject.net.pi.runtime.PiIndirectCounterCellId;
+import org.onosproject.net.pi.runtime.PiPipeconfService;
+import org.onosproject.p4runtime.api.P4RuntimeClient;
+import org.onosproject.p4runtime.api.P4RuntimeController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.pi.runtime.PiCounterType.INDIRECT;
+
+/**
+ * Implementation of the PortStatisticsDiscovery behaviour for the main.p4 program. This behaviour works by using a
+ * P4Runtime client to read the values of the ingress/egress port counters defined in the P4 program.
+ */
+public final class PortStatisticsDiscoveryImpl extends AbstractHandlerBehaviour implements PortStatisticsDiscovery {
+
+    private static final Logger log = LoggerFactory.getLogger(PortStatisticsDiscoveryImpl.class);
+
+    private static final PiCounterId INGRESS_COUNTER_ID = PiCounterId.of("igr_port_counter", INDIRECT);
+    private static final PiCounterId EGRESS_COUNTER_ID = PiCounterId.of("egr_port_counter", INDIRECT);
+
+    @Override
+    public Collection<PortStatistics> discoverPortStatistics() {
+
+        DeviceService deviceService = this.handler().get(DeviceService.class);
+        DeviceId deviceId = this.data().deviceId();
+
+        // Get a client for this device.
+        P4RuntimeController controller = handler().get(P4RuntimeController.class);
+        if (!controller.hasClient(deviceId)) {
+            log.warn("Unable to find client for {}, aborting operation", deviceId);
+            return Collections.emptyList();
+        }
+        P4RuntimeClient client = controller.getClient(deviceId);
+
+        // Get the pipeconf of this device.
+        PiPipeconfService piPipeconfService = handler().get(PiPipeconfService.class);
+        if (!piPipeconfService.ofDevice(deviceId).isPresent() ||
+                !piPipeconfService.getPipeconf(piPipeconfService.ofDevice(deviceId).get()).isPresent()) {
+            log.warn("Unable to get the pipeconf of {}, aborting operation", deviceId);
+            return Collections.emptyList();
+        }
+        PiPipeconf pipeconf = piPipeconfService.getPipeconf(piPipeconfService.ofDevice(deviceId).get()).get();
+
+        // Prepare PortStatistics objects to return, one per port of this device.
+        Map<Long, DefaultPortStatistics.Builder> portStatBuilders = Maps.newHashMap();
+        deviceService
+                .getPorts(deviceId)
+                .forEach(p -> portStatBuilders.put(p.number().toLong(),
+                                                   DefaultPortStatistics.builder()
+                                                           .setPort(p.number())
+                                                           .setDeviceId(deviceId)));
+
+        // Generate the counter cell IDs.
+        Set<PiCounterCellId> counterCellIds = Sets.newHashSet();
+        portStatBuilders.keySet().forEach(p -> {
+            // Counter cell/index = port number.
+            counterCellIds.add(PiIndirectCounterCellId.of(INGRESS_COUNTER_ID, p));
+            counterCellIds.add(PiIndirectCounterCellId.of(EGRESS_COUNTER_ID, p));
+        });
+
+        // Query the device.
+        Collection<PiCounterCellData> counterEntryResponse;
+        try {
+            counterEntryResponse = client.readCounterCells(counterCellIds, pipeconf).get();
+        } catch (InterruptedException | ExecutionException e) {
+            log.warn("Exception while reading port counters from {}: {}", deviceId, e.toString());
+            log.debug("", e);
+            return Collections.emptyList();
+        }
+
+        // Process response.
+        counterEntryResponse.forEach(counterData -> {
+            if (counterData.cellId().type() != INDIRECT) {
+                log.warn("Invalid counter data type {}, skipping", counterData.cellId().type());
+                return;
+            }
+            PiIndirectCounterCellId indCellId = (PiIndirectCounterCellId) counterData.cellId();
+            if (!portStatBuilders.containsKey(indCellId.index())) {
+                log.warn("Unrecognized counter index {}, skipping", counterData);
+                return;
+            }
+            DefaultPortStatistics.Builder statsBuilder = portStatBuilders.get(indCellId.index());
+            if (counterData.cellId().counterId().equals(INGRESS_COUNTER_ID)) {
+                statsBuilder.setPacketsReceived(counterData.packets());
+                statsBuilder.setBytesReceived(counterData.bytes());
+            } else if (counterData.cellId().counterId().equals(EGRESS_COUNTER_ID)) {
+                statsBuilder.setPacketsSent(counterData.packets());
+                statsBuilder.setBytesSent(counterData.bytes());
+            } else {
+                log.warn("Unrecognized counter ID {}, skipping", counterData);
+            }
+        });
+
+        return portStatBuilders
+                .values()
+                .stream()
+                .map(DefaultPortStatistics.Builder::build)
+                .collect(Collectors.toList());
+    }
+}
diff --git a/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/package-info.java b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/package-info.java
new file mode 100644
index 0000000..3adbcda
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/java/org/onosproject/p4tutorial/pipeconf/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * P4 tutorial pipeconf classes.
+ */
+package org.onosproject.p4tutorial.pipeconf;
\ No newline at end of file
diff --git a/apps/p4-tutorial/pipeconf/src/main/resources/Makefile b/apps/p4-tutorial/pipeconf/src/main/resources/Makefile
new file mode 100644
index 0000000..9a6a8d0
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/resources/Makefile
@@ -0,0 +1,6 @@
+all: main
+
+main: main.p4
+	p4c-bm2-ss -o main.json --p4runtime-file main.p4info --p4runtime-format text main.p4
+	# Fix for BMv2/p4c bug...
+	sed -i -e 's/"value" : "0xff"/"value" : "0x00ff"/g' main.json
\ No newline at end of file
diff --git a/apps/p4-tutorial/pipeconf/src/main/resources/main.json b/apps/p4-tutorial/pipeconf/src/main/resources/main.json
new file mode 100644
index 0000000..3d059bb
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/resources/main.json
@@ -0,0 +1,931 @@
+{
+  "program" : "main.p4",
+  "__meta__" : {
+    "version" : [2, 7],
+    "compiler" : "https://github.com/p4lang/p4c"
+  },
+  "header_types" : [
+    {
+      "name" : "scalars_0",
+      "id" : 0,
+      "fields" : [
+        ["tmp", 32, false],
+        ["tmp_0", 32, false]
+      ]
+    },
+    {
+      "name" : "ethernet_t",
+      "id" : 1,
+      "fields" : [
+        ["dst_addr", 48, false],
+        ["src_addr", 48, false],
+        ["ether_type", 16, false]
+      ]
+    },
+    {
+      "name" : "ipv4_t",
+      "id" : 2,
+      "fields" : [
+        ["version", 4, false],
+        ["ihl", 4, false],
+        ["diffserv", 8, false],
+        ["len", 16, false],
+        ["identification", 16, false],
+        ["flags", 3, false],
+        ["frag_offset", 13, false],
+        ["ttl", 8, false],
+        ["protocol", 8, false],
+        ["hdr_checksum", 16, false],
+        ["src_addr", 32, false],
+        ["dst_addr", 32, false]
+      ]
+    },
+    {
+      "name" : "packet_out_header_t",
+      "id" : 3,
+      "fields" : [
+        ["egress_port", 9, false],
+        ["_padding", 7, false]
+      ]
+    },
+    {
+      "name" : "packet_in_header_t",
+      "id" : 4,
+      "fields" : [
+        ["ingress_port", 9, false],
+        ["_padding_0", 7, false]
+      ]
+    },
+    {
+      "name" : "standard_metadata",
+      "id" : 5,
+      "fields" : [
+        ["ingress_port", 9, false],
+        ["egress_spec", 9, false],
+        ["egress_port", 9, false],
+        ["clone_spec", 32, false],
+        ["instance_type", 32, false],
+        ["drop", 1, false],
+        ["recirculate_port", 16, false],
+        ["packet_length", 32, false],
+        ["enq_timestamp", 32, false],
+        ["enq_qdepth", 19, false],
+        ["deq_timedelta", 32, false],
+        ["deq_qdepth", 19, false],
+        ["ingress_global_timestamp", 48, false],
+        ["lf_field_list", 32, false],
+        ["mcast_grp", 16, false],
+        ["resubmit_flag", 1, false],
+        ["egress_rid", 16, false],
+        ["_padding_1", 5, false]
+      ]
+    }
+  ],
+  "headers" : [
+    {
+      "name" : "scalars",
+      "id" : 0,
+      "header_type" : "scalars_0",
+      "metadata" : true,
+      "pi_omit" : true
+    },
+    {
+      "name" : "standard_metadata",
+      "id" : 1,
+      "header_type" : "standard_metadata",
+      "metadata" : true,
+      "pi_omit" : true
+    },
+    {
+      "name" : "ethernet",
+      "id" : 2,
+      "header_type" : "ethernet_t",
+      "metadata" : false,
+      "pi_omit" : true
+    },
+    {
+      "name" : "ipv4",
+      "id" : 3,
+      "header_type" : "ipv4_t",
+      "metadata" : false,
+      "pi_omit" : true
+    },
+    {
+      "name" : "packet_out",
+      "id" : 4,
+      "header_type" : "packet_out_header_t",
+      "metadata" : false,
+      "pi_omit" : true
+    },
+    {
+      "name" : "packet_in",
+      "id" : 5,
+      "header_type" : "packet_in_header_t",
+      "metadata" : false,
+      "pi_omit" : true
+    }
+  ],
+  "header_stacks" : [],
+  "header_union_types" : [],
+  "header_unions" : [],
+  "header_union_stacks" : [],
+  "field_lists" : [],
+  "errors" : [
+    ["NoError", 1],
+    ["PacketTooShort", 2],
+    ["NoMatch", 3],
+    ["StackOutOfBounds", 4],
+    ["HeaderTooShort", 5],
+    ["ParserTimeout", 6]
+  ],
+  "enums" : [],
+  "parsers" : [
+    {
+      "name" : "parser",
+      "id" : 0,
+      "init_state" : "start",
+      "parse_states" : [
+        {
+          "name" : "start",
+          "id" : 0,
+          "parser_ops" : [],
+          "transitions" : [
+            {
+              "value" : "0x00ff",
+              "mask" : null,
+              "next_state" : "parse_packet_out"
+            },
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : "parse_ethernet"
+            }
+          ],
+          "transition_key" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "ingress_port"]
+            }
+          ]
+        },
+        {
+          "name" : "parse_packet_out",
+          "id" : 1,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "packet_out"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : "parse_ethernet"
+            }
+          ],
+          "transition_key" : []
+        },
+        {
+          "name" : "parse_ethernet",
+          "id" : 2,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "ethernet"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "0x0800",
+              "mask" : null,
+              "next_state" : "parse_ipv4"
+            },
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : null
+            }
+          ],
+          "transition_key" : [
+            {
+              "type" : "field",
+              "value" : ["ethernet", "ether_type"]
+            }
+          ]
+        },
+        {
+          "name" : "parse_ipv4",
+          "id" : 3,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "ipv4"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : null
+            }
+          ],
+          "transition_key" : []
+        }
+      ]
+    }
+  ],
+  "deparsers" : [
+    {
+      "name" : "deparser",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "main.p4",
+        "line" : 264,
+        "column" : 8,
+        "source_fragment" : "DeparserImpl"
+      },
+      "order" : ["packet_in", "ethernet", "ipv4"]
+    }
+  ],
+  "meter_arrays" : [],
+  "counter_arrays" : [
+    {
+      "name" : "egr_port_counter",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "main.p4",
+        "line" : 181,
+        "column" : 38,
+        "source_fragment" : "egr_port_counter"
+      },
+      "size" : 511,
+      "is_direct" : false
+    },
+    {
+      "name" : "igr_port_counter",
+      "id" : 1,
+      "source_info" : {
+        "filename" : "main.p4",
+        "line" : 182,
+        "column" : 38,
+        "source_fragment" : "igr_port_counter"
+      },
+      "size" : 511,
+      "is_direct" : false
+    }
+  ],
+  "register_arrays" : [],
+  "calculations" : [],
+  "learn_lists" : [],
+  "actions" : [
+    {
+      "name" : "NoAction",
+      "id" : 0,
+      "runtime_data" : [],
+      "primitives" : []
+    },
+    {
+      "name" : "send_to_cpu",
+      "id" : 1,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "hexstr",
+              "value" : "0x00ff"
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 24,
+            "column" : 24,
+            "source_fragment" : "255; ..."
+          }
+        },
+        {
+          "op" : "add_header",
+          "parameters" : [
+            {
+              "type" : "header",
+              "value" : "packet_in"
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 137,
+            "column" : 8,
+            "source_fragment" : "hdr.packet_in.setValid()"
+          }
+        },
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["packet_in", "ingress_port"]
+            },
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "ingress_port"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 138,
+            "column" : 8,
+            "source_fragment" : "hdr.packet_in.ingress_port = standard_metadata.ingress_port"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "set_egress_port",
+      "id" : 2,
+      "runtime_data" : [
+        {
+          "name" : "port",
+          "bitwidth" : 9
+        }
+      ],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "runtime_data",
+              "value" : 0
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 142,
+            "column" : 8,
+            "source_fragment" : "standard_metadata.egress_spec = port"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "_drop",
+      "id" : 3,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "hexstr",
+              "value" : "0x01ff"
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 25,
+            "column" : 25,
+            "source_fragment" : "511; ..."
+          }
+        }
+      ]
+    },
+    {
+      "name" : "_drop",
+      "id" : 4,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "hexstr",
+              "value" : "0x01ff"
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 25,
+            "column" : 25,
+            "source_fragment" : "511; ..."
+          }
+        }
+      ]
+    },
+    {
+      "name" : "act",
+      "id" : 5,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "field",
+              "value" : ["packet_out", "egress_port"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 195,
+            "column" : 12,
+            "source_fragment" : "standard_metadata.egress_spec = hdr.packet_out.egress_port"
+          }
+        },
+        {
+          "op" : "remove_header",
+          "parameters" : [
+            {
+              "type" : "header",
+              "value" : "packet_out"
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 196,
+            "column" : 12,
+            "source_fragment" : "hdr.packet_out.setInvalid()"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "act_0",
+      "id" : 6,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp"]
+            },
+            {
+              "type" : "expression",
+              "value" : {
+                "type" : "expression",
+                "value" : {
+                  "op" : "&",
+                  "left" : {
+                    "type" : "field",
+                    "value" : ["standard_metadata", "egress_spec"]
+                  },
+                  "right" : {
+                    "type" : "hexstr",
+                    "value" : "0xffffffff"
+                  }
+                }
+              }
+            }
+          ]
+        },
+        {
+          "op" : "count",
+          "parameters" : [
+            {
+              "type" : "counter_array",
+              "value" : "egr_port_counter"
+            },
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 218,
+            "column" : 12,
+            "source_fragment" : "egr_port_counter.count((bit<32>) standard_metadata.egress_spec)"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "act_1",
+      "id" : 7,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp_0"]
+            },
+            {
+              "type" : "expression",
+              "value" : {
+                "type" : "expression",
+                "value" : {
+                  "op" : "&",
+                  "left" : {
+                    "type" : "field",
+                    "value" : ["standard_metadata", "ingress_port"]
+                  },
+                  "right" : {
+                    "type" : "hexstr",
+                    "value" : "0xffffffff"
+                  }
+                }
+              }
+            }
+          ]
+        },
+        {
+          "op" : "count",
+          "parameters" : [
+            {
+              "type" : "counter_array",
+              "value" : "igr_port_counter"
+            },
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp_0"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 221,
+            "column" : 12,
+            "source_fragment" : "igr_port_counter.count((bit<32>) standard_metadata.ingress_port)"
+          }
+        }
+      ]
+    }
+  ],
+  "pipelines" : [
+    {
+      "name" : "ingress",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "main.p4",
+        "line" : 126,
+        "column" : 8,
+        "source_fragment" : "IngressImpl"
+      },
+      "init_table" : "node_2",
+      "tables" : [
+        {
+          "name" : "tbl_act",
+          "id" : 0,
+          "key" : [],
+          "match_type" : "exact",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [5],
+          "actions" : ["act"],
+          "base_default_next" : "node_7",
+          "next_tables" : {
+            "act" : "node_7"
+          },
+          "default_entry" : {
+            "action_id" : 5,
+            "action_const" : true,
+            "action_data" : [],
+            "action_entry_const" : true
+          }
+        },
+        {
+          "name" : "table0",
+          "id" : 1,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 149,
+            "column" : 10,
+            "source_fragment" : "table0"
+          },
+          "key" : [
+            {
+              "match_type" : "ternary",
+              "target" : ["standard_metadata", "ingress_port"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "dst_addr"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "src_addr"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "ether_type"],
+              "mask" : null
+            }
+          ],
+          "match_type" : "ternary",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [2, 1, 3],
+          "actions" : ["set_egress_port", "send_to_cpu", "_drop"],
+          "base_default_next" : "node_7",
+          "next_tables" : {
+            "set_egress_port" : "node_5",
+            "send_to_cpu" : "node_7",
+            "_drop" : "node_7"
+          },
+          "default_entry" : {
+            "action_id" : 3,
+            "action_const" : false,
+            "action_data" : [],
+            "action_entry_const" : false
+          }
+        },
+        {
+          "name" : "ip_proto_filter_table",
+          "id" : 2,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 164,
+            "column" : 10,
+            "source_fragment" : "ip_proto_filter_table"
+          },
+          "key" : [
+            {
+              "match_type" : "ternary",
+              "target" : ["ipv4", "src_addr"],
+              "mask" : null
+            },
+            {
+              "match_type" : "exact",
+              "target" : ["ipv4", "protocol"],
+              "mask" : null
+            }
+          ],
+          "match_type" : "ternary",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [4, 0],
+          "actions" : ["_drop", "NoAction"],
+          "base_default_next" : "node_7",
+          "next_tables" : {
+            "_drop" : "node_7",
+            "NoAction" : "node_7"
+          },
+          "default_entry" : {
+            "action_id" : 0,
+            "action_const" : false,
+            "action_data" : [],
+            "action_entry_const" : false
+          }
+        },
+        {
+          "name" : "tbl_act_0",
+          "id" : 3,
+          "key" : [],
+          "match_type" : "exact",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [6],
+          "actions" : ["act_0"],
+          "base_default_next" : "node_9",
+          "next_tables" : {
+            "act_0" : "node_9"
+          },
+          "default_entry" : {
+            "action_id" : 6,
+            "action_const" : true,
+            "action_data" : [],
+            "action_entry_const" : true
+          }
+        },
+        {
+          "name" : "tbl_act_1",
+          "id" : 4,
+          "key" : [],
+          "match_type" : "exact",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [7],
+          "actions" : ["act_1"],
+          "base_default_next" : null,
+          "next_tables" : {
+            "act_1" : null
+          },
+          "default_entry" : {
+            "action_id" : 7,
+            "action_const" : true,
+            "action_data" : [],
+            "action_entry_const" : true
+          }
+        }
+      ],
+      "action_profiles" : [],
+      "conditionals" : [
+        {
+          "name" : "node_2",
+          "id" : 0,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 188,
+            "column" : 12,
+            "source_fragment" : "standard_metadata.ingress_port == CPU_PORT"
+          },
+          "expression" : {
+            "type" : "expression",
+            "value" : {
+              "op" : "==",
+              "left" : {
+                "type" : "field",
+                "value" : ["standard_metadata", "ingress_port"]
+              },
+              "right" : {
+                "type" : "hexstr",
+                "value" : "0x00ff"
+              }
+            }
+          },
+          "true_next" : "tbl_act",
+          "false_next" : "table0"
+        },
+        {
+          "name" : "node_5",
+          "id" : 1,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 205,
+            "column" : 24,
+            "source_fragment" : "hdr.ipv4.isValid()"
+          },
+          "expression" : {
+            "type" : "expression",
+            "value" : {
+              "op" : "==",
+              "left" : {
+                "type" : "field",
+                "value" : ["ipv4", "$valid$"]
+              },
+              "right" : {
+                "type" : "hexstr",
+                "value" : "0x01"
+              }
+            }
+          },
+          "true_next" : "ip_proto_filter_table",
+          "false_next" : "node_7"
+        },
+        {
+          "name" : "node_7",
+          "id" : 2,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 217,
+            "column" : 12,
+            "source_fragment" : "standard_metadata.egress_spec < 511"
+          },
+          "expression" : {
+            "type" : "expression",
+            "value" : {
+              "op" : "<",
+              "left" : {
+                "type" : "field",
+                "value" : ["standard_metadata", "egress_spec"]
+              },
+              "right" : {
+                "type" : "hexstr",
+                "value" : "0x01ff"
+              }
+            }
+          },
+          "true_next" : "tbl_act_0",
+          "false_next" : "node_9"
+        },
+        {
+          "name" : "node_9",
+          "id" : 3,
+          "source_info" : {
+            "filename" : "main.p4",
+            "line" : 220,
+            "column" : 12,
+            "source_fragment" : "standard_metadata.ingress_port < 511"
+          },
+          "expression" : {
+            "type" : "expression",
+            "value" : {
+              "op" : "<",
+              "left" : {
+                "type" : "field",
+                "value" : ["standard_metadata", "ingress_port"]
+              },
+              "right" : {
+                "type" : "hexstr",
+                "value" : "0x01ff"
+              }
+            }
+          },
+          "false_next" : null,
+          "true_next" : "tbl_act_1"
+        }
+      ]
+    },
+    {
+      "name" : "egress",
+      "id" : 1,
+      "source_info" : {
+        "filename" : "main.p4",
+        "line" : 230,
+        "column" : 8,
+        "source_fragment" : "EgressImpl"
+      },
+      "init_table" : null,
+      "tables" : [],
+      "action_profiles" : [],
+      "conditionals" : []
+    }
+  ],
+  "checksums" : [],
+  "force_arith" : [],
+  "extern_instances" : [],
+  "field_aliases" : [
+    [
+      "queueing_metadata.enq_timestamp",
+      ["standard_metadata", "enq_timestamp"]
+    ],
+    [
+      "queueing_metadata.enq_qdepth",
+      ["standard_metadata", "enq_qdepth"]
+    ],
+    [
+      "queueing_metadata.deq_timedelta",
+      ["standard_metadata", "deq_timedelta"]
+    ],
+    [
+      "queueing_metadata.deq_qdepth",
+      ["standard_metadata", "deq_qdepth"]
+    ],
+    [
+      "intrinsic_metadata.ingress_global_timestamp",
+      ["standard_metadata", "ingress_global_timestamp"]
+    ],
+    [
+      "intrinsic_metadata.lf_field_list",
+      ["standard_metadata", "lf_field_list"]
+    ],
+    [
+      "intrinsic_metadata.mcast_grp",
+      ["standard_metadata", "mcast_grp"]
+    ],
+    [
+      "intrinsic_metadata.resubmit_flag",
+      ["standard_metadata", "resubmit_flag"]
+    ],
+    [
+      "intrinsic_metadata.egress_rid",
+      ["standard_metadata", "egress_rid"]
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/apps/p4-tutorial/pipeconf/src/main/resources/main.p4 b/apps/p4-tutorial/pipeconf/src/main/resources/main.p4
new file mode 100644
index 0000000..9f0b809
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/resources/main.p4
@@ -0,0 +1,284 @@
+/*
+ * 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.
+ */
+
+#include <core.p4>
+#include <v1model.p4>
+
+#define ETH_TYPE_IPV4 0x0800
+#define MAX_PORTS 511
+
+typedef bit<9> port_t;
+const port_t CPU_PORT = 255;
+const port_t DROP_PORT = 511;
+
+//------------------------------------------------------------------------------
+// HEADERS
+//------------------------------------------------------------------------------
+
+header ethernet_t {
+    bit<48> dst_addr;
+    bit<48> src_addr;
+    bit<16> ether_type;
+}
+
+header ipv4_t {
+    bit<4>  version;
+    bit<4>  ihl;
+    bit<8>  diffserv;
+    bit<16> len;
+    bit<16> identification;
+    bit<3>  flags;
+    bit<13> frag_offset;
+    bit<8>  ttl;
+    bit<8>  protocol;
+    bit<16> hdr_checksum;
+    bit<32> src_addr;
+    bit<32> dst_addr;
+}
+
+/*
+Packet-in header. Prepended to packets sent to the controller and used to carry
+the original ingress port where the packet was received.
+ */
+@controller_header("packet_in")
+header packet_in_header_t {
+    bit<9> ingress_port;
+}
+
+/*
+Packet-out header. Prepended to packets received by the controller and used to
+tell the switch on which physical port this packet should be forwarded.
+ */
+@controller_header("packet_out")
+header packet_out_header_t {
+    bit<9> egress_port;
+}
+
+/*
+For convenience we collect all headers under the same struct.
+ */
+struct headers_t {
+    ethernet_t ethernet;
+    ipv4_t ipv4;
+    packet_out_header_t packet_out;
+    packet_in_header_t packet_in;
+}
+
+/*
+Metadata can be used to carry information from one table to another.
+ */
+struct metadata_t {
+    /* Empty. We don't use it in this program. */
+}
+
+//------------------------------------------------------------------------------
+// PARSER
+//------------------------------------------------------------------------------
+
+parser ParserImpl(packet_in packet,
+                  out headers_t hdr,
+                  inout metadata_t meta,
+                  inout standard_metadata_t standard_metadata) {
+
+    state start {
+        transition select(standard_metadata.ingress_port) {
+            CPU_PORT: parse_packet_out;
+            default: parse_ethernet;
+        }
+    }
+
+    state parse_packet_out {
+        packet.extract(hdr.packet_out);
+        transition parse_ethernet;
+    }
+
+    state parse_ethernet {
+        packet.extract(hdr.ethernet);
+        transition select(hdr.ethernet.ether_type) {
+            ETH_TYPE_IPV4: parse_ipv4;
+            default: accept;
+        }
+    }
+
+    state parse_ipv4 {
+        packet.extract(hdr.ipv4);
+        transition accept;
+    }
+}
+
+//------------------------------------------------------------------------------
+// INGRESS PIPELINE
+//------------------------------------------------------------------------------
+
+control IngressImpl(inout headers_t hdr,
+                    inout metadata_t meta,
+                    inout standard_metadata_t standard_metadata) {
+
+    action send_to_cpu() {
+        standard_metadata.egress_spec = CPU_PORT;
+        /*
+        Packets sent to the controller needs to be prepended with the packet-in
+        header. By setting it valid we make sure it will be deparsed before the
+        ethernet header (see DeparserImpl).
+         */
+        hdr.packet_in.setValid();
+        hdr.packet_in.ingress_port = standard_metadata.ingress_port;
+    }
+
+    action set_egress_port(port_t port) {
+        standard_metadata.egress_spec = port;
+    }
+
+    action _drop() {
+        standard_metadata.egress_spec = DROP_PORT;
+    }
+
+    table table0 {
+        key = {
+            standard_metadata.ingress_port  : ternary;
+            hdr.ethernet.dst_addr           : ternary;
+            hdr.ethernet.src_addr           : ternary;
+            hdr.ethernet.ether_type         : ternary;
+        }
+        actions = {
+            set_egress_port();
+            send_to_cpu();
+            _drop();
+        }
+        default_action = _drop();
+    }
+
+    table ip_proto_filter_table {
+        key = {
+            hdr.ipv4.src_addr : ternary;
+            hdr.ipv4.protocol : exact;
+        }
+        actions = {
+            _drop();
+        }
+    }
+
+    /*
+    Port counters.
+    We use these counter instances to count packets/bytes received/sent on each
+    port. BMv2 always counts both packets and bytes, even if the counter is
+    instantiated as "packets". For each counter we instantiate a number of cells
+    equal to MAX_PORTS.
+     */
+    counter(MAX_PORTS, CounterType.packets) egr_port_counter;
+    counter(MAX_PORTS, CounterType.packets) igr_port_counter;
+
+    /*
+    We define here the processing to be executed by this ingress pipeline.
+     */
+    apply {
+        if (standard_metadata.ingress_port == CPU_PORT) {
+            /*
+            Packet received from CPU_PORT, this is a packet-out sent by the
+            controller. Skip pipeline processing, set the egress port as
+            requested by the controller (packet_out header) and remove the
+            packet_out header.
+             */
+            standard_metadata.egress_spec = hdr.packet_out.egress_port;
+            hdr.packet_out.setInvalid();
+        } else {
+            /*
+            Packet received from switch port. Apply table0, if action is
+            set_egress_port and packet is IPv4, then apply
+            ip_proto_filter_table.
+             */
+            switch(table0.apply().action_run) {
+                set_egress_port: {
+                    if (hdr.ipv4.isValid()) {
+                        ip_proto_filter_table.apply();
+                    }
+                }
+            }
+        }
+
+        /*
+        For each port counter, we update the cell at index = ingress/egress
+        port. We avoid counting packets sent/received on CPU_PORT or dropped
+        (DROP_PORT).
+         */
+        if (standard_metadata.egress_spec < MAX_PORTS) {
+            egr_port_counter.count((bit<32>) standard_metadata.egress_spec);
+        }
+        if (standard_metadata.ingress_port < MAX_PORTS) {
+            igr_port_counter.count((bit<32>) standard_metadata.ingress_port);
+        }
+     }
+}
+
+//------------------------------------------------------------------------------
+// EGRESS PIPELINE
+//------------------------------------------------------------------------------
+
+control EgressImpl(inout headers_t hdr,
+                   inout metadata_t meta,
+                   inout standard_metadata_t standard_metadata) {
+    apply {
+        /*
+        Nothing to do on the egress pipeline.
+        */
+    }
+}
+
+//------------------------------------------------------------------------------
+// CHECKSUM HANDLING
+//------------------------------------------------------------------------------
+
+control VerifyChecksumImpl(in headers_t hdr, inout metadata_t meta) {
+    apply {
+        /*
+        Nothing to do here, we assume checksum is always correct.
+        */
+    }
+}
+
+control ComputeChecksumImpl(inout headers_t hdr, inout metadata_t meta) {
+    apply {
+        /*
+        Nothing to do here, as we do not modify packet headers.
+        */
+    }
+}
+
+//------------------------------------------------------------------------------
+// DEPARSER
+//------------------------------------------------------------------------------
+
+control DeparserImpl(packet_out packet, in headers_t hdr) {
+    apply {
+        /*
+        Deparse headers in order. Only valid headers are emitted.
+        */
+        packet.emit(hdr.packet_in);
+        packet.emit(hdr.ethernet);
+        packet.emit(hdr.ipv4);
+    }
+}
+
+//------------------------------------------------------------------------------
+// SWITCH INSTANTIATION
+//------------------------------------------------------------------------------
+
+V1Switch(ParserImpl(),
+         VerifyChecksumImpl(),
+         IngressImpl(),
+         EgressImpl(),
+         ComputeChecksumImpl(),
+         DeparserImpl()) main;
diff --git a/apps/p4-tutorial/pipeconf/src/main/resources/main.p4info b/apps/p4-tutorial/pipeconf/src/main/resources/main.p4info
new file mode 100644
index 0000000..c4882d2
--- /dev/null
+++ b/apps/p4-tutorial/pipeconf/src/main/resources/main.p4info
@@ -0,0 +1,147 @@
+tables {
+  preamble {
+    id: 33617813
+    name: "table0"
+    alias: "table0"
+  }
+  match_fields {
+    id: 1
+    name: "standard_metadata.ingress_port"
+    bitwidth: 9
+    match_type: TERNARY
+  }
+  match_fields {
+    id: 2
+    name: "hdr.ethernet.dst_addr"
+    bitwidth: 48
+    match_type: TERNARY
+  }
+  match_fields {
+    id: 3
+    name: "hdr.ethernet.src_addr"
+    bitwidth: 48
+    match_type: TERNARY
+  }
+  match_fields {
+    id: 4
+    name: "hdr.ethernet.ether_type"
+    bitwidth: 16
+    match_type: TERNARY
+  }
+  action_refs {
+    id: 16794308
+  }
+  action_refs {
+    id: 16829080
+  }
+  action_refs {
+    id: 16784184
+  }
+  size: 1024
+}
+tables {
+  preamble {
+    id: 33573361
+    name: "ip_proto_filter_table"
+    alias: "ip_proto_filter_table"
+  }
+  match_fields {
+    id: 1
+    name: "hdr.ipv4.src_addr"
+    bitwidth: 32
+    match_type: TERNARY
+  }
+  match_fields {
+    id: 2
+    name: "hdr.ipv4.protocol"
+    bitwidth: 8
+    match_type: EXACT
+  }
+  action_refs {
+    id: 16784184
+  }
+  action_refs {
+    id: 16800567
+    annotations: "@defaultonly()"
+  }
+  size: 1024
+}
+actions {
+  preamble {
+    id: 16800567
+    name: "NoAction"
+    alias: "NoAction"
+  }
+}
+actions {
+  preamble {
+    id: 16829080
+    name: "send_to_cpu"
+    alias: "send_to_cpu"
+  }
+}
+actions {
+  preamble {
+    id: 16794308
+    name: "set_egress_port"
+    alias: "set_egress_port"
+  }
+  params {
+    id: 1
+    name: "port"
+    bitwidth: 9
+  }
+}
+actions {
+  preamble {
+    id: 16784184
+    name: "_drop"
+    alias: "_drop"
+  }
+}
+counters {
+  preamble {
+    id: 302012419
+    name: "egr_port_counter"
+    alias: "egr_port_counter"
+  }
+  spec {
+    unit: PACKETS
+  }
+  size: 511
+}
+counters {
+  preamble {
+    id: 302054463
+    name: "igr_port_counter"
+    alias: "igr_port_counter"
+  }
+  spec {
+    unit: PACKETS
+  }
+  size: 511
+}
+controller_packet_metadata {
+  preamble {
+    id: 2868941301
+    name: "packet_in"
+    annotations: "@controller_header(\"packet_in\")"
+  }
+  metadata {
+    id: 1
+    name: "ingress_port"
+    bitwidth: 9
+  }
+}
+controller_packet_metadata {
+  preamble {
+    id: 2868916615
+    name: "packet_out"
+    annotations: "@controller_header(\"packet_out\")"
+  }
+  metadata {
+    id: 1
+    name: "egress_port"
+    bitwidth: 9
+  }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/flow/criteria/PiCriterion.java b/core/api/src/main/java/org/onosproject/net/flow/criteria/PiCriterion.java
index 487380f..d0a4732 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/criteria/PiCriterion.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/criteria/PiCriterion.java
@@ -342,7 +342,7 @@
          *
          * @return PiCriterion
          */
-        public Criterion build() {
+        public PiCriterion build() {
             ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatchMap = fieldMatchMapBuilder.build();
             checkArgument(fieldMatchMap.size() > 0, "Cannot build PI criterion with 0 field matches");
             return new PiCriterion(fieldMatchMap);
diff --git a/modules.defs b/modules.defs
index 4f67048..98bf292 100644
--- a/modules.defs
+++ b/modules.defs
@@ -219,6 +219,8 @@
     '//apps/evpn-route-service:onos-apps-evpn-route-service-oar',
     '//incubator/protobuf/registry:onos-incubator-protobuf-registry-oar',
     '//apps/openstacknetworkingui:onos-apps-openstacknetworkingui-oar',
+    '//apps/p4-tutorial/pipeconf:onos-apps-p4-tutorial-pipeconf-oar',
+    '//apps/p4-tutorial/icmpdropper:onos-apps-p4-tutorial-icmpdropper-oar',
 ]
 
 PROTOCOL_APPS = [