Major refactoring of the BMv2 protocol module (onos1.6 cherry-pick)

- Created 3 separate sub-modules: API (doesn't depend on
    Thrift), CTL (depends on Thrift), THRIFT-API (to generate Thrift
    sources)
- Implemented 2 new services (for device configuration swapping and
    table entry management) needed to distribute BMv2-specific state
    among ONOS instances.
- Implemented a BMv2 controller (previously other modules where
    using separately a Thrift client and a server)
- Added a default BMv2 JSON configuration (default.json) and interpreter
    to be used for devices that connect for the first time to ONOS.
    This allows for basic services to work (i.e. LLDP link discovery,
    ARP proxy. etc.).
- Changed behavior of the flow rule translator and extension selector,
    now it allows extension to specify only some of the match parameters
    (before extension selectors were expected to describe the whole
    match key, i.e. all fields)
- Various renaming to better represent the API
- Various java doc fixes / improvements

Change-Id: Ida4b5e546b0def97c3552a6c05f7bce76fd32c28
diff --git a/protocols/bmv2/ctl/pom.xml b/protocols/bmv2/ctl/pom.xml
new file mode 100644
index 0000000..5e8ae37
--- /dev/null
+++ b/protocols/bmv2/ctl/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016-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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>onos-bmv2-protocol</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.6.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>onos-bmv2-protocol-ctl</artifactId>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.thrift</groupId>
+            <artifactId>libthrift</artifactId>
+            <version>0.9.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-bmv2-protocol-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-bmv2-protocol-thrift-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+
+</project>
\ No newline at end of file
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2ControllerImpl.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2ControllerImpl.java
new file mode 100644
index 0000000..caee250
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2ControllerImpl.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+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.apache.thrift.TException;
+import org.apache.thrift.TProcessor;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.protocol.TMultiplexedProtocol;
+import org.apache.thrift.protocol.TProtocol;
+import org.apache.thrift.server.TThreadPoolServer;
+import org.apache.thrift.transport.TServerSocket;
+import org.apache.thrift.transport.TServerTransport;
+import org.apache.thrift.transport.TSocket;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.TTransportException;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.bmv2.api.service.Bmv2Controller;
+import org.onosproject.bmv2.api.runtime.Bmv2Device;
+import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
+import org.onosproject.bmv2.api.service.Bmv2DeviceListener;
+import org.onosproject.bmv2.api.service.Bmv2PacketListener;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.thriftapi.ControlPlaneService;
+import org.onosproject.bmv2.thriftapi.SimpleSwitch;
+import org.onosproject.bmv2.thriftapi.Standard;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Default implementation of a BMv2 controller.
+ */
+@Component(immediate = true)
+@Service
+public class Bmv2ControllerImpl implements Bmv2Controller {
+
+    private static final String APP_ID = "org.onosproject.bmv2";
+
+    // Seconds after a client is expired (and connection closed) in the cache.
+    private static final int CLIENT_CACHE_TIMEOUT = 60;
+    // Number of connection retries after a network error.
+    private static final int NUM_CONNECTION_RETRIES = 2;
+    // Time between retries in milliseconds.
+    private static final int TIME_BETWEEN_RETRIES = 10;
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    // Cache where clients are removed after a predefined timeout.
+    private final LoadingCache<DeviceId, Pair<TTransport, Bmv2DeviceThriftClient>> agentCache =
+            CacheBuilder.newBuilder()
+                    .expireAfterAccess(CLIENT_CACHE_TIMEOUT, TimeUnit.SECONDS)
+                    .removalListener(new ClientRemovalListener())
+                    .build(new ClientLoader());
+
+    private final InternalTrackingProcessor trackingProcessor = new InternalTrackingProcessor();
+
+    private final ExecutorService executorService = Executors
+            .newFixedThreadPool(16, groupedThreads("onos/bmv2", "controller", log));
+
+    private final Set<Bmv2DeviceListener> deviceListeners = new CopyOnWriteArraySet<>();
+    private final Set<Bmv2PacketListener> packetListeners = new CopyOnWriteArraySet<>();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    private TThreadPoolServer thriftServer;
+
+    // TODO: make it configurable trough component config
+    private int serverPort = DEFAULT_PORT;
+
+    @Activate
+    public void activate() {
+        coreService.registerApplication(APP_ID);
+        startServer(serverPort);
+        log.info("Activated");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        stopServer();
+        log.info("Deactivated");
+    }
+
+    private void startServer(int port) {
+        try {
+            TServerTransport transport = new TServerSocket(port);
+            log.info("Starting server on port {}...", port);
+            this.thriftServer = new TThreadPoolServer(new TThreadPoolServer.Args(transport)
+                                                              .processor(trackingProcessor)
+                                                              .executorService(executorService));
+            executorService.execute(thriftServer::serve);
+        } catch (TTransportException e) {
+            log.error("Unable to start server", e);
+        }
+    }
+
+    private void stopServer() {
+        // Stop the server if running...
+        if (thriftServer != null && !thriftServer.isServing()) {
+            thriftServer.stop();
+        }
+        try {
+            executorService.shutdown();
+            executorService.awaitTermination(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            List<Runnable> runningTasks = executorService.shutdownNow();
+            log.warn("Unable to stop server threads: {}", runningTasks);
+        }
+    }
+
+    @Override
+    public Bmv2DeviceAgent getAgent(DeviceId deviceId) throws Bmv2RuntimeException {
+        try {
+            checkNotNull(deviceId, "deviceId cannot be null");
+            return agentCache.get(deviceId).getRight();
+        } catch (ExecutionException e) {
+            throw new Bmv2RuntimeException(e);
+        }
+    }
+
+    @Override
+    public boolean isReacheable(DeviceId deviceId) {
+        try {
+            return getAgent(deviceId).ping();
+        } catch (Bmv2RuntimeException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public void addDeviceListener(Bmv2DeviceListener listener) {
+        if (!deviceListeners.contains(listener)) {
+            deviceListeners.add(listener);
+        }
+    }
+
+    @Override
+    public void removeDeviceListener(Bmv2DeviceListener listener) {
+        deviceListeners.remove(listener);
+    }
+
+    @Override
+    public void addPacketListener(Bmv2PacketListener listener) {
+        if (!packetListeners.contains(listener)) {
+            packetListeners.add(listener);
+        }
+    }
+
+    @Override
+    public void removePacketListener(Bmv2PacketListener listener) {
+        packetListeners.remove(listener);
+    }
+
+    /**
+     * Client cache removal listener. Close the connection on cache removal.
+     */
+    private static class ClientRemovalListener implements
+            RemovalListener<DeviceId, Pair<TTransport, Bmv2DeviceThriftClient>> {
+
+        @Override
+        public void onRemoval(RemovalNotification<DeviceId, Pair<TTransport, Bmv2DeviceThriftClient>> notification) {
+            // close the transport connection
+            Bmv2DeviceThriftClient client = notification.getValue().getRight();
+            TTransport transport = notification.getValue().getLeft();
+            // Locking here is ugly, but needed (see SafeThriftClient).
+            synchronized (transport) {
+                if (transport.isOpen()) {
+                    transport.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Handles Thrift calls from BMv2 devices using registered listeners.
+     */
+    private final class InternalServiceHandler implements ControlPlaneService.Iface {
+
+        private final TSocket socket;
+        private Bmv2Device remoteDevice;
+
+        private InternalServiceHandler(TSocket socket) {
+            this.socket = socket;
+        }
+
+        @Override
+        public boolean ping() {
+            return true;
+        }
+
+        @Override
+        public void hello(int thriftServerPort, int deviceId, int instanceId, String jsonConfigMd5) {
+            // Locally note the remote device for future uses.
+            String host = socket.getSocket().getInetAddress().getHostAddress();
+            remoteDevice = new Bmv2Device(host, thriftServerPort, deviceId);
+
+            if (deviceListeners.size() == 0) {
+                log.debug("Received hello, but there's no listener registered.");
+            } else {
+                deviceListeners.forEach(
+                        l -> executorService.execute(() -> l.handleHello(remoteDevice, instanceId, jsonConfigMd5)));
+            }
+        }
+
+        @Override
+        public void packetIn(int port, long reason, int tableId, int contextId, ByteBuffer packet) {
+            if (remoteDevice == null) {
+                log.debug("Received packet-in, but the remote device is still unknown. Need a hello first...");
+                return;
+            }
+
+            if (packetListeners.size() == 0) {
+                log.debug("Received packet-in, but there's no listener registered.");
+            } else {
+                packetListeners.forEach(
+                        l -> executorService.execute(() -> l.handlePacketIn(remoteDevice,
+                                                                            port,
+                                                                            reason,
+                                                                            tableId,
+                                                                            contextId,
+                                                                            ImmutableByteSequence.copyFrom(packet))));
+            }
+        }
+    }
+
+    /**
+     * Thrift Processor decorator. This class is needed in order to have access to the socket when handling a call.
+     * Socket is needed to get the IP address of the client originating the call (see InternalServiceHandler.hello())
+     */
+    private final class InternalTrackingProcessor implements TProcessor {
+
+        // Map sockets to processors.
+        // TODO: implement it as a cache so unused sockets are expired automatically
+        private final ConcurrentMap<TSocket, ControlPlaneService.Processor<InternalServiceHandler>> processors =
+                Maps.newConcurrentMap();
+
+        @Override
+        public boolean process(final TProtocol in, final TProtocol out) throws TException {
+            // Get the socket for this request.
+            TSocket socket = (TSocket) in.getTransport();
+            // Get or create a processor for this socket
+            ControlPlaneService.Processor<InternalServiceHandler> processor = processors.computeIfAbsent(socket, s -> {
+                InternalServiceHandler handler = new InternalServiceHandler(s);
+                return new ControlPlaneService.Processor<>(handler);
+            });
+            // Delegate to the processor we are decorating.
+            return processor.process(in, out);
+        }
+    }
+
+    /**
+     * Transport/client cache loader.
+     */
+    private class ClientLoader extends CacheLoader<DeviceId, Pair<TTransport, Bmv2DeviceThriftClient>> {
+
+        private final SafeThriftClient.Options options = new SafeThriftClient.Options(NUM_CONNECTION_RETRIES,
+                                                                                      TIME_BETWEEN_RETRIES);
+
+        @Override
+        public Pair<TTransport, Bmv2DeviceThriftClient> load(DeviceId deviceId)
+                throws TTransportException {
+            log.debug("Instantiating new client... > deviceId={}", deviceId);
+            // Make the expensive call
+            Bmv2Device device = Bmv2Device.of(deviceId);
+            TTransport transport = new TSocket(device.thriftServerHost(), device.thriftServerPort());
+            TProtocol protocol = new TBinaryProtocol(transport);
+            // Our BMv2 device implements multiple Thrift services, create a client for each one on the same transport.
+            Standard.Client standardClient = new Standard.Client(
+                    new TMultiplexedProtocol(protocol, "standard"));
+            SimpleSwitch.Client simpleSwitch = new SimpleSwitch.Client(
+                    new TMultiplexedProtocol(protocol, "simple_switch"));
+            // Wrap clients so to automatically have synchronization and resiliency to connectivity errors
+            Standard.Iface safeStandardClient = SafeThriftClient.wrap(standardClient,
+                                                                      Standard.Iface.class,
+                                                                      options);
+            SimpleSwitch.Iface safeSimpleSwitchClient = SafeThriftClient.wrap(simpleSwitch,
+                                                                              SimpleSwitch.Iface.class,
+                                                                              options);
+            Bmv2DeviceThriftClient client = new Bmv2DeviceThriftClient(deviceId,
+                                                                       transport,
+                                                                       safeStandardClient,
+                                                                       safeSimpleSwitchClient);
+            return Pair.of(transport, client);
+        }
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DefaultInterpreterImpl.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DefaultInterpreterImpl.java
new file mode 100644
index 0000000..f5e95df
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DefaultInterpreterImpl.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.collect.ImmutableBiMap;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.context.Bmv2Interpreter;
+import org.onosproject.bmv2.api.context.Bmv2InterpreterException;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.instructions.Instruction;
+
+import static org.onosproject.net.PortNumber.CONTROLLER;
+import static org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+
+/**
+ * Implementation of a BMv2 interpreter for the BMv2 default.json configuration.
+ */
+public final class Bmv2DefaultInterpreterImpl implements Bmv2Interpreter {
+
+    public static final String TABLE0 = "table0";
+    public static final String SEND_TO_CPU = "send_to_cpu";
+    public static final String DROP = "_drop";
+    public static final String SET_EGRESS_PORT = "set_egress_port";
+
+    // Lazily populate field map.
+    private static final ImmutableBiMap<Criterion.Type, String> CRITERION_MAP = ImmutableBiMap.of(
+            Criterion.Type.IN_PORT, "standard_metadata.ingress_port",
+            Criterion.Type.ETH_DST, "ethernet.dstAddr",
+            Criterion.Type.ETH_SRC, "ethernet.srcAddr",
+            Criterion.Type.ETH_TYPE, "ethernet.etherType");
+
+    private static final ImmutableBiMap<Integer, String> TABLE_MAP = ImmutableBiMap.of(
+            0, TABLE0);
+
+    @Override
+    public ImmutableBiMap<Criterion.Type, String> criterionTypeMap() {
+        return CRITERION_MAP;
+    }
+
+    @Override
+    public ImmutableBiMap<Integer, String> tableIdMap() {
+        return TABLE_MAP;
+    }
+
+    @Override
+    public Bmv2Action mapTreatment(TrafficTreatment treatment, Bmv2Configuration configuration)
+            throws Bmv2InterpreterException {
+
+
+        if (treatment.allInstructions().size() == 0) {
+            // No instructions means drop for us.
+            return Bmv2Action.builder().withName(DROP).build();
+        } else if (treatment.allInstructions().size() > 1) {
+            // Otherwise, we understand treatments with only 1 instruction.
+            throw new Bmv2InterpreterException("Treatment has multiple instructions");
+        }
+
+        Instruction instruction = treatment.allInstructions().get(0);
+
+        switch (instruction.type()) {
+            case OUTPUT:
+                OutputInstruction outInstruction = (OutputInstruction) instruction;
+                if (outInstruction.port() == CONTROLLER) {
+                    return Bmv2Action.builder().withName(SEND_TO_CPU).build();
+                } else {
+                    return buildEgressAction(outInstruction, configuration);
+                }
+            case NOACTION:
+                return Bmv2Action.builder().withName(DROP).build();
+            default:
+                throw new Bmv2InterpreterException("Instruction type not supported: " + instruction.type().name());
+        }
+    }
+
+
+    /**
+     * Builds an egress action equivalent to the given output instruction for the given configuration.
+     *
+     * @param instruction   an output instruction
+     * @param configuration a configuration
+     * @return a BMv2 action
+     * @throws Bmv2InterpreterException if the instruction cannot be translated to a BMv2 action
+     */
+    private Bmv2Action buildEgressAction(OutputInstruction instruction, Bmv2Configuration configuration)
+            throws Bmv2InterpreterException {
+
+        Bmv2Action.Builder actionBuilder = Bmv2Action.builder();
+
+        actionBuilder.withName(SET_EGRESS_PORT);
+
+        if (instruction.port().isLogical()) {
+            throw new Bmv2InterpreterException("Output on logic port not supported: " + instruction);
+        }
+
+        // Get the byte sequence for the out port with the right length
+        long portNumber = instruction.port().toLong();
+        int bitWidth = configuration.action(SET_EGRESS_PORT).runtimeData("port").bitWidth();
+        try {
+            ImmutableByteSequence outPort = Bmv2TranslatorUtils.fitByteSequence(
+                    ImmutableByteSequence.copyFrom(portNumber), bitWidth);
+            return Bmv2Action.builder().withName(SET_EGRESS_PORT).addParameter(outPort).build();
+        } catch (Bmv2TranslatorUtils.ByteSequenceFitException e) {
+            throw new Bmv2InterpreterException(e.getMessage());
+        }
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceContextServiceImpl.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceContextServiceImpl.java
new file mode 100644
index 0000000..bcdb939
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceContextServiceImpl.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.google.common.collect.Maps;
+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.onlab.util.KryoNamespace;
+import org.onlab.util.SharedExecutors;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.context.Bmv2DefaultConfiguration;
+import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
+import org.onosproject.bmv2.api.context.Bmv2Interpreter;
+import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.api.service.Bmv2Controller;
+import org.onosproject.bmv2.api.service.Bmv2DeviceContextService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Component(immediate = true)
+@Service
+public class Bmv2DeviceContextServiceImpl implements Bmv2DeviceContextService {
+
+    private static final String JSON_DEFAULT_CONFIG_PATH = "/default.json";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private Bmv2Controller controller;
+
+    private final ExecutorService executorService = SharedExecutors.getPoolThreadExecutor();
+
+    private ConsistentMap<DeviceId, Bmv2DeviceContext> contexts;
+    private Map<DeviceId, Bmv2DeviceContext> contextsMap;
+
+    private Map<String, ClassLoader> interpreterClassLoaders;
+
+    private Bmv2DeviceContext defaultContext;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Activate
+    public void activate() {
+        KryoNamespace kryo = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(new BmvDeviceContextSerializer(), Bmv2DeviceContext.class)
+                .build();
+
+        this.contexts = storageService.<DeviceId, Bmv2DeviceContext>consistentMapBuilder()
+                .withSerializer(Serializer.using(kryo))
+                .withName("onos-bmv2-contexts")
+                .build();
+        contextsMap = contexts.asJavaMap();
+
+        interpreterClassLoaders = Maps.newConcurrentMap();
+
+        Bmv2Configuration defaultConfiguration = loadDefaultConfiguration();
+        Bmv2Interpreter defaultInterpreter = new Bmv2DefaultInterpreterImpl();
+        defaultContext = new Bmv2DeviceContext(defaultConfiguration, defaultInterpreter);
+
+        interpreterClassLoaders.put(defaultInterpreter.getClass().getName(), this.getClass().getClassLoader());
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public Bmv2DeviceContext getContext(DeviceId deviceId) {
+        checkNotNull(deviceId, "device id cannot be null");
+        return contextsMap.get(deviceId);
+    }
+
+    @Override
+    public void triggerConfigurationSwap(DeviceId deviceId, Bmv2DeviceContext context) {
+        checkNotNull(deviceId, "device id cannot be null");
+        checkNotNull(context, "context cannot be null");
+        if (!interpreterClassLoaders.containsKey(context.interpreter().getClass().getName())) {
+            log.error("Unable to trigger configuration swap, missing class loader for context interpreter. " +
+                              "Please register it with registerInterpreterClassLoader()");
+        } else {
+            executorService.execute(() -> executeConfigurationSwap(deviceId, context));
+        }
+    }
+
+    @Override
+    public void registerInterpreterClassLoader(Class<? extends Bmv2Interpreter> interpreterClass, ClassLoader loader) {
+        interpreterClassLoaders.put(interpreterClass.getName(), loader);
+    }
+
+    private void executeConfigurationSwap(DeviceId deviceId, Bmv2DeviceContext context) {
+        contexts.compute(deviceId, (key, existingValue) -> {
+            if (context.equals(existingValue)) {
+                log.info("Dropping swap request as one has already been triggered for the given context.");
+                return existingValue;
+            }
+            try {
+                Bmv2DeviceAgent agent = controller.getAgent(deviceId);
+                String jsonString = context.configuration().json().toString();
+                agent.loadNewJsonConfig(jsonString);
+                agent.swapJsonConfig();
+                return context;
+            } catch (Bmv2RuntimeException e) {
+                log.error("Unable to swap configuration on {}: {}", deviceId, e.explain());
+                return existingValue;
+            }
+        });
+    }
+
+    @Override
+    public boolean notifyDeviceChange(DeviceId deviceId) {
+        checkNotNull(deviceId, "device id cannot be null");
+
+        Bmv2DeviceContext storedContext = getContext(deviceId);
+
+        if (storedContext == null) {
+            log.info("No context previously stored for {}, swapping to DEFAULT_CONTEXT.", deviceId);
+            triggerConfigurationSwap(deviceId, defaultContext);
+            // Device can be accepted.
+            return false;
+        } else {
+            Bmv2Configuration deviceConfiguration = loadDeviceConfiguration(deviceId);
+            if (deviceConfiguration == null) {
+                log.warn("Unable to load configuration from device {}", deviceId);
+                return false;
+            }
+            if (storedContext.configuration().equals(deviceConfiguration)) {
+                return true;
+            } else {
+                log.info("Device context is different from the stored one, triggering configuration swap for {}...",
+                         deviceId);
+                triggerConfigurationSwap(deviceId, storedContext);
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Load and parse a BMv2 JSON configuration from the given device.
+     *
+     * @param deviceId a device id
+     * @return a BMv2 configuration
+     */
+    private Bmv2Configuration loadDeviceConfiguration(DeviceId deviceId) {
+        try {
+            String jsonString = controller.getAgent(deviceId).dumpJsonConfig();
+            return Bmv2DefaultConfiguration.parse(Json.parse(jsonString).asObject());
+        } catch (Bmv2RuntimeException e) {
+            log.warn("Unable to load JSON configuration from {}: {}", deviceId, e.explain());
+            return null;
+        }
+    }
+
+    /**
+     * Loads default configuration from file.
+     *
+     * @return a BMv2 configuration
+     */
+    protected static Bmv2DefaultConfiguration loadDefaultConfiguration() {
+        try {
+            JsonObject json = Json.parse(new BufferedReader(new InputStreamReader(
+                    Bmv2DeviceContextServiceImpl.class.getResourceAsStream(JSON_DEFAULT_CONFIG_PATH)))).asObject();
+            return Bmv2DefaultConfiguration.parse(json);
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to load default configuration", e);
+        }
+    }
+
+    /**
+     * Internal BMv2 context serializer.
+     */
+    private class BmvDeviceContextSerializer extends com.esotericsoftware.kryo.Serializer<Bmv2DeviceContext> {
+
+        @Override
+        public void write(Kryo kryo, Output output, Bmv2DeviceContext context) {
+            kryo.writeObject(output, context.configuration().json().toString());
+            kryo.writeObject(output, context.interpreter().getClass().getName());
+        }
+
+        @Override
+        public Bmv2DeviceContext read(Kryo kryo, Input input, Class<Bmv2DeviceContext> type) {
+            String jsonStr = kryo.readObject(input, String.class);
+            String interpreterClassName = kryo.readObject(input, String.class);
+            Bmv2Configuration configuration = Bmv2DefaultConfiguration.parse(Json.parse(jsonStr).asObject());
+            ClassLoader loader = interpreterClassLoaders.get(interpreterClassName);
+            if (loader == null) {
+                throw new IllegalStateException("No class loader registered for interpreter: " + interpreterClassName);
+            }
+            try {
+                Bmv2Interpreter interpreter = (Bmv2Interpreter) loader.loadClass(interpreterClassName).newInstance();
+                return new Bmv2DeviceContext(configuration, interpreter);
+            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+                throw new RuntimeException("Unable to load interpreter class", e);
+            }
+        }
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java
new file mode 100644
index 0000000..ae852c3
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2DeviceThriftClient.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.thrift.TException;
+import org.apache.thrift.transport.TTransport;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2PortInfo;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2ValidMatchParam;
+import org.onosproject.bmv2.thriftapi.BmAddEntryOptions;
+import org.onosproject.bmv2.thriftapi.BmCounterValue;
+import org.onosproject.bmv2.thriftapi.BmMatchParam;
+import org.onosproject.bmv2.thriftapi.BmMatchParamExact;
+import org.onosproject.bmv2.thriftapi.BmMatchParamLPM;
+import org.onosproject.bmv2.thriftapi.BmMatchParamTernary;
+import org.onosproject.bmv2.thriftapi.BmMatchParamType;
+import org.onosproject.bmv2.thriftapi.BmMatchParamValid;
+import org.onosproject.bmv2.thriftapi.SimpleSwitch;
+import org.onosproject.bmv2.thriftapi.Standard;
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.onosproject.bmv2.ctl.Bmv2TExceptionParser.parseTException;
+
+/**
+ * Implementation of a Thrift client to control a BMv2 device.
+ */
+public final class Bmv2DeviceThriftClient implements Bmv2DeviceAgent {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    // FIXME: make context_id arbitrary for each call
+    // See: https://github.com/p4lang/behavioral-model/blob/master/modules/bm_sim/include/bm_sim/context.h
+    private static final int CONTEXT_ID = 0;
+
+    private final Standard.Iface standardClient;
+    private final SimpleSwitch.Iface simpleSwitchClient;
+    private final TTransport transport;
+    private final DeviceId deviceId;
+
+    // ban constructor
+    protected Bmv2DeviceThriftClient(DeviceId deviceId, TTransport transport, Standard.Iface standardClient,
+                                     SimpleSwitch.Iface simpleSwitchClient) {
+        this.deviceId = deviceId;
+        this.transport = transport;
+        this.standardClient = standardClient;
+        this.simpleSwitchClient = simpleSwitchClient;
+    }
+
+    @Override
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public boolean ping() {
+        try {
+            return this.simpleSwitchClient.ping();
+        } catch (TException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public final long addTableEntry(Bmv2TableEntry entry) throws Bmv2RuntimeException {
+
+        log.debug("Adding table entry... > deviceId={}, entry={}", deviceId, entry);
+
+        long entryId = -1;
+
+        try {
+            BmAddEntryOptions options = new BmAddEntryOptions();
+
+            if (entry.hasPriority()) {
+                options.setPriority(entry.priority());
+            }
+
+            entryId = standardClient.bm_mt_add_entry(
+                    CONTEXT_ID,
+                    entry.tableName(),
+                    buildMatchParamsList(entry.matchKey()),
+                    entry.action().name(),
+                    buildActionParamsList(entry.action()),
+                    options);
+
+            if (entry.hasTimeout()) {
+                /* bmv2 accepts timeouts in milliseconds */
+                int msTimeout = (int) Math.round(entry.timeout() * 1_000);
+                standardClient.bm_mt_set_entry_ttl(
+                        CONTEXT_ID, entry.tableName(), entryId, msTimeout);
+            }
+
+            log.debug("Table entry added! > deviceId={}, entryId={}/{}", deviceId, entry.tableName(), entryId);
+
+            return entryId;
+
+        } catch (TException e) {
+            log.debug("Exception while adding table entry: {} > deviceId={}, tableName={}",
+                      e, deviceId, entry.tableName());
+            if (entryId != -1) {
+                // entry is in inconsistent state (unable to add timeout), remove it
+                try {
+                    deleteTableEntry(entry.tableName(), entryId);
+                } catch (Bmv2RuntimeException e1) {
+                    log.debug("Unable to remove failed table entry: {} > deviceId={}, tableName={}",
+                              e1, deviceId, entry.tableName());
+                }
+            }
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void modifyTableEntry(String tableName,
+                                       long entryId, Bmv2Action action)
+            throws Bmv2RuntimeException {
+
+        log.debug("Modifying table entry... > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+
+        try {
+            standardClient.bm_mt_modify_entry(
+                    CONTEXT_ID,
+                    tableName,
+                    entryId,
+                    action.name(),
+                    buildActionParamsList(action));
+            log.debug("Table entry modified! > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+        } catch (TException e) {
+            log.debug("Exception while modifying table entry: {} > deviceId={}, entryId={}/{}",
+                      e, deviceId, tableName, entryId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void deleteTableEntry(String tableName,
+                                       long entryId) throws Bmv2RuntimeException {
+
+        log.debug("Deleting table entry... > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+
+        try {
+            standardClient.bm_mt_delete_entry(CONTEXT_ID, tableName, entryId);
+            log.debug("Table entry deleted! > deviceId={}, entryId={}/{}", deviceId, tableName, entryId);
+        } catch (TException e) {
+            log.debug("Exception while deleting table entry: {} > deviceId={}, entryId={}/{}",
+                      e, deviceId, tableName, entryId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public final void setTableDefaultAction(String tableName, Bmv2Action action)
+            throws Bmv2RuntimeException {
+
+        log.debug("Setting table default... > deviceId={}, tableName={}, action={}", deviceId, tableName, action);
+
+        try {
+            standardClient.bm_mt_set_default_action(
+                    CONTEXT_ID,
+                    tableName,
+                    action.name(),
+                    buildActionParamsList(action));
+            log.debug("Table default set! > deviceId={}, tableName={}, action={}", deviceId, tableName, action);
+        } catch (TException e) {
+            log.debug("Exception while setting table default : {} > deviceId={}, tableName={}, action={}",
+                      e, deviceId, tableName, action);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Collection<Bmv2PortInfo> getPortsInfo() throws Bmv2RuntimeException {
+
+        log.debug("Retrieving port info... > deviceId={}", deviceId);
+
+        try {
+            return standardClient.bm_dev_mgr_show_ports().stream()
+                    .map(p -> new Bmv2PortInfo(p.getIface_name(), p.getPort_num(), p.isIs_up()))
+                    .collect(Collectors.toList());
+        } catch (TException e) {
+            log.debug("Exception while retrieving port info: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String dumpTable(String tableName) throws Bmv2RuntimeException {
+
+        log.debug("Retrieving table dump... > deviceId={}, tableName={}", deviceId, tableName);
+
+        try {
+            String dump = standardClient.bm_dump_table(CONTEXT_ID, tableName);
+            log.debug("Table dump retrieved! > deviceId={}, tableName={}", deviceId, tableName);
+            return dump;
+        } catch (TException e) {
+            log.debug("Exception while retrieving table dump: {} > deviceId={}, tableName={}",
+                      e, deviceId, tableName);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void transmitPacket(int portNumber, ImmutableByteSequence packet) throws Bmv2RuntimeException {
+
+        log.debug("Requesting packet transmission... > portNumber={}, packetSize={}", portNumber, packet.size());
+
+        try {
+
+            simpleSwitchClient.push_packet(portNumber, ByteBuffer.wrap(packet.asArray()));
+            log.debug("Packet transmission requested! > portNumber={}, packetSize={}", portNumber, packet.size());
+        } catch (TException e) {
+            log.debug("Exception while requesting packet transmission: {} > portNumber={}, packetSize={}",
+                      e, portNumber, packet.size());
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void resetState() throws Bmv2RuntimeException {
+
+        log.debug("Resetting device state... > deviceId={}", deviceId);
+
+        try {
+            standardClient.bm_reset_state();
+            log.debug("Device state reset! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while resetting device state: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String dumpJsonConfig() throws Bmv2RuntimeException {
+
+        log.debug("Dumping device config... > deviceId={}", deviceId);
+
+        try {
+            String config = standardClient.bm_get_config();
+            log.debug("Device config dumped! > deviceId={}, configLength={}", deviceId, config.length());
+            return config;
+        } catch (TException e) {
+            log.debug("Exception while dumping device config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Pair<Long, Long> readTableEntryCounter(String tableName, long entryId) throws Bmv2RuntimeException {
+
+        log.debug("Reading table entry counters... > deviceId={}, tableName={}, entryId={}",
+                  deviceId, tableName, entryId);
+
+        try {
+            BmCounterValue counterValue = standardClient.bm_mt_read_counter(CONTEXT_ID, tableName, entryId);
+            log.debug("Table entry counters retrieved! > deviceId={}, tableName={}, entryId={}, bytes={}, packets={}",
+                      deviceId, tableName, entryId, counterValue.bytes, counterValue.packets);
+            return Pair.of(counterValue.bytes, counterValue.packets);
+        } catch (TException e) {
+            log.debug("Exception while reading table counters: {} > deviceId={}, tableName={}, entryId={}",
+                      e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public Pair<Long, Long> readCounter(String counterName, int index) throws Bmv2RuntimeException {
+
+        log.debug("Reading table entry counters... > deviceId={}, counterName={}, index={}",
+                  deviceId, counterName, index);
+
+        try {
+            BmCounterValue counterValue = standardClient.bm_counter_read(CONTEXT_ID, counterName, index);
+            log.debug("Table entry counters retrieved! >deviceId={}, counterName={}, index={}, bytes={}, packets={}",
+                      deviceId, counterName, index, counterValue.bytes, counterValue.packets);
+            return Pair.of(counterValue.bytes, counterValue.packets);
+        } catch (TException e) {
+            log.debug("Exception while reading table counters: {} > deviceId={}, counterName={}, index={}",
+                      e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public int getProcessInstanceId() throws Bmv2RuntimeException {
+        log.debug("Getting process instance ID... > deviceId={}", deviceId);
+        try {
+            int instanceId = simpleSwitchClient.get_process_instance_id();
+            log.debug("TProcess instance ID retrieved! > deviceId={}, instanceId={}",
+                      deviceId, instanceId);
+            return instanceId;
+        } catch (TException e) {
+            log.debug("Exception while getting process instance ID: {} > deviceId={}", e.toString(), deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public String getJsonConfigMd5() throws Bmv2RuntimeException {
+
+        log.debug("Getting device config md5... > deviceId={}", deviceId);
+
+        try {
+            String md5 = standardClient.bm_get_config_md5();
+            log.debug("Device config md5 received! > deviceId={}, configMd5={}", deviceId, md5);
+            return md5;
+        } catch (TException e) {
+            log.debug("Exception while getting device config md5: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void loadNewJsonConfig(String jsonString) throws Bmv2RuntimeException {
+
+        log.debug("Loading new JSON config on device... > deviceId={}, jsonStringLength={}",
+                  deviceId, jsonString.length());
+
+        try {
+            standardClient.bm_load_new_config(jsonString);
+            log.debug("JSON config loaded! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while loading JSON config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    @Override
+    public void swapJsonConfig() throws Bmv2RuntimeException {
+
+        log.debug("Swapping JSON config on device... > deviceId={}", deviceId);
+
+        try {
+            standardClient.bm_swap_configs();
+            log.debug("JSON config swapped! > deviceId={}", deviceId);
+        } catch (TException e) {
+            log.debug("Exception while swapping JSON config: {} > deviceId={}", e, deviceId);
+            throw parseTException(e);
+        }
+    }
+
+    /**
+     * Builds a list of Bmv2/Thrift compatible match parameters.
+     *
+     * @param matchKey a bmv2 matchKey
+     * @return list of thrift-compatible bm match parameters
+     */
+    private static List<BmMatchParam> buildMatchParamsList(Bmv2MatchKey matchKey) {
+        List<BmMatchParam> paramsList = Lists.newArrayList();
+        matchKey.matchParams().forEach(x -> {
+            ByteBuffer value;
+            ByteBuffer mask;
+            switch (x.type()) {
+                case EXACT:
+                    value = ByteBuffer.wrap(((Bmv2ExactMatchParam) x).value().asArray());
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.EXACT)
+                                    .setExact(new BmMatchParamExact(value)));
+                    break;
+                case TERNARY:
+                    value = ByteBuffer.wrap(((Bmv2TernaryMatchParam) x).value().asArray());
+                    mask = ByteBuffer.wrap(((Bmv2TernaryMatchParam) x).mask().asArray());
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.TERNARY)
+                                    .setTernary(new BmMatchParamTernary(value, mask)));
+                    break;
+                case LPM:
+                    value = ByteBuffer.wrap(((Bmv2LpmMatchParam) x).value().asArray());
+                    int prefixLength = ((Bmv2LpmMatchParam) x).prefixLength();
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.LPM)
+                                    .setLpm(new BmMatchParamLPM(value, prefixLength)));
+                    break;
+                case VALID:
+                    boolean flag = ((Bmv2ValidMatchParam) x).flag();
+                    paramsList.add(
+                            new BmMatchParam(BmMatchParamType.VALID)
+                                    .setValid(new BmMatchParamValid(flag)));
+                    break;
+                default:
+                    // should never be here
+                    throw new RuntimeException("Unknown match param type " + x.type().name());
+            }
+        });
+        return paramsList;
+    }
+
+    /**
+     * Build a list of Bmv2/Thrift compatible action parameters.
+     *
+     * @param action an action object
+     * @return list of ByteBuffers
+     */
+    private static List<ByteBuffer> buildActionParamsList(Bmv2Action action) {
+        List<ByteBuffer> buffers = Lists.newArrayList();
+        action.parameters().forEach(p -> buffers.add(ByteBuffer.wrap(p.asArray())));
+        return buffers;
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImpl.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImpl.java
new file mode 100644
index 0000000..b3c1e8a
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImpl.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.Sets;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.bmv2.api.context.Bmv2ActionModel;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
+import org.onosproject.bmv2.api.context.Bmv2FieldModel;
+import org.onosproject.bmv2.api.context.Bmv2FlowRuleTranslator;
+import org.onosproject.bmv2.api.context.Bmv2FlowRuleTranslatorException;
+import org.onosproject.bmv2.api.context.Bmv2Interpreter;
+import org.onosproject.bmv2.api.context.Bmv2InterpreterException;
+import org.onosproject.bmv2.api.context.Bmv2RuntimeDataModel;
+import org.onosproject.bmv2.api.context.Bmv2TableKeyModel;
+import org.onosproject.bmv2.api.context.Bmv2TableModel;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2ExtensionSelector;
+import org.onosproject.bmv2.api.runtime.Bmv2ExtensionTreatment;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils;
+import org.onosproject.net.flow.FlowRule;
+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.EthCriterion;
+import org.onosproject.net.flow.criteria.EthTypeCriterion;
+import org.onosproject.net.flow.criteria.ExtensionCriterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+import org.onosproject.net.flow.instructions.ExtensionTreatment;
+import org.onosproject.net.flow.instructions.ExtensionTreatmentType.ExtensionTreatmentTypes;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.Instructions.ExtensionInstructionWrapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils.roundToBytes;
+import static org.onosproject.net.flow.criteria.Criterion.Type.EXTENSION;
+import static org.onosproject.net.flow.criteria.ExtensionSelectorType.ExtensionSelectorTypes.BMV2_MATCH_PARAMS;
+
+/**
+ * Default implementation of a BMv2 flow rule translator.
+ */
+@Beta
+public class Bmv2FlowRuleTranslatorImpl implements Bmv2FlowRuleTranslator {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    @Override
+    public Bmv2TableEntry translate(FlowRule rule, Bmv2DeviceContext context)
+            throws Bmv2FlowRuleTranslatorException {
+
+        Bmv2Configuration configuration = context.configuration();
+        Bmv2Interpreter interpreter = context.interpreter();
+
+        int tableId = rule.tableId();
+        String tableName = interpreter.tableIdMap().get(tableId);
+
+        Bmv2TableModel table = (tableName == null) ? configuration.table(tableId) : configuration.table(tableName);
+
+        if (table == null) {
+            throw new Bmv2FlowRuleTranslatorException("Unknown table ID: " + tableId);
+        }
+
+        /* Translate selector */
+        Bmv2MatchKey bmv2MatchKey = buildMatchKey(interpreter, rule.selector(), table);
+
+        /* Translate treatment */
+        TrafficTreatment treatment = rule.treatment();
+        Bmv2Action bmv2Action = null;
+        // If treatment has only 1 instruction of type extension, use that
+        for (Instruction inst : treatment.allInstructions()) {
+            if (inst.type() == Instruction.Type.EXTENSION) {
+                if (treatment.allInstructions().size() == 1) {
+                    bmv2Action = getActionFromExtension((ExtensionInstructionWrapper) inst);
+                } else {
+                    throw new Bmv2FlowRuleTranslatorException("Unable to translate traffic treatment, found multiple " +
+                                                                      "instructions of which one is an extension: " +
+                                                                      treatment.toString());
+                }
+            }
+        }
+
+        if (bmv2Action == null) {
+            // No extension, use interpreter to build action.
+            try {
+                bmv2Action = interpreter.mapTreatment(treatment, configuration);
+            } catch (Bmv2InterpreterException e) {
+                throw new Bmv2FlowRuleTranslatorException("Unable to translate treatment. " + e.toString());
+            }
+        }
+
+        if (bmv2Action == null) {
+            // Interpreter returned null.
+            throw new Bmv2FlowRuleTranslatorException("Unable to translate treatment");
+        }
+
+        // Check action
+        Bmv2ActionModel actionModel = configuration.action(bmv2Action.name());
+        if (actionModel == null) {
+            throw new Bmv2FlowRuleTranslatorException("Unknown action " + bmv2Action.name());
+        }
+        if (!table.actions().contains(actionModel)) {
+            throw new Bmv2FlowRuleTranslatorException("Action " + bmv2Action.name()
+                                                              + " is not defined for table " + tableName);
+        }
+        if (actionModel.runtimeDatas().size() != bmv2Action.parameters().size()) {
+            throw new Bmv2FlowRuleTranslatorException("Wrong number of parameters for action "
+                                                              + actionModel.name() + ", expected "
+                                                              + actionModel.runtimeDatas().size() + ", but found "
+                                                              + bmv2Action.parameters().size());
+        }
+        for (int i = 0; i < actionModel.runtimeDatas().size(); i++) {
+            Bmv2RuntimeDataModel data = actionModel.runtimeDatas().get(i);
+            ImmutableByteSequence param = bmv2Action.parameters().get(i);
+            if (param.size() != roundToBytes(data.bitWidth())) {
+                throw new Bmv2FlowRuleTranslatorException("Wrong byte-width for parameter " + data.name()
+                                                                  + " of action " + actionModel.name()
+                                                                  + ", expected " + roundToBytes(data.bitWidth())
+                                                                  + " bytes, but found " + param.size());
+            }
+        }
+
+        Bmv2TableEntry.Builder tableEntryBuilder = Bmv2TableEntry.builder();
+
+        // In BMv2 0 is the highest priority.
+        int newPriority = Integer.MAX_VALUE - rule.priority();
+
+        tableEntryBuilder
+                .withTableName(table.name())
+                .withPriority(newPriority)
+                .withMatchKey(bmv2MatchKey)
+                .withAction(bmv2Action);
+
+        if (!rule.isPermanent()) {
+            if (table.hasTimeouts()) {
+                tableEntryBuilder.withTimeout((double) rule.timeout());
+            } else {
+                log.warn("Flow rule is temporary but table {} doesn't support timeouts, translating to permanent",
+                         table.name());
+            }
+
+        }
+
+        return tableEntryBuilder.build();
+    }
+
+    private Bmv2TernaryMatchParam buildTernaryParam(Bmv2FieldModel field, Criterion criterion, int bitWidth)
+            throws Bmv2FlowRuleTranslatorException {
+
+        // Value and mask will be filled according to criterion type
+        ImmutableByteSequence value;
+        ImmutableByteSequence mask = null;
+
+        int byteWidth = roundToBytes(bitWidth);
+
+        switch (criterion.type()) {
+            case IN_PORT:
+                long port = ((PortCriterion) criterion).port().toLong();
+                value = ImmutableByteSequence.copyFrom(port);
+                break;
+            case ETH_DST:
+                EthCriterion c = (EthCriterion) criterion;
+                value = ImmutableByteSequence.copyFrom(c.mac().toBytes());
+                if (c.mask() != null) {
+                    mask = ImmutableByteSequence.copyFrom(c.mask().toBytes());
+                }
+                break;
+            case ETH_SRC:
+                EthCriterion c2 = (EthCriterion) criterion;
+                value = ImmutableByteSequence.copyFrom(c2.mac().toBytes());
+                if (c2.mask() != null) {
+                    mask = ImmutableByteSequence.copyFrom(c2.mask().toBytes());
+                }
+                break;
+            case ETH_TYPE:
+                short ethType = ((EthTypeCriterion) criterion).ethType().toShort();
+                value = ImmutableByteSequence.copyFrom(ethType);
+                break;
+            // TODO: implement building for other criterion types (easy with DefaultCriterion of ONOS-4034)
+            default:
+                throw new Bmv2FlowRuleTranslatorException("Feature not implemented, ternary builder for criterion" +
+                                                                  "type: " + criterion.type().name());
+        }
+
+        // Fit byte sequence in field model bit-width.
+        try {
+            value = Bmv2TranslatorUtils.fitByteSequence(value, bitWidth);
+        } catch (Bmv2TranslatorUtils.ByteSequenceFitException e) {
+            throw new Bmv2FlowRuleTranslatorException(
+                    "Fit exception for criterion " + criterion.type().name() + " value, " + e.getMessage());
+        }
+
+        if (mask == null) {
+            // no mask, all ones
+            mask = ImmutableByteSequence.ofOnes(byteWidth);
+        } else {
+            try {
+                mask = Bmv2TranslatorUtils.fitByteSequence(mask, bitWidth);
+            } catch (Bmv2TranslatorUtils.ByteSequenceFitException e) {
+                throw new Bmv2FlowRuleTranslatorException(
+                        "Fit exception for criterion " + criterion.type().name() + " mask, " + e.getMessage());
+            }
+        }
+
+        return new Bmv2TernaryMatchParam(value, mask);
+    }
+
+    private Bmv2Action getActionFromExtension(Instructions.ExtensionInstructionWrapper inst)
+            throws Bmv2FlowRuleTranslatorException {
+
+        ExtensionTreatment extTreatment = inst.extensionInstruction();
+
+        if (extTreatment.type() == ExtensionTreatmentTypes.BMV2_ACTION.type()) {
+            if (extTreatment instanceof Bmv2ExtensionTreatment) {
+                return ((Bmv2ExtensionTreatment) extTreatment).getAction();
+            } else {
+                throw new Bmv2FlowRuleTranslatorException("Unable to decode treatment extension: " + extTreatment);
+            }
+        } else {
+            throw new Bmv2FlowRuleTranslatorException("Unsupported treatment extension type: " + extTreatment.type());
+        }
+    }
+
+    private Bmv2MatchKey buildMatchKey(Bmv2Interpreter interpreter, TrafficSelector selector, Bmv2TableModel tableModel)
+            throws Bmv2FlowRuleTranslatorException {
+
+        // Find a bmv2 extension selector (if any) and get the parameter map.
+        Optional<Bmv2ExtensionSelector> extSelector = selector.criteria().stream()
+                .filter(c -> c.type().equals(EXTENSION))
+                .map(c -> (ExtensionCriterion) c)
+                .map(ExtensionCriterion::extensionSelector)
+                .filter(c -> c.type().equals(BMV2_MATCH_PARAMS.type()))
+                .map(c -> (Bmv2ExtensionSelector) c)
+                .findFirst();
+        Map<String, Bmv2MatchParam> extParamMap =
+                (extSelector.isPresent()) ? extSelector.get().parameterMap() : Collections.emptyMap();
+
+        Set<Criterion> translatedCriteria = Sets.newHashSet();
+        Set<String> usedExtParams = Sets.newHashSet();
+
+        Bmv2MatchKey.Builder matchKeyBuilder = Bmv2MatchKey.builder();
+
+        keysLoop:
+        for (Bmv2TableKeyModel keyModel : tableModel.keys()) {
+
+            // use fieldName dot notation (e.g. ethernet.dstAddr)
+            String fieldName = keyModel.field().header().name() + "." + keyModel.field().type().name();
+
+            int bitWidth = keyModel.field().type().bitWidth();
+            int byteWidth = roundToBytes(bitWidth);
+
+            Criterion.Type criterionType = interpreter.criterionTypeMap().inverse().get(fieldName);
+
+            if (!extParamMap.containsKey(fieldName) &&
+                    (criterionType == null || selector.getCriterion(criterionType) == null)) {
+                // Neither an extension nor a mapping / criterion is available for this field.
+                switch (keyModel.matchType()) {
+                    case TERNARY:
+                        // Wildcard field
+                        matchKeyBuilder.withWildcard(byteWidth);
+                        break;
+                    case LPM:
+                        // LPM with prefix 0
+                        matchKeyBuilder.add(new Bmv2LpmMatchParam(ImmutableByteSequence.ofZeros(byteWidth), 0));
+                        break;
+                    default:
+                        throw new Bmv2FlowRuleTranslatorException("No value found for required match field "
+                                                                          + fieldName);
+                }
+                // Next key
+                continue keysLoop;
+            }
+
+            Bmv2MatchParam matchParam;
+
+            if (extParamMap.containsKey(fieldName)) {
+                // Parameter found in extension
+                if (criterionType != null && selector.getCriterion(criterionType) != null) {
+                    // Found also a criterion that can be mapped. This is bad.
+                    throw new Bmv2FlowRuleTranslatorException("Both an extension and a criterion mapping are defined " +
+                                                                      "for match field " + fieldName);
+                }
+
+                matchParam = extParamMap.get(fieldName);
+                usedExtParams.add(fieldName);
+
+                // Check parameter type and size
+                if (!keyModel.matchType().equals(matchParam.type())) {
+                    throw new Bmv2FlowRuleTranslatorException("Wrong match type for parameter " + fieldName
+                                                                      + ", expected " + keyModel.matchType().name()
+                                                                      + ", but found " + matchParam.type().name());
+                }
+                int foundByteWidth;
+                switch (keyModel.matchType()) {
+                    case EXACT:
+                        Bmv2ExactMatchParam m1 = (Bmv2ExactMatchParam) matchParam;
+                        foundByteWidth = m1.value().size();
+                        break;
+                    case TERNARY:
+                        Bmv2TernaryMatchParam m2 = (Bmv2TernaryMatchParam) matchParam;
+                        foundByteWidth = m2.value().size();
+                        break;
+                    case LPM:
+                        Bmv2LpmMatchParam m3 = (Bmv2LpmMatchParam) matchParam;
+                        foundByteWidth = m3.value().size();
+                        break;
+                    case VALID:
+                        foundByteWidth = -1;
+                        break;
+                    default:
+                        // should never be her
+                        throw new RuntimeException("Unrecognized match type " + keyModel.matchType().name());
+                }
+                if (foundByteWidth != -1 && foundByteWidth != byteWidth) {
+                    throw new Bmv2FlowRuleTranslatorException("Wrong byte-width for match parameter " + fieldName
+                                                                      + ", expected " + byteWidth + ", but found "
+                                                                      + foundByteWidth);
+                }
+
+            } else {
+                // A criterion mapping is available for this key
+                Criterion criterion = selector.getCriterion(criterionType);
+                translatedCriteria.add(criterion);
+                switch (keyModel.matchType()) {
+                    case TERNARY:
+                        matchParam = buildTernaryParam(keyModel.field(), criterion, bitWidth);
+                        break;
+                    default:
+                        // TODO: implement other match param builders (exact, LPM, etc.)
+                        throw new Bmv2FlowRuleTranslatorException("Feature not yet implemented, match param builder: "
+                                                                          + keyModel.matchType().name());
+                }
+            }
+
+            matchKeyBuilder.add(matchParam);
+        }
+
+        // Check if all criteria have been translated
+        Set<Criterion> ignoredCriteria = selector.criteria()
+                .stream()
+                .filter(c -> !c.type().equals(EXTENSION))
+                .filter(c -> !translatedCriteria.contains(c))
+                .collect(Collectors.toSet());
+
+        if (ignoredCriteria.size() > 0) {
+            throw new Bmv2FlowRuleTranslatorException("The following criteria cannot be translated for table "
+                                                              + tableModel.name() + ": " + ignoredCriteria.toString());
+        }
+
+        // Check is all extension parameters have been used
+        Set<String> ignoredExtParams = extParamMap.keySet()
+                .stream()
+                .filter(k -> !usedExtParams.contains(k))
+                .collect(Collectors.toSet());
+
+        if (ignoredExtParams.size() > 0) {
+            throw new Bmv2FlowRuleTranslatorException("The following extension match parameters cannot be used for " +
+                                                              "table " + tableModel.name() + ": "
+                                                              + ignoredExtParams.toString());
+        }
+
+        return matchKeyBuilder.build();
+    }
+
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TExceptionParser.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TExceptionParser.java
new file mode 100644
index 0000000..6ce3fbd
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TExceptionParser.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+
+import org.apache.thrift.TException;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.thriftapi.InvalidCounterOperation;
+import org.onosproject.bmv2.thriftapi.InvalidDevMgrOperation;
+import org.onosproject.bmv2.thriftapi.InvalidLearnOperation;
+import org.onosproject.bmv2.thriftapi.InvalidMcOperation;
+import org.onosproject.bmv2.thriftapi.InvalidMeterOperation;
+import org.onosproject.bmv2.thriftapi.InvalidRegisterOperation;
+import org.onosproject.bmv2.thriftapi.InvalidSwapOperation;
+import org.onosproject.bmv2.thriftapi.InvalidTableOperation;
+
+import static org.onosproject.bmv2.api.runtime.Bmv2RuntimeException.Code;
+
+/**
+ * Utility class to translate a Thrift exception into a Bmv2RuntimeException.
+ */
+final class Bmv2TExceptionParser {
+
+    private Bmv2TExceptionParser() {
+        // ban constructor.
+    }
+
+    static Bmv2RuntimeException parseTException(TException cause) {
+        try {
+            return new Bmv2RuntimeException(getCode(cause));
+        } catch (ParserException e) {
+            return new Bmv2RuntimeException(e.codeString);
+        }
+    }
+
+    private static Code getCode(TException e) throws ParserException {
+        if (e instanceof InvalidTableOperation) {
+            InvalidTableOperation t = (InvalidTableOperation) e;
+            switch (t.getCode()) {
+                case TABLE_FULL:
+                    return Code.TABLE_FULL;
+                case INVALID_HANDLE:
+                    return Code.TABLE_INVALID_HANDLE;
+                case EXPIRED_HANDLE:
+                    return Code.TABLE_EXPIRED_HANDLE;
+                case COUNTERS_DISABLED:
+                    return Code.TABLE_COUNTERS_DISABLED;
+                case METERS_DISABLED:
+                    return Code.TABLE_METERS_DISABLED;
+                case AGEING_DISABLED:
+                    return Code.TABLE_AGEING_DISABLED;
+                case INVALID_TABLE_NAME:
+                    return Code.TABLE_INVALID_TABLE_NAME;
+                case INVALID_ACTION_NAME:
+                    return Code.TABLE_INVALID_ACTION_NAME;
+                case WRONG_TABLE_TYPE:
+                    return Code.TABLE_WRONG_TABLE_TYPE;
+                case INVALID_MBR_HANDLE:
+                    return Code.TABLE_INVALID_MBR_HANDLE;
+                case MBR_STILL_USED:
+                    return Code.TABLE_MBR_STILL_USED;
+                case MBR_ALREADY_IN_GRP:
+                    return Code.TABLE_MBR_ALREADY_IN_GRP;
+                case MBR_NOT_IN_GRP:
+                    return Code.TABLE_MBR_NOT_IN_GRP;
+                case INVALID_GRP_HANDLE:
+                    return Code.TABLE_INVALID_GRP_HANDLE;
+                case GRP_STILL_USED:
+                    return Code.TABLE_GRP_STILL_USED;
+                case EMPTY_GRP:
+                    return Code.TABLE_EMPTY_GRP;
+                case DUPLICATE_ENTRY:
+                    return Code.TABLE_DUPLICATE_ENTRY;
+                case BAD_MATCH_KEY:
+                    return Code.TABLE_BAD_MATCH_KEY;
+                case INVALID_METER_OPERATION:
+                    return Code.TABLE_INVALID_METER_OPERATION;
+                case DEFAULT_ACTION_IS_CONST:
+                    return Code.TABLE_DEFAULT_ACTION_IS_CONST;
+                case DEFAULT_ENTRY_IS_CONST:
+                    return Code.TABLE_DEFAULT_ENTRY_IS_CONST;
+                case ERROR:
+                    return Code.TABLE_GENERAL_ERROR;
+                default:
+                    return Code.TABLE_UNKNOWN_ERROR;
+            }
+        } else if (e instanceof InvalidCounterOperation) {
+            InvalidCounterOperation t = (InvalidCounterOperation) e;
+            switch (t.getCode()) {
+                case INVALID_COUNTER_NAME:
+                    return Code.COUNTER_INVALID_NAME;
+                case INVALID_INDEX:
+                    return Code.COUNTER_INVALID_INDEX;
+                case ERROR:
+                    return Code.COUNTER_ERROR_GENERAL;
+                default:
+                    return Code.COUNTER_ERROR_UNKNOWN;
+            }
+        } else if (e instanceof InvalidDevMgrOperation) {
+            InvalidDevMgrOperation t = (InvalidDevMgrOperation) e;
+            switch (t.getCode()) {
+                case ERROR:
+                    return Code.DEV_MGR_ERROR_GENERAL;
+                default:
+                    return Code.DEV_MGR_UNKNOWN;
+            }
+        } else if (e instanceof InvalidSwapOperation) {
+            InvalidSwapOperation t = (InvalidSwapOperation) e;
+            switch (t.getCode()) {
+                case CONFIG_SWAP_DISABLED:
+                    return Code.SWAP_CONFIG_DISABLED;
+                case ONGOING_SWAP:
+                    return Code.SWAP_ONGOING;
+                case NO_ONGOING_SWAP:
+                    return Code.SWAP_NO_ONGOING;
+                default:
+                    return Code.SWAP_ERROR_UKNOWN;
+            }
+        } else if (e instanceof InvalidMeterOperation) {
+            // TODO
+            throw new ParserException(e.toString());
+        } else if (e instanceof InvalidRegisterOperation) {
+            // TODO
+            throw new ParserException(e.toString());
+        } else if (e instanceof InvalidLearnOperation) {
+            // TODO
+            throw new ParserException(e.toString());
+        } else if (e instanceof InvalidMcOperation) {
+            // TODO
+            throw new ParserException(e.toString());
+        } else {
+            throw new ParserException(e.toString());
+        }
+    }
+
+    private static class ParserException extends Exception {
+
+        private String codeString;
+
+        public ParserException(String codeString) {
+            this.codeString = codeString;
+        }
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java
new file mode 100644
index 0000000..04e37e5
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.HexString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onlab.util.SharedScheduledExecutors;
+import org.onosproject.bmv2.api.context.Bmv2ActionModel;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2ParsedTableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils.fitByteSequence;
+import static org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils.ByteSequenceFitException;
+
+/**
+ * BMv2 table dump parser.
+ */
+public final class Bmv2TableDumpParser {
+
+    // Examples of a BMv2 table dump can be found in Bmv2TableDumpParserTest
+
+    // 1: entry id, 2: match string, 3: action string
+    private static final String ENTRY_PATTERN_REGEX = "(\\d+): (.*) => (.*)";
+    // 1: match values, 2: masks
+    private static final String MATCH_TERNARY_PATTERN_REGEX = "([0-9a-fA-F ]+) &&& ([0-9a-fA-F ]+)";
+    // 1: match values, 2: masks
+    private static final String MATCH_LPM_PATTERN_REGEX = "([0-9a-fA-F ]+) / ([0-9a-fA-F ]+)";
+    // 1: match values
+    private static final String MATCH_EXACT_PATTERN_REGEX = "([0-9a-fA-F ]+)";
+    // 1: action name, 2: action params
+    private static final String ACTION_PATTERN_REGEX = "(.+) - ?([0-9a-fA-F ,]*)";
+
+    private static final Pattern ENTRY_PATTERN = Pattern.compile(ENTRY_PATTERN_REGEX);
+    private static final Pattern MATCH_TERNARY_PATTERN = Pattern.compile(MATCH_TERNARY_PATTERN_REGEX);
+    private static final Pattern MATCH_LPM_PATTERN = Pattern.compile(MATCH_LPM_PATTERN_REGEX);
+    private static final Pattern MATCH_EXACT_PATTERN = Pattern.compile(MATCH_EXACT_PATTERN_REGEX);
+    private static final Pattern ACTION_PATTERN = Pattern.compile(ACTION_PATTERN_REGEX);
+
+    // Cache to avoid re-parsing known lines.
+    // The assumption here is that entries are not updated too frequently, so that the entry id doesn't change often.
+    // Otherwise, we should cache only the match and action strings...
+    private static final LoadingCache<Pair<String, Bmv2Configuration>, Optional<Bmv2ParsedTableEntry>> ENTRY_CACHE =
+            CacheBuilder.newBuilder()
+            .expireAfterAccess(60, TimeUnit.SECONDS)
+            .recordStats()
+            .build(new CacheLoader<Pair<String, Bmv2Configuration>, Optional<Bmv2ParsedTableEntry>>() {
+                @Override
+                public Optional<Bmv2ParsedTableEntry> load(Pair<String, Bmv2Configuration> key) throws Exception {
+                    // Very expensive call.
+                    return Optional.ofNullable(parseLine(key.getLeft(), key.getRight()));
+                }
+            });
+
+    private static final Logger log = LoggerFactory.getLogger(Bmv2TableDumpParser.class);
+
+    private static final long STATS_LOG_FREQUENCY = 3; // minutes
+
+    static {
+        SharedScheduledExecutors.getSingleThreadExecutor().scheduleAtFixedRate(
+                () -> reportStats(), 0, STATS_LOG_FREQUENCY, TimeUnit.MINUTES);
+    }
+
+    private Bmv2TableDumpParser() {
+        // Ban constructor.
+    }
+
+    /**
+     * Parse the given BMv2 table dump.
+     *
+     * @param tableDump a string value
+     * @return a list of {@link Bmv2ParsedTableEntry}
+     */
+    public static List<Bmv2ParsedTableEntry> parse(String tableDump, Bmv2Configuration configuration) {
+        checkNotNull(tableDump, "tableDump cannot be null");
+        // Parse all lines
+        List<Bmv2ParsedTableEntry> result = Arrays.stream(tableDump.split("\n"))
+                .map(line -> Pair.of(line, configuration))
+                .map(Bmv2TableDumpParser::loadFromCache)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+        return result;
+    }
+
+    private static Optional<Bmv2ParsedTableEntry> loadFromCache(Pair<String, Bmv2Configuration> key) {
+        try {
+            return ENTRY_CACHE.get(key);
+        } catch (ExecutionException e) {
+            Throwable t = e.getCause();
+            if (t instanceof Bmv2TableDumpParserException) {
+                Bmv2TableDumpParserException parserException = (Bmv2TableDumpParserException) t;
+                log.warn("{}", parserException.getMessage());
+            } else {
+                log.error("Exception while parsing table dump line", e);
+            }
+            return Optional.empty();
+        }
+    }
+
+    private static void reportStats() {
+        CacheStats stats = ENTRY_CACHE.stats();
+        log.info("Cache stats: requestCount={}, hitRate={}, exceptionsCount={}, avgLoadPenalty={}",
+                 stats.requestCount(), stats.hitRate(), stats.loadExceptionCount(), stats.averageLoadPenalty());
+    }
+
+    private static Bmv2ParsedTableEntry parseLine(String line, Bmv2Configuration configuration)
+            throws Bmv2TableDumpParserException {
+        Matcher matcher = ENTRY_PATTERN.matcher(line);
+        if (matcher.find()) {
+            long entryId = parseEntryId(matcher, 1);
+            String matchString = parseMatchString(matcher, 2);
+            String actionString = parseActionString(matcher, 3);
+            Bmv2MatchKey matchKey = parseMatchKey(matchString);
+            Bmv2Action action = parseAction(actionString, configuration);
+            return new Bmv2ParsedTableEntry(entryId, matchKey, action);
+        } else {
+            // Not a table entry
+            return null;
+        }
+    }
+
+    private static Long parseEntryId(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find entry ID: " + matcher.group());
+        }
+        long entryId;
+        try {
+            entryId = Long.valueOf(str.trim());
+        } catch (NumberFormatException e) {
+            throw new Bmv2TableDumpParserException("Unable to parse entry id for string: " + matcher.group());
+        }
+        return entryId;
+    }
+
+    private static String parseMatchString(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find match string: " + matcher.group());
+        }
+        return str.trim();
+    }
+
+    private static String parseActionString(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action string: " + matcher.group());
+        }
+        return str.trim();
+    }
+
+    private static Bmv2MatchKey parseMatchKey(String str) throws Bmv2TableDumpParserException {
+
+        Bmv2MatchKey.Builder builder = Bmv2MatchKey.builder();
+
+        // Try with ternary...
+        Matcher matcher = MATCH_TERNARY_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Ternary Match.
+            List<ImmutableByteSequence> values = parseMatchValues(matcher, 1);
+            List<ImmutableByteSequence> masks = parseMatchMasks(matcher, 2, values);
+            for (int i = 0; i < values.size(); i++) {
+                builder.add(new Bmv2TernaryMatchParam(values.get(i), masks.get(i)));
+            }
+            return builder.build();
+        }
+
+        // FIXME: LPM match parsing broken if table key contains also a ternary match
+        // Also it assumes the lpm parameter is the last one, which is wrong.
+        // Try with LPM...
+        matcher = MATCH_LPM_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Lpm Match.
+            List<ImmutableByteSequence> values = parseMatchValues(matcher, 1);
+            int prefixLength = parseLpmPrefix(matcher, 2);
+            for (int i = 0; i < values.size() - 1; i++) {
+                builder.add(new Bmv2ExactMatchParam(values.get(i)));
+            }
+            builder.add(new Bmv2LpmMatchParam(values.get(values.size() - 1), prefixLength));
+            return builder.build();
+        }
+
+        // Try with exact...
+        matcher = MATCH_EXACT_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Exact match.
+            parseMatchValues(matcher, 1)
+                    .stream()
+                    .map(Bmv2ExactMatchParam::new)
+                    .forEach(builder::add);
+            return builder.build();
+        }
+
+        throw new Bmv2TableDumpParserException("Unable to parse match string: " + str);
+    }
+
+    private static List<ImmutableByteSequence> parseMatchValues(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String matchString = matcher.group(groupIdx);
+        if (matchString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find match params for string: " + matcher.group());
+        }
+        List<ImmutableByteSequence> result = Lists.newArrayList();
+        for (String paramString : matchString.split(" ")) {
+            byte[] bytes = HexString.fromHexString(paramString, null);
+            result.add(ImmutableByteSequence.copyFrom(bytes));
+        }
+        return result;
+    }
+
+    private static List<ImmutableByteSequence> parseMatchMasks(Matcher matcher, int groupIdx,
+                                                               List<ImmutableByteSequence> matchParams)
+            throws Bmv2TableDumpParserException {
+        String maskString = matcher.group(groupIdx);
+        if (maskString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find mask for string: " + matcher.group());
+        }
+        List<ImmutableByteSequence> result = Lists.newArrayList();
+        /*
+        Mask here is a hex string with no spaces, hence individual mask params can be derived according
+        to given matchParam sizes.
+         */
+        byte[] maskBytes = HexString.fromHexString(maskString, null);
+        int startPosition = 0;
+        for (ImmutableByteSequence bs : matchParams) {
+            if (startPosition + bs.size() > maskBytes.length) {
+                throw new Bmv2TableDumpParserException("Invalid length for mask in string: " + matcher.group());
+            }
+            ImmutableByteSequence maskParam = ImmutableByteSequence.copyFrom(maskBytes,
+                                                                             startPosition,
+                                                                             startPosition + bs.size() - 1);
+            result.add(maskParam);
+            startPosition += bs.size();
+        }
+        return result;
+    }
+
+    private static int parseLpmPrefix(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find LPM prefix for string: " + matcher.group());
+        }
+        // For some reason the dumped prefix has 16 bits more than the one programmed
+        try {
+            return Integer.valueOf(str.trim()) - 16;
+        } catch (NumberFormatException e) {
+            throw new Bmv2TableDumpParserException("Unable to parse LPM prefix from string: " + matcher.group());
+        }
+    }
+
+    private static Bmv2Action parseAction(String str, Bmv2Configuration configuration)
+            throws Bmv2TableDumpParserException {
+        Matcher matcher = ACTION_PATTERN.matcher(str);
+        if (matcher.find()) {
+            String actionName = parseActionName(matcher, 1);
+            Bmv2ActionModel actionModel = configuration.action(actionName);
+            if (actionModel == null) {
+                throw new Bmv2TableDumpParserException("Not such an action in configuration: " + actionName);
+            }
+            Bmv2Action.Builder builder = Bmv2Action.builder().withName(actionName);
+            List<ImmutableByteSequence> actionParams = parseActionParams(matcher, 2);
+            if (actionParams.size() != actionModel.runtimeDatas().size()) {
+                throw new Bmv2TableDumpParserException("Invalid number of parameters for action: " + actionName);
+            }
+            for (int i = 0; i < actionModel.runtimeDatas().size(); i++) {
+                try {
+                    // fit param byte-width according to configuration.
+                    builder.addParameter(fitByteSequence(actionParams.get(i),
+                                                         actionModel.runtimeDatas().get(i).bitWidth()));
+                } catch (ByteSequenceFitException e) {
+                    throw new Bmv2TableDumpParserException("Unable to parse action param: " + e.toString());
+                }
+            }
+            return builder.build();
+        }
+        throw new Bmv2TableDumpParserException("Unable to parse action string: " + str.trim());
+    }
+
+    private static String parseActionName(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String actionName = matcher.group(groupIdx);
+        if (actionName == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action name for string: " + matcher.group());
+        }
+        return actionName.trim();
+    }
+
+    private static List<ImmutableByteSequence> parseActionParams(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String paramsString = matcher.group(groupIdx);
+        if (paramsString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action params for string: " + matcher.group());
+        }
+        if (paramsString.length() == 0) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(paramsString.split(","))
+                .map(String::trim)
+                .map(s -> HexString.fromHexString(s, null))
+                .map(ImmutableByteSequence::copyFrom)
+                .collect(Collectors.toList());
+    }
+
+    public static class Bmv2TableDumpParserException extends Exception {
+        public Bmv2TableDumpParserException(String msg) {
+            super(msg);
+        }
+    }
+}
\ No newline at end of file
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableEntryServiceImpl.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableEntryServiceImpl.java
new file mode 100644
index 0000000..8258334
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableEntryServiceImpl.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+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.onlab.util.KryoNamespace;
+import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
+import org.onosproject.bmv2.api.context.Bmv2FlowRuleTranslator;
+import org.onosproject.bmv2.api.service.Bmv2DeviceContextService;
+import org.onosproject.bmv2.api.service.Bmv2Controller;
+import org.onosproject.bmv2.api.runtime.Bmv2DeviceAgent;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2FlowRuleWrapper;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2ParsedTableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2RuntimeException;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntryReference;
+import org.onosproject.bmv2.api.service.Bmv2TableEntryService;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2ValidMatchParam;
+import org.onosproject.net.DeviceId;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the Bmv2TableEntryService.
+ */
+@Component(immediate = true)
+@Service
+public class Bmv2TableEntryServiceImpl implements Bmv2TableEntryService {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    private final Bmv2FlowRuleTranslator translator = new Bmv2FlowRuleTranslatorImpl();
+
+    private EventuallyConsistentMap<Bmv2TableEntryReference, Bmv2FlowRuleWrapper> flowRules;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected Bmv2Controller controller;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected Bmv2DeviceContextService contextService;
+
+
+    @Activate
+    public void activate() {
+        KryoNamespace kryo = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(Bmv2TableEntryReference.class)
+                .register(Bmv2MatchKey.class)
+                .register(Bmv2ExactMatchParam.class)
+                .register(Bmv2TernaryMatchParam.class)
+                .register(Bmv2LpmMatchParam.class)
+                .register(Bmv2ValidMatchParam.class)
+                .register(Bmv2FlowRuleWrapper.class)
+                .build();
+
+        flowRules = storageService.<Bmv2TableEntryReference, Bmv2FlowRuleWrapper>eventuallyConsistentMapBuilder()
+                .withSerializer(kryo)
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .withName("onos-bmv2-flowrules")
+                .build();
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public Bmv2FlowRuleTranslator getFlowRuleTranslator() {
+        return translator;
+    }
+
+    @Override
+    public List<Bmv2ParsedTableEntry> getTableEntries(DeviceId deviceId, String tableName) {
+        try {
+            Bmv2DeviceContext context = contextService.getContext(deviceId);
+            if (context == null) {
+                log.warn("Unable to get table entries, found null context for {}", deviceId);
+                return Collections.emptyList();
+            }
+            Bmv2DeviceAgent agent = controller.getAgent(deviceId);
+            String tableDump = agent.dumpTable(tableName);
+            return Bmv2TableDumpParser.parse(tableDump, context.configuration());
+        } catch (Bmv2RuntimeException e) {
+            log.warn("Unable to get table entries for {}: {}", deviceId, e.explain());
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public Bmv2FlowRuleWrapper lookupEntryReference(Bmv2TableEntryReference entryRef) {
+        checkNotNull(entryRef, "table entry reference cannot be null");
+        return flowRules.get(entryRef);
+    }
+
+    @Override
+    public void bindEntryReference(Bmv2TableEntryReference entryRef, Bmv2FlowRuleWrapper rule) {
+        checkNotNull(entryRef, "table entry reference cannot be null");
+        checkNotNull(rule, "bmv2 flow rule cannot be null");
+        flowRules.put(entryRef, rule);
+    }
+
+    @Override
+    public void unbindEntryReference(Bmv2TableEntryReference entryRef) {
+        checkNotNull(entryRef, "table entry reference cannot be null");
+        flowRules.remove(entryRef);
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/SafeThriftClient.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/SafeThriftClient.java
new file mode 100644
index 0000000..f6e4813
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/SafeThriftClient.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/*
+ * Most of the code of this class was copied from:
+ * http://liveramp.com/engineering/reconnecting-thrift-client/
+ */
+
+package org.onosproject.bmv2.ctl;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.thrift.TServiceClient;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.TTransportException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Set;
+
+/**
+ * Thrift client wrapper that attempts a few reconnects before giving up a method call execution. It also provides
+ * synchronization between calls over the same transport.
+ */
+public final class SafeThriftClient {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SafeThriftClient.class);
+
+    /**
+     * List of causes which suggest a restart might fix things (defined as constants in {@link TTransportException}).
+     */
+    private static final Set<Integer> RESTARTABLE_CAUSES = ImmutableSet.of(TTransportException.NOT_OPEN,
+                                                                           TTransportException.END_OF_FILE,
+                                                                           TTransportException.TIMED_OUT,
+                                                                           TTransportException.UNKNOWN);
+
+    private SafeThriftClient() {
+        // ban constructor.
+    }
+
+    /**
+     * Reflectively wraps an already existing Thrift client.
+     *
+     * @param baseClient      the client to wrap
+     * @param clientInterface the interface that the client implements
+     * @param options         options that control behavior of the reconnecting client
+     * @param <T>
+     * @param <C>
+     * @return
+     */
+    public static <T extends TServiceClient, C> C wrap(T baseClient, Class<C> clientInterface, Options options) {
+        Object proxyObject = Proxy.newProxyInstance(clientInterface.getClassLoader(),
+                                                    new Class<?>[]{clientInterface},
+                                                    new ReconnectingClientProxy<T>(baseClient,
+                                                                                   options.getNumRetries(),
+                                                                                   options.getTimeBetweenRetries()));
+
+        return (C) proxyObject;
+    }
+
+    /**
+     * Reflectively wraps an already existing Thrift client.
+     *
+     * @param baseClient the client to wrap
+     * @param options    options that control behavior of the reconnecting client
+     * @param <T>
+     * @param <C>
+     * @return
+     */
+    public static <T extends TServiceClient, C> C wrap(T baseClient, Options options) {
+        Class<?>[] interfaces = baseClient.getClass().getInterfaces();
+
+        for (Class<?> iface : interfaces) {
+            if (iface.getSimpleName().equals("Iface")
+                    && iface.getEnclosingClass().equals(baseClient.getClass().getEnclosingClass())) {
+                return (C) wrap(baseClient, iface, options);
+            }
+        }
+
+        throw new RuntimeException("Class needs to implement Iface directly. Use wrap(TServiceClient, Class) instead.");
+    }
+
+    /**
+     * Reflectively wraps an already existing Thrift client.
+     *
+     * @param baseClient      the client to wrap
+     * @param clientInterface the interface that the client implements
+     * @param <T>
+     * @param <C>
+     * @return
+     */
+    public static <T extends TServiceClient, C> C wrap(T baseClient, Class<C> clientInterface) {
+        return wrap(baseClient, clientInterface, Options.defaults());
+    }
+
+    /**
+     * Reflectively wraps an already existing Thrift client.
+     *
+     * @param baseClient the client to wrap
+     * @param <T>
+     * @param <C>
+     * @return
+     */
+    public static <T extends TServiceClient, C> C wrap(T baseClient) {
+        return wrap(baseClient, Options.defaults());
+    }
+
+    /**
+     * Reconnection options for {@link SafeThriftClient}.
+     */
+    public static class Options {
+        private int numRetries;
+        private long timeBetweenRetries;
+
+        /**
+         * Creates new options with the given parameters.
+         *
+         * @param numRetries         the maximum number of times to try reconnecting before giving up and throwing an
+         *                           exception
+         * @param timeBetweenRetries the number of milliseconds to wait in between reconnection attempts.
+         */
+        public Options(int numRetries, long timeBetweenRetries) {
+            this.numRetries = numRetries;
+            this.timeBetweenRetries = timeBetweenRetries;
+        }
+
+        private static Options defaults() {
+            return new Options(5, 10000L);
+        }
+
+        private int getNumRetries() {
+            return numRetries;
+        }
+
+        private long getTimeBetweenRetries() {
+            return timeBetweenRetries;
+        }
+    }
+
+    /**
+     * Helper proxy class. Attempts to call method on proxy object wrapped in try/catch. If it fails, it attempts a
+     * reconnect and tries the method again.
+     *
+     * @param <T>
+     */
+    private static class ReconnectingClientProxy<T extends TServiceClient> implements InvocationHandler {
+        private final T baseClient;
+        private final TTransport transport;
+        private final int maxRetries;
+        private final long timeBetweenRetries;
+
+        public ReconnectingClientProxy(T baseClient, int maxRetries, long timeBetweenRetries) {
+            this.baseClient = baseClient;
+            this.transport = baseClient.getInputProtocol().getTransport();
+            this.maxRetries = maxRetries;
+            this.timeBetweenRetries = timeBetweenRetries;
+        }
+
+        private void reconnectOrThrowException()
+                throws TTransportException {
+            int errors = 0;
+            try {
+                if (transport.isOpen()) {
+                    transport.close();
+                }
+            } catch (Exception e) {
+                // Thrift seems to have a bug where if the transport is already closed a SocketException is thrown.
+                // However, such an exception is not advertised by .close(), hence the general-purpose catch.
+                LOG.debug("Exception while closing transport", e);
+            }
+
+            while (errors < maxRetries) {
+                try {
+                    LOG.debug("Attempting to reconnect...");
+                    transport.open();
+                    LOG.debug("Reconnection successful");
+                    break;
+                } catch (TTransportException e) {
+                    LOG.debug("Error while reconnecting:", e);
+                    errors++;
+
+                    if (errors < maxRetries) {
+                        try {
+                            LOG.debug("Sleeping for {} milliseconds before retrying", timeBetweenRetries);
+                            Thread.sleep(timeBetweenRetries);
+                        } catch (InterruptedException e2) {
+                            Thread.currentThread().interrupt();
+                            throw new RuntimeException(e);
+                        }
+                    }
+                }
+            }
+
+            if (errors >= maxRetries) {
+                throw new TTransportException("Failed to reconnect");
+            }
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+
+            // Thrift transport layer is not thread-safe (it's a wrapper on a socket), hence we need locking.
+            synchronized (transport) {
+
+                LOG.debug("Invoking method... > fromThread={}, method={}, args={}",
+                          Thread.currentThread().getId(), method.getName(), args);
+
+                try {
+
+                    return method.invoke(baseClient, args);
+                } catch (InvocationTargetException e) {
+                    if (e.getTargetException() instanceof TTransportException) {
+                        TTransportException cause = (TTransportException) e.getTargetException();
+
+                        if (RESTARTABLE_CAUSES.contains(cause.getType())) {
+                            // Try to reconnect. If fail, a TTransportException will be thrown.
+                            reconnectOrThrowException();
+                            try {
+                                // If here, transport has been successfully open, hence new exceptions will be thrown.
+                                return method.invoke(baseClient, args);
+                            } catch (InvocationTargetException e1) {
+                                LOG.debug("Exception: {}", e1.getTargetException());
+                                throw e1.getTargetException();
+                            }
+                        }
+                    }
+                    // Target exception is neither a TTransportException nor a restartable cause.
+                    LOG.debug("Exception: {}", e.getTargetException());
+                    throw e.getTargetException();
+                }
+            }
+        }
+    }
+}
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/package-info.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/package-info.java
new file mode 100644
index 0000000..dcd9a28
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/**
+ * BMv2 Thrift client implementation.
+ */
+package org.onosproject.bmv2.ctl;
\ No newline at end of file
diff --git a/protocols/bmv2/ctl/src/main/resources/default.json b/protocols/bmv2/ctl/src/main/resources/default.json
new file mode 100644
index 0000000..abc0452
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/resources/default.json
@@ -0,0 +1,730 @@
+{
+    "header_types": [
+        {
+            "name": "standard_metadata_t",
+            "id": 0,
+            "fields": [
+                [
+                    "ingress_port",
+                    9
+                ],
+                [
+                    "packet_length",
+                    32
+                ],
+                [
+                    "egress_spec",
+                    9
+                ],
+                [
+                    "egress_port",
+                    9
+                ],
+                [
+                    "egress_instance",
+                    32
+                ],
+                [
+                    "instance_type",
+                    32
+                ],
+                [
+                    "clone_spec",
+                    32
+                ],
+                [
+                    "_padding",
+                    5
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        },
+        {
+            "name": "intrinsic_metadata_t",
+            "id": 1,
+            "fields": [
+                [
+                    "ingress_global_timestamp",
+                    32
+                ],
+                [
+                    "lf_field_list",
+                    32
+                ],
+                [
+                    "mcast_grp",
+                    16
+                ],
+                [
+                    "egress_rid",
+                    16
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        },
+        {
+            "name": "ethernet_t",
+            "id": 2,
+            "fields": [
+                [
+                    "dstAddr",
+                    48
+                ],
+                [
+                    "srcAddr",
+                    48
+                ],
+                [
+                    "etherType",
+                    16
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        },
+        {
+            "name": "ipv4_t",
+            "id": 3,
+            "fields": [
+                [
+                    "version",
+                    4
+                ],
+                [
+                    "ihl",
+                    4
+                ],
+                [
+                    "diffserv",
+                    8
+                ],
+                [
+                    "totalLen",
+                    16
+                ],
+                [
+                    "identification",
+                    16
+                ],
+                [
+                    "flags",
+                    3
+                ],
+                [
+                    "fragOffset",
+                    13
+                ],
+                [
+                    "ttl",
+                    8
+                ],
+                [
+                    "protocol",
+                    8
+                ],
+                [
+                    "hdrChecksum",
+                    16
+                ],
+                [
+                    "srcAddr",
+                    32
+                ],
+                [
+                    "dstAddr",
+                    32
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        },
+        {
+            "name": "tcp_t",
+            "id": 4,
+            "fields": [
+                [
+                    "srcPort",
+                    16
+                ],
+                [
+                    "dstPort",
+                    16
+                ],
+                [
+                    "seqNo",
+                    32
+                ],
+                [
+                    "ackNo",
+                    32
+                ],
+                [
+                    "dataOffset",
+                    4
+                ],
+                [
+                    "res",
+                    3
+                ],
+                [
+                    "ecn",
+                    3
+                ],
+                [
+                    "ctrl",
+                    6
+                ],
+                [
+                    "window",
+                    16
+                ],
+                [
+                    "checksum",
+                    16
+                ],
+                [
+                    "urgentPtr",
+                    16
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        },
+        {
+            "name": "udp_t",
+            "id": 5,
+            "fields": [
+                [
+                    "srcPort",
+                    16
+                ],
+                [
+                    "dstPort",
+                    16
+                ],
+                [
+                    "length_",
+                    16
+                ],
+                [
+                    "checksum",
+                    16
+                ]
+            ],
+            "length_exp": null,
+            "max_length": null
+        }
+    ],
+    "headers": [
+        {
+            "name": "standard_metadata",
+            "id": 0,
+            "header_type": "standard_metadata_t",
+            "metadata": true
+        },
+        {
+            "name": "intrinsic_metadata",
+            "id": 1,
+            "header_type": "intrinsic_metadata_t",
+            "metadata": true
+        },
+        {
+            "name": "ethernet",
+            "id": 2,
+            "header_type": "ethernet_t",
+            "metadata": false
+        },
+        {
+            "name": "ipv4",
+            "id": 3,
+            "header_type": "ipv4_t",
+            "metadata": false
+        },
+        {
+            "name": "tcp",
+            "id": 4,
+            "header_type": "tcp_t",
+            "metadata": false
+        },
+        {
+            "name": "udp",
+            "id": 5,
+            "header_type": "udp_t",
+            "metadata": false
+        }
+    ],
+    "header_stacks": [],
+    "parsers": [
+        {
+            "name": "parser",
+            "id": 0,
+            "init_state": "start",
+            "parse_states": [
+                {
+                    "name": "start",
+                    "id": 0,
+                    "parser_ops": [],
+                    "transition_key": [],
+                    "transitions": [
+                        {
+                            "value": "default",
+                            "mask": null,
+                            "next_state": "parse_ethernet"
+                        }
+                    ]
+                },
+                {
+                    "name": "parse_ethernet",
+                    "id": 1,
+                    "parser_ops": [
+                        {
+                            "op": "extract",
+                            "parameters": [
+                                {
+                                    "type": "regular",
+                                    "value": "ethernet"
+                                }
+                            ]
+                        }
+                    ],
+                    "transition_key": [
+                        {
+                            "type": "field",
+                            "value": [
+                                "ethernet",
+                                "etherType"
+                            ]
+                        }
+                    ],
+                    "transitions": [
+                        {
+                            "value": "0x0800",
+                            "mask": null,
+                            "next_state": "parse_ipv4"
+                        },
+                        {
+                            "value": "default",
+                            "mask": null,
+                            "next_state": null
+                        }
+                    ]
+                },
+                {
+                    "name": "parse_ipv4",
+                    "id": 2,
+                    "parser_ops": [
+                        {
+                            "op": "extract",
+                            "parameters": [
+                                {
+                                    "type": "regular",
+                                    "value": "ipv4"
+                                }
+                            ]
+                        }
+                    ],
+                    "transition_key": [
+                        {
+                            "type": "field",
+                            "value": [
+                                "ipv4",
+                                "fragOffset"
+                            ]
+                        },
+                        {
+                            "type": "field",
+                            "value": [
+                                "ipv4",
+                                "protocol"
+                            ]
+                        }
+                    ],
+                    "transitions": [
+                        {
+                            "value": "0x000006",
+                            "mask": null,
+                            "next_state": "parse_tcp"
+                        },
+                        {
+                            "value": "0x000011",
+                            "mask": null,
+                            "next_state": "parse_udp"
+                        },
+                        {
+                            "value": "default",
+                            "mask": null,
+                            "next_state": null
+                        }
+                    ]
+                },
+                {
+                    "name": "parse_tcp",
+                    "id": 3,
+                    "parser_ops": [
+                        {
+                            "op": "extract",
+                            "parameters": [
+                                {
+                                    "type": "regular",
+                                    "value": "tcp"
+                                }
+                            ]
+                        }
+                    ],
+                    "transition_key": [],
+                    "transitions": [
+                        {
+                            "value": "default",
+                            "mask": null,
+                            "next_state": null
+                        }
+                    ]
+                },
+                {
+                    "name": "parse_udp",
+                    "id": 4,
+                    "parser_ops": [
+                        {
+                            "op": "extract",
+                            "parameters": [
+                                {
+                                    "type": "regular",
+                                    "value": "udp"
+                                }
+                            ]
+                        }
+                    ],
+                    "transition_key": [],
+                    "transitions": [
+                        {
+                            "value": "default",
+                            "mask": null,
+                            "next_state": null
+                        }
+                    ]
+                }
+            ]
+        }
+    ],
+    "deparsers": [
+        {
+            "name": "deparser",
+            "id": 0,
+            "order": [
+                "ethernet",
+                "ipv4",
+                "udp",
+                "tcp"
+            ]
+        }
+    ],
+    "meter_arrays": [],
+    "actions": [
+        {
+            "name": "_drop",
+            "id": 0,
+            "runtime_data": [],
+            "primitives": [
+                {
+                    "op": "modify_field",
+                    "parameters": [
+                        {
+                            "type": "field",
+                            "value": [
+                                "standard_metadata",
+                                "egress_spec"
+                            ]
+                        },
+                        {
+                            "type": "hexstr",
+                            "value": "0x1ff"
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "count_packet",
+            "id": 1,
+            "runtime_data": [],
+            "primitives": [
+                {
+                    "op": "count",
+                    "parameters": [
+                        {
+                            "type": "counter_array",
+                            "value": "ingress_port_counter"
+                        },
+                        {
+                            "type": "field",
+                            "value": [
+                                "standard_metadata",
+                                "ingress_port"
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "op": "count",
+                    "parameters": [
+                        {
+                            "type": "counter_array",
+                            "value": "egress_port_counter"
+                        },
+                        {
+                            "type": "field",
+                            "value": [
+                                "standard_metadata",
+                                "egress_spec"
+                            ]
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "send_to_cpu",
+            "id": 2,
+            "runtime_data": [],
+            "primitives": [
+                {
+                    "op": "modify_field",
+                    "parameters": [
+                        {
+                            "type": "field",
+                            "value": [
+                                "standard_metadata",
+                                "egress_spec"
+                            ]
+                        },
+                        {
+                            "type": "hexstr",
+                            "value": "0xff"
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "set_egress_port",
+            "id": 3,
+            "runtime_data": [
+                {
+                    "name": "port",
+                    "bitwidth": 9
+                }
+            ],
+            "primitives": [
+                {
+                    "op": "modify_field",
+                    "parameters": [
+                        {
+                            "type": "field",
+                            "value": [
+                                "standard_metadata",
+                                "egress_spec"
+                            ]
+                        },
+                        {
+                            "type": "runtime_data",
+                            "value": 0
+                        }
+                    ]
+                }
+            ]
+        }
+    ],
+    "pipelines": [
+        {
+            "name": "ingress",
+            "id": 0,
+            "init_table": "table0",
+            "tables": [
+                {
+                    "name": "port_count_table",
+                    "id": 0,
+                    "match_type": "exact",
+                    "type": "simple",
+                    "max_size": 16384,
+                    "with_counters": false,
+                    "direct_meters": null,
+                    "support_timeout": false,
+                    "key": [],
+                    "actions": [
+                        "count_packet"
+                    ],
+                    "next_tables": {
+                        "count_packet": null
+                    },
+                    "default_action": null,
+                    "base_default_next": null
+                },
+                {
+                    "name": "table0",
+                    "id": 1,
+                    "match_type": "ternary",
+                    "type": "simple",
+                    "max_size": 16384,
+                    "with_counters": true,
+                    "direct_meters": null,
+                    "support_timeout": true,
+                    "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
+                        }
+                    ],
+                    "actions": [
+                        "set_egress_port",
+                        "send_to_cpu",
+                        "_drop"
+                    ],
+                    "next_tables": {
+                        "set_egress_port": "_condition_0",
+                        "send_to_cpu": "_condition_0",
+                        "_drop": "_condition_0"
+                    },
+                    "default_action": null,
+                    "base_default_next": "_condition_0"
+                }
+            ],
+            "conditionals": [
+                {
+                    "name": "_condition_0",
+                    "id": 0,
+                    "expression": {
+                        "type": "expression",
+                        "value": {
+                            "op": "<",
+                            "left": {
+                                "type": "field",
+                                "value": [
+                                    "standard_metadata",
+                                    "egress_spec"
+                                ]
+                            },
+                            "right": {
+                                "type": "hexstr",
+                                "value": "0xfe"
+                            }
+                        }
+                    },
+                    "true_next": "port_count_table",
+                    "false_next": null
+                }
+            ]
+        },
+        {
+            "name": "egress",
+            "id": 1,
+            "init_table": null,
+            "tables": [],
+            "conditionals": []
+        }
+    ],
+    "calculations": [],
+    "checksums": [],
+    "learn_lists": [],
+    "field_lists": [],
+    "counter_arrays": [
+        {
+            "name": "ingress_port_counter",
+            "id": 0,
+            "is_direct": false,
+            "size": 254
+        },
+        {
+            "name": "egress_port_counter",
+            "id": 1,
+            "is_direct": false,
+            "size": 254
+        },
+        {
+            "name": "table0_counter",
+            "id": 2,
+            "is_direct": true,
+            "binding": "table0"
+        }
+    ],
+    "register_arrays": [],
+    "force_arith": [
+        [
+            "standard_metadata",
+            "ingress_port"
+        ],
+        [
+            "standard_metadata",
+            "packet_length"
+        ],
+        [
+            "standard_metadata",
+            "egress_spec"
+        ],
+        [
+            "standard_metadata",
+            "egress_port"
+        ],
+        [
+            "standard_metadata",
+            "egress_instance"
+        ],
+        [
+            "standard_metadata",
+            "instance_type"
+        ],
+        [
+            "standard_metadata",
+            "clone_spec"
+        ],
+        [
+            "standard_metadata",
+            "_padding"
+        ],
+        [
+            "intrinsic_metadata",
+            "ingress_global_timestamp"
+        ],
+        [
+            "intrinsic_metadata",
+            "lf_field_list"
+        ],
+        [
+            "intrinsic_metadata",
+            "mcast_grp"
+        ],
+        [
+            "intrinsic_metadata",
+            "egress_rid"
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImplTest.java b/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImplTest.java
new file mode 100644
index 0000000..41a774f
--- /dev/null
+++ b/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2FlowRuleTranslatorImplTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.context.Bmv2DeviceContext;
+import org.onosproject.bmv2.api.context.Bmv2FlowRuleTranslator;
+import org.onosproject.bmv2.api.context.Bmv2Interpreter;
+import org.onosproject.bmv2.api.runtime.Bmv2TableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+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 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.bmv2.ctl.Bmv2DefaultInterpreterImpl.TABLE0;
+
+/**
+ * Tests for {@link Bmv2FlowRuleTranslatorImpl}.
+ */
+public class Bmv2FlowRuleTranslatorImplTest {
+
+    private Random random = new Random();
+    private Bmv2Configuration configuration = Bmv2DeviceContextServiceImpl.loadDefaultConfiguration();
+    private Bmv2Interpreter interpreter = new Bmv2DefaultInterpreterImpl();
+    private Bmv2DeviceContext context = new Bmv2DeviceContext(configuration, interpreter);
+    private Bmv2FlowRuleTranslator translator = new Bmv2FlowRuleTranslatorImpl();
+
+    @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();
+
+        Bmv2TableEntry entry1 = translator.translate(rule1, context);
+        Bmv2TableEntry entry2 = translator.translate(rule1, context);
+
+        // check equality, i.e. same rules must produce same entries
+        new EqualsTester()
+                .addEqualityGroup(rule1, rule2)
+                .addEqualityGroup(entry1, entry2)
+                .testEquals();
+
+        int numMatchParams = configuration.table(TABLE0).keys().size();
+        // parse values stored in entry1
+        Bmv2TernaryMatchParam inPortParam = (Bmv2TernaryMatchParam) entry1.matchKey().matchParams().get(0);
+        Bmv2TernaryMatchParam ethDstParam = (Bmv2TernaryMatchParam) entry1.matchKey().matchParams().get(1);
+        Bmv2TernaryMatchParam ethSrcParam = (Bmv2TernaryMatchParam) entry1.matchKey().matchParams().get(2);
+        Bmv2TernaryMatchParam ethTypeParam = (Bmv2TernaryMatchParam) entry1.matchKey().matchParams().get(3);
+        double expectedTimeout = (double) (configuration.table(TABLE0).hasTimeouts() ? rule1.timeout() : -1);
+
+        // 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.matchKey().matchParams().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(), is(equalTo(Integer.MAX_VALUE - rule1.priority())));
+        assertThat("Incorrect timeout value",
+                   entry1.timeout(), is(equalTo(expectedTimeout)));
+
+    }
+}
\ No newline at end of file
diff --git a/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParserTest.java b/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParserTest.java
new file mode 100644
index 0000000..973fcd5
--- /dev/null
+++ b/protocols/bmv2/ctl/src/test/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParserTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-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.bmv2.ctl;
+
+import org.junit.Test;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.runtime.Bmv2ParsedTableEntry;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+public class Bmv2TableDumpParserTest {
+
+    private Bmv2Configuration configuration = Bmv2DeviceContextServiceImpl.loadDefaultConfiguration();
+
+    @Test
+    public void testParse() throws Exception {
+        String text = readFile();
+        List<Bmv2ParsedTableEntry> result = Bmv2TableDumpParser.parse(text, configuration);
+        assertThat(result.size(), equalTo(10));
+    }
+
+    private String readFile()
+            throws IOException, URISyntaxException {
+        byte[] encoded = Files.readAllBytes(Paths.get(this.getClass().getResource("/tabledump.txt").toURI()));
+        return new String(encoded, Charset.defaultCharset());
+    }
+}
diff --git a/protocols/bmv2/ctl/src/test/resources/tabledump.txt b/protocols/bmv2/ctl/src/test/resources/tabledump.txt
new file mode 100644
index 0000000..392ceb8
--- /dev/null
+++ b/protocols/bmv2/ctl/src/test/resources/tabledump.txt
@@ -0,0 +1,10 @@
+0: 0000 000000000000 000000000000 0806 &&& 0000000000000000000000000000ffff => send_to_cpu -
+1: 0000 000000000000 000000000000 0800 &&& 0000000000000000000000000000ffff => send_to_cpu -
+2: 0000 000000000000 000000000000 88cc &&& 0000000000000000000000000000ffff => send_to_cpu -
+3: 0000 000000000000 000000000000 8942 &&& 0000000000000000000000000000ffff => send_to_cpu -
+4: 0001 000400000001 000400000000 0000 &&& ffffffffffffffffffffffffffff0000 => set_egress_port - 2,
+5: 0002 000400000000 000400000001 0000 &&& ffffffffffffffffffffffffffff0000 => set_egress_port - 1,
+51539607552: 0001 0000 => set_egress_port - 1,
+51539607553: 0001 0002 => set_egress_port - 3,
+51539607554: 0001 0001 => set_egress_port - 2,
+51539607555: 0001 0003 => set_egress_port - 4,
\ No newline at end of file