ONOS-6605 PI flow rule translator implementation

Change-Id: Icac66f17677c494152207f4b52355ad647e1227b
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
index 443c15b..8d06100 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiPipelineInterpreter.java
@@ -18,15 +18,17 @@
 
 import com.google.common.annotations.Beta;
 import org.onosproject.net.driver.HandlerBehaviour;
+import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.net.flow.criteria.Criterion;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableId;
 
 import java.util.Optional;
 
 /**
- * An interpreter of a protocol-independent pipeline.
+ * An interpreter of a protocol-independent pipeline model.
  */
 @Beta
 public interface PiPipelineInterpreter extends HandlerBehaviour {
@@ -52,6 +54,16 @@
     Optional<Criterion.Type> mapPiHeaderFieldId(PiHeaderFieldId headerFieldId);
 
     /**
+     * Returns a protocol-independent table id equivalent to the given numeric table id (as in
+     * {@link FlowRule#tableId()}). If not present, it means that the given numeric table id cannot
+     * be mapped to any table of the pipeline model.
+     *
+     * @param flowRuleTableId a numeric table id
+     * @return a protocol-independent table id
+     */
+    Optional<PiTableId> mapFlowRuleTableId(int flowRuleTableId);
+
+    /**
      * Returns a table action of a protocol-independent pipeline that is functionally equivalent to
      * the given ONOS traffic treatment for the given pipeline configuration.
      *
@@ -72,4 +84,4 @@
             super(message);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java b/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
index 420b5f6..5709e4e 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/model/PiTableModel.java
@@ -19,6 +19,7 @@
 import com.google.common.annotations.Beta;
 
 import java.util.Collection;
+import java.util.Optional;
 
 /**
  * Model of a match+action table in a protocol-independent pipeline.
@@ -67,4 +68,14 @@
      * @return a collection of action models
      */
     Collection<PiActionModel> actions();
+
+    /**
+     * Returns the action model associated with the given name, if present.
+     * If not present, it means that this table does not support such an action.
+     *
+     * @param name string value
+     * @return optional action model
+     */
+    Optional<PiActionModel> action(String name);
+
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionId.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionId.java
index d0655da..7a04318 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionId.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionId.java
@@ -38,6 +38,15 @@
     }
 
     /**
+     * Returns the name of the action.
+     *
+     * @return action name
+     */
+    public String name() {
+        return this.identifier;
+    }
+
+    /**
      * Returns an action identifier with the given name.
      *
      * @param name action name
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index c6f6094..63e478d 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -19,7 +19,7 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 
 import java.util.Collection;
@@ -39,7 +39,7 @@
     private static final double NO_TIMEOUT = -1;
 
     private final PiTableId tableId;
-    private final Collection<PiFieldMatch> fieldMatches;
+    private final Map<PiHeaderFieldId, PiFieldMatch> fieldMatches;
     private final PiTableAction tableAction;
     private final long cookie;
     private final int priority;
@@ -48,7 +48,7 @@
     private PiTableEntry(PiTableId tableId, Map<PiHeaderFieldId, PiFieldMatch> fieldMatches,
                          PiTableAction tableAction, long cookie, int priority, double timeout) {
         this.tableId = tableId;
-        this.fieldMatches = ImmutableSet.copyOf(fieldMatches.values());
+        this.fieldMatches = ImmutableMap.copyOf(fieldMatches);
         this.tableAction = tableAction;
         this.cookie = cookie;
         this.priority = priority;
@@ -70,7 +70,17 @@
      * @return collection of field matches
      */
     public Collection<PiFieldMatch> fieldMatches() {
-        return fieldMatches;
+        return fieldMatches.values();
+    }
+
+    /**
+     * If present, returns the field match associated with the given header field identifier.
+     *
+     * @param fieldId field identifier
+     * @return optional field match
+     */
+    public Optional<PiFieldMatch> fieldMatch(PiHeaderFieldId fieldId) {
+        return Optional.ofNullable(fieldMatches.get(fieldId));
     }
 
     /**
@@ -171,7 +181,7 @@
          * @param tableId table identifier
          * @return this
          */
-        Builder forTable(PiTableId tableId) {
+        public Builder forTable(PiTableId tableId) {
             this.tableId = checkNotNull(tableId);
             return this;
         }
@@ -182,7 +192,7 @@
          * @param tableAction table action
          * @return this
          */
-        Builder withAction(PiTableAction tableAction) {
+        public Builder withAction(PiTableAction tableAction) {
             this.tableAction = checkNotNull(tableAction);
             return this;
         }
@@ -193,7 +203,7 @@
          * @param fieldMatch field match
          * @return this
          */
-        Builder withFieldMatch(PiFieldMatch fieldMatch) {
+        public Builder withFieldMatch(PiFieldMatch fieldMatch) {
             this.fieldMatches.put(fieldMatch.fieldId(), fieldMatch);
             return this;
         }
@@ -204,7 +214,7 @@
          * @param fieldMatches collection of field matches
          * @return this
          */
-        Builder withFieldMatches(Collection<PiFieldMatch> fieldMatches) {
+        public Builder withFieldMatches(Collection<PiFieldMatch> fieldMatches) {
             fieldMatches.forEach(f -> this.fieldMatches.put(f.fieldId(), f));
             return this;
         }
@@ -215,7 +225,7 @@
          * @param cookie cookie
          * @return this
          */
-        Builder withCookie(long cookie) {
+        public Builder withCookie(long cookie) {
             this.cookie = cookie;
             return this;
         }
@@ -226,7 +236,7 @@
          * @param priority priority
          * @return this
          */
-        Builder withPriority(int priority) {
+        public Builder withPriority(int priority) {
             checkArgument(priority >= 0, "Priority must be a positive integer.");
             this.priority = priority;
             return this;
@@ -238,7 +248,7 @@
          * @param seconds timeout in seconds
          * @return this
          */
-        Builder withTimeout(double seconds) {
+        public Builder withTimeout(double seconds) {
             checkArgument(seconds > 0, "Timeout must be greater than zero.");
             this.timeout = seconds;
             return this;
@@ -249,7 +259,7 @@
          *
          * @return a new table entry
          */
-        PiTableEntry build() {
+        public PiTableEntry build() {
             checkNotNull(tableId);
             checkNotNull(tableAction);
             return new PiTableEntry(tableId, fieldMatches, tableAction, cookie, priority, timeout);
diff --git a/core/net/BUCK b/core/net/BUCK
index a9864d5..383cf54 100644
--- a/core/net/BUCK
+++ b/core/net/BUCK
@@ -13,6 +13,7 @@
     '//core/store/dist:onos-core-dist',
     '//core/store/dist:onos-core-dist-tests',
     '//utils/osgi:onlab-osgi-tests',
+    '//incubator/bmv2/model:onos-incubator-bmv2-model'
 ]
 
 osgi_jar_with_tests (
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractCriterionTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractCriterionTranslator.java
new file mode 100644
index 0000000..80639b4
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractCriterionTranslator.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiMatchType;
+
+import java.util.Optional;
+
+import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
+import static org.onlab.util.ImmutableByteSequence.fit;
+
+/**
+ * Abstract implementation of a criterion translator that opportunistically tries to generate different types of match
+ * based on an initialization method. It throws an exception when a safe translation is not possible, e.g. when trying
+ * to translate a masked criterion to an exact match.
+ */
+abstract class AbstractCriterionTranslator implements CriterionTranslator {
+
+    private PiMatchType initType;
+    private int bitWidth;
+    private ImmutableByteSequence value;
+    private ImmutableByteSequence mask;
+    private Integer prefixLength;
+
+    /**
+     * Initializes the translator as an exact match.
+     *
+     * @param value     exact match value
+     * @param bitWidth  field bit-width
+     * @throws ByteSequenceTrimException if the match value cannot be trimmed to fit the field match bit-width
+     */
+    void initAsExactMatch(ImmutableByteSequence value, int bitWidth)
+            throws ByteSequenceTrimException {
+        this.value = fit(value, bitWidth);
+        this.bitWidth = bitWidth;
+        this.initType = PiMatchType.EXACT;
+    }
+
+    /**
+     * Initializes the translator as a ternary match.
+     *
+     * @param value     match value
+     * @param mask      match mask
+     * @param bitWidth  field bit-width
+     * @throws ByteSequenceTrimException if the match value or mask cannot be trimmed to fit the field match bit-width
+     */
+    void initAsTernaryMatch(ImmutableByteSequence value, ImmutableByteSequence mask, int bitWidth)
+            throws ByteSequenceTrimException {
+        this.value = fit(value, bitWidth);
+        this.mask = fit(mask, bitWidth);
+        this.bitWidth = bitWidth;
+        this.initType = PiMatchType.TERNARY;
+    }
+
+    /**
+     * Initializes the translator as a longest-prefix match.
+     *
+     * @param value        match value
+     * @param prefixLength prefix length
+     * @param bitWidth     field bit-width
+     * @throws ByteSequenceTrimException if the match value cannot be trimmed to fit the field match bit-width
+     */
+    void initAsLpm(ImmutableByteSequence value, int prefixLength, int bitWidth)
+            throws ByteSequenceTrimException {
+        this.value = fit(value, bitWidth);
+        this.prefixLength = prefixLength;
+        this.bitWidth = bitWidth;
+        this.initType = PiMatchType.LPM;
+    }
+
+    @Override
+    public ImmutableByteSequence exactMatch() throws CriterionTranslatorException {
+        switch (initType) {
+            case EXACT:
+                break;
+            case TERNARY:
+                ImmutableByteSequence exactMask = ImmutableByteSequence.ofOnes(value.size());
+                if (!mask.equals(exactMask)) {
+                    throw new CriterionTranslator.CriterionTranslatorException(
+                            "trying to use masked field as an exact match, but mask is not exact");
+                }
+                break;
+            case LPM:
+                if (prefixLength < bitWidth) {
+                    throw new CriterionTranslator.CriterionTranslatorException(
+                            "trying to use LPM field as an exact match, but prefix is not full");
+                }
+                break;
+            default:
+                throw new RuntimeException("Unrecognized init type " + initType.name());
+        }
+        return value;
+    }
+
+    @Override
+    public Pair<ImmutableByteSequence, ImmutableByteSequence> ternaryMatch() {
+        switch (initType) {
+            case EXACT:
+                mask = ImmutableByteSequence.ofOnes(value.size());
+                break;
+            case TERNARY:
+                break;
+            case LPM:
+                mask = getMaskFromPrefixLength(prefixLength, value.size());
+            default:
+                throw new RuntimeException("Unrecognized init type " + initType.name());
+        }
+
+        return Pair.of(value, mask);
+    }
+
+    @Override
+    public Pair<ImmutableByteSequence, Integer> lpmMatch() throws CriterionTranslatorException {
+        switch (initType) {
+            case EXACT:
+                prefixLength = bitWidth;
+                break;
+            case TERNARY:
+                prefixLength = getPrefixLengthFromMask(mask).orElseThrow(
+                        () -> new CriterionTranslatorException(
+                                "trying to use masked field as a longest-prefix match, " +
+                                        "but mask is not equivalent to a prefix length"));
+                break;
+            case LPM:
+                break;
+            default:
+                throw new RuntimeException("Unrecognized init type " + initType.name());
+        }
+
+        return Pair.of(value, prefixLength);
+    }
+
+    /**
+     * Returns a bit mask equivalent to the given prefix length.
+     *
+     * @param prefixLength prefix length (in bits)
+     * @param maskSize     mask size (in bytes)
+     * @return a byte sequence
+     */
+    private ImmutableByteSequence getMaskFromPrefixLength(int prefixLength, int maskSize) {
+        // TODO: implement.
+        throw new RuntimeException("getMaskFromPrefixLength() not implemented yet.");
+    }
+
+    /**
+     * Checks that the given mask is equivalent to a longest-prefix match and returns the prefix length. If not
+     * possible, the optional value will not be present.
+     *
+     * @param mask byte sequence
+     * @return optional prefix length
+     */
+    private Optional<Integer> getPrefixLengthFromMask(ImmutableByteSequence mask) {
+        // TODO: implement.
+        throw new RuntimeException("getPrefixLengthFromMask() not implemented yet.");
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslator.java
new file mode 100644
index 0000000..10b92af
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslator.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.flow.criteria.Criterion;
+
+import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
+
+/**
+ * A translator of a criterion instance to different match types represented as primitive byte sequences.
+ */
+interface CriterionTranslator {
+
+    /**
+     * Initialize this translator with the given criterion and field match bit-width.
+     *
+     * @param criterion criterion
+     * @param bitWidth  field bit-width
+     * @throws ByteSequenceTrimException if the criterion cannot be trimmed to fit the field match bit-width
+     */
+    void init(Criterion criterion, int bitWidth) throws ByteSequenceTrimException;
+
+    /**
+     * Returns a pair of byte sequences representing a ternary match (value and mask) equivalent to the criterion given
+     * when initialized. The returned byte sequences will have minimum size (i.e. rounded to the nearest byte) to
+     * contain the initialized field bit-width.
+     *
+     * @return a pair of byte sequences with value on the left and mask on the right
+     * @throws CriterionTranslatorException if the criterion cannot be translated
+     */
+    Pair<ImmutableByteSequence, ImmutableByteSequence> ternaryMatch() throws CriterionTranslatorException;
+
+    /**
+     * Returns a pair comprising a byte sequence and a prefix length representing a longest-prefix match equivalent to
+     * the criterion given when initialized. The returned byte sequence will have minimum size (i.e. rounded to the
+     * nearest byte) to contain the initialized field bit-width.
+     *
+     * @return a pair with the match value's byte sequence on the left, and prefix length on the right
+     * @throws CriterionTranslatorException if the criterion cannot be translated
+     */
+    Pair<ImmutableByteSequence, Integer> lpmMatch() throws CriterionTranslatorException;
+
+    /**
+     * Returns a byte sequence representing an exact match equivalent to the criterion given when initialized. The
+     * returned byte sequence will have minimum size (i.e. rounded to the nearest byte) to contain the initialized field
+     * bit-width.
+     *
+     * @return exact match value as a byte sequence
+     * @throws CriterionTranslatorException if the criterion cannot be translated
+     */
+    ImmutableByteSequence exactMatch() throws CriterionTranslatorException;
+
+    /**
+     * Signifies that the criterion cannot be translated.
+     */
+    class CriterionTranslatorException extends Exception {
+        CriterionTranslatorException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
new file mode 100644
index 0000000..0791f3a
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.EthCriterion;
+import org.onosproject.net.flow.criteria.EthTypeCriterion;
+import org.onosproject.net.flow.criteria.IPCriterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+import org.onosproject.net.pi.impl.CriterionTranslators.EthCriterionTranslator;
+import org.onosproject.net.pi.impl.CriterionTranslators.EthTypeCriterionTranslator;
+import org.onosproject.net.pi.impl.CriterionTranslators.IpCriterionTranslator;
+import org.onosproject.net.pi.impl.CriterionTranslators.PortCriterionTranslator;
+import org.onosproject.net.pi.model.PiMatchType;
+import org.onosproject.net.pi.runtime.PiExactFieldMatch;
+import org.onosproject.net.pi.runtime.PiFieldMatch;
+import org.onosproject.net.pi.runtime.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+
+import java.util.Map;
+
+import static java.lang.String.format;
+import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
+import static org.onosproject.net.pi.impl.CriterionTranslator.CriterionTranslatorException;
+import static org.onosproject.net.pi.runtime.PiFlowRuleTranslationService.PiFlowRuleTranslationException;
+
+/**
+ * Helper class to translate criterion instances to PI field matches.
+ */
+final class CriterionTranslatorHelper {
+
+    private static final Map<Class<? extends Criterion>, CriterionTranslator> TRANSLATORS = ImmutableMap.of(
+            // Add here new CriterionTranslator implementations.
+            PortCriterion.class, new PortCriterionTranslator(),
+            EthCriterion.class, new EthCriterionTranslator(),
+            EthTypeCriterion.class, new EthTypeCriterionTranslator(),
+            IPCriterion.class, new IpCriterionTranslator()
+    );
+
+    private CriterionTranslatorHelper() {
+        // Hides constructor.
+    }
+
+    /**
+     * Translates a given criterion instance to a PiFieldMatch with the given id, match type, and bit-width.
+     *
+     * @param fieldId   PI header field identifier
+     * @param criterion criterion
+     * @param matchType match type
+     * @param bitWidth  size of the field match in bits
+     * @return a PI field match
+     * @throws PiFlowRuleTranslationException if the criterion cannot be translated (see exception message)
+     */
+    static PiFieldMatch translateCriterion(Criterion criterion, PiHeaderFieldId fieldId, PiMatchType matchType,
+                                           int bitWidth)
+            throws PiFlowRuleTranslationException {
+
+        if (!TRANSLATORS.containsKey(criterion.getClass())) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Translation of criterion class %s is not implemented.",
+                    criterion.getClass().getSimpleName()));
+        }
+
+        CriterionTranslator translator = TRANSLATORS.get(criterion.getClass());
+
+        try {
+            translator.init(criterion, bitWidth);
+            switch (matchType) {
+                case EXACT:
+                    return new PiExactFieldMatch(fieldId, translator.exactMatch());
+                case TERNARY:
+                    Pair<ImmutableByteSequence, ImmutableByteSequence> tp = translator.ternaryMatch();
+                    return new PiTernaryFieldMatch(fieldId, tp.getLeft(), tp.getRight());
+                case LPM:
+                    Pair<ImmutableByteSequence, Integer> lp = translator.lpmMatch();
+                    return new PiLpmFieldMatch(fieldId, lp.getLeft(), lp.getRight());
+                default:
+                    throw new PiFlowRuleTranslationException(format(
+                            "Translation of criterion %s (%s class) to match type %s is not implemented.",
+                            criterion.type().name(), criterion.getClass().getSimpleName(), matchType.name()));
+            }
+        } catch (ByteSequenceTrimException e) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Size mismatch for criterion %s: %s", criterion.type(), e.getMessage()));
+        } catch (CriterionTranslatorException e) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Unable to translate criterion %s: %s", criterion.type(), e.getMessage()));
+        }
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslators.java b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslators.java
new file mode 100644
index 0000000..089f80a
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslators.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.EthCriterion;
+import org.onosproject.net.flow.criteria.EthTypeCriterion;
+import org.onosproject.net.flow.criteria.IPCriterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+
+import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+
+/**
+ * Factory class of criterion translator implementations.
+ */
+final class CriterionTranslators {
+
+    /**
+     * Translator of PortCriterion.
+     */
+    static final class PortCriterionTranslator extends AbstractCriterionTranslator {
+        @Override
+        public void init(Criterion criterion, int bitWidth) throws ByteSequenceTrimException {
+            PortCriterion c = (PortCriterion) criterion;
+            initAsExactMatch(copyFrom(c.port().toLong()), bitWidth);
+        }
+    }
+
+    /**
+     * Translator of EthTypeCriterion.
+     */
+    static final class EthTypeCriterionTranslator extends AbstractCriterionTranslator {
+        @Override
+        public void init(Criterion criterion, int bitWidth) throws ByteSequenceTrimException {
+            EthTypeCriterion c = (EthTypeCriterion) criterion;
+            initAsExactMatch(copyFrom(c.ethType().toShort()), bitWidth);
+        }
+    }
+
+    /**
+     * Translator of EthCriterion.
+     */
+    static final class EthCriterionTranslator extends AbstractCriterionTranslator {
+        @Override
+        public void init(Criterion criterion, int bitWidth) throws ByteSequenceTrimException {
+            EthCriterion c = (EthCriterion) criterion;
+            ImmutableByteSequence value = copyFrom(c.mac().toBytes());
+            if (c.mask() == null) {
+                initAsExactMatch(value, bitWidth);
+            } else {
+                ImmutableByteSequence mask = copyFrom(c.mask().toBytes());
+                initAsTernaryMatch(value, mask, bitWidth);
+            }
+        }
+    }
+
+    /**
+     * Translator of IpCriterion.
+     */
+    static final class IpCriterionTranslator extends AbstractCriterionTranslator {
+        public void init(Criterion criterion, int bitWidth) throws ByteSequenceTrimException {
+            IPCriterion c = (IPCriterion) criterion;
+            initAsLpm(copyFrom(c.ip().address().toOctets()), c.ip().prefixLength(), bitWidth);
+        }
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java
new file mode 100644
index 0000000..f6f181d
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslationServiceImpl.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+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.apache.felix.scr.annotations.Service;
+import org.onosproject.net.Device;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiFlowRuleTranslationService;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.translateFlowRule;
+
+/**
+ * Implementation of the protocol-independent flow rule translation service.
+ */
+@Component(immediate = true)
+@Service
+public class PiFlowRuleTranslationServiceImpl implements PiFlowRuleTranslationService {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    // TODO: implement cache to speed up translation of flow rules.
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public PiTableEntry translate(FlowRule rule, PiPipeconf pipeconf)
+            throws PiFlowRuleTranslationException {
+
+        Device device = deviceService.getDevice(rule.deviceId());
+        if (device == null) {
+            throw new PiFlowRuleTranslationException("Unable to get device " + rule.deviceId());
+        }
+
+        return translateFlowRule(rule, pipeconf, device);
+    }
+}
+
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
new file mode 100644
index 0000000..14f15a5
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.Device;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.IndexTableId;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.PiCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.PiInstruction;
+import org.onosproject.net.pi.model.PiActionModel;
+import org.onosproject.net.pi.model.PiActionParamModel;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiPipelineInterpreter;
+import org.onosproject.net.pi.model.PiPipelineModel;
+import org.onosproject.net.pi.model.PiTableMatchFieldModel;
+import org.onosproject.net.pi.model.PiTableModel;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiExactFieldMatch;
+import org.onosproject.net.pi.runtime.PiFieldMatch;
+import org.onosproject.net.pi.runtime.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableId;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.StringJoiner;
+
+import static java.lang.String.format;
+import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
+import static org.onlab.util.ImmutableByteSequence.fit;
+import static org.onosproject.net.flow.criteria.Criterion.Type.PROTOCOL_INDEPENDENT;
+import static org.onosproject.net.pi.impl.CriterionTranslatorHelper.translateCriterion;
+import static org.onosproject.net.pi.runtime.PiFlowRuleTranslationService.PiFlowRuleTranslationException;
+
+/**
+ * Implementation of flow rule translation logic.
+ */
+final class PiFlowRuleTranslator {
+
+    private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslationServiceImpl.class);
+
+    private PiFlowRuleTranslator() {
+        // Hide constructor.
+    }
+
+    static PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf, Device device)
+            throws PiFlowRuleTranslationException {
+
+        PiPipelineModel pipelineModel = pipeconf.pipelineModel();
+
+        // Retrieve interpreter, if any.
+        // FIXME: get interpreter via driver once implemented.
+        // final PiPipelineInterpreter interpreter = device.is(PiPipelineInterpreter.class)
+        //        ? device.as(PiPipelineInterpreter.class) : null;
+
+        final PiPipelineInterpreter interpreter;
+        try {
+            interpreter = (PiPipelineInterpreter) pipeconf.implementation(PiPipelineInterpreter.class)
+                    .orElse(null)
+                    .newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Unable to instantiate interpreter of pipeconf %s", pipeconf.id()));
+        }
+
+        PiTableId piTableId;
+        switch (rule.table().type()) {
+            case PIPELINE_INDEPENDENT:
+                piTableId = (PiTableId) rule.table();
+                break;
+            case INDEX:
+                IndexTableId indexId = (IndexTableId) rule.table();
+                if (interpreter == null) {
+                    throw new PiFlowRuleTranslationException(format(
+                            "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
+                } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
+                    throw new PiFlowRuleTranslationException(format(
+                            "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
+                } else {
+                    piTableId = interpreter.mapFlowRuleTableId(indexId.id()).get();
+                }
+                break;
+            default:
+                throw new PiFlowRuleTranslationException(format(
+                        "Unrecognized table ID type %s", rule.table().type().name()));
+        }
+
+        PiTableModel table = pipelineModel.table(piTableId.toString())
+                .orElseThrow(() -> new PiFlowRuleTranslationException(format(
+                        "Not such a table in pipeline model: %s", piTableId)));
+
+        /* Translate selector */
+        Collection<PiFieldMatch> fieldMatches = buildFieldMatches(interpreter, rule.selector(), table);
+
+        /* Translate treatment */
+        PiAction piAction = buildAction(rule.treatment(), interpreter, pipeconf);
+        piAction = typeCheckAction(piAction, table);
+
+        PiTableEntry.Builder tableEntryBuilder = PiTableEntry.builder();
+
+        // In BMv2 0 is the highest priority.
+        // FIXME: Check P4Runtime and agree on maximum priority in the TableEntry javadoc.
+        // int newPriority = Integer.MAX_VALUE - rule.priority();
+
+        tableEntryBuilder
+                .forTable(piTableId)
+                .withPriority(rule.priority())
+                .withFieldMatches(fieldMatches)
+                .withAction(piAction);
+
+        if (!rule.isPermanent()) {
+            if (table.supportsAging()) {
+                tableEntryBuilder.withTimeout((double) rule.timeout());
+            } else {
+                log.warn("Flow rule is temporary, but table '{}' doesn't support " +
+                                 "aging, translating to permanent.", table.name());
+            }
+
+        }
+
+        return tableEntryBuilder.build();
+    }
+
+    /**
+     * Builds a PI action out of the given treatment, optionally using the given interpreter.
+     */
+    private static PiAction buildAction(TrafficTreatment treatment, PiPipelineInterpreter interpreter,
+                                        PiPipeconf pipeconf)
+            throws PiFlowRuleTranslationException {
+
+        PiTableAction piTableAction = null;
+
+        // If treatment has only one instruction of type PiInstruction, use that.
+        for (Instruction inst : treatment.allInstructions()) {
+            if (inst.type() == Instruction.Type.PROTOCOL_INDEPENDENT) {
+                if (treatment.allInstructions().size() == 1) {
+                    piTableAction = ((PiInstruction) inst).action();
+                } else {
+                    throw new PiFlowRuleTranslationException(format(
+                            "Unable to translate treatment, found multiple instructions " +
+                                    "of which one is protocol-independent: %s", treatment));
+                }
+            }
+        }
+
+        if (piTableAction == null && interpreter != null) {
+            // No PiInstruction, use interpreter to build action.
+            try {
+                piTableAction = interpreter.mapTreatment(treatment, pipeconf);
+            } catch (PiPipelineInterpreter.PiInterpreterException e) {
+                throw new PiFlowRuleTranslationException(
+                        "Interpreter was unable to translate treatment. " + e.getMessage());
+            }
+        }
+
+        if (piTableAction == null) {
+            // No PiInstruction, no interpreter. It's time to give up.
+            throw new PiFlowRuleTranslationException(
+                    "Unable to translate treatment, neither an interpreter or a "
+                            + "protocol-independent instruction were provided.");
+        }
+
+        if (piTableAction.type() != PiTableAction.Type.ACTION) {
+            // TODO: implement handling of other table action types, e.g. action profiles.
+            throw new PiFlowRuleTranslationException(format(
+                    "PiTableAction type %s is not supported yet.", piTableAction.type()));
+        }
+
+        return (PiAction) piTableAction;
+    }
+
+    /**
+     * Checks that the given PI action is suitable for the given table model and returns a new action instance with
+     * parameters well-sized, according to the table model. If not suitable, throws an exception explaining why.
+     */
+    private static PiAction typeCheckAction(PiAction piAction, PiTableModel table)
+            throws PiFlowRuleTranslationException {
+
+        // Table supports this action?
+        PiActionModel actionModel = table.action(piAction.id().name()).orElseThrow(
+                () -> new PiFlowRuleTranslationException(format("Not such action '%s' for table '%s'",
+                                                                piAction.id(), table.name())));
+
+        // Is the number of runtime parameters correct?
+        if (actionModel.params().size() != piAction.parameters().size()) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Wrong number of runtime parameters for action '%s', expected %d but found %d",
+                    actionModel.name(), actionModel.params().size(), piAction.parameters().size()));
+        }
+
+        // Forge a new action instance with well-sized parameters.
+        // The same comment as in typeCheckFieldMatch() about duplicating field match instances applies here.
+        PiAction.Builder newActionBuilder = PiAction.builder().withId(piAction.id());
+        for (PiActionParam param : piAction.parameters()) {
+            PiActionParamModel paramModel = actionModel.param(param.id().name())
+                    .orElseThrow(() -> new PiFlowRuleTranslationException(format(
+                            "Not such parameter '%s' for action '%s'", param.id(), actionModel.name())));
+            try {
+                newActionBuilder.withParameter(new PiActionParam(param.id(),
+                                                                 fit(param.value(), paramModel.bitWidth())));
+            } catch (ByteSequenceTrimException e) {
+                throw new PiFlowRuleTranslationException(format(
+                        "Size mismatch for parameter '%s' of action '%s': %s",
+                        param.id(), piAction.id(), e.getMessage()));
+            }
+        }
+
+        return newActionBuilder.build();
+    }
+
+    /**
+     * Builds a collection of PI field matches out of the given selector, optionally using the given interpreter. The
+     * field matches returned are guaranteed to be compatible for the given table model.
+     */
+    private static Collection<PiFieldMatch> buildFieldMatches(PiPipelineInterpreter interpreter,
+                                                              TrafficSelector selector, PiTableModel tableModel)
+            throws PiFlowRuleTranslationException {
+
+        Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
+
+        // If present, find a PiCriterion and get its field matches as a map. Otherwise, use an empty map.
+        Map<PiHeaderFieldId, PiFieldMatch> piCriterionFields = selector.criteria().stream()
+                .filter(c -> c.type().equals(PROTOCOL_INDEPENDENT))
+                .map(c -> (PiCriterion) c)
+                .findFirst()
+                .map(PiCriterion::fieldMatches)
+                .map(c -> {
+                    Map<PiHeaderFieldId, PiFieldMatch> fieldMap = Maps.newHashMap();
+                    c.forEach(fieldMatch -> fieldMap.put(fieldMatch.fieldId(), fieldMatch));
+                    return fieldMap;
+                })
+                .orElse(Maps.newHashMap());
+
+        Set<Criterion> translatedCriteria = Sets.newHashSet();
+        Set<Criterion> ignoredCriteria = Sets.newHashSet();
+        Set<PiHeaderFieldId> usedPiCriterionFields = Sets.newHashSet();
+        Set<PiHeaderFieldId> ignoredPiCriterionFields = Sets.newHashSet();
+
+        for (PiTableMatchFieldModel fieldModel : tableModel.matchFields()) {
+
+            PiHeaderFieldId fieldId = PiHeaderFieldId.of(fieldModel.field().header().type().name(),
+                                                         fieldModel.field().type().name(),
+                                                         fieldModel.field().header().index());
+
+            int bitWidth = fieldModel.field().type().bitWidth();
+            int fieldByteWidth = (int) Math.ceil((double) bitWidth / 8);
+
+            Optional<Criterion.Type> criterionType =
+                    interpreter == null
+                            ? Optional.empty()
+                            : interpreter.mapPiHeaderFieldId(fieldId);
+
+            Criterion criterion = criterionType.map(selector::getCriterion).orElse(null);
+
+            if (!piCriterionFields.containsKey(fieldId) && criterion == null) {
+                // Neither a field in PiCriterion is available nor a Criterion mapping is possible.
+                // Can ignore if the match is ternary or LPM.
+                switch (fieldModel.matchType()) {
+                    case TERNARY:
+                        // Wildcard the whole field.
+                        fieldMatches.put(fieldId, new PiTernaryFieldMatch(
+                                fieldId,
+                                ImmutableByteSequence.ofZeros(fieldByteWidth),
+                                ImmutableByteSequence.ofZeros(fieldByteWidth)));
+                        break;
+                    case LPM:
+                        // LPM with prefix 0
+                        fieldMatches.put(fieldId, new PiLpmFieldMatch(fieldId,
+                                                                      ImmutableByteSequence.ofZeros(fieldByteWidth),
+                                                                      0));
+                        break;
+                    // FIXME: Can we handle the case of RANGE or VALID match?
+                    default:
+                        throw new PiFlowRuleTranslationException(format(
+                                "No value found for required match field '%s'", fieldId));
+                }
+                // Next field.
+                continue;
+            }
+
+            PiFieldMatch fieldMatch = null;
+
+            if (criterion != null) {
+                // Criterion mapping is possible for this field id.
+                try {
+                    fieldMatch = translateCriterion(criterion, fieldId, fieldModel.matchType(), bitWidth);
+                    translatedCriteria.add(criterion);
+                } catch (PiFlowRuleTranslationException ex) {
+                    // Ignore exception if the same field was found in PiCriterion.
+                    if (piCriterionFields.containsKey(fieldId)) {
+                        ignoredCriteria.add(criterion);
+                    } else {
+                        throw ex;
+                    }
+                }
+            }
+
+            if (piCriterionFields.containsKey(fieldId)) {
+                // Field was found in PiCriterion.
+                if (fieldMatch != null) {
+                    // Field was already translated from other criterion.
+                    // Throw exception only if we are trying to match on different values of the same field...
+                    if (!fieldMatch.equals(piCriterionFields.get(fieldId))) {
+                        throw new PiFlowRuleTranslationException(format(
+                                "Duplicate match field '%s': instance translated from criterion '%s' is different to " +
+                                        "what found in PiCriterion.", fieldId, criterion.type()));
+                    }
+                    ignoredPiCriterionFields.add(fieldId);
+                } else {
+                    fieldMatch = piCriterionFields.get(fieldId);
+                    fieldMatch = typeCheckFieldMatch(fieldMatch, fieldModel);
+                    usedPiCriterionFields.add(fieldId);
+                }
+            }
+
+            fieldMatches.put(fieldId, fieldMatch);
+        }
+
+        // Check if all criteria have been translated.
+        StringJoiner skippedCriteriaJoiner = new StringJoiner(", ");
+        selector.criteria().stream()
+                .filter(c -> !c.type().equals(PROTOCOL_INDEPENDENT))
+                .filter(c -> !translatedCriteria.contains(c) && !ignoredCriteria.contains(c))
+                .forEach(c -> skippedCriteriaJoiner.add(c.type().name()));
+        if (skippedCriteriaJoiner.length() > 0) {
+            throw new PiFlowRuleTranslationException(format(
+                    "The following criteria cannot be translated for table '%s': %s",
+                    tableModel.name(), skippedCriteriaJoiner.toString()));
+        }
+
+        // Check if all fields found in PiCriterion have been used.
+        StringJoiner skippedPiFieldsJoiner = new StringJoiner(", ");
+        piCriterionFields.keySet().stream()
+                .filter(k -> !usedPiCriterionFields.contains(k) && !ignoredPiCriterionFields.contains(k))
+                .forEach(k -> skippedPiFieldsJoiner.add(k.id()));
+        if (skippedPiFieldsJoiner.length() > 0) {
+            throw new PiFlowRuleTranslationException(format(
+                    "The following PiCriterion field matches are not supported in table '%s': %s",
+                    tableModel.name(), skippedPiFieldsJoiner.toString()));
+        }
+
+        return fieldMatches.values();
+    }
+
+    private static PiFieldMatch typeCheckFieldMatch(PiFieldMatch fieldMatch, PiTableMatchFieldModel fieldModel)
+            throws PiFlowRuleTranslationException {
+
+        // Check parameter type and size
+        if (!fieldModel.matchType().equals(fieldMatch.type())) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Wrong match type for field '%s', expected %s, but found %s",
+                    fieldMatch.fieldId(), fieldModel.matchType().name(), fieldMatch.type().name()));
+        }
+
+        int modelBitWidth = fieldModel.field().type().bitWidth();
+
+        /*
+        Here we try to be robust against wrong size fields with the goal of having PiCriterion independent of the
+        pipeline model. We duplicate the field match, fitting the byte sequences to the bit-width specified in the
+        model. This operation is expensive when performed for each field match of each flow rule, but should be
+        mitigated by the translation cache provided by PiFlowRuleTranslationServiceImpl.
+        */
+
+        try {
+            switch (fieldModel.matchType()) {
+                case EXACT:
+                    return new PiExactFieldMatch(fieldMatch.fieldId(),
+                                                 fit(((PiExactFieldMatch) fieldMatch).value(), modelBitWidth));
+                case TERNARY:
+                    return new PiTernaryFieldMatch(fieldMatch.fieldId(),
+                                                   fit(((PiTernaryFieldMatch) fieldMatch).value(), modelBitWidth),
+                                                   fit(((PiTernaryFieldMatch) fieldMatch).mask(), modelBitWidth));
+                case LPM:
+                    PiLpmFieldMatch lpmfield = (PiLpmFieldMatch) fieldMatch;
+                    if (lpmfield.prefixLength() > modelBitWidth) {
+                        throw new PiFlowRuleTranslationException(format(
+                                "Invalid prefix length for LPM field '%s', found %d but field has bit-width %d",
+                                fieldMatch.fieldId(), lpmfield.prefixLength(), modelBitWidth));
+                    }
+                    return new PiLpmFieldMatch(fieldMatch.fieldId(),
+                                               fit(lpmfield.value(), modelBitWidth),
+                                               lpmfield.prefixLength());
+                case RANGE:
+                    return new PiRangeFieldMatch(fieldMatch.fieldId(),
+                                                 fit(((PiRangeFieldMatch) fieldMatch).lowValue(), modelBitWidth),
+                                                 fit(((PiRangeFieldMatch) fieldMatch).highValue(), modelBitWidth));
+                case VALID:
+                    return fieldMatch;
+                default:
+                    // Should never be here.
+                    throw new RuntimeException(
+                            "Unrecognized match type " + fieldModel.matchType().name());
+            }
+        } catch (ByteSequenceTrimException e) {
+            throw new PiFlowRuleTranslationException(format(
+                    "Size mismatch for field %s: %s", fieldMatch.fieldId(), e.getMessage()));
+        }
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/package-info.java b/core/net/src/main/java/org/onosproject/net/pi/impl/package-info.java
new file mode 100644
index 0000000..16e053a
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * Core subsystem for handling protocol-independence.
+ */
+package org.onosproject.net.pi.impl;
\ No newline at end of file
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java b/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java
new file mode 100644
index 0000000..ff8ac93
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/MockInterpreter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import com.google.common.collect.ImmutableBiMap;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.PortNumber;
+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.pi.model.PiPipeconf;
+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.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableId;
+
+import java.util.Optional;
+
+import static org.onosproject.net.PortNumber.CONTROLLER;
+import static org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+
+/**
+ * Mock interpreter implementation.
+ */
+public class MockInterpreter extends AbstractHandlerBehaviour implements PiPipelineInterpreter {
+
+    static final String TABLE0 = "table0";
+    static final String SEND_TO_CPU = "send_to_cpu_0";
+    static final String PORT = "port";
+    static final String DROP = "_drop_0";
+    static final String SET_EGRESS_PORT = "set_egress_port_0";
+
+    static final PiHeaderFieldId IN_PORT_ID = PiHeaderFieldId.of("standard_metadata", "ingress_port");
+    static final PiHeaderFieldId ETH_DST_ID = PiHeaderFieldId.of("ethernet_t", "dstAddr");
+    static final PiHeaderFieldId ETH_SRC_ID = PiHeaderFieldId.of("ethernet_t", "srcAddr");
+    static final PiHeaderFieldId ETH_TYPE_ID = PiHeaderFieldId.of("ethernet_t", "etherType");
+
+    private static final ImmutableBiMap<Criterion.Type, PiHeaderFieldId> CRITERION_MAP = ImmutableBiMap.of(
+            Criterion.Type.IN_PORT, IN_PORT_ID,
+            Criterion.Type.ETH_DST, ETH_DST_ID,
+            Criterion.Type.ETH_SRC, ETH_SRC_ID,
+            Criterion.Type.ETH_TYPE, ETH_TYPE_ID);
+
+    private static final ImmutableBiMap<Integer, PiTableId> TABLE_MAP = ImmutableBiMap.of(
+            0, PiTableId.of(TABLE0));
+
+    @Override
+    public PiTableAction mapTreatment(TrafficTreatment treatment, PiPipeconf pipeconf) throws PiInterpreterException {
+
+        if (treatment.allInstructions().size() == 0) {
+            // No instructions means drop for us.
+            return actionWithName(DROP);
+        } else if (treatment.allInstructions().size() > 1) {
+            // Otherwise, we understand treatments with only 1 instruction.
+            throw new PiPipelineInterpreter.PiInterpreterException("Treatment has multiple instructions");
+        }
+
+        Instruction instruction = treatment.allInstructions().get(0);
+
+        switch (instruction.type()) {
+            case OUTPUT:
+                OutputInstruction outInstruction = (OutputInstruction) instruction;
+                PortNumber port = outInstruction.port();
+                if (!port.isLogical()) {
+                    PiAction.builder()
+                            .withId(PiActionId.of(SET_EGRESS_PORT))
+                            .withParameter(new PiActionParam(PiActionParamId.of(PORT),
+                                                             ImmutableByteSequence.copyFrom(port.toLong())))
+                            .build();
+                } else if (port.equals(CONTROLLER)) {
+                    return actionWithName(SEND_TO_CPU);
+                } else {
+                    throw new PiInterpreterException("Egress on logical port not supported: " + port);
+                }
+            case NOACTION:
+                return actionWithName(DROP);
+            default:
+                throw new PiInterpreterException("Instruction type not supported: " + instruction.type().name());
+        }
+    }
+
+    /**
+     * Returns an action instance with no runtime parameters.
+     */
+    private PiAction actionWithName(String name) {
+        return PiAction.builder().withId(PiActionId.of(name)).build();
+    }
+
+    @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));
+    }
+
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/MockPipeconf.java b/core/net/src/test/java/org/onosproject/net/pi/impl/MockPipeconf.java
new file mode 100644
index 0000000..6694215
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/MockPipeconf.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import com.eclipsesource.json.Json;
+import com.google.common.collect.Maps;
+import org.onosproject.bmv2.model.Bmv2PipelineModelParser;
+import org.onosproject.net.driver.Behaviour;
+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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Mock pipeconf implementation.
+ */
+public class MockPipeconf implements PiPipeconf {
+
+    private static final String PIPECONF_ID = "org.project.pipeconf.default";
+    private static final String JSON_PATH = "/org/onosproject/net/pi/impl/default.json";
+
+    private final PiPipeconfId id;
+    private final PiPipelineModel pipelineModel;
+    private final Map<Class<? extends Behaviour>, Class<? extends Behaviour>> behaviours;
+
+    public MockPipeconf() throws IOException {
+        this.id = new PiPipeconfId(PIPECONF_ID);
+        this.pipelineModel = loadDefaultModel();
+        this.behaviours = Maps.newHashMap();
+
+        behaviours.put(PiPipelineInterpreter.class, MockInterpreter.class);
+    }
+
+    static PiPipelineModel loadDefaultModel() throws IOException {
+        return Bmv2PipelineModelParser.parse(Json.parse(new BufferedReader(new InputStreamReader(
+                MockPipeconf.class.getResourceAsStream(JSON_PATH)))).asObject());
+    }
+
+    @Override
+    public PiPipeconfId id() {
+        return this.id;
+    }
+
+    @Override
+    public PiPipelineModel pipelineModel() {
+        return pipelineModel;
+    }
+
+    @Override
+    public Collection<Class<? extends Behaviour>> behaviours() {
+        return behaviours.values();
+    }
+
+    @Override
+    public Optional<Class<? extends Behaviour>> implementation(Class<? extends Behaviour> behaviour) {
+        return Optional.ofNullable(behaviours.get(behaviour));
+    }
+
+    @Override
+    public boolean hasBehaviour(Class<? extends Behaviour> behaviourClass) {
+        return behaviours.containsKey(behaviourClass);
+    }
+
+    @Override
+    public Optional<ByteBuffer> extension(ExtensionType type) {
+        return Optional.empty();
+    }
+}
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
new file mode 100644
index 0000000..547e957
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * 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.net.pi.impl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+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.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+
+import java.util.Optional;
+import java.util.Random;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.onosproject.net.pi.impl.MockInterpreter.*;
+
+/**
+ * Tests for {@link PiFlowRuleTranslator}.
+ */
+@SuppressWarnings("ConstantConditions")
+public class PiFlowRuleTranslatorTest {
+
+    private Random random = new Random();
+    private PiPipeconf pipeconf;
+
+    @Before
+    public void setUp() throws Exception {
+        pipeconf = new MockPipeconf();
+    }
+
+    @Test
+    public void testTranslate() throws Exception {
+
+        DeviceId deviceId = DeviceId.NONE;
+        ApplicationId appId = new DefaultApplicationId(1, "test");
+        int tableId = 0;
+        MacAddress ethDstMac = MacAddress.valueOf(random.nextLong());
+        MacAddress ethSrcMac = MacAddress.valueOf(random.nextLong());
+        short ethType = (short) (0x0000FFFF & random.nextInt());
+        short outPort = (short) random.nextInt(65);
+        short inPort = (short) random.nextInt(65);
+        int timeout = random.nextInt(100);
+        int priority = random.nextInt(100);
+
+        TrafficSelector matchInPort1 = DefaultTrafficSelector
+                .builder()
+                .matchInPort(PortNumber.portNumber(inPort))
+                .matchEthDst(ethDstMac)
+                .matchEthSrc(ethSrcMac)
+                .matchEthType(ethType)
+                .build();
+
+        TrafficTreatment outPort2 = DefaultTrafficTreatment
+                .builder()
+                .setOutput(PortNumber.portNumber(outPort))
+                .build();
+
+        FlowRule rule1 = DefaultFlowRule.builder()
+                .forDevice(deviceId)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        FlowRule rule2 = DefaultFlowRule.builder()
+                .forDevice(deviceId)
+                .forTable(tableId)
+                .fromApp(appId)
+                .withSelector(matchInPort1)
+                .withTreatment(outPort2)
+                .makeTemporary(timeout)
+                .withPriority(priority)
+                .build();
+
+        PiTableEntry entry1 = PiFlowRuleTranslator.translateFlowRule(rule1, pipeconf, null);
+        PiTableEntry entry2 = PiFlowRuleTranslator.translateFlowRule(rule1, pipeconf, null);
+
+        // check equality, i.e. same rules must produce same entries
+        new EqualsTester()
+                .addEqualityGroup(rule1, rule2)
+                .addEqualityGroup(entry1, entry2)
+                .testEquals();
+
+        int numMatchParams = pipeconf.pipelineModel().table(TABLE0).get().matchFields().size();
+        // parse values stored in entry1
+        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.fieldMatch(IN_PORT_ID).get();
+        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_DST_ID).get();
+        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_SRC_ID).get();
+        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_TYPE_ID).get();
+        Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TABLE0).get().supportsAging()
+                ? Optional.of((double) rule1.timeout()) : Optional.empty();
+
+        // check that the number of parameters in the entry is the same as the number of table keys
+        assertThat("Incorrect number of match parameters",
+                   entry1.fieldMatches().size(), is(equalTo(numMatchParams)));
+
+        // check that values stored in entry are the same used for the flow rule
+        assertThat("Incorrect inPort match param value",
+                   inPortParam.value().asReadOnlyBuffer().getShort(), is(equalTo(inPort)));
+        assertThat("Incorrect ethDestMac match param value",
+                   ethDstParam.value().asArray(), is(equalTo(ethDstMac.toBytes())));
+        assertThat("Incorrect ethSrcMac match param value",
+                   ethSrcParam.value().asArray(), is(equalTo(ethSrcMac.toBytes())));
+        assertThat("Incorrect ethType match param value",
+                   ethTypeParam.value().asReadOnlyBuffer().getShort(), is(equalTo(ethType)));
+        assertThat("Incorrect priority value",
+                   entry1.priority().get(), is(equalTo(rule1.priority())));
+        assertThat("Incorrect timeout value",
+                   entry1.timeout(), is(equalTo(expectedTimeout)));
+
+    }
+}
diff --git a/core/net/src/test/resources/org/onosproject/net/pi/impl/default.json b/core/net/src/test/resources/org/onosproject/net/pi/impl/default.json
new file mode 100644
index 0000000..f0bcc71
--- /dev/null
+++ b/core/net/src/test/resources/org/onosproject/net/pi/impl/default.json
@@ -0,0 +1,777 @@
+{
+  "program" : "default.p4",
+  "__meta__" : null,
+  "header_types" : [
+    {
+      "name" : "scalars",
+      "id" : 0,
+      "fields" : [
+        ["tmp", 32, false],
+        ["tmp_0", 32, false]
+      ]
+    },
+    {
+      "name" : "ethernet_t",
+      "id" : 1,
+      "fields" : [
+        ["dstAddr", 48, false],
+        ["srcAddr", 48, false],
+        ["etherType", 16, false]
+      ]
+    },
+    {
+      "name" : "ipv4_t",
+      "id" : 2,
+      "fields" : [
+        ["version", 4, false],
+        ["ihl", 4, false],
+        ["diffserv", 8, false],
+        ["totalLen", 16, false],
+        ["identification", 16, false],
+        ["flags", 3, false],
+        ["fragOffset", 13, false],
+        ["ttl", 8, false],
+        ["protocol", 8, false],
+        ["hdrChecksum", 16, false],
+        ["srcAddr", 32, false],
+        ["dstAddr", 32, false]
+      ]
+    },
+    {
+      "name" : "tcp_t",
+      "id" : 3,
+      "fields" : [
+        ["srcPort", 16, false],
+        ["dstPort", 16, false],
+        ["seqNo", 32, false],
+        ["ackNo", 32, false],
+        ["dataOffset", 4, false],
+        ["res", 3, false],
+        ["ecn", 3, false],
+        ["ctrl", 6, false],
+        ["window", 16, false],
+        ["checksum", 16, false],
+        ["urgentPtr", 16, false]
+      ]
+    },
+    {
+      "name" : "udp_t",
+      "id" : 4,
+      "fields" : [
+        ["srcPort", 16, false],
+        ["dstPort", 16, false],
+        ["length_", 16, false],
+        ["checksum", 16, false]
+      ]
+    },
+    {
+      "name" : "ecmp_metadata_t",
+      "id" : 5,
+      "fields" : [
+        ["groupId", 16, false],
+        ["selector", 16, false]
+      ]
+    },
+    {
+      "name" : "wcmp_meta_t",
+      "id" : 6,
+      "fields" : [
+        ["groupId", 16, false],
+        ["numBits", 8, false],
+        ["selector", 64, false]
+      ]
+    },
+    {
+      "name" : "intrinsic_metadata_t",
+      "id" : 7,
+      "fields" : [
+        ["ingress_global_timestamp", 32, false],
+        ["lf_field_list", 32, false],
+        ["mcast_grp", 16, false],
+        ["egress_rid", 16, false]
+      ]
+    },
+    {
+      "name" : "standard_metadata",
+      "id" : 8,
+      "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", 5, false]
+      ]
+    }
+  ],
+  "headers" : [
+    {
+      "name" : "scalars",
+      "id" : 0,
+      "header_type" : "scalars",
+      "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" : "tcp",
+      "id" : 4,
+      "header_type" : "tcp_t",
+      "metadata" : false,
+      "pi_omit" : true
+    },
+    {
+      "name" : "udp",
+      "id" : 5,
+      "header_type" : "udp_t",
+      "metadata" : false,
+      "pi_omit" : true
+    },
+    {
+      "name" : "ecmp_metadata",
+      "id" : 6,
+      "header_type" : "ecmp_metadata_t",
+      "metadata" : true,
+      "pi_omit" : true
+    },
+    {
+      "name" : "wcmp_meta",
+      "id" : 7,
+      "header_type" : "wcmp_meta_t",
+      "metadata" : true,
+      "pi_omit" : true
+    },
+    {
+      "name" : "intrinsic_metadata",
+      "id" : 8,
+      "header_type" : "intrinsic_metadata_t",
+      "metadata" : true,
+      "pi_omit" : true
+    }
+  ],
+  "header_stacks" : [],
+  "field_lists" : [],
+  "errors" : [
+    ["NoError", 0],
+    ["PacketTooShort", 1],
+    ["NoMatch", 2],
+    ["StackOutOfBounds", 3],
+    ["HeaderTooShort", 4],
+    ["ParserTimeout", 5]
+  ],
+  "enums" : [],
+  "parsers" : [
+    {
+      "name" : "parser",
+      "id" : 0,
+      "init_state" : "start",
+      "parse_states" : [
+        {
+          "name" : "parse_ethernet",
+          "id" : 0,
+          "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", "etherType"]
+            }
+          ]
+        },
+        {
+          "name" : "parse_ipv4",
+          "id" : 1,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "ipv4"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "0x000006",
+              "mask" : null,
+              "next_state" : "parse_tcp"
+            },
+            {
+              "value" : "0x000011",
+              "mask" : null,
+              "next_state" : "parse_udp"
+            },
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : null
+            }
+          ],
+          "transition_key" : [
+            {
+              "type" : "field",
+              "value" : ["ipv4", "fragOffset"]
+            },
+            {
+              "type" : "field",
+              "value" : ["ipv4", "protocol"]
+            }
+          ]
+        },
+        {
+          "name" : "parse_tcp",
+          "id" : 2,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "tcp"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : null
+            }
+          ],
+          "transition_key" : []
+        },
+        {
+          "name" : "parse_udp",
+          "id" : 3,
+          "parser_ops" : [
+            {
+              "parameters" : [
+                {
+                  "type" : "regular",
+                  "value" : "udp"
+                }
+              ],
+              "op" : "extract"
+            }
+          ],
+          "transitions" : [
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : null
+            }
+          ],
+          "transition_key" : []
+        },
+        {
+          "name" : "start",
+          "id" : 4,
+          "parser_ops" : [],
+          "transitions" : [
+            {
+              "value" : "default",
+              "mask" : null,
+              "next_state" : "parse_ethernet"
+            }
+          ],
+          "transition_key" : []
+        }
+      ]
+    }
+  ],
+  "deparsers" : [
+    {
+      "name" : "deparser",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "./include/parsers.p4",
+        "line" : 34,
+        "column" : 8,
+        "source_fragment" : "DeparserImpl"
+      },
+      "order" : ["ethernet", "ipv4", "udp", "tcp"]
+    }
+  ],
+  "meter_arrays" : [],
+  "counter_arrays" : [
+    {
+      "name" : "process_port_counters_0.egress_port_counter",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "./include/port_counters.p4",
+        "line" : 5,
+        "column" : 41,
+        "source_fragment" : "egress_port_counter"
+      },
+      "size" : 254,
+      "is_direct" : false
+    },
+    {
+      "name" : "process_port_counters_0.ingress_port_counter",
+      "id" : 1,
+      "source_info" : {
+        "filename" : "./include/port_counters.p4",
+        "line" : 6,
+        "column" : 41,
+        "source_fragment" : "ingress_port_counter"
+      },
+      "size" : 254,
+      "is_direct" : false
+    }
+  ],
+  "register_arrays" : [],
+  "calculations" : [],
+  "learn_lists" : [],
+  "actions" : [
+    {
+      "name" : "NoAction",
+      "id" : 0,
+      "runtime_data" : [],
+      "primitives" : []
+    },
+    {
+      "name" : "NoAction",
+      "id" : 1,
+      "runtime_data" : [],
+      "primitives" : []
+    },
+    {
+      "name" : "set_egress_port_0",
+      "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" : "default.p4",
+            "line" : 22,
+            "column" : 8,
+            "source_fragment" : "standard_metadata.egress_spec = port"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "send_to_cpu_0",
+      "id" : 3,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "hexstr",
+              "value" : "0x00ff"
+            }
+          ],
+          "source_info" : {
+            "filename" : "default.p4",
+            "line" : 26,
+            "column" : 8,
+            "source_fragment" : "standard_metadata.egress_spec = 9w255"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "_drop_0",
+      "id" : 4,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["standard_metadata", "egress_spec"]
+            },
+            {
+              "type" : "hexstr",
+              "value" : "0x01ff"
+            }
+          ],
+          "source_info" : {
+            "filename" : "default.p4",
+            "line" : 30,
+            "column" : 8,
+            "source_fragment" : "standard_metadata.egress_spec = 9w511"
+          }
+        }
+      ]
+    },
+    {
+      "name" : "process_port_counters_0.count_packet",
+      "id" : 5,
+      "runtime_data" : [],
+      "primitives" : [
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp"]
+            },
+            {
+              "type" : "expression",
+              "value" : {
+                "type" : "expression",
+                "value" : {
+                  "op" : "&",
+                  "left" : {
+                    "type" : "expression",
+                    "value" : {
+                      "op" : "&",
+                      "left" : {
+                        "type" : "field",
+                        "value" : ["standard_metadata", "ingress_port"]
+                      },
+                      "right" : {
+                        "type" : "hexstr",
+                        "value" : "0xff"
+                      }
+                    }
+                  },
+                  "right" : {
+                    "type" : "hexstr",
+                    "value" : "0xffffffff"
+                  }
+                }
+              }
+            }
+          ]
+        },
+        {
+          "op" : "count",
+          "parameters" : [
+            {
+              "type" : "counter_array",
+              "value" : "process_port_counters_0.ingress_port_counter"
+            },
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "./include/port_counters.p4",
+            "line" : 8,
+            "column" : 8,
+            "source_fragment" : "ingress_port_counter.count((bit<32>)(bit<8>)standard_metadata.ingress_port)"
+          }
+        },
+        {
+          "op" : "assign",
+          "parameters" : [
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp_0"]
+            },
+            {
+              "type" : "expression",
+              "value" : {
+                "type" : "expression",
+                "value" : {
+                  "op" : "&",
+                  "left" : {
+                    "type" : "expression",
+                    "value" : {
+                      "op" : "&",
+                      "left" : {
+                        "type" : "field",
+                        "value" : ["standard_metadata", "egress_spec"]
+                      },
+                      "right" : {
+                        "type" : "hexstr",
+                        "value" : "0xff"
+                      }
+                    }
+                  },
+                  "right" : {
+                    "type" : "hexstr",
+                    "value" : "0xffffffff"
+                  }
+                }
+              }
+            }
+          ]
+        },
+        {
+          "op" : "count",
+          "parameters" : [
+            {
+              "type" : "counter_array",
+              "value" : "process_port_counters_0.egress_port_counter"
+            },
+            {
+              "type" : "field",
+              "value" : ["scalars", "tmp_0"]
+            }
+          ],
+          "source_info" : {
+            "filename" : "./include/port_counters.p4",
+            "line" : 9,
+            "column" : 8,
+            "source_fragment" : "egress_port_counter.count((bit<32>)(bit<8>)standard_metadata.egress_spec)"
+          }
+        }
+      ]
+    }
+  ],
+  "pipelines" : [
+    {
+      "name" : "ingress",
+      "id" : 0,
+      "source_info" : {
+        "filename" : "default.p4",
+        "line" : 9,
+        "column" : 8,
+        "source_fragment" : "ingress"
+      },
+      "init_table" : "table0",
+      "tables" : [
+        {
+          "name" : "table0",
+          "id" : 0,
+          "source_info" : {
+            "filename" : "default.p4",
+            "line" : 32,
+            "column" : 10,
+            "source_fragment" : "table0"
+          },
+          "key" : [
+            {
+              "match_type" : "ternary",
+              "target" : ["standard_metadata", "ingress_port"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "dstAddr"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "srcAddr"],
+              "mask" : null
+            },
+            {
+              "match_type" : "ternary",
+              "target" : ["ethernet", "etherType"],
+              "mask" : null
+            }
+          ],
+          "match_type" : "ternary",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [2, 3, 4, 0],
+          "actions" : ["set_egress_port_0", "send_to_cpu_0", "_drop_0", "NoAction"],
+          "base_default_next" : "node_3",
+          "next_tables" : {
+            "set_egress_port_0" : "node_3",
+            "send_to_cpu_0" : "node_3",
+            "_drop_0" : "node_3",
+            "NoAction" : "node_3"
+          },
+          "default_entry" : {
+            "action_id" : 0,
+            "action_const" : false,
+            "action_data" : [],
+            "action_entry_const" : false
+          }
+        },
+        {
+          "name" : "process_port_counters_0.port_count_table",
+          "id" : 1,
+          "source_info" : {
+            "filename" : "./include/port_counters.p4",
+            "line" : 11,
+            "column" : 10,
+            "source_fragment" : "port_count_table"
+          },
+          "key" : [],
+          "match_type" : "exact",
+          "type" : "simple",
+          "max_size" : 1024,
+          "with_counters" : false,
+          "support_timeout" : false,
+          "direct_meters" : null,
+          "action_ids" : [5, 1],
+          "actions" : ["process_port_counters_0.count_packet", "NoAction"],
+          "base_default_next" : null,
+          "next_tables" : {
+            "process_port_counters_0.count_packet" : null,
+            "NoAction" : null
+          },
+          "default_entry" : {
+            "action_id" : 1,
+            "action_const" : false,
+            "action_data" : [],
+            "action_entry_const" : false
+          }
+        }
+      ],
+      "action_profiles" : [],
+      "conditionals" : [
+        {
+          "name" : "node_3",
+          "id" : 0,
+          "source_info" : {
+            "filename" : "./include/port_counters.p4",
+            "line" : 17,
+            "column" : 12,
+            "source_fragment" : "standard_metadata.egress_spec < 9w254"
+          },
+          "expression" : {
+            "type" : "expression",
+            "value" : {
+              "op" : "<",
+              "left" : {
+                "type" : "field",
+                "value" : ["standard_metadata", "egress_spec"]
+              },
+              "right" : {
+                "type" : "hexstr",
+                "value" : "0x00fe"
+              }
+            }
+          },
+          "false_next" : null,
+          "true_next" : "process_port_counters_0.port_count_table"
+        }
+      ]
+    },
+    {
+      "name" : "egress",
+      "id" : 1,
+      "source_info" : {
+        "filename" : "default.p4",
+        "line" : 54,
+        "column" : 8,
+        "source_fragment" : "egress"
+      },
+      "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/incubator/bmv2/model/src/main/java/org/onosproject/bmv2/model/Bmv2TableModel.java b/incubator/bmv2/model/src/main/java/org/onosproject/bmv2/model/Bmv2TableModel.java
index 8226542..542c8c2 100644
--- a/incubator/bmv2/model/src/main/java/org/onosproject/bmv2/model/Bmv2TableModel.java
+++ b/incubator/bmv2/model/src/main/java/org/onosproject/bmv2/model/Bmv2TableModel.java
@@ -18,13 +18,16 @@
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import org.onosproject.net.pi.model.PiActionModel;
 import org.onosproject.net.pi.model.PiTableMatchFieldModel;
 import org.onosproject.net.pi.model.PiTableModel;
 
 import java.util.Collection;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import static com.google.common.base.Preconditions.*;
@@ -40,7 +43,7 @@
     private final boolean hasCounters;
     private final boolean supportAging;
     private final Set<PiTableMatchFieldModel> matchFields;
-    private final Set<PiActionModel> actions;
+    private final Map<String, PiActionModel> actions;
 
     /**
      * Creates new BMv2 table model.
@@ -68,7 +71,9 @@
         this.hasCounters = hasCounters;
         this.supportAging = supportAging;
         this.matchFields = ImmutableSet.copyOf(matchFields);
-        this.actions = ImmutableSet.copyOf(actions);
+        ImmutableMap.Builder<String, PiActionModel> mapBuilder = ImmutableMap.builder();
+        actions.forEach(a -> mapBuilder.put(a.name(), a));
+        this.actions = mapBuilder.build();
     }
 
     /**
@@ -107,7 +112,12 @@
 
     @Override
     public Collection<PiActionModel> actions() {
-        return actions;
+        return actions.values();
+    }
+
+    @Override
+    public Optional<PiActionModel> action(String name) {
+        return Optional.ofNullable(actions.get(name));
     }
 
     @Override
@@ -143,7 +153,7 @@
                 .add("hasCounters", hasCounters)
                 .add("supportAging", supportAging)
                 .add("matchFields", matchFields)
-                .add("actions", actions)
+                .add("actions", actions.values())
                 .toString();
     }
 }
\ No newline at end of file
diff --git a/utils/misc/src/main/java/org/onlab/util/ImmutableByteSequence.java b/utils/misc/src/main/java/org/onlab/util/ImmutableByteSequence.java
index c274e6a..1db1d66 100644
--- a/utils/misc/src/main/java/org/onlab/util/ImmutableByteSequence.java
+++ b/utils/misc/src/main/java/org/onlab/util/ImmutableByteSequence.java
@@ -23,6 +23,8 @@
 import java.util.Arrays;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
 import static org.apache.commons.lang3.ArrayUtils.reverse;
 
 /**
@@ -241,4 +243,71 @@
     public String toString() {
         return HexString.toHexString(value.array());
     }
+
+    /**
+     * Trims or expands the given byte sequence so to fit a given bit-width. When trimming, the
+     * operations is deemed to be safe only if the trimmed bits are zero, otherwise an exception
+     * will be thrown. When expanding, the sequence will be padded with zeros. The returned byte
+     * sequence will have minimum size to contain the given bit-width.
+     *
+     * @param original a byte sequence
+     * @param bitWidth a non-zero positive integer
+     * @return a new byte sequence
+     * @throws ByteSequenceTrimException if the byte sequence cannot be fitted
+     */
+    public static ImmutableByteSequence fit(ImmutableByteSequence original, int bitWidth)
+            throws ByteSequenceTrimException {
+
+        checkNotNull(original, "byte sequence cannot be null");
+        checkArgument(bitWidth > 0, "bit-width must be a non-zero positive integer");
+
+        int newByteWidth = (int) Math.ceil((double) bitWidth / 8);
+
+        byte[] originalBytes = original.asArray();
+
+        if (newByteWidth > original.size()) {
+            // pad missing bytes with zeros
+            return ImmutableByteSequence.copyFrom(Arrays.copyOf(originalBytes, newByteWidth));
+        }
+
+        byte[] newBytes = new byte[newByteWidth];
+        // ImmutableByteSequence is always big-endian, hence check the array in reverse order
+        int diff = originalBytes.length - newByteWidth;
+        for (int i = originalBytes.length - 1; i >= 0; i--) {
+            byte ob = originalBytes[i]; // original byte
+            byte nb; // new byte
+            if (i > diff) {
+                // no need to truncate, copy as is
+                nb = ob;
+            } else if (i == diff) {
+                // truncate this byte, check if we're loosing something
+                byte mask = (byte) ((1 >> ((bitWidth % 8) + 1)) - 1);
+                if ((ob & ~mask) != 0) {
+                    throw new ByteSequenceTrimException(originalBytes, bitWidth);
+                } else {
+                    nb = (byte) (ob & mask);
+                }
+            } else {
+                // drop this byte, check if we're loosing something
+                if (originalBytes[i] != 0) {
+                    throw new ByteSequenceTrimException(originalBytes, bitWidth);
+                } else {
+                    continue;
+                }
+            }
+            newBytes[i - diff] = nb;
+        }
+
+        return ImmutableByteSequence.copyFrom(newBytes);
+    }
+
+    /**
+     * Signals that a byte sequence cannot be trimmed.
+     */
+    public static class ByteSequenceTrimException extends Exception {
+        ByteSequenceTrimException(byte[] bytes, int bitWidth) {
+            super(format("cannot trim %s into a %d long bits value",
+                         HexString.toHexString(bytes), bitWidth));
+        }
+    }
 }