P4-related cherry-picks for 1.12

Squashed. Includes the following commits from master:

8b19a07297 Fixed onos.py
74174bf177 Fix 'unable to translate flow rule' in p4-tutorial icmpdropper
4555c5f308 Minor refactoring of BMv2 mininet scripts
07b2b70f53 Refactored PI-ECMP app to use action profiles of basic.p4
6ffd3772b7 ONOS-7050 First stab at PI translation store
2d4271fc20 ONOS-7050 Refactored PI translation service and store
3874b44821 ONOS-7050 Refactored P4Runtime FRP to use distributed stores
41efe435be ONOS-7050 Refactored P4Runtime GP to use distributed stores
806f7b7418 ONOS-6810 Implement Mastership handling in general DeviceProvider
c7922a4b40 ONOS-7267 Fix pipeconf UI

Change-Id: I279b6477f48ebec768b494799feb12faadbd559c
diff --git a/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java
index e1f54f6..ce1c682 100644
--- a/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java
+++ b/apps/p4-tutorial/icmpdropper/src/main/java/org/onosproject/p4tutorial/icmpdropper/IcmpDropper.java
@@ -116,7 +116,7 @@
     }
 
     private void installDropRule(DeviceId deviceId) {
-        PiMatchFieldId ipv4ProtoFieldId = PiMatchFieldId.of("ipv4.protocol");
+        PiMatchFieldId ipv4ProtoFieldId = PiMatchFieldId.of("hdr.ipv4.protocol");
         PiActionId dropActionId = PiActionId.of("_drop");
 
         PiCriterion piCriterion = PiCriterion.builder()
diff --git a/apps/pi-demo/common/src/main/java/org/onosproject/pi/demo/app/common/AbstractUpgradableFabricApp.java b/apps/pi-demo/common/src/main/java/org/onosproject/pi/demo/app/common/AbstractUpgradableFabricApp.java
index 60c0e38..7c91bf3 100644
--- a/apps/pi-demo/common/src/main/java/org/onosproject/pi/demo/app/common/AbstractUpgradableFabricApp.java
+++ b/apps/pi-demo/common/src/main/java/org/onosproject/pi/demo/app/common/AbstractUpgradableFabricApp.java
@@ -16,7 +16,6 @@
 
 package org.onosproject.pi.demo.app.common;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -43,6 +42,7 @@
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleOperations;
 import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.group.GroupService;
 import org.onosproject.net.host.HostService;
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.model.PiPipeconfId;
@@ -55,6 +55,7 @@
 import org.onosproject.net.topology.TopologyVertex;
 import org.slf4j.Logger;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -81,17 +82,19 @@
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
- * Abstract implementation of an app providing fabric connectivity for a 2-stage Clos topology of P4Runtime devices.
+ * Abstract implementation of an app providing fabric connectivity for a 2-stage
+ * Clos topology of P4Runtime devices.
  */
 @Component(immediate = true)
 public abstract class AbstractUpgradableFabricApp {
 
-    private static final Map<String, AbstractUpgradableFabricApp> APP_HANDLES = Maps.newConcurrentMap();
+    private static final Map<String, AbstractUpgradableFabricApp>
+            APP_HANDLES = Maps.newConcurrentMap();
 
     // TOPO_SIZE should be the same of the --size argument when running bmv2-demo.py
     private static final int TOPO_SIZE = 2;
     private static final boolean WITH_IMBALANCED_STRIPING = false;
-    protected static final int HASHED_LINKS = TOPO_SIZE + (WITH_IMBALANCED_STRIPING ? 1 : 0);
+    private static final int HASHED_LINKS = TOPO_SIZE + (WITH_IMBALANCED_STRIPING ? 1 : 0);
 
     private static final int FLOW_PRIORITY = 100;
     private static final int CHECK_TOPOLOGY_INTERVAL_SECONDS = 5;
@@ -122,6 +125,9 @@
     private FlowRuleService flowRuleService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected GroupService groupService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private ApplicationAdminService appService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -137,7 +143,7 @@
     private AbstractUpgradableFabricApp otherApp;
 
     private boolean flowRuleGenerated = false;
-    private ApplicationId appId;
+    protected ApplicationId appId;
 
     private Collection<PiPipeconf> appPipeconfs;
 
@@ -156,7 +162,8 @@
      * @param appName      app name
      * @param appPipeconfs collection of compatible pipeconfs
      */
-    protected AbstractUpgradableFabricApp(String appName, Collection<PiPipeconf> appPipeconfs) {
+    protected AbstractUpgradableFabricApp(String appName,
+                                          Collection<PiPipeconf> appPipeconfs) {
         this.appName = checkNotNull(appName);
         this.appPipeconfs = checkNotNull(appPipeconfs);
         checkArgument(appPipeconfs.size() > 0, "appPipeconfs cannot have size 0");
@@ -171,11 +178,13 @@
 
         if (APP_HANDLES.size() > 0) {
             if (APP_HANDLES.size() > 1) {
-                throw new IllegalStateException("Found more than 1 active app handles");
+                throw new IllegalStateException(
+                        "Found more than 1 active app handles");
             }
             otherAppFound = true;
             otherApp = APP_HANDLES.values().iterator().next();
-            log.info("Found other fabric app active, signaling to freeze to {}...", otherApp.appName);
+            log.info("Found other fabric app active, signaling to freeze to {}...",
+                     otherApp.appName);
             otherApp.setAppFreezed(true);
         }
 
@@ -221,12 +230,12 @@
             pipeconfFlags = Maps.newConcurrentMap();
         }
 
-        /*
-        Schedules a thread that periodically checks the topology, as soon as it corresponds to the expected
-        one, it generates the necessary flow rules and starts the deploy process on each device.
-         */
-        scheduledExecutorService.scheduleAtFixedRate(this::checkTopologyAndGenerateFlowRules,
-                                                     0, CHECK_TOPOLOGY_INTERVAL_SECONDS, TimeUnit.SECONDS);
+        // Schedules a thread that periodically checks the topology, as soon as
+        // it corresponds to the expected one, it generates the necessary flow
+        // rules and starts the deploy process on each device.
+        scheduledExecutorService.scheduleAtFixedRate(
+                this::checkTopologyAndGenerateFlowRules,
+                0, CHECK_TOPOLOGY_INTERVAL_SECONDS, TimeUnit.SECONDS);
     }
 
     private void setAppFreezed(boolean appFreezed) {
@@ -239,7 +248,8 @@
     }
 
     /**
-     * Perform device initialization. Returns true if the operation was successful, false otherwise.
+     * Perform device initialization. Returns true if the operation was
+     * successful, false otherwise.
      *
      * @param deviceId a device id
      * @return a boolean value
@@ -247,8 +257,8 @@
     public abstract boolean initDevice(DeviceId deviceId);
 
     /**
-     * Generates a list of flow rules for the given leaf switch, source host, destination hosts, spine switches and
-     * topology.
+     * Generates a list of flow rules for the given leaf switch, source host,
+     * destination hosts, spine switches and topology.
      *
      * @param leaf     a leaf device id
      * @param srcHost  a source host
@@ -258,12 +268,15 @@
      * @return a list of flow rules
      * @throws FlowRuleGeneratorException if flow rules cannot be generated
      */
-    public abstract List<FlowRule> generateLeafRules(DeviceId leaf, Host srcHost, Collection<Host> dstHosts,
-                                                     Collection<DeviceId> spines, Topology topology)
+    public abstract List<FlowRule> generateLeafRules(DeviceId leaf, Host srcHost,
+                                                     Set<Host> dstHosts,
+                                                     Set<DeviceId> spines,
+                                                     Topology topology)
             throws FlowRuleGeneratorException;
 
     /**
-     * Generates a list of flow rules for the given spine switch, destination hosts and topology.
+     * Generates a list of flow rules for the given spine switch, destination
+     * hosts and topology.
      *
      * @param deviceId a spine device id
      * @param dstHosts a collection of destination hosts
@@ -271,7 +284,9 @@
      * @return a list of flow rules
      * @throws FlowRuleGeneratorException if flow rules cannot be generated
      */
-    public abstract List<FlowRule> generateSpineRules(DeviceId deviceId, Collection<Host> dstHosts, Topology topology)
+    public abstract List<FlowRule> generateSpineRules(DeviceId deviceId,
+                                                      Set<Host> dstHosts,
+                                                      Topology topology)
             throws FlowRuleGeneratorException;
 
     private void deployAllDevices() {
@@ -301,7 +316,7 @@
      *
      * @param device a device
      */
-    public void deployDevice(Device device) {
+    private void deployDevice(Device device) {
 
         DeviceId deviceId = device.id();
 
@@ -317,7 +332,7 @@
                     pipeconfFlags.put(device.id(), true);
                 } else {
                     log.warn("Wrong pipeconf for {}, expecting {}, but found {}, aborting deploy",
-                             deviceId, MoreObjects.toStringHelper(appPipeconfs),
+                             deviceId, Arrays.toString(appPipeconfs.toArray()),
                              piPipeconfService.ofDevice(deviceId).get());
                     return;
                 }
@@ -331,7 +346,8 @@
             // Install rules.
             if (!ruleFlags.getOrDefault(deviceId, false) &&
                     deviceFlowRules.containsKey(deviceId)) {
-                log.info("Installing {} rules for {}...", deviceFlowRules.get(deviceId).size(), deviceId);
+                log.info("Installing {} rules for {}...",
+                         deviceFlowRules.get(deviceId).size(), deviceId);
                 installFlowRules(deviceFlowRules.get(deviceId));
                 ruleFlags.put(deviceId, true);
             }
@@ -352,7 +368,8 @@
     }
 
     /**
-     * Generates flow rules to provide host-to-host connectivity for the given topology and hosts.
+     * Generates flow rules to provide host-to-host connectivity for the given
+     * topology and hosts.
      */
     private synchronized void checkTopologyAndGenerateFlowRules() {
 
@@ -374,7 +391,7 @@
                 .forEach(did -> (isSpine(did, topo) ? spines : leafs).add(did));
 
         if (spines.size() != TOPO_SIZE || leafs.size() != TOPO_SIZE) {
-            log.info("Invalid leaf/spine switches count, aborting... > leafCount={}, spineCount={}",
+            log.info("Invalid leaf/spine count, aborting... > leafCount={}, spineCount={}",
                      spines.size(), leafs.size());
             return;
         }
@@ -383,7 +400,8 @@
             int portCount = deviceService.getPorts(did).size();
             // Expected port count: num leafs + 1 redundant leaf link (if imbalanced)
             if (portCount != HASHED_LINKS) {
-                log.info("Invalid port count for spine, aborting... > deviceId={}, portCount={}", did, portCount);
+                log.info("Invalid port count for spine, aborting... > deviceId={}, portCount={}",
+                         did, portCount);
                 return;
             }
         }
@@ -391,7 +409,8 @@
             int portCount = deviceService.getPorts(did).size();
             // Expected port count: num spines + host port + 1 redundant spine link
             if (portCount != HASHED_LINKS + 1) {
-                log.info("Invalid port count for leaf, aborting... > deviceId={}, portCount={}", did, portCount);
+                log.info("Invalid port count for leaf, aborting... > deviceId={}, portCount={}",
+                         did, portCount);
                 return;
             }
         }
@@ -400,7 +419,8 @@
         Map<DeviceId, Host> hostMap = Maps.newHashMap();
         hosts.forEach(h -> hostMap.put(h.location().deviceId(), h));
         if (hosts.size() != TOPO_SIZE || !leafs.equals(hostMap.keySet())) {
-            log.info("Wrong host configuration, aborting... > hostCount={}, hostMapz={}", hosts.size(), hostMap);
+            log.info("Wrong host configuration, aborting... > hostCount={}, hostMapz={}",
+                     hosts.size(), hostMap);
             return;
         }
 
@@ -409,14 +429,18 @@
         try {
             for (DeviceId deviceId : leafs) {
                 Host srcHost = hostMap.get(deviceId);
-                Set<Host> dstHosts = hosts.stream().filter(h -> h != srcHost).collect(toSet());
-                newFlowRules.addAll(generateLeafRules(deviceId, srcHost, dstHosts, spines, topo));
+                Set<Host> dstHosts = hosts.stream()
+                        .filter(h -> h != srcHost)
+                        .collect(toSet());
+                newFlowRules.addAll(generateLeafRules(deviceId, srcHost,
+                                                      dstHosts, spines, topo));
             }
             for (DeviceId deviceId : spines) {
                 newFlowRules.addAll(generateSpineRules(deviceId, hosts, topo));
             }
         } catch (FlowRuleGeneratorException e) {
-            log.warn("Exception while executing flow rule generator: {}", e.getMessage());
+            log.warn("Exception while executing flow rule generator: {}",
+                     e.getMessage());
             return;
         }
 
@@ -428,12 +452,13 @@
 
         // All good!
         // Divide flow rules per device id...
-        ImmutableMap.Builder<DeviceId, List<FlowRule>> mapBuilder = ImmutableMap.builder();
+        ImmutableMap.Builder<DeviceId, List<FlowRule>> mapBuilder =
+                ImmutableMap.builder();
         concat(spines.stream(), leafs.stream())
-                .map(deviceId -> ImmutableList.copyOf(newFlowRules
-                                                              .stream()
-                                                              .filter(fr -> fr.deviceId().equals(deviceId))
-                                                              .iterator()))
+                .map(deviceId -> ImmutableList.copyOf(
+                        newFlowRules.stream()
+                                .filter(fr -> fr.deviceId().equals(deviceId))
+                                .iterator()))
                 .forEach(frs -> mapBuilder.put(frs.get(0).deviceId(), frs));
         this.deviceFlowRules = mapBuilder.build();
 
@@ -443,7 +468,8 @@
         // Avoid other executions to modify the generated flow rules.
         flowRuleGenerated = true;
 
-        log.info("Generated {} flow rules for {} devices", newFlowRules.size(), spines.size() + leafs.size());
+        log.info("Generated {} flow rules for {} devices",
+                 newFlowRules.size(), spines.size() + leafs.size());
 
         spawnTask(this::deployAllDevices);
     }
@@ -455,11 +481,13 @@
      * @param tableId a table id
      * @return a new flow rule builder
      */
-    protected FlowRule.Builder flowRuleBuilder(DeviceId did, PiTableId tableId) throws FlowRuleGeneratorException {
+    protected FlowRule.Builder flowRuleBuilder(DeviceId did, PiTableId tableId)
+            throws FlowRuleGeneratorException {
 
         final Device device = deviceService.getDevice(did);
         if (!device.is(PiPipelineInterpreter.class)) {
-            throw new FlowRuleGeneratorException(format("Device %s has no PiPipelineInterpreter", did));
+            throw new FlowRuleGeneratorException(format(
+                    "Device %s has no PiPipelineInterpreter", did));
         }
 
         return DefaultFlowRule.builder()
@@ -486,12 +514,13 @@
 
     protected boolean isFabricPort(Port port, Topology topology) {
         // True if the port connects this device to another infrastructure device.
-        return topologyService.isInfrastructure(topology, new ConnectPoint(port.element().id(), port.number()));
+        return topologyService.isInfrastructure(
+                topology, new ConnectPoint(port.element().id(), port.number()));
     }
 
     /**
-     * A listener of device events that executes a device deploy task each time a device is added, updated or
-     * re-connects.
+     * A listener of device events that executes a device deploy task each time
+     * a device is added, updated or re-connects.
      */
     private class InternalDeviceListener implements DeviceListener {
         @Override
@@ -512,12 +541,12 @@
     /**
      * An exception occurred while generating flow rules for this fabric.
      */
-    public class FlowRuleGeneratorException extends Exception {
+    public static class FlowRuleGeneratorException extends Exception {
 
         public FlowRuleGeneratorException() {
         }
 
-        public FlowRuleGeneratorException(String msg) {
+        FlowRuleGeneratorException(String msg) {
             super(msg);
         }
     }
diff --git a/apps/pi-demo/ecmp/src/main/java/org/onosproject/pi/demo/app/ecmp/EcmpFabricApp.java b/apps/pi-demo/ecmp/src/main/java/org/onosproject/pi/demo/app/ecmp/EcmpFabricApp.java
index 5a40df4..bb15afc 100644
--- a/apps/pi-demo/ecmp/src/main/java/org/onosproject/pi/demo/app/ecmp/EcmpFabricApp.java
+++ b/apps/pi-demo/ecmp/src/main/java/org/onosproject/pi/demo/app/ecmp/EcmpFabricApp.java
@@ -18,9 +18,12 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.felix.scr.annotations.Component;
-import org.onlab.util.ImmutableByteSequence;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.onlab.packet.IpAddress;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.Host;
 import org.onosproject.net.Path;
@@ -29,12 +32,17 @@
 import org.onosproject.net.flow.DefaultTrafficSelector;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.flow.TrafficSelector;
-import org.onosproject.net.flow.TrafficTreatment;
-import org.onosproject.net.flow.criteria.Criterion;
 import org.onosproject.net.flow.criteria.PiCriterion;
+import org.onosproject.net.group.DefaultGroupBucket;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupKey;
 import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionGroupId;
 import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.net.pi.runtime.PiGroupKey;
 import org.onosproject.net.pi.runtime.PiTableAction;
 import org.onosproject.net.topology.DefaultTopologyVertex;
 import org.onosproject.net.topology.Topology;
@@ -42,37 +50,44 @@
 import org.onosproject.pi.demo.app.common.AbstractUpgradableFabricApp;
 import org.onosproject.pipelines.basic.PipeconfLoader;
 
-import java.util.Collection;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import static java.lang.String.format;
 import static java.util.Collections.singleton;
 import static java.util.stream.Collectors.toSet;
-import static org.onlab.packet.EthType.EtherType.IPV4;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_NEXT_HOP_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_NEXT_HOP_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.HDR_NEXT_HOP_ID;
-import static org.onosproject.pipelines.basic.BasicConstants.HDR_SELECTOR_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.TBL_TABLE0_ID;
-import static org.onosproject.pipelines.basic.EcmpConstants.TBL_ECMP_TABLE_ID;
-
+import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
 
 /**
- * Implementation of an upgradable fabric app for the ECMP pipeconf.
+ * Implementation of an upgradable fabric app for the Basic pipeconf (basic.p4)
+ * with ECMP support.
  */
 @Component(immediate = true)
 public class EcmpFabricApp extends AbstractUpgradableFabricApp {
 
     private static final String APP_NAME = "org.onosproject.pi-ecmp";
 
-    private static final Map<DeviceId, Map<Set<PortNumber>, Short>> DEVICE_GROUP_ID_MAP = Maps.newHashMap();
+    private static final Map<DeviceId, Map<Set<PortNumber>, Short>>
+            DEVICE_GROUP_ID_MAP = Maps.newHashMap();
+
+    private final Set<Pair<DeviceId, GroupKey>> groupKeys = Sets.newHashSet();
 
     public EcmpFabricApp() {
-        super(APP_NAME, singleton(PipeconfLoader.ECMP_PIPECONF));
+        super(APP_NAME, singleton(PipeconfLoader.BASIC_PIPECONF));
+    }
+
+    @Deactivate
+    public void deactivate() {
+        groupKeys.forEach(pair -> groupService.removeGroup(
+                pair.getLeft(), pair.getRight(), appId));
+        super.deactivate();
     }
 
     @Override
@@ -82,8 +97,10 @@
     }
 
     @Override
-    public List<FlowRule> generateLeafRules(DeviceId leaf, Host srcHost, Collection<Host> dstHosts,
-                                            Collection<DeviceId> availableSpines, Topology topo)
+    public List<FlowRule> generateLeafRules(DeviceId leaf, Host localHost,
+                                            Set<Host> remoteHosts,
+                                            Set<DeviceId> availableSpines,
+                                            Topology topo)
             throws FlowRuleGeneratorException {
 
         // Get ports which connect this leaf switch to hosts.
@@ -91,18 +108,19 @@
                 .stream()
                 .filter(port -> !isFabricPort(port, topo))
                 .map(Port::number)
-                .collect(Collectors.toSet());
+                .collect(toSet());
 
         // Get ports which connect this leaf to the given available spines.
         TopologyGraph graph = topologyService.getGraph(topo);
-        Set<PortNumber> fabricPorts = graph.getEdgesFrom(new DefaultTopologyVertex(leaf))
+        Set<PortNumber> fabricPorts = graph
+                .getEdgesFrom(new DefaultTopologyVertex(leaf))
                 .stream()
                 .filter(e -> availableSpines.contains(e.dst().deviceId()))
                 .map(e -> e.link().src().port())
-                .collect(Collectors.toSet());
+                .collect(toSet());
 
         if (hostPorts.size() != 1 || fabricPorts.size() == 0) {
-            log.error("Leaf switch has invalid port configuration: hostPorts={}, fabricPorts={}",
+            log.error("Leaf has invalid port configuration: hostPorts={}, fabricPorts={}",
                       hostPorts.size(), fabricPorts.size());
             throw new FlowRuleGeneratorException();
         }
@@ -110,41 +128,40 @@
 
         List<FlowRule> rules = Lists.newArrayList();
 
-        TrafficTreatment treatment;
-        if (fabricPorts.size() > 1) {
-            // Do ECMP.
-            Pair<PiTableAction, List<FlowRule>> result = provisionEcmpPiTableAction(leaf, fabricPorts);
-            rules.addAll(result.getRight());
-            treatment = DefaultTrafficTreatment.builder().piTableAction(result.getLeft()).build();
-        } else {
-            // Output on port.
-            PortNumber outPort = fabricPorts.iterator().next();
-            treatment = DefaultTrafficTreatment.builder().setOutput(outPort).build();
-        }
+        // From local host to remote ones.
+        for (Host remoteHost : remoteHosts) {
+            int groupId = provisionGroup(leaf, fabricPorts);
 
-        // From srHost to dstHosts.
-        for (Host dstHost : dstHosts) {
-            FlowRule rule = flowRuleBuilder(leaf, TBL_TABLE0_ID)
-                    .withSelector(
-                            DefaultTrafficSelector.builder()
-                                    .matchInPort(hostPort)
-                                    .matchEthType(IPV4.ethType().toShort())
-                                    .matchEthSrc(srcHost.mac())
-                                    .matchEthDst(dstHost.mac())
-                                    .build())
-                    .withTreatment(treatment)
+            rules.add(groupFlowRule(leaf, groupId));
+
+            PiTableAction piTableAction = PiAction.builder()
+                    .withId(ACT_SET_NEXT_HOP_ID)
+                    .withParameter(new PiActionParam(
+                            ACT_PRM_NEXT_HOP_ID,
+                            copyFrom(groupId)))
                     .build();
-            rules.add(rule);
+
+            for (IpAddress ipAddr : remoteHost.ipAddresses()) {
+                FlowRule rule = flowRuleBuilder(leaf, TBL_TABLE0_ID)
+                        .withSelector(
+                                DefaultTrafficSelector.builder()
+                                        .matchIPDst(ipAddr.toIpPrefix())
+                                        .build())
+                        .withTreatment(
+                                DefaultTrafficTreatment.builder()
+                                        .piTableAction(piTableAction)
+                                        .build())
+                        .build();
+                rules.add(rule);
+            }
         }
 
-        // From fabric ports to this leaf host.
-        for (PortNumber port : fabricPorts) {
+        // From remote hosts to the local one
+        for (IpAddress dstIpAddr : localHost.ipAddresses()) {
             FlowRule rule = flowRuleBuilder(leaf, TBL_TABLE0_ID)
                     .withSelector(
                             DefaultTrafficSelector.builder()
-                                    .matchInPort(port)
-                                    .matchEthType(IPV4.ethType().toShort())
-                                    .matchEthDst(srcHost.mac())
+                                    .matchIPDst(dstIpAddr.toIpPrefix())
                                     .build())
                     .withTreatment(
                             DefaultTrafficTreatment.builder()
@@ -158,97 +175,105 @@
     }
 
     @Override
-    public List<FlowRule> generateSpineRules(DeviceId deviceId, Collection<Host> dstHosts, Topology topo)
+    public List<FlowRule> generateSpineRules(DeviceId spine, Set<Host> hosts,
+                                             Topology topo)
             throws FlowRuleGeneratorException {
 
         List<FlowRule> rules = Lists.newArrayList();
 
-        // for each host
-        for (Host dstHost : dstHosts) {
+        // For each host pair (src -> dst)
+        for (Host dstHost : hosts) {
 
-            Set<Path> paths = topologyService.getPaths(topo, deviceId, dstHost.location().deviceId());
+            Set<Path> paths = topologyService.getPaths(
+                    topo, spine, dstHost.location().deviceId());
 
             if (paths.size() == 0) {
-                log.warn("Can't find any path between spine {} and host {}", deviceId, dstHost);
+                log.warn("No path between spine {} and host {}",
+                         spine, dstHost);
                 throw new FlowRuleGeneratorException();
             }
 
-            TrafficTreatment treatment;
+            Set<PortNumber> ports = paths.stream()
+                    .map(p -> p.src().port())
+                    .collect(toSet());
 
-            if (paths.size() == 1) {
-                // Only one path, do output on port.
-                PortNumber port = paths.iterator().next().src().port();
-                treatment = DefaultTrafficTreatment.builder().setOutput(port).build();
-            } else {
-                // Multiple paths, do ECMP.
-                Set<PortNumber> portNumbers = paths.stream().map(p -> p.src().port()).collect(toSet());
-                Pair<PiTableAction, List<FlowRule>> result = provisionEcmpPiTableAction(deviceId, portNumbers);
-                rules.addAll(result.getRight());
-                treatment = DefaultTrafficTreatment.builder().piTableAction(result.getLeft()).build();
-            }
+            int groupId = provisionGroup(spine, ports);
 
-            FlowRule rule = flowRuleBuilder(deviceId, TBL_TABLE0_ID)
-                    .withSelector(
-                            DefaultTrafficSelector.builder()
-                                    .matchEthType(IPV4.ethType().toShort())
-                                    .matchEthDst(dstHost.mac())
-                                    .build())
-                    .withTreatment(treatment)
+            rules.add(groupFlowRule(spine, groupId));
+
+            PiTableAction piTableAction = PiAction.builder()
+                    .withId(ACT_SET_NEXT_HOP_ID)
+                    .withParameter(new PiActionParam(ACT_PRM_NEXT_HOP_ID,
+                                                     copyFrom(groupId)))
                     .build();
 
-            rules.add(rule);
+            for (IpAddress dstIpAddr : dstHost.ipAddresses()) {
+                FlowRule rule = flowRuleBuilder(spine, TBL_TABLE0_ID)
+                        .withSelector(DefaultTrafficSelector.builder()
+                                              .matchIPDst(dstIpAddr.toIpPrefix())
+                                              .build())
+                        .withTreatment(DefaultTrafficTreatment.builder()
+                                               .piTableAction(piTableAction)
+                                               .build())
+                        .build();
+                rules.add(rule);
+            }
         }
 
         return rules;
     }
 
-    private Pair<PiTableAction, List<FlowRule>> provisionEcmpPiTableAction(DeviceId deviceId,
-                                                                           Set<PortNumber> fabricPorts)
+    /**
+     * Provisions an ECMP group for the given device and set of ports, returns
+     * the group ID.
+     */
+    private int provisionGroup(DeviceId deviceId, Set<PortNumber> ports)
             throws FlowRuleGeneratorException {
 
-        // Install ECMP group table entries that map from hash values to actual fabric ports...
-        int groupId = groupIdOf(deviceId, fabricPorts);
-        if (fabricPorts.size() != HASHED_LINKS) {
-            throw new FlowRuleGeneratorException(format(
-                    "Invalid number of fabric ports for %s, expected %d but found %d",
-                    deviceId, HASHED_LINKS, fabricPorts.size()));
-        }
-        Iterator<PortNumber> portIterator = fabricPorts.iterator();
-        List<FlowRule> rules = Lists.newArrayList();
-        for (short i = 0; i < HASHED_LINKS; i++) {
-            FlowRule rule = flowRuleBuilder(deviceId, TBL_ECMP_TABLE_ID)
-                    .withSelector(
-                            buildEcmpTrafficSelector(groupId, i))
-                    .withTreatment(
-                            DefaultTrafficTreatment.builder()
-                                    .setOutput(portIterator.next())
-                                    .build())
-                    .build();
-            rules.add(rule);
-        }
+        int groupId = groupIdOf(deviceId, ports);
 
-        PiTableAction piTableAction = buildEcmpPiTableAction(groupId);
+        // Group buckets
+        List<GroupBucket> bucketList = ports.stream()
+                .map(port -> DefaultTrafficTreatment.builder()
+                        .setOutput(port)
+                        .build())
+                .map(DefaultGroupBucket::createSelectGroupBucket)
+                .collect(Collectors.toList());
 
-        return Pair.of(piTableAction, rules);
+        // Group cookie (with action profile ID)
+        PiGroupKey groupKey = new PiGroupKey(TBL_WCMP_TABLE_ID,
+                                             ACT_PRF_WCMP_SELECTOR_ID,
+                                             groupId);
+
+        log.info("Adding group {} to {}...", groupId, deviceId);
+        groupService.addGroup(
+                new DefaultGroupDescription(deviceId,
+                                            GroupDescription.Type.SELECT,
+                                            new GroupBuckets(bucketList),
+                                            groupKey,
+                                            groupId,
+                                            appId));
+
+        groupKeys.add(ImmutablePair.of(deviceId, groupKey));
+
+        return groupId;
     }
 
-    private PiTableAction buildEcmpPiTableAction(int groupId) {
-
-        return PiAction.builder()
-                .withId(ACT_SET_NEXT_HOP_ID)
-                .withParameter(new PiActionParam(ACT_PRM_NEXT_HOP_ID,
-                                                 ImmutableByteSequence.copyFrom(groupId)))
-                .build();
-    }
-
-    private TrafficSelector buildEcmpTrafficSelector(int groupId, int selector) {
-        Criterion ecmpCriterion = PiCriterion.builder()
-                .matchExact(HDR_NEXT_HOP_ID, groupId)
-                .matchExact(HDR_SELECTOR_ID, selector)
-                .build();
-
-        return DefaultTrafficSelector.builder()
-                .matchPi((PiCriterion) ecmpCriterion)
+    private FlowRule groupFlowRule(DeviceId deviceId, int groupId)
+            throws FlowRuleGeneratorException {
+        return flowRuleBuilder(deviceId, TBL_WCMP_TABLE_ID)
+                .withSelector(
+                        DefaultTrafficSelector.builder()
+                                .matchPi(
+                                        PiCriterion.builder()
+                                                .matchExact(HDR_NEXT_HOP_ID,
+                                                            groupId)
+                                                .build())
+                                .build())
+                .withTreatment(
+                        DefaultTrafficTreatment.builder()
+                                .piTableAction(PiActionGroupId.of(groupId))
+                                .build())
                 .build();
     }
 
@@ -259,4 +284,4 @@
         return DEVICE_GROUP_ID_MAP.get(deviceId).computeIfAbsent(ports, (pp) ->
                 (short) (DEVICE_GROUP_ID_MAP.get(deviceId).size() + 1));
     }
-}
\ No newline at end of file
+}
diff --git a/core/api/src/main/java/org/onosproject/net/flow/FlowRule.java b/core/api/src/main/java/org/onosproject/net/flow/FlowRule.java
index 27a604c..b62007f 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/FlowRule.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/FlowRule.java
@@ -18,12 +18,13 @@
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.GroupId;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.service.PiTranslatable;
 
 /**
  * Represents a generalized match &amp; action pair to be applied to an
  * infrastructure device.
  */
-public interface FlowRule {
+public interface FlowRule extends PiTranslatable {
 
     IndexTableId DEFAULT_TABLE = IndexTableId.of(0);
     int MAX_TIMEOUT = 60;
diff --git a/core/api/src/main/java/org/onosproject/net/group/Group.java b/core/api/src/main/java/org/onosproject/net/group/Group.java
index 3e2494c..39c6a3d 100644
--- a/core/api/src/main/java/org/onosproject/net/group/Group.java
+++ b/core/api/src/main/java/org/onosproject/net/group/Group.java
@@ -16,11 +16,12 @@
 package org.onosproject.net.group;
 
 import org.onosproject.core.GroupId;
+import org.onosproject.net.pi.service.PiTranslatable;
 
 /**
  * ONOS representation of group that is stored in the system.
  */
-public interface Group extends GroupDescription {
+public interface Group extends GroupDescription, PiTranslatable {
     /**
      * State of the group object in ONOS.
      */
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroup.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroup.java
index 1d9a94b..9084bea 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroup.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroup.java
@@ -34,7 +34,7 @@
  * Instance of an action group of a protocol-independent pipeline.
  */
 @Beta
-public final class PiActionGroup {
+public final class PiActionGroup implements PiEntity {
 
     private final PiActionGroupId id;
     private final PiActionGroupType type;
@@ -125,6 +125,11 @@
         return new Builder();
     }
 
+    @Override
+    public PiEntityType piEntityType() {
+        return PiEntityType.GROUP;
+    }
+
     /**
      * Builder of action groups.
      */
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroupHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroupHandle.java
new file mode 100644
index 0000000..25a035b
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiActionGroupHandle.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Global identifier of a PI action group applied to a device, uniquely defined
+ * by a device ID, action profile ID and group ID.
+ */
+@Beta
+public final class PiActionGroupHandle extends PiHandle<PiActionGroup> {
+
+    private PiActionGroupHandle(DeviceId deviceId, PiActionGroup group) {
+        super(deviceId, group);
+    }
+
+    /**
+     * Creates a new handle for the given device ID and PI action group.
+     *
+     * @param deviceId device ID
+     * @param group PI action group
+     * @return PI action group handle
+     */
+    public static PiActionGroupHandle of(DeviceId deviceId,
+                                         PiActionGroup group) {
+        return new PiActionGroupHandle(deviceId, group);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(deviceId(),
+                                piEntity().actionProfileId(),
+                                piEntity().id());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PiActionGroupHandle that = (PiActionGroupHandle) o;
+        return Objects.equal(deviceId(), that.deviceId()) &&
+                Objects.equal(piEntity().actionProfileId(),
+                              that.piEntity().actionProfileId()) &&
+                Objects.equal(piEntity().id(), piEntity().id());
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceId", deviceId())
+                .add("actionProfileId", piEntity().actionProfileId())
+                .add("groupId", piEntity().id())
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java
new file mode 100644
index 0000000..c3d5a01
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Abstraction of an entity of a protocol-independent that can be read or write
+ * at runtime.
+ */
+@Beta
+public interface PiEntity {
+
+    /**
+     * Returns the type of this entity.
+     *
+     * @return entity type
+     */
+    PiEntityType piEntityType();
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
new file mode 100644
index 0000000..e01e520
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiEntityType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Type of runtime entity of a protocol-independent pipeline.
+ */
+@Beta
+public enum PiEntityType {
+    /**
+     * Table entry.
+     */
+    TABLE_ENTRY,
+
+    /**
+     * Action profile group.
+     */
+    GROUP
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java
new file mode 100644
index 0000000..e8e70d1
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiHandle.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Global identifier of a PI entity applied to a device, unique in the scope of
+ * the whole network.
+ */
+@Beta
+public abstract class PiHandle<E extends PiEntity> {
+
+    private final DeviceId deviceId;
+    private final E piEntity;
+
+    protected PiHandle(DeviceId deviceId, E piEntity) {
+        this.deviceId = checkNotNull(deviceId);
+        this.piEntity = checkNotNull(piEntity);
+    }
+
+    /**
+     * Returns the device ID of this handle.
+     *
+     * @return device ID
+     */
+    public final DeviceId deviceId() {
+        return deviceId;
+    }
+
+    /**
+     * Returns the type of entity identified by this handle.
+     *
+     * @return PI entity type
+     */
+    public final PiEntityType entityType() {
+        return piEntity.piEntityType();
+    }
+
+    /**
+     * The entity to which this handle is associated.
+     *
+     * @return PI entity
+     */
+    public final E piEntity() {
+        return piEntity;
+    }
+
+    @Override
+    public abstract int hashCode();
+
+    @Override
+    public abstract boolean equals(Object obj);
+
+    @Override
+    public abstract String toString();
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index cd7e493..5770c6b 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -30,7 +30,7 @@
  * Instance of a table entry in a protocol-independent pipeline.
  */
 @Beta
-public final class PiTableEntry {
+public final class PiTableEntry implements PiEntity {
 
     public static final PiTableEntry EMTPY = new PiTableEntry();
 
@@ -160,6 +160,11 @@
         return new Builder();
     }
 
+    @Override
+    public PiEntityType piEntityType() {
+        return PiEntityType.TABLE_ENTRY;
+    }
+
     public static final class Builder {
 
         private PiTableId tableId;
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java
new file mode 100644
index 0000000..7eeb7f6
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntryHandle.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Global identifier of a PI table entry applied on a device, uniquely defined
+ * by a device ID, table ID and match key.
+ */
+@Beta
+public final class PiTableEntryHandle extends PiHandle<PiTableEntry> {
+
+    private PiTableEntryHandle(DeviceId deviceId, PiTableEntry entry) {
+        super(deviceId, entry);
+    }
+
+    /**
+     * Creates a new handle for the given PI table entry and device ID.
+     *
+     * @param deviceId device ID
+     * @param entry    PI table entry
+     * @return PI table entry handle
+     */
+    public static PiTableEntryHandle of(DeviceId deviceId, PiTableEntry entry) {
+        return new PiTableEntryHandle(deviceId, entry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(deviceId(),
+                                piEntity().table(),
+                                piEntity().matchKey());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final PiTableEntryHandle other = (PiTableEntryHandle) obj;
+        return Objects.equal(this.deviceId(), other.deviceId())
+                && Objects.equal(this.piEntity().table(),
+                                 other.piEntity().table())
+                && Objects.equal(this.piEntity().matchKey(),
+                                 other.piEntity().matchKey());
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceId", deviceId())
+                .add("tableId", piEntity().table())
+                .add("matchKey", piEntity().matchKey())
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslationStore.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslationStore.java
new file mode 100644
index 0000000..a80de10
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslationStore.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+
+/**
+ * A PI translation store that keeps track of which flow rules have been
+ * translated to which PI table entries.
+ */
+@Beta
+public interface PiFlowRuleTranslationStore
+        extends PiTranslationStore<FlowRule, PiTableEntry> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslator.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslator.java
new file mode 100644
index 0000000..cc82f73
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiFlowRuleTranslator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+
+/**
+ * A translator of flow rules to PI table entries.
+ */
+@Beta
+public interface PiFlowRuleTranslator
+        extends PiTranslator<FlowRule, PiTableEntry> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslationStore.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslationStore.java
new file mode 100644
index 0000000..4fe526a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslationStore.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+
+/**
+ * A PI translation store that keeps track of which groups have been
+ * translated to which PI action groups.
+ */
+@Beta
+public interface PiGroupTranslationStore
+        extends PiTranslationStore<Group, PiActionGroup> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslator.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslator.java
new file mode 100644
index 0000000..d5eb5af
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiGroupTranslator.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+
+/**
+ * A translator of groups to PI action groups.
+ */
+@Beta
+public interface PiGroupTranslator
+        extends PiTranslator<Group, PiActionGroup> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatable.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatable.java
new file mode 100644
index 0000000..316c3ed
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatable.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Abstraction of protocol-dependent (PD) entity that can be translated to an
+ * equivalent protocol-independent (PI) one.
+ */
+@Beta
+public interface PiTranslatable {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java
new file mode 100644
index 0000000..4ca094b
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslatedEntity.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
+import org.onosproject.net.pi.runtime.PiHandle;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Representation of the result of a PD-to-PI translation associated to a PI
+ * entity handle.
+ */
+@Beta
+public final class PiTranslatedEntity<T extends PiTranslatable, E extends PiEntity> {
+
+    private final T original;
+    private final E translated;
+    private final PiHandle<E> handle;
+
+    /**
+     * Creates a new translated entity.
+     *
+     * @param original PD entity
+     * @param translated PI entity
+     * @param handle PI entity handle
+     */
+    public PiTranslatedEntity(T original, E translated, PiHandle<E> handle) {
+        this.original = checkNotNull(original);
+        this.translated = checkNotNull(translated);
+        this.handle = checkNotNull(handle);
+    }
+
+    /**
+     * Returns the type of the translated entity.
+     *
+     * @return type of the translated entity
+     */
+    public final PiEntityType entityType() {
+        return translated.piEntityType();
+    }
+
+    /**
+     * Returns the original PD entity.
+     *
+     * @return instance of PI translatable entity
+     */
+    public final T original() {
+        return original;
+    }
+
+    /**
+     * Returns the translated PI entity.
+     *
+     * @return PI entity
+     */
+    public final E translated() {
+        return translated;
+    }
+
+    /**
+     * Returns the PI entity handle.
+     *
+     * @return PI entity handle
+     */
+    public final PiHandle<E> handle() {
+        return handle;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationEvent.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationEvent.java
new file mode 100644
index 0000000..9e2411d
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.event.AbstractEvent;
+import org.onosproject.net.pi.runtime.PiEntity;
+
+/**
+ * Signals an event related to the translation of a protocol-dependent (PD)
+ * entity to a protocol-independent (PI) one.
+ */
+@Beta
+public final class PiTranslationEvent<T extends PiTranslatable, E extends PiEntity>
+        extends AbstractEvent<PiTranslationEvent.Type, PiTranslatedEntity<T, E>> {
+
+    /**
+     * Type of event.
+     */
+    public enum Type {
+        /**
+         * Signals that A PD entity has been translated to a PI one, and the
+         * mapping between the two entities has been learned by the system.
+         */
+        LEARNED,
+
+        /**
+         * Signals that a previously learned mapping between a PD entity and its
+         * PI counterpart has been removed.
+         */
+        FORGOT,
+    }
+
+    /**
+     * Creates a new translation event.
+     *
+     * @param type    type of event
+     * @param subject subject of event
+     */
+    public PiTranslationEvent(Type type, PiTranslatedEntity<T, E> subject) {
+        super(type, subject);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationException.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationException.java
new file mode 100644
index 0000000..d8a6c63
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Signals that an error was encountered while translating an entity.
+ */
+@Beta
+public final class PiTranslationException extends Exception {
+
+    /**
+     * Creates a new exception with the given message.
+     *
+     * @param message a message
+     */
+    public PiTranslationException(String message) {
+        super(message);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
index 210b7e0..c7a95c3 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationService.java
@@ -17,54 +17,25 @@
 package org.onosproject.net.pi.service;
 
 import com.google.common.annotations.Beta;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.pi.model.PiPipeconf;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiTableEntry;
 
 /**
- * A service to translate protocol-dependent entities to protocol-independent ones.
+ * A service to translate protocol-dependent (PD) entities to
+ * protocol-independent (PI) ones.
  */
 @Beta
 public interface PiTranslationService {
 
     /**
-     * Returns a PI table entry equivalent to the given flow rule for the given protocol-independent pipeline
-     * configuration.
+     * Returns a flow rule translator.
      *
-     * @param rule     a flow rule
-     * @param pipeconf a pipeline configuration
-     * @return a table entry
-     * @throws PiTranslationException if the flow rule cannot be translated
+     * @return flow rule translator
      */
-    PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf)
-            throws PiTranslationException;
+    PiFlowRuleTranslator flowRuleTranslator();
 
     /**
-     * Returns a PI action group equivalent to the given group for the given protocol-independent pipeline
-     * configuration.
+     * Returns a group translator.
      *
-     * @param group    a group
-     * @param pipeconf a pipeline configuration
-     * @return a PI action group
-     * @throws PiTranslationException if the group cannot be translated
+     * @return group translator
      */
-    PiActionGroup translateGroup(Group group, PiPipeconf pipeconf)
-            throws PiTranslationException;
-
-    /**
-     * Signals that an error was encountered while translating an entity.
-     */
-    class PiTranslationException extends Exception {
-
-        /**
-         * Creates a new exception with the given message.
-         *
-         * @param message a message
-         */
-        public PiTranslationException(String message) {
-            super(message);
-        }
-    }
+    PiGroupTranslator groupTranslator();
 }
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java
new file mode 100644
index 0000000..6274deb
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStore.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.store.Store;
+
+/**
+ * PI translation store abstraction that maintains a mapping between a PI entity
+ * handle and a translated entity.
+ *
+ * @param <T> PD entity class (translatable to PI)
+ * @param <E> PI entity class
+ */
+@Beta
+public interface PiTranslationStore<T extends PiTranslatable, E extends PiEntity>
+        extends Store<PiTranslationEvent<T, E>, PiTranslationStoreDelegate<T, E>> {
+
+    /**
+     * Adds or update a mapping between the given PI entity handle and
+     * translated entity.
+     *
+     * @param handle PI entity handle
+     * @param entity PI translated entity
+     */
+    void addOrUpdate(PiHandle<E> handle, PiTranslatedEntity<T, E> entity);
+
+    /**
+     * Returns a PI translated entity for the given handle. Returns null if this
+     * store does not contain a mapping between the two for the given pipeconf
+     * ID.
+     *
+     * @param handle PI entity handle
+     * @return PI translated entity
+     */
+    PiTranslatedEntity<T, E> get(PiHandle<E> handle);
+
+    /**
+     * Removes a previously added mapping for the given PI entity handle.
+     *
+     * @param handle PI entity handle
+     */
+    void remove(PiHandle<E> handle);
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStoreDelegate.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStoreDelegate.java
new file mode 100644
index 0000000..2a8d16f
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslationStoreDelegate.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.store.StoreDelegate;
+
+/**
+ * PI translation store delegate abstraction.
+ *
+ * @param <T> PD entity class (translatable to PI)
+ * @param <E> PI entity class
+ */
+@Beta
+public interface PiTranslationStoreDelegate
+        <T extends PiTranslatable, E extends PiEntity>
+        extends StoreDelegate<PiTranslationEvent<T, E>> {
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java
new file mode 100644
index 0000000..202636a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/service/PiTranslator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.service;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+
+import java.util.Optional;
+
+/**
+ * A translator of PI entities to equivalent PD ones which offer means to learn
+ * translated entities for later use.
+ *
+ * @param <T> PD entity class (translatable to PI)
+ * @param <E> PI entity class
+ */
+@Beta
+public interface PiTranslator<T extends PiTranslatable, E extends PiEntity> {
+
+    /**
+     * Translate the given PD entity (original) and returns a PI entity that is
+     * equivalent to he PD one for the given pipeconf.
+     *
+     * @param original PD entity
+     * @param pipeconf pipeconf
+     * @return PI entity
+     * @throws PiTranslationException if a translation is not possible (see
+     *                                message for an explanation)
+     */
+    E translate(T original, PiPipeconf pipeconf)
+            throws PiTranslationException;
+
+    /**
+     * Stores a mapping between the given translated entity and handle.
+     *
+     * @param handle PI entity handle
+     * @param entity PI translated entity
+     */
+    void learn(PiHandle<E> handle, PiTranslatedEntity<T, E> entity);
+
+    /**
+     * Returns a PI translated entity that was previously associated with the
+     * given  handle, if present. If not present, it means a mapping between the
+     * two has not been learned by the system (via {@link #learn(PiHandle,
+     * PiTranslatedEntity)}) or that it has been removed (via {@link
+     * #forget(PiHandle)}). the
+     *
+     * @param handle PI entity handle
+     * @return optional PI translated entity
+     */
+    Optional<PiTranslatedEntity<T, E>> lookup(PiHandle<E> handle);
+
+    /**
+     * Removes any mapping for the given PI entity handle.
+     *
+     * @param handle PI entity handle.
+     */
+    void forget(PiHandle<E> handle);
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java
new file mode 100644
index 0000000..5d7178c
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/AbstractPiTranslatorImpl.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.net.pi.impl;
+
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.net.pi.service.PiTranslatable;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
+import org.onosproject.net.pi.service.PiTranslationStore;
+import org.onosproject.net.pi.service.PiTranslator;
+
+import java.util.Optional;
+
+/**
+ * Abstract implementation of a PI translator backed by a PI translation store.
+ *
+ * @param <T> PD entity class
+ * @param <E> PI entity class
+ */
+public abstract class AbstractPiTranslatorImpl
+        <T extends PiTranslatable, E extends PiEntity>
+        implements PiTranslator<T, E> {
+
+    private final PiTranslationStore<T, E> store;
+
+    AbstractPiTranslatorImpl(PiTranslationStore<T, E> store) {
+        this.store = store;
+    }
+
+    @Override
+    public void learn(PiHandle<E> handle, PiTranslatedEntity<T, E> entity) {
+        store.addOrUpdate(handle, entity);
+    }
+
+    @Override
+    public Optional<PiTranslatedEntity<T, E>> lookup(PiHandle<E> handle) {
+        return Optional.ofNullable(store.get(handle));
+    }
+
+    @Override
+    public void forget(PiHandle<E> handle) {
+        store.remove(handle);
+    }
+}
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
index 22d53fd..7f43e22 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/CriterionTranslatorHelper.java
@@ -86,13 +86,13 @@
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
 import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.net.pi.service.PiTranslationException;
 
 import java.util.Map;
 
 import static java.lang.String.format;
 import static org.onlab.util.ImmutableByteSequence.ByteSequenceTrimException;
 import static org.onosproject.net.pi.impl.CriterionTranslator.CriterionTranslatorException;
-import static org.onosproject.net.pi.service.PiTranslationService.PiTranslationException;
 
 /**
  * Helper class to translate criterion instances to PI field matches.
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
similarity index 98%
rename from core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
rename to core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
index d856fd3..f04e7b0 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorImpl.java
@@ -46,6 +46,7 @@
 import org.onosproject.net.pi.runtime.PiTableAction;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.net.pi.service.PiTranslationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,17 +63,16 @@
 import static org.onosproject.net.pi.impl.CriterionTranslatorHelper.translateCriterion;
 import static org.onosproject.net.pi.impl.PiUtils.getInterpreterOrNull;
 import static org.onosproject.net.pi.impl.PiUtils.translateTableId;
-import static org.onosproject.net.pi.service.PiTranslationService.PiTranslationException;
 
 /**
  * Implementation of flow rule translation logic.
  */
-final class PiFlowRuleTranslator {
+final class PiFlowRuleTranslatorImpl {
 
     public static final int MAX_PI_PRIORITY = (int) Math.pow(2, 24);
-    private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslator.class);
+    private static final Logger log = LoggerFactory.getLogger(PiFlowRuleTranslatorImpl.class);
 
-    private PiFlowRuleTranslator() {
+    private PiFlowRuleTranslatorImpl() {
         // Hide constructor.
     }
 
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
similarity index 95%
rename from core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java
rename to core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
index 255ccb9..eeabebc 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslator.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiGroupTranslatorImpl.java
@@ -29,21 +29,21 @@
 import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
 import org.onosproject.net.pi.runtime.PiGroupKey;
 import org.onosproject.net.pi.runtime.PiTableAction;
-import org.onosproject.net.pi.service.PiTranslationService.PiTranslationException;
+import org.onosproject.net.pi.service.PiTranslationException;
 
 import java.nio.ByteBuffer;
 
 import static java.lang.String.format;
-import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.translateTreatment;
+import static org.onosproject.net.pi.impl.PiFlowRuleTranslatorImpl.translateTreatment;
 import static org.onosproject.net.pi.impl.PiUtils.getInterpreterOrNull;
 import static org.onosproject.net.pi.runtime.PiTableAction.Type.ACTION;
 
 /**
  * Implementation of group translation logic.
  */
-final class PiGroupTranslator {
+final class PiGroupTranslatorImpl {
 
-    private PiGroupTranslator() {
+    private PiGroupTranslatorImpl() {
         // Hides constructor.
     }
 
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
index d60e9b3..212cf1c 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiPipeconfManager.java
@@ -28,6 +28,9 @@
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.util.ItemNotFoundException;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.cluster.NodeId;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
@@ -79,6 +82,9 @@
     protected NetworkConfigRegistry cfgService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LeadershipService leadershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DriverService driverService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -87,6 +93,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected PiPipeconfMappingStore pipeconfMappingStore;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
     // Registered pipeconf are replicated through the app subsystem and registered on app activated events.
     protected ConcurrentHashMap<PiPipeconfId, PiPipeconf> piPipeconfs = new ConcurrentHashMap<>();
 
@@ -200,21 +209,28 @@
                     // due to 1:1:1 pipeconf:driver:provider maybe find better way
                     DriverProvider provider = new PiPipeconfDriverProviderInternal(completeDriver);
 
-                    //we register to the dirver susbystem the driver provider containing the merged driver
+                    //we register to the driver susbystem the driver provider containing the merged driver
                     driverAdminService.registerProvider(provider);
                 }
 
                 // Changing the configuration for the device to enforce the full driver with pipipeconf
-                // and base behaviours
-                ObjectNode newCfg = (ObjectNode) basicDeviceConfig.node();
-                newCfg = newCfg.put(DRIVER, completeDriverName);
-                ObjectMapper mapper = new ObjectMapper();
-                JsonNode newCfgNode = mapper.convertValue(newCfg, JsonNode.class);
-                cfgService.applyConfig(deviceId, BasicDeviceConfig.class, newCfgNode);
-                // Completable future is needed for when this method will also apply the pipeline to the device.
-                // FIXME (maybe): the pipeline is currently applied by the general device provider. But we store here
-                // the association between device and pipeconf.
-                pipeconfMappingStore.createOrUpdateBinding(deviceId, pipeconfId);
+                // and base behaviours, updating binding only first time something changes
+                NodeId leaderNodeId = leadershipService.getLeader("deploy-" +
+                        deviceId.toString() + "-pipeconf");
+                NodeId localNodeId = clusterService.getLocalNode().id();
+
+                if (!basicDeviceConfig.driver().equals(completeDriverName) && localNodeId.equals(leaderNodeId)) {
+                    ObjectNode newCfg = (ObjectNode) basicDeviceConfig.node();
+                    newCfg = newCfg.put(DRIVER, completeDriverName);
+                    ObjectMapper mapper = new ObjectMapper();
+                    JsonNode newCfgNode = mapper.convertValue(newCfg, JsonNode.class);
+                    log.debug("New driver {} for device {}", completeDriverName, deviceId);
+                    cfgService.applyConfig(deviceId, BasicDeviceConfig.class, newCfgNode);
+                    // Completable future is needed for when this method will also apply the pipeline to the device.
+                    // FIXME (maybe): the pipeline is currently applied by the general device provider.
+                    // But we store here the association between device and pipeconf.
+                    pipeconfMappingStore.createOrUpdateBinding(deviceId, pipeconfId);
+                }
                 operationResult.complete(true);
             }
         });
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
index 3cb0a09..9780fac 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiTranslationServiceImpl.java
@@ -30,12 +30,17 @@
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.runtime.PiActionGroup;
 import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.service.PiFlowRuleTranslationStore;
+import org.onosproject.net.pi.service.PiFlowRuleTranslator;
+import org.onosproject.net.pi.service.PiGroupTranslationStore;
+import org.onosproject.net.pi.service.PiGroupTranslator;
+import org.onosproject.net.pi.service.PiTranslationException;
 import org.onosproject.net.pi.service.PiTranslationService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Implementation of the protocol-independent translation service.
+ * Implementation of the PI translation service.
  */
 @Component(immediate = true)
 @Service
@@ -48,24 +53,37 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DeviceService deviceService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiFlowRuleTranslationStore flowRuleTranslationStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private PiGroupTranslationStore groupTranslationStore;
+
+    private PiFlowRuleTranslator flowRuleTranslator;
+    private PiGroupTranslator groupTranslator;
+
     @Activate
     public void activate() {
+        flowRuleTranslator = new InternalFlowRuleTranslator(flowRuleTranslationStore);
+        groupTranslator = new InternalGroupTranslator(groupTranslationStore);
         log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
+        flowRuleTranslator = null;
+        groupTranslator = null;
         log.info("Stopped");
     }
 
     @Override
-    public PiTableEntry translateFlowRule(FlowRule rule, PiPipeconf pipeconf) throws PiTranslationException {
-        return PiFlowRuleTranslator.translate(rule, pipeconf, getDevice(rule.deviceId()));
+    public PiFlowRuleTranslator flowRuleTranslator() {
+        return flowRuleTranslator;
     }
 
     @Override
-    public PiActionGroup translateGroup(Group group, PiPipeconf pipeconf) throws PiTranslationException {
-        return PiGroupTranslator.translate(group, pipeconf, getDevice(group.deviceId()));
+    public PiGroupTranslator groupTranslator() {
+        return groupTranslator;
     }
 
     private Device getDevice(DeviceId deviceId) throws PiTranslationException {
@@ -75,5 +93,37 @@
         }
         return device;
     }
+
+    private final class InternalFlowRuleTranslator
+            extends AbstractPiTranslatorImpl<FlowRule, PiTableEntry>
+            implements PiFlowRuleTranslator {
+
+        private InternalFlowRuleTranslator(PiFlowRuleTranslationStore store) {
+            super(store);
+        }
+
+        @Override
+        public PiTableEntry translate(FlowRule original, PiPipeconf pipeconf)
+                throws PiTranslationException {
+            return PiFlowRuleTranslatorImpl
+                    .translate(original, pipeconf, getDevice(original.deviceId()));
+        }
+    }
+
+    private final class InternalGroupTranslator
+            extends AbstractPiTranslatorImpl<Group, PiActionGroup>
+            implements PiGroupTranslator {
+
+        private InternalGroupTranslator(PiGroupTranslationStore store) {
+            super(store);
+        }
+
+        @Override
+        public PiActionGroup translate(Group original, PiPipeconf pipeconf)
+                throws PiTranslationException {
+            return PiGroupTranslatorImpl
+                    .translate(original, pipeconf, getDevice(original.deviceId()));
+        }
+    }
 }
 
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java
index 6e93d67..78bca5f 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiUtils.java
@@ -22,7 +22,7 @@
 import org.onosproject.net.pi.model.PiPipeconf;
 import org.onosproject.net.pi.model.PiPipelineInterpreter;
 import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.service.PiTranslationService;
+import org.onosproject.net.pi.service.PiTranslationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,23 +60,23 @@
     }
 
     static PiTableId translateTableId(TableId tableId, PiPipelineInterpreter interpreter)
-            throws PiTranslationService.PiTranslationException {
+            throws PiTranslationException {
         switch (tableId.type()) {
             case PIPELINE_INDEPENDENT:
                 return (PiTableId) tableId;
             case INDEX:
                 IndexTableId indexId = (IndexTableId) tableId;
                 if (interpreter == null) {
-                    throw new PiTranslationService.PiTranslationException(format(
+                    throw new PiTranslationException(format(
                             "Unable to map table ID '%d' from index to PI: missing interpreter", indexId.id()));
                 } else if (!interpreter.mapFlowRuleTableId(indexId.id()).isPresent()) {
-                    throw new PiTranslationService.PiTranslationException(format(
+                    throw new PiTranslationException(format(
                             "Unable to map table ID '%d' from index to PI: missing ID in interpreter", indexId.id()));
                 } else {
                     return interpreter.mapFlowRuleTableId(indexId.id()).get();
                 }
             default:
-                throw new PiTranslationService.PiTranslationException(format(
+                throw new PiTranslationException(format(
                         "Unrecognized table ID type %s", tableId.type().name()));
         }
     }
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
index d13def7..9a80e9f 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiTranslatorServiceTest.java
@@ -67,7 +67,7 @@
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onlab.util.ImmutableByteSequence.fit;
 import static org.onosproject.net.group.GroupDescription.Type.SELECT;
-import static org.onosproject.net.pi.impl.PiFlowRuleTranslator.MAX_PI_PRIORITY;
+import static org.onosproject.net.pi.impl.PiFlowRuleTranslatorImpl.MAX_PI_PRIORITY;
 import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRF_WCMP_SELECTOR_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.ACT_PRM_PORT_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.ACT_SET_EGRESS_PORT_ID;
@@ -80,7 +80,7 @@
 import static org.onosproject.pipelines.basic.BasicConstants.TBL_WCMP_TABLE_ID;
 
 /**
- * Tests for {@link PiFlowRuleTranslator}.
+ * Tests for {@link PiFlowRuleTranslatorImpl}.
  */
 @SuppressWarnings("ConstantConditions")
 public class PiTranslatorServiceTest {
@@ -161,8 +161,8 @@
                 .withPriority(priority)
                 .build();
 
-        PiTableEntry entry1 = PiFlowRuleTranslator.translate(rule1, pipeconf, null);
-        PiTableEntry entry2 = PiFlowRuleTranslator.translate(rule1, pipeconf, null);
+        PiTableEntry entry1 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
+        PiTableEntry entry2 = PiFlowRuleTranslatorImpl.translate(rule1, pipeconf, null);
 
         // check equality, i.e. same rules must produce same entries
         new EqualsTester()
@@ -236,8 +236,8 @@
     @Test
     public void testTranslateGroups() throws Exception {
 
-        PiActionGroup piGroup1 = PiGroupTranslator.translate(GROUP, pipeconf, null);
-        PiActionGroup piGroup2 = PiGroupTranslator.translate(GROUP, pipeconf, null);
+        PiActionGroup piGroup1 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
+        PiActionGroup piGroup2 = PiGroupTranslatorImpl.translate(GROUP, pipeconf, null);
 
         new EqualsTester()
                 .addEqualityGroup(piGroup1, piGroup2)
diff --git a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java
new file mode 100644
index 0000000..9df6a86
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/AbstractDistributedPiTranslationStore.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.store.pi.impl;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.net.pi.service.PiTranslatable;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
+import org.onosproject.net.pi.service.PiTranslationEvent;
+import org.onosproject.net.pi.service.PiTranslationStore;
+import org.onosproject.net.pi.service.PiTranslationStoreDelegate;
+import org.onosproject.store.AbstractStore;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Distributed implementation of PiTranslationStore.
+ */
+@Component(immediate = true)
+public abstract class AbstractDistributedPiTranslationStore
+        <T extends PiTranslatable, E extends PiEntity>
+        extends AbstractStore<PiTranslationEvent<T, E>, PiTranslationStoreDelegate<T, E>>
+        implements PiTranslationStore<T, E> {
+
+    private static final String MAP_NAME_TEMPLATE = "onos-pi-translated-%s-map";
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    private EventuallyConsistentMap<PiHandle<E>, PiTranslatedEntity<T, E>>
+            translatedEntities;
+
+    private final EventuallyConsistentMapListener
+            <PiHandle<E>, PiTranslatedEntity<T, E>> entityMapListener =
+            new InternalEntityMapListener();
+
+    /**
+     * Returns a string that identifies the map maintained by this store among
+     * others that uses this abstract class.
+     *
+     * @return string
+     */
+    protected abstract String mapSimpleName();
+
+    @Activate
+    public void activate() {
+        final String fullMapName = format(MAP_NAME_TEMPLATE, mapSimpleName());
+        translatedEntities = storageService
+                .<PiHandle<E>, PiTranslatedEntity<T, E>>eventuallyConsistentMapBuilder()
+                .withName(fullMapName)
+                .withSerializer(KryoNamespaces.API)
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+        translatedEntities.addListener(entityMapListener);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        translatedEntities.removeListener(entityMapListener);
+        translatedEntities = null;
+        log.info("Stopped");
+    }
+
+    @Override
+    public void addOrUpdate(PiHandle<E> handle, PiTranslatedEntity<T, E> entity) {
+        checkNotNull(handle);
+        checkNotNull(entity);
+        checkArgument(handle.entityType().equals(entity.entityType()),
+                      "Entity type must be the same for handle and translated entity");
+        translatedEntities.put(handle, entity);
+    }
+
+    @Override
+    public void remove(PiHandle<E> handle) {
+        checkNotNull(handle);
+        translatedEntities.remove(handle);
+    }
+
+    @Override
+    public PiTranslatedEntity<T, E> get(PiHandle<E> handle) {
+        checkNotNull(handle);
+        return translatedEntities.get(handle);
+    }
+
+    public Iterable<PiTranslatedEntity<T, E>> getAll() {
+        return translatedEntities.values();
+    }
+
+    private class InternalEntityMapListener
+            implements EventuallyConsistentMapListener
+                               <PiHandle<E>, PiTranslatedEntity<T, E>> {
+
+        @Override
+        public void event(EventuallyConsistentMapEvent<PiHandle<E>,
+                PiTranslatedEntity<T, E>> event) {
+            final PiTranslationEvent.Type type;
+            switch (event.type()) {
+                case PUT:
+                    type = PiTranslationEvent.Type.LEARNED;
+                    break;
+                case REMOVE:
+                    type = PiTranslationEvent.Type.FORGOT;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown event type " + event.type().name());
+            }
+            notifyDelegate(new PiTranslationEvent<>(type, event.value()));
+        }
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiFlowRuleTranslationStore.java b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiFlowRuleTranslationStore.java
new file mode 100644
index 0000000..c123ec5
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiFlowRuleTranslationStore.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.store.pi.impl;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.service.PiFlowRuleTranslationStore;
+
+/**
+ * Distributed implementation of a PI translation store for flow rules.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedPiFlowRuleTranslationStore
+        extends AbstractDistributedPiTranslationStore<FlowRule, PiTableEntry>
+        implements PiFlowRuleTranslationStore {
+
+    private static final String MAP_SIMPLE_NAME = "flowrule";
+
+    @Override
+    protected String mapSimpleName() {
+        return MAP_SIMPLE_NAME;
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiGroupTranslationStore.java b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiGroupTranslationStore.java
new file mode 100644
index 0000000..fa62ef1
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onosproject/store/pi/impl/DistributedPiGroupTranslationStore.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.store.pi.impl;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.service.PiGroupTranslationStore;
+
+/**
+ * Distributed implementation of a PI translation store for groups.
+ */
+@Component(immediate = true)
+@Service
+public class DistributedPiGroupTranslationStore
+        extends AbstractDistributedPiTranslationStore<Group, PiActionGroup>
+        implements PiGroupTranslationStore {
+
+    private static final String MAP_SIMPLE_NAME = "group";
+
+    @Override
+    protected String mapSimpleName() {
+        return MAP_SIMPLE_NAME;
+    }
+}
diff --git a/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedDevicePipeconfMappingStoreTest.java b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedDevicePipeconfMappingStoreTest.java
index 83b997d..9a31b0c 100644
--- a/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedDevicePipeconfMappingStoreTest.java
+++ b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedDevicePipeconfMappingStoreTest.java
@@ -119,4 +119,4 @@
         store.deviceToPipeconf.clear();
     }
 
-}
\ No newline at end of file
+}
diff --git a/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java
new file mode 100644
index 0000000..2ba52a2
--- /dev/null
+++ b/core/store/dist/src/test/java/org/onosproject/store/pi/impl/DistributedPiTranslationStoreTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.store.pi.impl;
+
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.RandomUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.net.pi.service.PiTranslatable;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
+import org.onosproject.store.service.TestStorageService;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test for {@link AbstractDistributedPiTranslationStore}.
+ */
+public class DistributedPiTranslationStoreTest {
+
+    private AbstractDistributedPiTranslationStore<PiTranslatable, PiEntity> store;
+
+    private static final int HANDLE_HASH = RandomUtils.nextInt();
+    private static final PiTranslatable PI_TRANSLATABLE =
+            new PiTranslatable() {
+            };
+    private static final PiEntity PI_ENTITY = () -> PiEntityType.TABLE_ENTRY;
+    private static final PiHandle<PiEntity> PI_HANDLE =
+            new PiHandle<PiEntity>(DeviceId.NONE, PI_ENTITY) {
+                @Override
+                public int hashCode() {
+                    return HANDLE_HASH;
+                }
+
+                @Override
+                public boolean equals(Object other) {
+                    return other instanceof PiHandle && other.hashCode() == hashCode();
+                }
+
+                @Override
+                public String toString() {
+                    return String.valueOf(HANDLE_HASH);
+                }
+            };
+    private static final PiTranslatedEntity<PiTranslatable, PiEntity> TRANSLATED_ENTITY =
+            new PiTranslatedEntity<>(PI_TRANSLATABLE, PI_ENTITY, PI_HANDLE);
+
+    /**
+     * Sets up the store and the storage service test harness.
+     */
+    @Before
+    public void setUp() {
+        store = new AbstractDistributedPiTranslationStore<PiTranslatable, PiEntity>() {
+            @Override
+            protected String mapSimpleName() {
+                return "test";
+            }
+        };
+        store.storageService = new TestStorageService();
+        store.setDelegate(event -> {
+        });
+        store.activate();
+    }
+
+    /**
+     * Tests equality of key and value used in other tests.
+     */
+    @Test
+    public void testEquality() {
+        assertEquals(PI_HANDLE, PI_HANDLE);
+        assertEquals(TRANSLATED_ENTITY, TRANSLATED_ENTITY);
+    }
+
+    /**
+     * Test for activate.
+     */
+    @Test
+    public void activate() {
+        assertNotNull(store.storageService);
+        assertTrue("Store must have delegate",
+                   store.hasDelegate());
+        assertTrue("No value should be in the map",
+                   Lists.newArrayList(store.getAll()).isEmpty());
+    }
+
+    /**
+     * Test for deactivate.
+     */
+    @Test(expected = NullPointerException.class)
+    public void deactivate() {
+        store.deactivate();
+        store.getAll();
+    }
+
+    /**
+     * Test of value add or update.
+     */
+    @Test
+    public void addOrUpdate() {
+        store.addOrUpdate(PI_HANDLE, TRANSLATED_ENTITY);
+        assertTrue("Value should be in the map",
+                   store.get(PI_HANDLE) != null);
+        assertTrue("Exactly 1 value should be in the map",
+                   Lists.newArrayList(store.getAll()).size() == 1);
+
+        // Add again, expect 1 value.
+        store.addOrUpdate(PI_HANDLE, TRANSLATED_ENTITY);
+        assertTrue("Exactly 1 value should be in the map",
+                   Lists.newArrayList(store.getAll()).size() == 1);
+    }
+
+    /**
+     * Test of value lookup.
+     */
+    @Test
+    public void lookup() throws Exception {
+        clear();
+        addOrUpdate();
+        assertEquals("Wrong value in the map",
+                     store.get(PI_HANDLE), TRANSLATED_ENTITY);
+    }
+
+    /**
+     * Test of value removal.
+     */
+    @Test
+    public void clear() {
+        store.remove(PI_HANDLE);
+        assertTrue("Value should NOT be in the map",
+                   store.get(PI_HANDLE) == null);
+        assertTrue("No value should be in the map",
+                   Lists.newArrayList(store.getAll()).isEmpty());
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
index cc3c05d..6a03032 100644
--- a/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onosproject/store/serializers/KryoNamespaces.java
@@ -220,6 +220,7 @@
 import org.onosproject.net.pi.model.PiTableType;
 import org.onosproject.net.pi.runtime.PiAction;
 import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupHandle;
 import org.onosproject.net.pi.runtime.PiActionGroupId;
 import org.onosproject.net.pi.runtime.PiActionGroupMember;
 import org.onosproject.net.pi.runtime.PiActionGroupMemberId;
@@ -227,9 +228,12 @@
 import org.onosproject.net.pi.runtime.PiControlMetadata;
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiEntityType;
 import org.onosproject.net.pi.runtime.PiExactFieldMatch;
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiGroupKey;
+import org.onosproject.net.pi.runtime.PiHandle;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
 import org.onosproject.net.pi.runtime.PiMatchKey;
 import org.onosproject.net.pi.runtime.PiPacketOperation;
@@ -239,6 +243,9 @@
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
 import org.onosproject.net.pi.runtime.PiValidFieldMatch;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.net.pi.service.PiTranslatable;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
 import org.onosproject.net.provider.ProviderId;
 import org.onosproject.net.region.DefaultRegion;
 import org.onosproject.net.region.Region;
@@ -630,6 +637,7 @@
                     // PI Runtime
                     PiAction.class,
                     PiActionGroup.class,
+                    PiActionGroupHandle.class,
                     PiActionGroupId.class,
                     PiActionGroupMember.class,
                     PiActionGroupMemberId.class,
@@ -637,9 +645,12 @@
                     PiControlMetadata.class,
                     PiCounterCellData.class,
                     PiCounterCellId.class,
+                    PiEntity.class,
+                    PiEntityType.class,
                     PiExactFieldMatch.class,
                     PiFieldMatch.class,
                     PiGroupKey.class,
+                    PiHandle.class,
                     PiLpmFieldMatch.class,
                     PiMatchKey.class,
                     PiPacketOperation.class,
@@ -649,6 +660,10 @@
                     PiTableEntry.class,
                     PiTernaryFieldMatch.class,
                     PiValidFieldMatch.class,
+                    // PI service
+                    PiTableEntryHandle.class,
+                    PiTranslatedEntity.class,
+                    PiTranslatable.class,
                     // Other
                     PiCriterion.class,
                     PiInstruction.class
diff --git a/drivers/p4runtime/BUCK b/drivers/p4runtime/BUCK
index b8167e8..09c9bf7 100644
--- a/drivers/p4runtime/BUCK
+++ b/drivers/p4runtime/BUCK
@@ -2,9 +2,11 @@
 
 COMPILE_DEPS = [
     '//lib:CORE_DEPS',
+    '//lib:KRYO',
     '//protocols/p4runtime/api:onos-protocols-p4runtime-api',
     '//incubator/grpc-dependencies:grpc-core-repkg-' + GRPC_VER,
     '//lib:grpc-netty-' + GRPC_VER,
+    '//core/store/serializers:onos-core-serializers',
 ]
 
 BUNDLES = [
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
index 5f95eda..ca9392f 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeFlowRuleProgrammable.java
@@ -20,6 +20,9 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import io.grpc.StatusRuntimeException;
+import org.onlab.util.SharedExecutors;
+import org.onosproject.drivers.p4runtime.mirror.P4RuntimeTableMirror;
+import org.onosproject.drivers.p4runtime.mirror.TimedEntry;
 import org.onosproject.net.flow.DefaultFlowEntry;
 import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.flow.FlowRule;
@@ -32,15 +35,17 @@
 import org.onosproject.net.pi.runtime.PiCounterCellData;
 import org.onosproject.net.pi.runtime.PiCounterCellId;
 import org.onosproject.net.pi.runtime.PiTableEntry;
-import org.onosproject.net.pi.service.PiTranslationService;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.net.pi.service.PiFlowRuleTranslator;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
+import org.onosproject.net.pi.service.PiTranslationException;
 import org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType;
-import org.onosproject.p4runtime.api.P4RuntimeFlowRuleWrapper;
-import org.onosproject.p4runtime.api.P4RuntimeTableEntryReference;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
@@ -49,6 +54,7 @@
 import java.util.stream.Collectors;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Collections.singleton;
 import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.APPLY;
 import static org.onosproject.drivers.p4runtime.P4RuntimeFlowRuleProgrammable.Operation.REMOVE;
 import static org.onosproject.net.flow.FlowEntry.FlowEntryState.ADDED;
@@ -59,44 +65,40 @@
 /**
  * Implementation of the flow rule programmable behaviour for P4Runtime.
  */
-public class P4RuntimeFlowRuleProgrammable extends AbstractP4RuntimeHandlerBehaviour implements FlowRuleProgrammable {
+public class P4RuntimeFlowRuleProgrammable
+        extends AbstractP4RuntimeHandlerBehaviour
+        implements FlowRuleProgrammable {
 
-    /*
-    When updating an existing rule, if true, we issue a DELETE operation before inserting the new one, otherwise we
-    issue a MODIFY operation. This is useful fore devices that do not support MODIFY operations for table entries.
-     */
+    // When updating an existing rule, if true, we issue a DELETE operation
+    // before inserting the new one, otherwise we issue a MODIFY operation. This
+    // is useful fore devices that do not support MODIFY operations for table
+    // entries.
     // TODO: make this attribute configurable by child drivers (e.g. BMv2 or Tofino)
     private boolean deleteEntryBeforeUpdate = true;
 
-    /*
-    If true, we ignore re-installing rules that are already known in the ENTRY_STORE, i.e. same match key and action.
-     */
-    // TODO: can remove this check as soon as the multi-apply-per-same-flow rule bug is fixed.
-    private boolean checkEntryStoreBeforeUpdate = true;
+    // If true, we ignore re-installing rules that are already exists the
+    // device, i.e. same match key and action.
+    // FIXME: can remove this check as soon as the multi-apply-per-same-flow rule bug is fixed.
+    private boolean checkStoreBeforeUpdate = true;
 
-    /*
-    If true, we avoid querying the device and return the content of the ENTRY_STORE.
-     */
-    // TODO: set to false after bmv2/PI bug fixed
+    // If true, we avoid querying the device and return what's already known by
+    // the ONOS store.
     private boolean ignoreDeviceWhenGet = true;
 
-    /*
-    If true, we read all direct counters of a table with one request. Otherwise, send as many request as the number of
-    table entries.
-     */
-    // TODO: set to true as soon as the feature is implemented in P4Runtime.
+    /* If true, we read all direct counters of a table with one request.
+    Otherwise, we send as many requests as the number of table entries. */
+    // FIXME: set to true as soon as the feature is implemented in P4Runtime.
     private boolean readAllDirectCounters = false;
 
     // Needed to synchronize operations over the same table entry.
-    private static final ConcurrentMap<P4RuntimeTableEntryReference, Lock> ENTRY_LOCKS = Maps.newConcurrentMap();
-
-    // TODO: replace with distributed store.
-    // Can reuse old BMv2TableEntryService from ONOS 1.6
-    private static final ConcurrentMap<P4RuntimeTableEntryReference, P4RuntimeFlowRuleWrapper> ENTRY_STORE =
-            Maps.newConcurrentMap();
+    // FIXME: locks should be removed when unused (hint use cache with timeout)
+    private static final ConcurrentMap<PiTableEntryHandle, Lock>
+            ENTRY_LOCKS = Maps.newConcurrentMap();
 
     private PiPipelineModel pipelineModel;
     private PiPipelineInterpreter interpreter;
+    private P4RuntimeTableMirror tableMirror;
+    private PiFlowRuleTranslator translator;
 
     @Override
     protected boolean setupBehaviour() {
@@ -111,6 +113,8 @@
         }
         interpreter = device.as(PiPipelineInterpreter.class);
         pipelineModel = pipeconf.pipelineModel();
+        tableMirror = handler().get(P4RuntimeTableMirror.class);
+        translator = piTranslationService.flowRuleTranslator();
         return true;
     }
 
@@ -122,98 +126,70 @@
         }
 
         if (ignoreDeviceWhenGet) {
-            return ENTRY_STORE.values().stream()
-                    .filter(frWrapper -> frWrapper.rule().deviceId().equals(this.deviceId))
-                    .map(frWrapper -> new DefaultFlowEntry(frWrapper.rule(), ADDED, frWrapper.lifeInSeconds(),
-                                                           0, 0))
-                    .collect(Collectors.toList());
+            return getFlowEntriesFromMirror();
         }
 
-        ImmutableList.Builder<FlowEntry> resultBuilder = ImmutableList.builder();
-        List<PiTableEntry> inconsistentEntries = Lists.newArrayList();
+        final ImmutableList.Builder<FlowEntry> result = ImmutableList.builder();
+        final List<PiTableEntry> inconsistentEntries = Lists.newArrayList();
 
         for (PiTableModel tableModel : pipelineModel.tables()) {
 
-            PiTableId piTableId = tableModel.id();
+            final PiTableId piTableId = tableModel.id();
 
-            Collection<PiTableEntry> installedEntries;
+            // Read table entries.
+            final Collection<PiTableEntry> installedEntries;
             try {
-                // TODO: optimize by dumping entries and counters in parallel, from ALL tables with the same request.
+                // TODO: optimize by dumping entries and counters in parallel
+                // From ALL tables with the same request.
                 installedEntries = client.dumpTable(piTableId, pipeconf).get();
             } catch (InterruptedException | ExecutionException e) {
                 if (!(e.getCause() instanceof StatusRuntimeException)) {
                     // gRPC errors are logged in the client.
-                    log.error("Exception while dumping table {} of {}", piTableId, deviceId, e);
+                    log.error("Exception while dumping table {} of {}",
+                              piTableId, deviceId, e);
                 }
-                return Collections.emptyList();
+                continue; // next table
             }
 
-            Map<PiTableEntry, PiCounterCellData> counterCellMap;
-            try {
-                if (interpreter.mapTableCounter(piTableId).isPresent()) {
-                    PiCounterId piCounterId = interpreter.mapTableCounter(piTableId).get();
-                    Collection<PiCounterCellData> cellDatas;
-                    if (readAllDirectCounters) {
-                        cellDatas = client.readAllCounterCells(Collections.singleton(piCounterId), pipeconf).get();
-                    } else {
-                        Set<PiCounterCellId> cellIds = installedEntries.stream()
-                                .map(entry -> PiCounterCellId.ofDirect(piCounterId, entry))
-                                .collect(Collectors.toSet());
-                        cellDatas = client.readCounterCells(cellIds, pipeconf).get();
-                    }
-                    counterCellMap = cellDatas.stream()
-                            .collect(Collectors.toMap(c -> (c.cellId()).tableEntry(), c -> c));
-                } else {
-                    counterCellMap = Collections.emptyMap();
-                }
-                installedEntries = client.dumpTable(piTableId, pipeconf).get();
-            } catch (InterruptedException | ExecutionException e) {
-                if (!(e.getCause() instanceof StatusRuntimeException)) {
-                    // gRPC errors are logged in the client.
-                    log.error("Exception while reading counters of table {} of {}", piTableId, deviceId, e);
-                }
+            if (installedEntries.size() == 0) {
+                continue; // next table
+            }
+
+            // Read table direct counters (if any).
+            final Map<PiTableEntry, PiCounterCellData> counterCellMap;
+            if (interpreter.mapTableCounter(piTableId).isPresent()) {
+                PiCounterId piCounterId = interpreter.mapTableCounter(piTableId).get();
+                counterCellMap = readEntryCounters(piCounterId, installedEntries);
+            } else {
                 counterCellMap = Collections.emptyMap();
             }
 
+            // Forge flow entries with counter values.
             for (PiTableEntry installedEntry : installedEntries) {
 
-                P4RuntimeTableEntryReference entryRef = new P4RuntimeTableEntryReference(deviceId,
-                                                                                         piTableId,
-                                                                                         installedEntry.matchKey());
+                final FlowEntry flowEntry = forgeFlowEntry(
+                        installedEntry, counterCellMap.get(installedEntry));
 
-                if (!ENTRY_STORE.containsKey(entryRef)) {
-                    // Inconsistent entry
+                if (flowEntry == null) {
+                    // Entry is on device but unknown to translation service or
+                    // device mirror. Inconsistent. Mark for removal.
+                    // TODO: make this behaviour configurable
+                    // In some cases it's fine for the device to have rules
+                    // that were not installed by us.
                     inconsistentEntries.add(installedEntry);
-                    continue; // next one.
+                } else {
+                    result.add(flowEntry);
                 }
-
-                P4RuntimeFlowRuleWrapper frWrapper = ENTRY_STORE.get(entryRef);
-
-                long bytes = 0L;
-                long packets = 0L;
-                if (counterCellMap.containsKey(installedEntry)) {
-                    PiCounterCellData counterCellData = counterCellMap.get(installedEntry);
-                    bytes = counterCellData.bytes();
-                    packets = counterCellData.packets();
-                }
-
-                resultBuilder.add(new DefaultFlowEntry(frWrapper.rule(),
-                                                       ADDED,
-                                                       frWrapper.lifeInSeconds(),
-                                                       packets,
-                                                       bytes));
             }
         }
 
         if (inconsistentEntries.size() > 0) {
-            log.warn("Found {} entries in {} that are not known by table entry service," +
-                             " removing them", inconsistentEntries.size(), deviceId);
-            inconsistentEntries.forEach(entry -> log.debug(entry.toString()));
-            // Async remove them.
-            client.writeTableEntries(inconsistentEntries, DELETE, pipeconf);
+            // Async clean up inconsistent entries.
+            SharedExecutors.getSingleThreadExecutor().execute(
+                    () -> cleanUpInconsistentEntries(inconsistentEntries));
         }
 
-        return resultBuilder.build();
+        return result.build();
     }
 
     @Override
@@ -226,109 +202,206 @@
         return processFlowRules(rules, REMOVE);
     }
 
-    private Collection<FlowRule> processFlowRules(Collection<FlowRule> rules, Operation operation) {
+    private FlowEntry forgeFlowEntry(PiTableEntry entry,
+                                     PiCounterCellData cellData) {
+        final PiTableEntryHandle handle = PiTableEntryHandle
+                .of(deviceId, entry);
+        final Optional<PiTranslatedEntity<FlowRule, PiTableEntry>>
+                translatedEntity = translator.lookup(handle);
+        final TimedEntry<PiTableEntry> timedEntry = tableMirror.get(handle);
+
+        if (!translatedEntity.isPresent()) {
+            log.debug("Handle not found in store: {}", handle);
+            return null;
+        }
+
+        if (timedEntry == null) {
+            log.debug("Handle not found in device mirror: {}", handle);
+            return null;
+        }
+
+        if (cellData != null) {
+            return new DefaultFlowEntry(translatedEntity.get().original(),
+                                        ADDED, timedEntry.lifeSec(), cellData.bytes(),
+                                        cellData.bytes());
+        } else {
+            return new DefaultFlowEntry(translatedEntity.get().original(),
+                                        ADDED, timedEntry.lifeSec(), 0, 0);
+        }
+    }
+
+    private Collection<FlowEntry> getFlowEntriesFromMirror() {
+        return tableMirror.getAll(deviceId).stream()
+                .map(timedEntry -> forgeFlowEntry(
+                        timedEntry.entry(), null))
+                .collect(Collectors.toList());
+    }
+
+    private void cleanUpInconsistentEntries(Collection<PiTableEntry> piEntries) {
+        log.warn("Found {} entries from {} not on translation store, removing them...",
+                 piEntries.size(), deviceId);
+        piEntries.forEach(entry -> {
+            log.debug(entry.toString());
+            applyEntry(PiTableEntryHandle.of(deviceId, entry),
+                       entry, null, REMOVE);
+        });
+    }
+
+    private Collection<FlowRule> processFlowRules(Collection<FlowRule> rules,
+                                                  Operation driverOperation) {
 
         if (!setupBehaviour()) {
             return Collections.emptyList();
         }
 
-        ImmutableList.Builder<FlowRule> processedFlowRuleListBuilder = ImmutableList.builder();
+        final ImmutableList.Builder<FlowRule> result = ImmutableList.builder();
 
-        // TODO: send write operations in bulk (e.g. all entries to insert, modify or delete).
+        // TODO: send writes in bulk (e.g. all entries to insert, modify or delete).
         // Instead of calling the client for each one of them.
 
-        for (FlowRule rule : rules) {
+        for (FlowRule ruleToApply : rules) {
 
-            PiTableEntry piTableEntry;
-
+            final PiTableEntry piEntryToApply;
             try {
-                piTableEntry = piTranslationService.translateFlowRule(rule, pipeconf);
-            } catch (PiTranslationService.PiTranslationException e) {
-                log.warn("Unable to translate flow rule: {} - {}", e.getMessage(), rule);
-                continue; // next rule
+                piEntryToApply = translator.translate(ruleToApply, pipeconf);
+            } catch (PiTranslationException e) {
+                log.warn("Unable to translate flow rule for pipeconf '{}': {} - {}",
+                         pipeconf.id(), e.getMessage(), ruleToApply);
+                // Next rule.
+                continue;
             }
 
-            PiTableId tableId = piTableEntry.table();
-            P4RuntimeTableEntryReference entryRef = new P4RuntimeTableEntryReference(deviceId,
-                                                                                     tableId, piTableEntry.matchKey());
+            final PiTableEntryHandle handle = PiTableEntryHandle
+                    .of(deviceId, piEntryToApply);
 
-            Lock lock = ENTRY_LOCKS.computeIfAbsent(entryRef, k -> new ReentrantLock());
+            // Serialize operations over the same match key/table/device ID.
+            final Lock lock = ENTRY_LOCKS.computeIfAbsent(handle, k -> new ReentrantLock());
             lock.lock();
-
             try {
-
-                P4RuntimeFlowRuleWrapper frWrapper = ENTRY_STORE.get(entryRef);
-                WriteOperationType opType = null;
-                boolean doApply = true;
-
-                if (operation == APPLY) {
-                    if (frWrapper == null) {
-                        // Entry is first-timer.
-                        opType = INSERT;
-                    } else {
-                        // This match key already exists in the device.
-                        if (checkEntryStoreBeforeUpdate &&
-                                piTableEntry.action().equals(frWrapper.piTableEntry().action())) {
-                            doApply = false;
-                            log.debug("Ignoring re-apply of existing entry: {}", piTableEntry);
-                        }
-                        if (doApply) {
-                            if (deleteEntryBeforeUpdate) {
-                                // We've seen some strange error when trying to modify existing flow rules.
-                                // Remove before re-adding the modified one.
-                                try {
-                                    if (client.writeTableEntries(newArrayList(piTableEntry), DELETE, pipeconf).get()) {
-                                        frWrapper = null;
-                                    } else {
-                                        log.warn("Unable to DELETE table entry (before re-adding) in {}: {}",
-                                                 deviceId, piTableEntry);
-                                    }
-                                } catch (InterruptedException | ExecutionException e) {
-                                    log.warn("Exception while deleting table entry:", operation.name(), e);
-                                }
-                                opType = INSERT;
-                            } else {
-                                opType = MODIFY;
-                            }
-                        }
-                    }
-                } else {
-                    opType = DELETE;
+                if (applyEntry(handle, piEntryToApply,
+                               ruleToApply, driverOperation)) {
+                    result.add(ruleToApply);
                 }
-
-                if (doApply) {
-                    try {
-                        if (client.writeTableEntries(newArrayList(piTableEntry), opType, pipeconf).get()) {
-                            processedFlowRuleListBuilder.add(rule);
-                            if (operation == APPLY) {
-                                frWrapper = new P4RuntimeFlowRuleWrapper(rule, piTableEntry,
-                                                                         System.currentTimeMillis());
-                            } else {
-                                frWrapper = null;
-                            }
-                        } else {
-                            log.warn("Unable to {} table entry in {}: {}", opType.name(), deviceId, piTableEntry);
-                        }
-                    } catch (InterruptedException | ExecutionException e) {
-                        log.warn("Exception while performing {} table entry operation:", operation.name(), e);
-                    }
-                } else {
-                    processedFlowRuleListBuilder.add(rule);
-                }
-
-                // Update entryRef binding in table entry service.
-                if (frWrapper != null) {
-                    ENTRY_STORE.put(entryRef, frWrapper);
-                } else {
-                    ENTRY_STORE.remove(entryRef);
-                }
-
             } finally {
                 lock.unlock();
             }
         }
 
-        return processedFlowRuleListBuilder.build();
+        return result.build();
+    }
+
+    /**
+     * Applies the given entry to the device, and returns true if the operation
+     * was successful, false otherwise.
+     */
+    private boolean applyEntry(PiTableEntryHandle handle,
+                               PiTableEntry piEntryToApply,
+                               FlowRule ruleToApply,
+                               Operation driverOperation) {
+        // Depending on the driver operation, and if a matching rule exists on
+        // the device, decide which P4 Runtime write operation to perform for
+        // this entry.
+        final TimedEntry<PiTableEntry> piEntryOnDevice = tableMirror.get(handle);
+        final WriteOperationType p4Operation;
+        if (driverOperation == APPLY) {
+            if (piEntryOnDevice == null) {
+                // Entry is first-timer.
+                p4Operation = INSERT;
+            } else {
+                if (checkStoreBeforeUpdate
+                        && piEntryToApply.action().equals(piEntryOnDevice.entry().action())) {
+                    log.debug("Ignoring re-apply of existing entry: {}", piEntryToApply);
+                    p4Operation = null;
+                } else if (deleteEntryBeforeUpdate) {
+                    // Some devices return error when updating existing
+                    // entries. If requested, remove entry before
+                    // re-inserting the modified one.
+                    applyEntry(handle, piEntryOnDevice.entry(), null, REMOVE);
+                    p4Operation = INSERT;
+                } else {
+                    p4Operation = MODIFY;
+                }
+            }
+        } else {
+            p4Operation = DELETE;
+        }
+
+        if (p4Operation != null) {
+            if (writeEntry(piEntryToApply, p4Operation)) {
+                updateStores(handle, piEntryToApply, ruleToApply, p4Operation);
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            // If no operation, let's pretend we applied the rule to the device.
+            return true;
+        }
+    }
+
+    /**
+     * Performs a write operation on the device.
+     */
+    private boolean writeEntry(PiTableEntry entry,
+                               WriteOperationType p4Operation) {
+        try {
+            if (client.writeTableEntries(
+                    newArrayList(entry), p4Operation, pipeconf).get()) {
+                return true;
+            } else {
+                log.warn("Unable to {} table entry in {}: {}",
+                         p4Operation.name(), deviceId, entry);
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            log.warn("Exception while performing {} table entry operation:",
+                     p4Operation, e);
+        }
+        return false;
+    }
+
+    private void updateStores(PiTableEntryHandle handle,
+                              PiTableEntry entry,
+                              FlowRule rule,
+                              WriteOperationType p4Operation) {
+        switch (p4Operation) {
+            case INSERT:
+            case MODIFY:
+                tableMirror.put(handle, entry);
+                translator.learn(handle, new PiTranslatedEntity<>(rule, entry, handle));
+                break;
+            case DELETE:
+                tableMirror.remove(handle);
+                translator.forget(handle);
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unknown operation " + p4Operation.name());
+        }
+    }
+
+    private Map<PiTableEntry, PiCounterCellData> readEntryCounters(
+            PiCounterId counterId, Collection<PiTableEntry> tableEntries) {
+        Collection<PiCounterCellData> cellDatas;
+        try {
+            if (readAllDirectCounters) {
+                cellDatas = client.readAllCounterCells(
+                        singleton(counterId), pipeconf).get();
+            } else {
+                Set<PiCounterCellId> cellIds = tableEntries.stream()
+                        .map(entry -> PiCounterCellId.ofDirect(counterId, entry))
+                        .collect(Collectors.toSet());
+                cellDatas = client.readCounterCells(cellIds, pipeconf).get();
+            }
+            return cellDatas.stream()
+                    .collect(Collectors.toMap(c -> c.cellId().tableEntry(), c -> c));
+        } catch (InterruptedException | ExecutionException e) {
+            if (!(e.getCause() instanceof StatusRuntimeException)) {
+                // gRPC errors are logged in the client.
+                log.error("Exception while reading counter '{}' from {}: {}",
+                          counterId, deviceId, e);
+            }
+            return Collections.emptyMap();
+        }
     }
 
     enum Operation {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
index 464c4b6..fb5e892 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeGroupProgrammable.java
@@ -17,180 +17,86 @@
 package org.onosproject.drivers.p4runtime;
 
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import org.onosproject.core.GroupId;
-import org.onosproject.net.Device;
+import org.onosproject.drivers.p4runtime.mirror.P4RuntimeGroupMirror;
 import org.onosproject.net.DeviceId;
-import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.group.DefaultGroup;
 import org.onosproject.net.group.Group;
 import org.onosproject.net.group.GroupOperation;
 import org.onosproject.net.group.GroupOperations;
 import org.onosproject.net.group.GroupProgrammable;
 import org.onosproject.net.group.GroupStore;
 import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiActionProfileModel;
 import org.onosproject.net.pi.runtime.PiActionGroup;
-import org.onosproject.net.pi.runtime.PiActionGroupId;
-import org.onosproject.net.pi.service.PiTranslationService;
-import org.onosproject.p4runtime.api.P4RuntimeClient;
-import org.onosproject.p4runtime.api.P4RuntimeGroupReference;
-import org.onosproject.p4runtime.api.P4RuntimeGroupWrapper;
+import org.onosproject.net.pi.runtime.PiActionGroupHandle;
+import org.onosproject.net.pi.service.PiGroupTranslator;
+import org.onosproject.net.pi.service.PiTranslatedEntity;
+import org.onosproject.net.pi.service.PiTranslationException;
 import org.slf4j.Logger;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
+import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.DELETE;
+import static org.onosproject.p4runtime.api.P4RuntimeClient.WriteOperationType.INSERT;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Implementation of the group programmable behaviour for P4Runtime.
  */
-public class P4RuntimeGroupProgrammable extends AbstractP4RuntimeHandlerBehaviour implements GroupProgrammable {
-    private static final String ACT_GRP_MEMS = "action group members";
-    private static final String DELETE = "delete";
-    private static final String ACT_GRP = "action group";
-    private static final String INSERT = "insert";
+public class P4RuntimeGroupProgrammable
+        extends AbstractP4RuntimeHandlerBehaviour
+        implements GroupProgrammable {
+
+    private enum Operation {
+        APPLY, REMOVE
+    }
+
+    private static final String ACT_GRP_MEMS_STR = "action group members";
+    private static final String DELETE_STR = "delete";
+    private static final String ACT_GRP_STR = "action group";
+    private static final String INSERT_STR = "insert";
+
     private static final Logger log = getLogger(P4RuntimeGroupProgrammable.class);
 
-    /*
-     * About action groups in P4runtime:
-     * The type field is a place holder in p4runtime.proto right now, and we haven't defined it yet. You can assume all
-     * the groups are "select" as per the OF spec. As a remainder, in the P4 terminology a member corresponds to an OF
-     * bucket. Each member can also be used directly in the match table (kind of like an OF indirect group).
-     */
+    // If true, we ignore re-installing groups that are already known in the
+    // device mirror.
+    private boolean checkMirrorBeforeUpdate = true;
 
-    // TODO: make this attribute configurable by child drivers (e.g. BMv2 or Tofino)
-    /*
-    When updating an existing rule, if true, we issue a DELETE operation before inserting the new one, otherwise we
-    issue a MODIFY operation. This is useful fore devices that do not support MODIFY operations for table entries.
-     */
-    private boolean deleteBeforeUpdate = true;
-
-    // TODO: can remove this check as soon as the multi-apply-per-same-flow rule bug is fixed.
-    /*
-    If true, we ignore re-installing rules that are already known in the ENTRY_STORE, i.e. same match key and action.
-     */
-    private boolean checkStoreBeforeUpdate = true;
+    private GroupStore groupStore;
+    private P4RuntimeGroupMirror groupMirror;
+    private PiGroupTranslator translator;
 
     // Needed to synchronize operations over the same group.
-    private static final Map<P4RuntimeGroupReference, Lock> GROUP_LOCKS = Maps.newConcurrentMap();
-
-    // TODO: replace with distribute store
-    private static final Map<P4RuntimeGroupReference, P4RuntimeGroupWrapper> GROUP_STORE = Maps.newConcurrentMap();
+    private static final Map<PiActionGroupHandle, Lock> GROUP_LOCKS =
+            Maps.newConcurrentMap();
 
     @Override
-    public void performGroupOperation(DeviceId deviceId, GroupOperations groupOps) {
+    protected boolean setupBehaviour() {
+        if (!super.setupBehaviour()) {
+            return false;
+        }
+        groupMirror = this.handler().get(P4RuntimeGroupMirror.class);
+        groupStore = handler().get(GroupStore.class);
+        translator = piTranslationService.groupTranslator();
+        return true;
+    }
+
+    @Override
+    public void performGroupOperation(DeviceId deviceId,
+                                      GroupOperations groupOps) {
         if (!setupBehaviour()) {
             return;
         }
-
-        Device device = handler().get(DeviceService.class).getDevice(deviceId);
-
-        for (GroupOperation groupOp : groupOps.operations()) {
-            processGroupOp(device, groupOp);
-        }
-    }
-
-    private void processGroupOp(Device device, GroupOperation groupOp) {
-        GroupId groupId = groupOp.groupId();
-        GroupStore groupStore = handler().get(GroupStore.class);
-        Group group = groupStore.getGroup(device.id(), groupId);
-
-        PiActionGroup piActionGroup;
-        try {
-            piActionGroup = piTranslationService.translateGroup(group, pipeconf);
-        } catch (PiTranslationService.PiTranslationException e) {
-            log.warn("Unable translate group, aborting group operation {}: {}", groupOp.opType(), e.getMessage());
-            return;
-        }
-
-        P4RuntimeGroupReference groupRef = new P4RuntimeGroupReference(deviceId, piActionGroup.actionProfileId(),
-                                                                       piActionGroup.id());
-
-        Lock lock = GROUP_LOCKS.computeIfAbsent(groupRef, k -> new ReentrantLock());
-        lock.lock();
-
-        try {
-            P4RuntimeGroupWrapper oldGroupWrapper = GROUP_STORE.get(groupRef);
-            P4RuntimeGroupWrapper newGroupWrapper = new P4RuntimeGroupWrapper(piActionGroup, group,
-                                                                              System.currentTimeMillis());
-            switch (groupOp.opType()) {
-                case ADD:
-                case MODIFY:
-                    if (writeGroupToDevice(oldGroupWrapper, piActionGroup)) {
-                        GROUP_STORE.put(groupRef, newGroupWrapper);
-                    }
-                    break;
-                case DELETE:
-                    if (deleteGroupFromDevice(piActionGroup)) {
-                        GROUP_STORE.remove(groupRef);
-                    }
-                    break;
-                default:
-                    log.warn("Group operation {} not supported", groupOp.opType());
-            }
-        } finally {
-            lock.unlock();
-        }
-    }
-
-    /**
-     * Installs action group and members to device via client interface.
-     *
-     * @param oldGroupWrapper old group wrapper for the group; null if not exists
-     * @param piActionGroup   the action group to be installed
-     * @return true if install success; false otherwise
-     */
-    private boolean writeGroupToDevice(P4RuntimeGroupWrapper oldGroupWrapper, PiActionGroup piActionGroup) {
-        boolean success = true;
-        CompletableFuture<Boolean> writeSuccess;
-        if (checkStoreBeforeUpdate && oldGroupWrapper != null &&
-                oldGroupWrapper.piActionGroup().equals(piActionGroup)) {
-            // Action group already exists, ignore it
-            return true;
-        }
-        if (deleteBeforeUpdate && oldGroupWrapper != null) {
-            success = deleteGroupFromDevice(oldGroupWrapper.piActionGroup());
-        }
-        writeSuccess = client.writeActionGroupMembers(piActionGroup,
-                                                      P4RuntimeClient.WriteOperationType.INSERT,
-                                                      pipeconf);
-        success = success && completeSuccess(writeSuccess, ACT_GRP_MEMS, INSERT);
-
-        writeSuccess = client.writeActionGroup(piActionGroup,
-                                               P4RuntimeClient.WriteOperationType.INSERT,
-                                               pipeconf);
-        success = success && completeSuccess(writeSuccess, ACT_GRP, INSERT);
-        return success;
-    }
-
-    private boolean deleteGroupFromDevice(PiActionGroup piActionGroup) {
-        boolean success;
-        CompletableFuture<Boolean> writeSuccess;
-        writeSuccess = client.writeActionGroup(piActionGroup,
-                                               P4RuntimeClient.WriteOperationType.DELETE,
-                                               pipeconf);
-        success = completeSuccess(writeSuccess, ACT_GRP, DELETE);
-        writeSuccess = client.writeActionGroupMembers(piActionGroup,
-                                                      P4RuntimeClient.WriteOperationType.DELETE,
-                                                      pipeconf);
-        success = success && completeSuccess(writeSuccess, ACT_GRP_MEMS, DELETE);
-        return success;
-    }
-
-    private boolean completeSuccess(CompletableFuture<Boolean> completableFuture,
-                                    String topic, String action) {
-        try {
-            return completableFuture.get();
-        } catch (InterruptedException | ExecutionException e) {
-            log.warn("Can't {} {} due to {}", action, topic, e.getMessage());
-            return false;
-        }
+        groupOps.operations().forEach(op -> processGroupOp(deviceId, op));
     }
 
     @Override
@@ -198,58 +104,147 @@
         if (!setupBehaviour()) {
             return Collections.emptyList();
         }
+        return pipeconf.pipelineModel().actionProfiles().stream()
+                .map(PiActionProfileModel::id)
+                .flatMap(this::streamGroupsFromDevice)
+                .collect(Collectors.toList());
+    }
 
-        Collection<Group> result = Sets.newHashSet();
-        Collection<PiActionProfileId> piActionProfileIds = Sets.newHashSet();
+    private void processGroupOp(DeviceId deviceId, GroupOperation groupOp) {
+        final Group pdGroup = groupStore.getGroup(deviceId, groupOp.groupId());
 
-        // TODO: find better way to get all action profile ids. e.g. by providing them in the interpreter
-        GROUP_STORE.forEach((groupRef, wrapper) -> piActionProfileIds.add(groupRef.actionProfileId()));
+        final PiActionGroup piGroup;
+        try {
+            piGroup = translator.translate(pdGroup, pipeconf);
+        } catch (PiTranslationException e) {
+            log.warn("Unable translate group, aborting {} operation: {}",
+                     groupOp.opType(), e.getMessage());
+            return;
+        }
 
-        AtomicBoolean success = new AtomicBoolean(true);
-        piActionProfileIds.forEach(actionProfileId -> {
-            Collection<PiActionGroup> piActionGroups = Sets.newHashSet();
-            try {
-                Collection<PiActionGroup> groupsFromDevice =
-                        client.dumpGroups(actionProfileId, pipeconf).get();
-                if (groupsFromDevice == null) {
-                    // Got error
-                    success.set(false);
-                } else {
-                    piActionGroups.addAll(groupsFromDevice);
-                }
-            } catch (ExecutionException | InterruptedException e) {
-                log.error("Exception while dumping groups for action profile {}: {}",
-                          actionProfileId.id(), deviceId, e);
-                success.set(false);
+        final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, piGroup);
+
+        final PiActionGroup groupOnDevice = groupMirror.get(handle) == null
+                ? null
+                : groupMirror.get(handle).entry();
+
+        final Lock lock = GROUP_LOCKS.computeIfAbsent(handle, k -> new ReentrantLock());
+        lock.lock();
+        try {
+            final Operation operation;
+            switch (groupOp.opType()) {
+                case ADD:
+                case MODIFY:
+                    operation = Operation.APPLY;
+                    break;
+                case DELETE:
+                    operation = Operation.REMOVE;
+                    break;
+                default:
+                    log.warn("Group operation {} not supported", groupOp.opType());
+                    return;
             }
-
-            piActionGroups.forEach(piActionGroup -> {
-                PiActionGroupId actionGroupId = piActionGroup.id();
-                P4RuntimeGroupReference groupRef =
-                        new P4RuntimeGroupReference(deviceId, actionProfileId, actionGroupId);
-                P4RuntimeGroupWrapper wrapper = GROUP_STORE.get(groupRef);
-
-                if (wrapper == null) {
-                    // group exists in client, but can't find in ONOS
-                    log.warn("Can't find action profile group {} from local store.",
-                             groupRef);
-                    return;
-                }
-                if (!wrapper.piActionGroup().equals(piActionGroup)) {
-                    log.warn("Group from device is different to group from local store.");
-                    return;
-                }
-                result.add(wrapper.group());
-
-            });
-        });
-
-        if (!success.get()) {
-            // Got error while dump groups from device.
-            return Collections.emptySet();
-        } else {
-            return result;
+            processPiGroup(handle, piGroup,
+                           groupOnDevice, pdGroup, operation);
+        } finally {
+            lock.unlock();
         }
     }
 
+    private void processPiGroup(PiActionGroupHandle handle,
+                                PiActionGroup groupToApply,
+                                PiActionGroup groupOnDevice,
+                                Group pdGroup, Operation operation) {
+        if (operation == Operation.APPLY) {
+            if (groupOnDevice != null) {
+                if (checkMirrorBeforeUpdate
+                        && groupOnDevice.equals(groupToApply)) {
+                    // Group on device has the same members, ignore operation.
+                    return;
+                }
+                // Remove before adding it.
+                processPiGroup(handle, groupToApply, groupOnDevice,
+                               pdGroup, Operation.REMOVE);
+            }
+            if (writeGroupToDevice(groupToApply)) {
+                groupMirror.put(handle, groupToApply);
+                translator.learn(handle, new PiTranslatedEntity<>(
+                        pdGroup, groupToApply, handle));
+            }
+        } else {
+            if (deleteGroupFromDevice(groupToApply)) {
+                groupMirror.remove(handle);
+                translator.forget(handle);
+            }
+        }
+    }
+
+    private boolean writeGroupToDevice(PiActionGroup groupToApply) {
+        // First insert members, then group.
+        // The operation is deemed successful if both operations are successful.
+        // FIXME: add transactional semantics, i.e. remove members if group fails.
+        final boolean membersSuccess = completeFuture(
+                client.writeActionGroupMembers(groupToApply, INSERT, pipeconf),
+                ACT_GRP_MEMS_STR, INSERT_STR);
+        return membersSuccess && completeFuture(
+                client.writeActionGroup(groupToApply, INSERT, pipeconf),
+                ACT_GRP_STR, INSERT_STR);
+    }
+
+    private boolean deleteGroupFromDevice(PiActionGroup piActionGroup) {
+        // First delete group, then members.
+        // The operation is deemed successful if both operations are successful.
+        final boolean groupSuccess = completeFuture(
+                client.writeActionGroup(piActionGroup, DELETE, pipeconf),
+                ACT_GRP_STR, DELETE_STR);
+        return groupSuccess && completeFuture(
+                client.writeActionGroupMembers(piActionGroup, DELETE, pipeconf),
+                ACT_GRP_MEMS_STR, DELETE_STR);
+    }
+
+    private boolean completeFuture(CompletableFuture<Boolean> completableFuture,
+                                   String topic, String action) {
+        try {
+            if (completableFuture.get()) {
+                return true;
+            } else {
+                log.warn("Unable to {} {}", action, topic);
+                return false;
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            log.warn("Exception while performing {} {}: {}", action, topic, e.getMessage());
+            log.debug("Exception", e);
+            return false;
+        }
+    }
+
+    private Stream<Group> streamGroupsFromDevice(PiActionProfileId actProfId) {
+        try {
+            // Read PI groups and return original PD one.
+            return client.dumpGroups(actProfId, pipeconf).get().stream()
+                    .map(this::forgeGroupEntry)
+                    .filter(Objects::nonNull);
+        } catch (ExecutionException | InterruptedException e) {
+            log.error("Exception while dumping groups from action profile '{}' on {}: {}",
+                      actProfId.id(), deviceId, e);
+            return Stream.empty();
+        }
+    }
+
+    private Group forgeGroupEntry(PiActionGroup piGroup) {
+        final PiActionGroupHandle handle = PiActionGroupHandle.of(deviceId, piGroup);
+        if (!translator.lookup(handle).isPresent()) {
+            log.warn("Missing PI group from translation store: {} - {}:{}",
+                     pipeconf.id(), piGroup.actionProfileId(),
+                     piGroup.id());
+            return null;
+        }
+        final long life = groupMirror.get(handle) != null
+                ? groupMirror.get(handle).lifeSec() : 0;
+        final Group original = translator.lookup(handle).get().original();
+        final DefaultGroup forgedGroup = new DefaultGroup(original.id(), original);
+        forgedGroup.setState(Group.GroupState.ADDED);
+        forgedGroup.setLife(life);
+        return forgedGroup;
+    }
 }
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
index 7e41a99..f49ad18 100644
--- a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/P4RuntimeHandshaker.java
@@ -66,7 +66,7 @@
 
         client = controller.getClient(deviceId);
         if (client == null || !controller.isReacheable(deviceId)) {
-            result.complete(MastershipRole.STANDBY);
+            result.complete(MastershipRole.NONE);
             return result;
         }
         if (newRole.equals(MastershipRole.MASTER)) {
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
new file mode 100644
index 0000000..be0b0b3
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/AbstractDistributedP4RuntimeMirror.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import com.google.common.annotations.Beta;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Abstract implementation of a distributed P4Runtime mirror, backed by an
+ * {@link EventuallyConsistentMap}.
+ *
+ * @param <H> handle class
+ * @param <E> entry class
+ */
+@Beta
+@Component(immediate = true)
+public abstract class AbstractDistributedP4RuntimeMirror
+        <H extends PiHandle, E extends PiEntity>
+        implements P4RuntimeMirror<H, E> {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private StorageService storageService;
+
+    private EventuallyConsistentMap<H, TimedEntry<E>> mirrorMap;
+
+    @Activate
+    public void activate() {
+        mirrorMap = storageService
+                .<H, TimedEntry<E>>eventuallyConsistentMapBuilder()
+                .withName(mapName())
+                .withSerializer(storeSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+        log.info("Started");
+    }
+
+    abstract String mapName();
+
+    abstract KryoNamespace storeSerializer();
+
+    @Deactivate
+    public void deactivate() {
+        mirrorMap = null;
+        log.info("Stopped");
+    }
+
+    @Override
+    public Collection<TimedEntry<E>> getAll(DeviceId deviceId) {
+        checkNotNull(deviceId);
+        return mirrorMap.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                .map(Map.Entry::getValue)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public TimedEntry<E> get(H handle) {
+        checkNotNull(handle);
+        return mirrorMap.get(handle);
+    }
+
+    @Override
+    public void put(H handle, E entry) {
+        checkNotNull(handle);
+        checkNotNull(entry);
+        final long now = new WallClockTimestamp().unixTimestamp();
+        final TimedEntry<E> timedEntry = new TimedEntry<>(now, entry);
+        mirrorMap.put(handle, timedEntry);
+    }
+
+    @Override
+    public void remove(H handle) {
+        checkNotNull(handle);
+        mirrorMap.remove(handle);
+    }
+
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeGroupMirror.java
new file mode 100644
index 0000000..4c963a6
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeGroupMirror.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupHandle;
+import org.onosproject.store.serializers.KryoNamespaces;
+
+/**
+ * Distributed implementation of a P4Runtime group mirror.
+ */
+@Component(immediate = true)
+@Service
+public final class DistributedP4RuntimeGroupMirror
+        extends AbstractDistributedP4RuntimeMirror
+                        <PiActionGroupHandle, PiActionGroup>
+        implements P4RuntimeGroupMirror {
+
+    private static final String DIST_MAP_NAME = "onos-p4runtime-group-mirror";
+
+    @Override
+    String mapName() {
+        return DIST_MAP_NAME;
+    }
+
+    @Override
+    KryoNamespace storeSerializer() {
+        return KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(TimedEntry.class)
+                .build();
+    }
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java
new file mode 100644
index 0000000..f37cf44
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/DistributedP4RuntimeTableMirror.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.store.serializers.KryoNamespaces;
+
+/**
+ * Distributed implementation of a P4Runtime table mirror.
+ */
+@Component(immediate = true)
+@Service
+public final class DistributedP4RuntimeTableMirror
+        extends AbstractDistributedP4RuntimeMirror
+                        <PiTableEntryHandle, PiTableEntry>
+        implements P4RuntimeTableMirror {
+
+    private static final String DIST_MAP_NAME = "onos-p4runtime-table-mirror";
+
+    @Override
+    String mapName() {
+        return DIST_MAP_NAME;
+    }
+
+    @Override
+    KryoNamespace storeSerializer() {
+        return KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(TimedEntry.class)
+                .build();
+    }
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeGroupMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeGroupMirror.java
new file mode 100644
index 0000000..f363e71
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeGroupMirror.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.onosproject.net.pi.runtime.PiActionGroup;
+import org.onosproject.net.pi.runtime.PiActionGroupHandle;
+
+/**
+ * Mirror of action groups installed on a P4Runtime device.
+ */
+public interface P4RuntimeGroupMirror
+        extends P4RuntimeMirror<PiActionGroupHandle, PiActionGroup> {
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
new file mode 100644
index 0000000..ab18c9d
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeMirror.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+
+import java.util.Collection;
+
+/**
+ * Service to keep track of the device state for a given class of PI entities.
+ * The need of this service comes from the fact that P4 Runtime makes a
+ * distinction between INSERT and MODIFY operations, while ONOS drivers use a
+ * more generic "APPLY" behaviour (i.e. ADD or UPDATE). When applying an entry,
+ * we need to know if another one with the same handle (e.g. table entry with
+ * same match key) is already on the device to decide between INSERT or MODIFY.
+ * Moreover, this service maintains a "timed" version of PI entities such that
+ * we can compute the life of the entity on the device.
+ *
+ * @param <H> Handle class
+ * @param <E> Entity class
+ */
+@Beta
+public interface P4RuntimeMirror
+        <H extends PiHandle, E extends PiEntity> {
+
+    /**
+     * Returns all entries for the given device ID.
+     *
+     * @param deviceId device ID
+     * @return collection of table entries
+     */
+    Collection<TimedEntry<E>> getAll(DeviceId deviceId);
+
+    /**
+     * Returns entry associated to the given handle, if present, otherwise
+     * null.
+     *
+     * @param handle handle
+     * @return PI table entry
+     */
+    TimedEntry<E> get(H handle);
+
+    /**
+     * Stores the given entry associating it to the given handle.
+     *
+     * @param handle handle
+     * @param entry  entry
+     */
+    void put(H handle, E entry);
+
+    /**
+     * Removes the entry associated to the given handle.
+     *
+     * @param handle handle
+     */
+    void remove(H handle);
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeTableMirror.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeTableMirror.java
new file mode 100644
index 0000000..318e2b0
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/P4RuntimeTableMirror.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+
+/**
+ * Mirror of table entries installed on a P4Runtime device.
+ */
+public interface P4RuntimeTableMirror
+        extends P4RuntimeMirror<PiTableEntryHandle, PiTableEntry> {
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/TimedEntry.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/TimedEntry.java
new file mode 100644
index 0000000..76b44a0
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/TimedEntry.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.drivers.p4runtime.mirror;
+
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.store.service.WallClockTimestamp;
+
+public class TimedEntry<E extends PiEntity> {
+
+    private final long timestamp;
+    private final E entity;
+
+    TimedEntry(long timestamp, E entity) {
+        this.timestamp = timestamp;
+        this.entity = entity;
+    }
+
+    public long timestamp() {
+        return timestamp;
+    }
+
+    public E entry() {
+        return entity;
+    }
+
+    public long lifeSec() {
+        final long now = new WallClockTimestamp().unixTimestamp();
+        return (now - timestamp) / 1000;
+    }
+}
diff --git a/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/package-info.java b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/package-info.java
new file mode 100644
index 0000000..d9b21d6
--- /dev/null
+++ b/drivers/p4runtime/src/main/java/org/onosproject/drivers/p4runtime/mirror/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * P4 Runtime device mirror.
+ */
+package org.onosproject.drivers.p4runtime.mirror;
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
index b71acbf..b6ee3c2 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicConstants.java
@@ -38,12 +38,15 @@
     public static final String DOT =  ".";
     public static final String HDR = "hdr";
     public static final String ETHERNET = "ethernet";
+    public static final String IPV4 = "ipv4";
     public static final String LOCAL_METADATA = "local_metadata";
     public static final String STANDARD_METADATA = "standard_metadata";
     public static final PiMatchFieldId HDR_IN_PORT_ID = PiMatchFieldId.of(STANDARD_METADATA + DOT + "ingress_port");
     public static final PiMatchFieldId HDR_ETH_DST_ID = PiMatchFieldId.of(HDR + DOT + ETHERNET + DOT + "dst_addr");
     public static final PiMatchFieldId HDR_ETH_SRC_ID = PiMatchFieldId.of(HDR + DOT + ETHERNET + DOT + "src_addr");
     public static final PiMatchFieldId HDR_ETH_TYPE_ID = PiMatchFieldId.of(HDR + DOT + ETHERNET + DOT + "ether_type");
+    public static final PiMatchFieldId HDR_IPV4_DST_ID = PiMatchFieldId.of(HDR + DOT + IPV4 + DOT + "dst_addr");
+    public static final PiMatchFieldId HDR_IPV4_SRC_ID = PiMatchFieldId.of(HDR + DOT + IPV4 + DOT + "src_addr");
     public static final PiMatchFieldId HDR_NEXT_HOP_ID = PiMatchFieldId.of(LOCAL_METADATA + DOT + "next_hop_id");
     public static final PiMatchFieldId HDR_SELECTOR_ID = PiMatchFieldId.of(LOCAL_METADATA + DOT + "selector");
     // Table IDs
@@ -66,7 +69,7 @@
     public static final PiActionParamId ACT_PRM_PORT_ID = PiActionParamId.of("port");
     public static final PiActionParamId ACT_PRM_NEXT_HOP_ID = PiActionParamId.of("next_hop_id");
     // Action Profile IDs
-    public static final PiActionProfileId ACT_PRF_WCMP_SELECTOR_ID = PiActionProfileId.of("wcmp_selector");
+    public static final PiActionProfileId ACT_PRF_WCMP_SELECTOR_ID = PiActionProfileId.of("wcmp_control.wcmp_selector");
     // Packet Metadata IDs
     public static final PiControlMetadataId PKT_META_EGRESS_PORT_ID = PiControlMetadataId.of("egress_port");
     public static final PiControlMetadataId PKT_META_INGRESS_PORT_ID = PiControlMetadataId.of("ingress_port");
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
index cc0ef46..8e7b399 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/BasicInterpreterImpl.java
@@ -66,6 +66,8 @@
 import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_SRC_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.HDR_ETH_TYPE_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.HDR_IN_PORT_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_IPV4_DST_ID;
+import static org.onosproject.pipelines.basic.BasicConstants.HDR_IPV4_SRC_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.PKT_META_EGRESS_PORT_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.PKT_META_INGRESS_PORT_ID;
 import static org.onosproject.pipelines.basic.BasicConstants.PORT_BITWIDTH;
@@ -93,6 +95,8 @@
                     .put(Criterion.Type.ETH_DST, HDR_ETH_DST_ID)
                     .put(Criterion.Type.ETH_SRC, HDR_ETH_SRC_ID)
                     .put(Criterion.Type.ETH_TYPE, HDR_ETH_TYPE_ID)
+                    .put(Criterion.Type.IPV4_SRC, HDR_IPV4_SRC_ID)
+                    .put(Criterion.Type.IPV4_DST, HDR_IPV4_DST_ID)
                     .build();
 
     @Override
diff --git a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PipeconfLoader.java b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PipeconfLoader.java
index 399d4db..9b33883 100644
--- a/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PipeconfLoader.java
+++ b/pipelines/basic/src/main/java/org/onosproject/pipelines/basic/PipeconfLoader.java
@@ -50,14 +50,9 @@
     private static final String BASIC_JSON_PATH = "/p4c-out/bmv2/basic.json";
     private static final String BASIC_P4INFO = "/p4c-out/bmv2/basic.p4info";
 
-    private static final PiPipeconfId ECMP_PIPECONF_ID = new PiPipeconfId("org.onosproject.pipelines.ecmp");
-    private static final String ECMP_JSON_PATH = "/p4c-out/bmv2/ecmp.json";
-    private static final String ECMP_P4INFO = "/p4c-out/bmv2/ecmp.p4info";
-
     public static final PiPipeconf BASIC_PIPECONF = buildBasicPipeconf();
-    public static final PiPipeconf ECMP_PIPECONF = buildEcmpPipeconf();
 
-    private static final Collection<PiPipeconf> ALL_PIPECONFS = ImmutableList.of(BASIC_PIPECONF, ECMP_PIPECONF);
+    private static final Collection<PiPipeconf> ALL_PIPECONFS = ImmutableList.of(BASIC_PIPECONF);
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     private PiPipeconfService piPipeconfService;
@@ -90,21 +85,6 @@
                 .build();
     }
 
-    private static PiPipeconf buildEcmpPipeconf() {
-        final URL jsonUrl = PipeconfLoader.class.getResource(ECMP_JSON_PATH);
-        final URL p4InfoUrl = PipeconfLoader.class.getResource(ECMP_P4INFO);
-
-        return DefaultPiPipeconf.builder()
-                .withId(ECMP_PIPECONF_ID)
-                .withPipelineModel(parseP4Info(p4InfoUrl))
-                .addBehaviour(PiPipelineInterpreter.class, EcmpInterpreterImpl.class)
-                .addBehaviour(Pipeliner.class, DefaultSingleTablePipeline.class)
-                .addBehaviour(PortStatisticsDiscovery.class, PortStatisticsDiscoveryImpl.class)
-                .addExtension(P4_INFO_TEXT, p4InfoUrl)
-                .addExtension(BMV2_JSON, jsonUrl)
-                .build();
-    }
-
     private static PiPipelineModel parseP4Info(URL p4InfoUrl) {
         try {
             return P4InfoParser.parse(p4InfoUrl);
diff --git a/pipelines/basic/src/main/resources/Makefile b/pipelines/basic/src/main/resources/Makefile
index f695e02..2348ba6 100644
--- a/pipelines/basic/src/main/resources/Makefile
+++ b/pipelines/basic/src/main/resources/Makefile
@@ -1,14 +1,10 @@
-all: basic ecmp
+all: basic
 
 basic: basic.p4
 	p4c-bm2-ss -o p4c-out/bmv2/basic.json \
 		--p4runtime-file p4c-out/bmv2/basic.p4info \
 		--p4runtime-format text basic.p4
 
-ecmp: ecmp.p4
-	p4c-bm2-ss -o p4c-out/bmv2/ecmp.json \
-		--p4runtime-file p4c-out/bmv2/ecmp.p4info \
-		--p4runtime-format text ecmp.p4
 clean:
 	rm -rf p4c-out/bmv2/*.json
-	rm -rf p4c-out/bmv2/*.p4info
\ No newline at end of file
+	rm -rf p4c-out/bmv2/*.p4info
diff --git a/pipelines/basic/src/main/resources/ecmp.p4 b/pipelines/basic/src/main/resources/ecmp.p4
deleted file mode 100644
index ab86c39..0000000
--- a/pipelines/basic/src/main/resources/ecmp.p4
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <core.p4>
-#include <v1model.p4>
-
-#include "include/headers.p4"
-#include "include/defines.p4"
-#include "include/parsers.p4"
-#include "include/actions.p4"
-#include "include/port_counters.p4"
-#include "include/checksums.p4"
-#include "include/packet_io.p4"
-#include "include/table0.p4"
-
-// FIXME: this program is obsolete and should be removed.
-// The PI ECMP demo app should be refactored to use the WCMP capability of default.p4
-
-// Expected number of ports of an ECMP group.
-// This value is fixed, .i.e. we do not support ECMP over port groups of different
-// size. Due to hardware limitations, this value must be constant and a power of 2.
-
-#define ECMP_GROUP_SIZE 128w2
-
-//------------------------------------------------------------------------------
-// INGRESS PIPELINE
-//------------------------------------------------------------------------------
-
-control ingress(inout headers_t hdr,
-                inout local_metadata_t local_metadata,
-                inout standard_metadata_t standard_metadata) {
-
-    direct_counter(CounterType.packets_and_bytes) ecmp_table_counter;
-
-    table ecmp_table {
-        key = {
-            local_metadata.next_hop_id : exact;
-            local_metadata.selector    : exact;
-        }
-        actions = {
-            set_egress_port(standard_metadata);
-        }
-        counters = ecmp_table_counter;
-    }
-
-    action set_ecmp_selector() {
-        hash(local_metadata.selector, HashAlgorithm.crc16, (bit<64>) 0,
-             {
-                 hdr.ipv4.src_addr,
-                 hdr.ipv4.dst_addr,
-                 hdr.ipv4.protocol,
-                 local_metadata.l4_src_port,
-                 local_metadata.l4_dst_port
-             },
-             ECMP_GROUP_SIZE);
-    }
-
-    apply {
-        port_counters_ingress.apply(hdr, standard_metadata);
-        packetio_ingress.apply(hdr, standard_metadata);
-        table0_control.apply(hdr, local_metadata, standard_metadata);
-        if (local_metadata.next_hop_id > 0) {
-            set_ecmp_selector();
-            ecmp_table.apply();
-        }
-     }
-}
-
-//------------------------------------------------------------------------------
-// EGRESS PIPELINE
-//------------------------------------------------------------------------------
-
-control egress(inout headers_t hdr,
-               inout local_metadata_t local_metadata,
-               inout standard_metadata_t standard_metadata) {
-
-    apply {
-        port_counters_egress.apply(hdr, standard_metadata);
-        packetio_egress.apply(hdr, standard_metadata);
-    }
-}
-
-//------------------------------------------------------------------------------
-// SWITCH INSTANTIATION
-//------------------------------------------------------------------------------
-
-V1Switch(parser_impl(),
-         verify_checksum_control(),
-         ingress(),
-         egress(),
-         compute_checksum_control(),
-         deparser()) main;
diff --git a/pipelines/basic/src/main/resources/include/headers.p4 b/pipelines/basic/src/main/resources/include/headers.p4
index b943c82..038e952 100644
--- a/pipelines/basic/src/main/resources/include/headers.p4
+++ b/pipelines/basic/src/main/resources/include/headers.p4
@@ -84,7 +84,6 @@
     bit<16>       l4_src_port;
     bit<16>       l4_dst_port;
     next_hop_id_t next_hop_id;
-    bit<16>       selector;
 }
 
 #endif
diff --git a/pipelines/basic/src/main/resources/p4c-out/bmv2/basic.json b/pipelines/basic/src/main/resources/p4c-out/bmv2/basic.json
index a38b290..f1a65d1 100644
--- a/pipelines/basic/src/main/resources/p4c-out/bmv2/basic.json
+++ b/pipelines/basic/src/main/resources/p4c-out/bmv2/basic.json
@@ -13,8 +13,7 @@
         ["tmp_0", 32, false],
         ["local_metadata_t.l4_src_port", 16, false],
         ["local_metadata_t.l4_dst_port", 16, false],
-        ["local_metadata_t.next_hop_id", 16, false],
-        ["local_metadata_t.selector", 16, false]
+        ["local_metadata_t.next_hop_id", 16, false]
       ]
     },
     {
diff --git a/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.json b/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.json
deleted file mode 100644
index 9c687f9..0000000
--- a/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.json
+++ /dev/null
@@ -1,1244 +0,0 @@
-{
-  "program" : "ecmp.p4",
-  "__meta__" : {
-    "version" : [2, 7],
-    "compiler" : "https://github.com/p4lang/p4c"
-  },
-  "header_types" : [
-    {
-      "name" : "scalars_0",
-      "id" : 0,
-      "fields" : [
-        ["tmp", 32, false],
-        ["tmp_0", 32, false],
-        ["local_metadata_t.l4_src_port", 16, false],
-        ["local_metadata_t.l4_dst_port", 16, false],
-        ["local_metadata_t.next_hop_id", 16, false],
-        ["local_metadata_t.selector", 16, false]
-      ]
-    },
-    {
-      "name" : "ethernet_t",
-      "id" : 1,
-      "fields" : [
-        ["dst_addr", 48, false],
-        ["src_addr", 48, false],
-        ["ether_type", 16, false]
-      ]
-    },
-    {
-      "name" : "ipv4_t",
-      "id" : 2,
-      "fields" : [
-        ["version", 4, false],
-        ["ihl", 4, false],
-        ["diffserv", 8, false],
-        ["len", 16, false],
-        ["identification", 16, false],
-        ["flags", 3, false],
-        ["frag_offset", 13, false],
-        ["ttl", 8, false],
-        ["protocol", 8, false],
-        ["hdr_checksum", 16, false],
-        ["src_addr", 32, false],
-        ["dst_addr", 32, false]
-      ]
-    },
-    {
-      "name" : "tcp_t",
-      "id" : 3,
-      "fields" : [
-        ["src_port", 16, false],
-        ["dst_port", 16, false],
-        ["seq_no", 32, false],
-        ["ack_no", 32, false],
-        ["data_offset", 4, false],
-        ["res", 3, false],
-        ["ecn", 3, false],
-        ["ctrl", 6, false],
-        ["window", 16, false],
-        ["checksum", 16, false],
-        ["urgent_ptr", 16, false]
-      ]
-    },
-    {
-      "name" : "udp_t",
-      "id" : 4,
-      "fields" : [
-        ["src_port", 16, false],
-        ["dst_port", 16, false],
-        ["length_", 16, false],
-        ["checksum", 16, false]
-      ]
-    },
-    {
-      "name" : "packet_out_header_t",
-      "id" : 5,
-      "fields" : [
-        ["egress_port", 9, false],
-        ["_padding", 7, false]
-      ]
-    },
-    {
-      "name" : "packet_in_header_t",
-      "id" : 6,
-      "fields" : [
-        ["ingress_port", 9, false],
-        ["_padding_0", 7, false]
-      ]
-    },
-    {
-      "name" : "standard_metadata",
-      "id" : 7,
-      "fields" : [
-        ["ingress_port", 9, false],
-        ["egress_spec", 9, false],
-        ["egress_port", 9, false],
-        ["clone_spec", 32, false],
-        ["instance_type", 32, false],
-        ["drop", 1, false],
-        ["recirculate_port", 16, false],
-        ["packet_length", 32, false],
-        ["enq_timestamp", 32, false],
-        ["enq_qdepth", 19, false],
-        ["deq_timedelta", 32, false],
-        ["deq_qdepth", 19, false],
-        ["ingress_global_timestamp", 48, false],
-        ["lf_field_list", 32, false],
-        ["mcast_grp", 16, false],
-        ["resubmit_flag", 1, false],
-        ["egress_rid", 16, false],
-        ["checksum_error", 1, false],
-        ["_padding_1", 4, false]
-      ]
-    }
-  ],
-  "headers" : [
-    {
-      "name" : "scalars",
-      "id" : 0,
-      "header_type" : "scalars_0",
-      "metadata" : true,
-      "pi_omit" : true
-    },
-    {
-      "name" : "standard_metadata",
-      "id" : 1,
-      "header_type" : "standard_metadata",
-      "metadata" : true,
-      "pi_omit" : true
-    },
-    {
-      "name" : "ethernet",
-      "id" : 2,
-      "header_type" : "ethernet_t",
-      "metadata" : false,
-      "pi_omit" : true
-    },
-    {
-      "name" : "ipv4",
-      "id" : 3,
-      "header_type" : "ipv4_t",
-      "metadata" : false,
-      "pi_omit" : true
-    },
-    {
-      "name" : "tcp",
-      "id" : 4,
-      "header_type" : "tcp_t",
-      "metadata" : false,
-      "pi_omit" : true
-    },
-    {
-      "name" : "udp",
-      "id" : 5,
-      "header_type" : "udp_t",
-      "metadata" : false,
-      "pi_omit" : true
-    },
-    {
-      "name" : "packet_out",
-      "id" : 6,
-      "header_type" : "packet_out_header_t",
-      "metadata" : false,
-      "pi_omit" : true
-    },
-    {
-      "name" : "packet_in",
-      "id" : 7,
-      "header_type" : "packet_in_header_t",
-      "metadata" : false,
-      "pi_omit" : true
-    }
-  ],
-  "header_stacks" : [],
-  "header_union_types" : [],
-  "header_unions" : [],
-  "header_union_stacks" : [],
-  "field_lists" : [],
-  "errors" : [
-    ["NoError", 1],
-    ["PacketTooShort", 2],
-    ["NoMatch", 3],
-    ["StackOutOfBounds", 4],
-    ["HeaderTooShort", 5],
-    ["ParserTimeout", 6]
-  ],
-  "enums" : [],
-  "parsers" : [
-    {
-      "name" : "parser",
-      "id" : 0,
-      "init_state" : "start",
-      "parse_states" : [
-        {
-          "name" : "start",
-          "id" : 0,
-          "parser_ops" : [],
-          "transitions" : [
-            {
-              "value" : "0x00ff",
-              "mask" : null,
-              "next_state" : "parse_packet_out"
-            },
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : "parse_ethernet"
-            }
-          ],
-          "transition_key" : [
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "ingress_port"]
-            }
-          ]
-        },
-        {
-          "name" : "parse_packet_out",
-          "id" : 1,
-          "parser_ops" : [
-            {
-              "parameters" : [
-                {
-                  "type" : "regular",
-                  "value" : "packet_out"
-                }
-              ],
-              "op" : "extract"
-            }
-          ],
-          "transitions" : [
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : "parse_ethernet"
-            }
-          ],
-          "transition_key" : []
-        },
-        {
-          "name" : "parse_ethernet",
-          "id" : 2,
-          "parser_ops" : [
-            {
-              "parameters" : [
-                {
-                  "type" : "regular",
-                  "value" : "ethernet"
-                }
-              ],
-              "op" : "extract"
-            }
-          ],
-          "transitions" : [
-            {
-              "value" : "0x0800",
-              "mask" : null,
-              "next_state" : "parse_ipv4"
-            },
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : null
-            }
-          ],
-          "transition_key" : [
-            {
-              "type" : "field",
-              "value" : ["ethernet", "ether_type"]
-            }
-          ]
-        },
-        {
-          "name" : "parse_ipv4",
-          "id" : 3,
-          "parser_ops" : [
-            {
-              "parameters" : [
-                {
-                  "type" : "regular",
-                  "value" : "ipv4"
-                }
-              ],
-              "op" : "extract"
-            }
-          ],
-          "transitions" : [
-            {
-              "value" : "0x06",
-              "mask" : null,
-              "next_state" : "parse_tcp"
-            },
-            {
-              "value" : "0x11",
-              "mask" : null,
-              "next_state" : "parse_udp"
-            },
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : null
-            }
-          ],
-          "transition_key" : [
-            {
-              "type" : "field",
-              "value" : ["ipv4", "protocol"]
-            }
-          ]
-        },
-        {
-          "name" : "parse_tcp",
-          "id" : 4,
-          "parser_ops" : [
-            {
-              "parameters" : [
-                {
-                  "type" : "regular",
-                  "value" : "tcp"
-                }
-              ],
-              "op" : "extract"
-            },
-            {
-              "parameters" : [
-                {
-                  "type" : "field",
-                  "value" : ["scalars", "local_metadata_t.l4_src_port"]
-                },
-                {
-                  "type" : "field",
-                  "value" : ["tcp", "src_port"]
-                }
-              ],
-              "op" : "set"
-            },
-            {
-              "parameters" : [
-                {
-                  "type" : "field",
-                  "value" : ["scalars", "local_metadata_t.l4_dst_port"]
-                },
-                {
-                  "type" : "field",
-                  "value" : ["tcp", "dst_port"]
-                }
-              ],
-              "op" : "set"
-            }
-          ],
-          "transitions" : [
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : null
-            }
-          ],
-          "transition_key" : []
-        },
-        {
-          "name" : "parse_udp",
-          "id" : 5,
-          "parser_ops" : [
-            {
-              "parameters" : [
-                {
-                  "type" : "regular",
-                  "value" : "udp"
-                }
-              ],
-              "op" : "extract"
-            },
-            {
-              "parameters" : [
-                {
-                  "type" : "field",
-                  "value" : ["scalars", "local_metadata_t.l4_src_port"]
-                },
-                {
-                  "type" : "field",
-                  "value" : ["udp", "src_port"]
-                }
-              ],
-              "op" : "set"
-            },
-            {
-              "parameters" : [
-                {
-                  "type" : "field",
-                  "value" : ["scalars", "local_metadata_t.l4_dst_port"]
-                },
-                {
-                  "type" : "field",
-                  "value" : ["udp", "dst_port"]
-                }
-              ],
-              "op" : "set"
-            }
-          ],
-          "transitions" : [
-            {
-              "value" : "default",
-              "mask" : null,
-              "next_state" : null
-            }
-          ],
-          "transition_key" : []
-        }
-      ]
-    }
-  ],
-  "deparsers" : [
-    {
-      "name" : "deparser",
-      "id" : 0,
-      "source_info" : {
-        "filename" : "include/parsers.p4",
-        "line" : 72,
-        "column" : 8,
-        "source_fragment" : "deparser"
-      },
-      "order" : ["packet_in", "ethernet", "ipv4", "tcp", "udp"]
-    }
-  ],
-  "meter_arrays" : [],
-  "counter_arrays" : [
-    {
-      "name" : "port_counters_ingress.ingress_port_counter",
-      "id" : 0,
-      "source_info" : {
-        "filename" : "include/port_counters.p4",
-        "line" : 26,
-        "column" : 38,
-        "source_fragment" : "ingress_port_counter"
-      },
-      "size" : 511,
-      "is_direct" : false
-    },
-    {
-      "name" : "table0_control.table0_counter",
-      "id" : 1,
-      "is_direct" : true,
-      "binding" : "table0_control.table0"
-    },
-    {
-      "name" : "ecmp_table_counter",
-      "id" : 2,
-      "is_direct" : true,
-      "binding" : "ecmp_table"
-    },
-    {
-      "name" : "port_counters_egress.egress_port_counter",
-      "id" : 3,
-      "source_info" : {
-        "filename" : "include/port_counters.p4",
-        "line" : 36,
-        "column" : 38,
-        "source_fragment" : "egress_port_counter"
-      },
-      "size" : 511,
-      "is_direct" : false
-    }
-  ],
-  "register_arrays" : [],
-  "calculations" : [
-    {
-      "name" : "calc",
-      "id" : 0,
-      "algo" : "crc16",
-      "input" : [
-        {
-          "type" : "field",
-          "value" : ["ipv4", "src_addr"]
-        },
-        {
-          "type" : "field",
-          "value" : ["ipv4", "dst_addr"]
-        },
-        {
-          "type" : "field",
-          "value" : ["ipv4", "protocol"]
-        },
-        {
-          "type" : "field",
-          "value" : ["scalars", "local_metadata_t.l4_src_port"]
-        },
-        {
-          "type" : "field",
-          "value" : ["scalars", "local_metadata_t.l4_dst_port"]
-        }
-      ]
-    }
-  ],
-  "learn_lists" : [],
-  "actions" : [
-    {
-      "name" : "set_egress_port",
-      "id" : 0,
-      "runtime_data" : [
-        {
-          "name" : "port",
-          "bitwidth" : 9
-        }
-      ],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "egress_spec"]
-            },
-            {
-              "type" : "runtime_data",
-              "value" : 0
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/actions.p4",
-            "line" : 28,
-            "column" : 36,
-            "source_fragment" : "port; ..."
-          }
-        }
-      ]
-    },
-    {
-      "name" : "set_egress_port",
-      "id" : 1,
-      "runtime_data" : [
-        {
-          "name" : "port",
-          "bitwidth" : 9
-        }
-      ],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "egress_spec"]
-            },
-            {
-              "type" : "runtime_data",
-              "value" : 0
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/actions.p4",
-            "line" : 28,
-            "column" : 36,
-            "source_fragment" : "port; ..."
-          }
-        }
-      ]
-    },
-    {
-      "name" : "send_to_cpu",
-      "id" : 2,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "egress_spec"]
-            },
-            {
-              "type" : "hexstr",
-              "value" : "0x00ff"
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/headers.p4",
-            "line" : 19,
-            "column" : 24,
-            "source_fragment" : "255; ..."
-          }
-        }
-      ]
-    },
-    {
-      "name" : "_drop",
-      "id" : 3,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "drop",
-          "parameters" : [],
-          "source_info" : {
-            "filename" : "include/actions.p4",
-            "line" : 32,
-            "column" : 4,
-            "source_fragment" : "mark_to_drop()"
-          }
-        }
-      ]
-    },
-    {
-      "name" : "NoAction",
-      "id" : 4,
-      "runtime_data" : [],
-      "primitives" : []
-    },
-    {
-      "name" : "table0_control.set_next_hop_id",
-      "id" : 5,
-      "runtime_data" : [
-        {
-          "name" : "next_hop_id",
-          "bitwidth" : 16
-        }
-      ],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["scalars", "local_metadata_t.next_hop_id"]
-            },
-            {
-              "type" : "runtime_data",
-              "value" : 0
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/table0.p4",
-            "line" : 30,
-            "column" : 8,
-            "source_fragment" : "local_metadata.next_hop_id = next_hop_id"
-          }
-        }
-      ]
-    },
-    {
-      "name" : "set_ecmp_selector",
-      "id" : 6,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "modify_field_with_hash_based_offset",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["scalars", "local_metadata_t.selector"]
-            },
-            {
-              "type" : "hexstr",
-              "value" : "0x0000000000000000"
-            },
-            {
-              "type" : "calculation",
-              "value" : "calc"
-            },
-            {
-              "type" : "hexstr",
-              "value" : "0x00000000000000000000000000000002"
-            }
-          ],
-          "source_info" : {
-            "filename" : "ecmp.p4",
-            "line" : 60,
-            "column" : 8,
-            "source_fragment" : "hash(local_metadata.selector, HashAlgorithm.crc16, (bit<64>) 0, ..."
-          }
-        }
-      ]
-    },
-    {
-      "name" : "act",
-      "id" : 7,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "egress_spec"]
-            },
-            {
-              "type" : "field",
-              "value" : ["packet_out", "egress_port"]
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 27,
-            "column" : 12,
-            "source_fragment" : "standard_metadata.egress_spec = hdr.packet_out.egress_port"
-          }
-        },
-        {
-          "op" : "remove_header",
-          "parameters" : [
-            {
-              "type" : "header",
-              "value" : "packet_out"
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 28,
-            "column" : 12,
-            "source_fragment" : "hdr.packet_out.setInvalid()"
-          }
-        }
-      ]
-    },
-    {
-      "name" : "act_0",
-      "id" : 8,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["scalars", "tmp"]
-            },
-            {
-              "type" : "expression",
-              "value" : {
-                "type" : "expression",
-                "value" : {
-                  "op" : "&",
-                  "left" : {
-                    "type" : "field",
-                    "value" : ["standard_metadata", "ingress_port"]
-                  },
-                  "right" : {
-                    "type" : "hexstr",
-                    "value" : "0xffffffff"
-                  }
-                }
-              }
-            }
-          ]
-        },
-        {
-          "op" : "count",
-          "parameters" : [
-            {
-              "type" : "counter_array",
-              "value" : "port_counters_ingress.ingress_port_counter"
-            },
-            {
-              "type" : "field",
-              "value" : ["scalars", "tmp"]
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/port_counters.p4",
-            "line" : 29,
-            "column" : 8,
-            "source_fragment" : "ingress_port_counter.count((bit<32>) standard_metadata.ingress_port)"
-          }
-        }
-      ]
-    },
-    {
-      "name" : "act_1",
-      "id" : 9,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "add_header",
-          "parameters" : [
-            {
-              "type" : "header",
-              "value" : "packet_in"
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 38,
-            "column" : 12,
-            "source_fragment" : "hdr.packet_in.setValid()"
-          }
-        },
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["packet_in", "ingress_port"]
-            },
-            {
-              "type" : "field",
-              "value" : ["standard_metadata", "ingress_port"]
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 39,
-            "column" : 12,
-            "source_fragment" : "hdr.packet_in.ingress_port = standard_metadata.ingress_port"
-          }
-        }
-      ]
-    },
-    {
-      "name" : "act_2",
-      "id" : 10,
-      "runtime_data" : [],
-      "primitives" : [
-        {
-          "op" : "assign",
-          "parameters" : [
-            {
-              "type" : "field",
-              "value" : ["scalars", "tmp_0"]
-            },
-            {
-              "type" : "expression",
-              "value" : {
-                "type" : "expression",
-                "value" : {
-                  "op" : "&",
-                  "left" : {
-                    "type" : "field",
-                    "value" : ["standard_metadata", "egress_port"]
-                  },
-                  "right" : {
-                    "type" : "hexstr",
-                    "value" : "0xffffffff"
-                  }
-                }
-              }
-            }
-          ]
-        },
-        {
-          "op" : "count",
-          "parameters" : [
-            {
-              "type" : "counter_array",
-              "value" : "port_counters_egress.egress_port_counter"
-            },
-            {
-              "type" : "field",
-              "value" : ["scalars", "tmp_0"]
-            }
-          ],
-          "source_info" : {
-            "filename" : "include/port_counters.p4",
-            "line" : 39,
-            "column" : 8,
-            "source_fragment" : "egress_port_counter.count((bit<32>) standard_metadata.egress_port)"
-          }
-        }
-      ]
-    }
-  ],
-  "pipelines" : [
-    {
-      "name" : "ingress",
-      "id" : 0,
-      "source_info" : {
-        "filename" : "ecmp.p4",
-        "line" : 42,
-        "column" : 8,
-        "source_fragment" : "ingress"
-      },
-      "init_table" : "tbl_act",
-      "tables" : [
-        {
-          "name" : "tbl_act",
-          "id" : 0,
-          "key" : [],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "with_counters" : false,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [8],
-          "actions" : ["act_0"],
-          "base_default_next" : "node_3",
-          "next_tables" : {
-            "act_0" : "node_3"
-          },
-          "default_entry" : {
-            "action_id" : 8,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        },
-        {
-          "name" : "tbl_act_0",
-          "id" : 1,
-          "key" : [],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "with_counters" : false,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [7],
-          "actions" : ["act"],
-          "base_default_next" : null,
-          "next_tables" : {
-            "act" : null
-          },
-          "default_entry" : {
-            "action_id" : 7,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        },
-        {
-          "name" : "table0_control.table0",
-          "id" : 2,
-          "source_info" : {
-            "filename" : "include/table0.p4",
-            "line" : 33,
-            "column" : 10,
-            "source_fragment" : "table0"
-          },
-          "key" : [
-            {
-              "match_type" : "ternary",
-              "target" : ["standard_metadata", "ingress_port"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ethernet", "src_addr"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ethernet", "dst_addr"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ethernet", "ether_type"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ipv4", "src_addr"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ipv4", "dst_addr"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["ipv4", "protocol"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["scalars", "local_metadata_t.l4_src_port"],
-              "mask" : null
-            },
-            {
-              "match_type" : "ternary",
-              "target" : ["scalars", "local_metadata_t.l4_dst_port"],
-              "mask" : null
-            }
-          ],
-          "match_type" : "ternary",
-          "type" : "simple",
-          "max_size" : 1024,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [0, 2, 5, 3],
-          "actions" : ["set_egress_port", "send_to_cpu", "table0_control.set_next_hop_id", "_drop"],
-          "base_default_next" : "node_6",
-          "next_tables" : {
-            "set_egress_port" : "node_6",
-            "send_to_cpu" : "node_6",
-            "table0_control.set_next_hop_id" : "node_6",
-            "_drop" : "node_6"
-          },
-          "default_entry" : {
-            "action_id" : 3,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        },
-        {
-          "name" : "tbl_set_ecmp_selector",
-          "id" : 3,
-          "key" : [],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "with_counters" : false,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [6],
-          "actions" : ["set_ecmp_selector"],
-          "base_default_next" : "ecmp_table",
-          "next_tables" : {
-            "set_ecmp_selector" : "ecmp_table"
-          },
-          "default_entry" : {
-            "action_id" : 6,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        },
-        {
-          "name" : "ecmp_table",
-          "id" : 4,
-          "source_info" : {
-            "filename" : "ecmp.p4",
-            "line" : 48,
-            "column" : 10,
-            "source_fragment" : "ecmp_table"
-          },
-          "key" : [
-            {
-              "match_type" : "exact",
-              "target" : ["scalars", "local_metadata_t.next_hop_id"],
-              "mask" : null
-            },
-            {
-              "match_type" : "exact",
-              "target" : ["scalars", "local_metadata_t.selector"],
-              "mask" : null
-            }
-          ],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [1, 4],
-          "actions" : ["set_egress_port", "NoAction"],
-          "base_default_next" : null,
-          "next_tables" : {
-            "set_egress_port" : null,
-            "NoAction" : null
-          },
-          "default_entry" : {
-            "action_id" : 4,
-            "action_const" : false,
-            "action_data" : [],
-            "action_entry_const" : false
-          }
-        }
-      ],
-      "action_profiles" : [],
-      "conditionals" : [
-        {
-          "name" : "node_3",
-          "id" : 0,
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 26,
-            "column" : 12,
-            "source_fragment" : "standard_metadata.ingress_port == CPU_PORT"
-          },
-          "expression" : {
-            "type" : "expression",
-            "value" : {
-              "op" : "==",
-              "left" : {
-                "type" : "field",
-                "value" : ["standard_metadata", "ingress_port"]
-              },
-              "right" : {
-                "type" : "hexstr",
-                "value" : "0x00ff"
-              }
-            }
-          },
-          "true_next" : "tbl_act_0",
-          "false_next" : "table0_control.table0"
-        },
-        {
-          "name" : "node_6",
-          "id" : 1,
-          "source_info" : {
-            "filename" : "ecmp.p4",
-            "line" : 75,
-            "column" : 12,
-            "source_fragment" : "local_metadata.next_hop_id > 0"
-          },
-          "expression" : {
-            "type" : "expression",
-            "value" : {
-              "op" : ">",
-              "left" : {
-                "type" : "field",
-                "value" : ["scalars", "local_metadata_t.next_hop_id"]
-              },
-              "right" : {
-                "type" : "hexstr",
-                "value" : "0x0000"
-              }
-            }
-          },
-          "false_next" : null,
-          "true_next" : "tbl_set_ecmp_selector"
-        }
-      ]
-    },
-    {
-      "name" : "egress",
-      "id" : 1,
-      "source_info" : {
-        "filename" : "ecmp.p4",
-        "line" : 86,
-        "column" : 8,
-        "source_fragment" : "egress"
-      },
-      "init_table" : "tbl_act_1",
-      "tables" : [
-        {
-          "name" : "tbl_act_1",
-          "id" : 5,
-          "key" : [],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "with_counters" : false,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [10],
-          "actions" : ["act_2"],
-          "base_default_next" : "node_12",
-          "next_tables" : {
-            "act_2" : "node_12"
-          },
-          "default_entry" : {
-            "action_id" : 10,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        },
-        {
-          "name" : "tbl_act_2",
-          "id" : 6,
-          "key" : [],
-          "match_type" : "exact",
-          "type" : "simple",
-          "max_size" : 1024,
-          "with_counters" : false,
-          "support_timeout" : false,
-          "direct_meters" : null,
-          "action_ids" : [9],
-          "actions" : ["act_1"],
-          "base_default_next" : null,
-          "next_tables" : {
-            "act_1" : null
-          },
-          "default_entry" : {
-            "action_id" : 9,
-            "action_const" : true,
-            "action_data" : [],
-            "action_entry_const" : true
-          }
-        }
-      ],
-      "action_profiles" : [],
-      "conditionals" : [
-        {
-          "name" : "node_12",
-          "id" : 2,
-          "source_info" : {
-            "filename" : "include/packet_io.p4",
-            "line" : 37,
-            "column" : 12,
-            "source_fragment" : "standard_metadata.egress_port == CPU_PORT"
-          },
-          "expression" : {
-            "type" : "expression",
-            "value" : {
-              "op" : "==",
-              "left" : {
-                "type" : "field",
-                "value" : ["standard_metadata", "egress_port"]
-              },
-              "right" : {
-                "type" : "hexstr",
-                "value" : "0x00ff"
-              }
-            }
-          },
-          "false_next" : null,
-          "true_next" : "tbl_act_2"
-        }
-      ]
-    }
-  ],
-  "checksums" : [],
-  "force_arith" : [],
-  "extern_instances" : [],
-  "field_aliases" : [
-    [
-      "queueing_metadata.enq_timestamp",
-      ["standard_metadata", "enq_timestamp"]
-    ],
-    [
-      "queueing_metadata.enq_qdepth",
-      ["standard_metadata", "enq_qdepth"]
-    ],
-    [
-      "queueing_metadata.deq_timedelta",
-      ["standard_metadata", "deq_timedelta"]
-    ],
-    [
-      "queueing_metadata.deq_qdepth",
-      ["standard_metadata", "deq_qdepth"]
-    ],
-    [
-      "intrinsic_metadata.ingress_global_timestamp",
-      ["standard_metadata", "ingress_global_timestamp"]
-    ],
-    [
-      "intrinsic_metadata.lf_field_list",
-      ["standard_metadata", "lf_field_list"]
-    ],
-    [
-      "intrinsic_metadata.mcast_grp",
-      ["standard_metadata", "mcast_grp"]
-    ],
-    [
-      "intrinsic_metadata.resubmit_flag",
-      ["standard_metadata", "resubmit_flag"]
-    ],
-    [
-      "intrinsic_metadata.egress_rid",
-      ["standard_metadata", "egress_rid"]
-    ]
-  ]
-}
\ No newline at end of file
diff --git a/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.p4info b/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.p4info
deleted file mode 100644
index 08a3dd9..0000000
--- a/pipelines/basic/src/main/resources/p4c-out/bmv2/ecmp.p4info
+++ /dev/null
@@ -1,224 +0,0 @@
-tables {
-  preamble {
-    id: 33571508
-    name: "table0_control.table0"
-    alias: "table0"
-  }
-  match_fields {
-    id: 1
-    name: "standard_metadata.ingress_port"
-    bitwidth: 9
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 2
-    name: "hdr.ethernet.src_addr"
-    bitwidth: 48
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 3
-    name: "hdr.ethernet.dst_addr"
-    bitwidth: 48
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 4
-    name: "hdr.ethernet.ether_type"
-    bitwidth: 16
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 5
-    name: "hdr.ipv4.src_addr"
-    bitwidth: 32
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 6
-    name: "hdr.ipv4.dst_addr"
-    bitwidth: 32
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 7
-    name: "hdr.ipv4.protocol"
-    bitwidth: 8
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 8
-    name: "local_metadata.l4_src_port"
-    bitwidth: 16
-    match_type: TERNARY
-  }
-  match_fields {
-    id: 9
-    name: "local_metadata.l4_dst_port"
-    bitwidth: 16
-    match_type: TERNARY
-  }
-  action_refs {
-    id: 16794308
-  }
-  action_refs {
-    id: 16829080
-  }
-  action_refs {
-    id: 16802895
-  }
-  action_refs {
-    id: 16784184
-  }
-  const_default_action_id: 16784184
-  direct_resource_ids: 302046050
-  size: 1024
-}
-tables {
-  preamble {
-    id: 33601431
-    name: "ecmp_table"
-    alias: "ecmp_table"
-  }
-  match_fields {
-    id: 1
-    name: "local_metadata.next_hop_id"
-    bitwidth: 16
-    match_type: EXACT
-  }
-  match_fields {
-    id: 2
-    name: "local_metadata.selector"
-    bitwidth: 16
-    match_type: EXACT
-  }
-  action_refs {
-    id: 16794308
-  }
-  action_refs {
-    id: 16800567
-    annotations: "@defaultonly()"
-  }
-  direct_resource_ids: 302010883
-  size: 1024
-}
-actions {
-  preamble {
-    id: 16794308
-    name: "set_egress_port"
-    alias: "set_egress_port"
-  }
-  params {
-    id: 1
-    name: "port"
-    bitwidth: 9
-  }
-}
-actions {
-  preamble {
-    id: 16829080
-    name: "send_to_cpu"
-    alias: "send_to_cpu"
-  }
-}
-actions {
-  preamble {
-    id: 16784184
-    name: "_drop"
-    alias: "_drop"
-  }
-}
-actions {
-  preamble {
-    id: 16800567
-    name: "NoAction"
-    alias: "NoAction"
-  }
-}
-actions {
-  preamble {
-    id: 16802895
-    name: "table0_control.set_next_hop_id"
-    alias: "set_next_hop_id"
-  }
-  params {
-    id: 1
-    name: "next_hop_id"
-    bitwidth: 16
-  }
-}
-actions {
-  preamble {
-    id: 16789898
-    name: "set_ecmp_selector"
-    alias: "set_ecmp_selector"
-  }
-}
-counters {
-  preamble {
-    id: 302012579
-    name: "port_counters_ingress.ingress_port_counter"
-    alias: "ingress_port_counter"
-  }
-  spec {
-    unit: PACKETS
-  }
-  size: 511
-}
-counters {
-  preamble {
-    id: 302012501
-    name: "port_counters_egress.egress_port_counter"
-    alias: "egress_port_counter"
-  }
-  spec {
-    unit: PACKETS
-  }
-  size: 511
-}
-direct_counters {
-  preamble {
-    id: 302046050
-    name: "table0_control.table0_counter"
-    alias: "table0_counter"
-  }
-  spec {
-    unit: BOTH
-  }
-  direct_table_id: 33571508
-}
-direct_counters {
-  preamble {
-    id: 302010883
-    name: "ecmp_table_counter"
-    alias: "ecmp_table_counter"
-  }
-  spec {
-    unit: BOTH
-  }
-  direct_table_id: 33601431
-}
-controller_packet_metadata {
-  preamble {
-    id: 2868941301
-    name: "packet_in"
-    annotations: "@controller_header(\"packet_in\")"
-  }
-  metadata {
-    id: 1
-    name: "ingress_port"
-    bitwidth: 9
-  }
-}
-controller_packet_metadata {
-  preamble {
-    id: 2868916615
-    name: "packet_out"
-    annotations: "@controller_header(\"packet_out\")"
-  }
-  metadata {
-    id: 1
-    name: "egress_port"
-    bitwidth: 9
-  }
-}
diff --git a/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/GrpcControllerImpl.java b/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/GrpcControllerImpl.java
index 91b9db0..269936a 100644
--- a/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/GrpcControllerImpl.java
+++ b/protocols/grpc/ctl/src/main/java/org/onosproject/grpc/ctl/GrpcControllerImpl.java
@@ -168,6 +168,8 @@
                 doDummyMessage(channels.get(channelId));
                 return true;
             } catch (IOException e) {
+                log.warn("Error in sending dummy message to device {}", channelId);
+                log.debug("Exception ", e);
                 return false;
             }
         } finally {
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeFlowRuleWrapper.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeFlowRuleWrapper.java
deleted file mode 100644
index 634116f..0000000
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeFlowRuleWrapper.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.api;
-
-import com.google.common.annotations.Beta;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Objects;
-import org.onosproject.net.flow.FlowRule;
-import org.onosproject.net.pi.runtime.PiTableEntry;
-
-/**
- * A wrapper for a ONOS flow rule installed on a P4Runtime device.
- */
-@Beta
-public final class P4RuntimeFlowRuleWrapper {
-
-    private final FlowRule rule;
-    private final PiTableEntry piTableEntry;
-    private final long installedOnMillis;
-
-    /**
-     * Creates a new flow rule wrapper.
-     *
-     * @param rule              a flow rule
-     * @param piTableEntry      PI table entry
-     * @param installedOnMillis the time (in milliseconds, since January 1, 1970 UTC) when the flow rule was installed
-     *                          on the device
-     */
-    public P4RuntimeFlowRuleWrapper(FlowRule rule, PiTableEntry piTableEntry, long installedOnMillis) {
-        this.rule = rule;
-        this.piTableEntry = piTableEntry;
-        this.installedOnMillis = installedOnMillis;
-    }
-
-    /**
-     * Returns the flow rule contained by this wrapper.
-     *
-     * @return a flow rule
-     */
-    public FlowRule rule() {
-        return rule;
-    }
-
-    /**
-     * Returns the PI table entry defined by this wrapper.
-     *
-     * @return table entry
-     */
-    public PiTableEntry piTableEntry() {
-        return piTableEntry;
-    }
-
-    /**
-     * Return the number of seconds since when this flow rule was installed on the device.
-     *
-     * @return an integer value
-     */
-    public long lifeInSeconds() {
-        return (System.currentTimeMillis() - installedOnMillis) / 1000;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(rule, installedOnMillis);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null || getClass() != obj.getClass()) {
-            return false;
-        }
-        final P4RuntimeFlowRuleWrapper other = (P4RuntimeFlowRuleWrapper) obj;
-        return Objects.equal(this.rule, other.rule)
-                && Objects.equal(this.installedOnMillis, other.installedOnMillis);
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(this)
-                .add("rule", rule)
-                .add("installedOnMillis", installedOnMillis)
-                .toString();
-    }
-}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupReference.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupReference.java
deleted file mode 100644
index 55c9e61..0000000
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupReference.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.api;
-
-import com.google.common.annotations.Beta;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Objects;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.pi.model.PiActionProfileId;
-import org.onosproject.net.pi.runtime.PiActionGroupId;
-
-/**
- * Class containing the reference for a group in P4Runtime.
- */
-@Beta
-public final class P4RuntimeGroupReference {
-    private final DeviceId deviceId;
-    private final PiActionProfileId piActionProfileId;
-    private final PiActionGroupId groupId;
-
-    /**
-     * Creates P4 runtime group reference.
-     *
-     * @param deviceId the device id of group
-     * @param piActionProfileId the action profile id
-     * @param groupId the group Id of group
-     */
-    public P4RuntimeGroupReference(DeviceId deviceId, PiActionProfileId piActionProfileId,
-                                   PiActionGroupId groupId) {
-        this.deviceId = deviceId;
-        this.piActionProfileId = piActionProfileId;
-        this.groupId = groupId;
-    }
-
-    /**
-     * Gets device id of this group.
-     *
-     * @return the device id
-     */
-    public DeviceId deviceId() {
-        return deviceId;
-    }
-
-    /**
-     * Gets action profile id of this group.
-     *
-     * @return the action profile id
-     */
-    public PiActionProfileId actionProfileId() {
-        return piActionProfileId;
-    }
-
-    /**
-     * Gets group id of this group.
-     *
-     * @return group id
-     */
-    public PiActionGroupId groupId() {
-        return groupId;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        P4RuntimeGroupReference that = (P4RuntimeGroupReference) o;
-        return Objects.equal(deviceId, that.deviceId) &&
-                Objects.equal(piActionProfileId, that.piActionProfileId) &&
-                Objects.equal(groupId, that.groupId);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(deviceId, piActionProfileId, groupId);
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(this)
-                .add("deviceId", deviceId)
-                .add("piActionProfileId", piActionProfileId)
-                .add("groupId", groupId)
-                .toString();
-    }
-}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupWrapper.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupWrapper.java
deleted file mode 100644
index 8cb4b83..0000000
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeGroupWrapper.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.api;
-
-import com.google.common.annotations.Beta;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.pi.runtime.PiActionGroup;
-
-/**
- * A wrapper for a ONOS group installed on a P4Runtime device.
- */
-@Beta
-public class P4RuntimeGroupWrapper {
-    private final PiActionGroup piActionGroup;
-    private final Group group;
-    private final long installMilliSeconds;
-
-    /**
-     * Creates new group wrapper.
-     *
-     * @param piActionGroup the Pi action group
-     * @param group the group
-     * @param installMilliSeconds the installation time
-     */
-    public P4RuntimeGroupWrapper(PiActionGroup piActionGroup, Group group,
-                                 long installMilliSeconds) {
-        this.piActionGroup = piActionGroup;
-        this.group = group;
-        this.installMilliSeconds = installMilliSeconds;
-    }
-
-    /**
-     * Gets PI action group from this wrapper.
-     *
-     * @return the PI action group
-     */
-    public PiActionGroup piActionGroup() {
-        return piActionGroup;
-    }
-
-    /**
-     * Gets group from this wrapper.
-     *
-     * @return the group
-     */
-    public Group group() {
-        return group;
-    }
-
-    /**
-     * Gets installation time of this wrapper.
-     *
-     * @return the installation time
-     */
-    public long installMilliSeconds() {
-        return installMilliSeconds;
-    }
-}
diff --git a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeTableEntryReference.java b/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeTableEntryReference.java
deleted file mode 100644
index d345f15..0000000
--- a/protocols/p4runtime/api/src/main/java/org/onosproject/p4runtime/api/P4RuntimeTableEntryReference.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2017-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.onosproject.p4runtime.api;
-
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Objects;
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.pi.model.PiTableId;
-import org.onosproject.net.pi.runtime.PiMatchKey;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-/**
- * Class containing the reference for a table entry in P4Runtime.
- */
-public final class P4RuntimeTableEntryReference {
-
-    private final DeviceId deviceId;
-    private final PiTableId tableId;
-    private final PiMatchKey matchKey;
-
-    /**
-     * Creates a new table entry reference.
-     *
-     * @param deviceId a device ID
-     * @param tableId  a table name
-     * @param matchKey a match key
-     */
-    public P4RuntimeTableEntryReference(DeviceId deviceId, PiTableId tableId, PiMatchKey matchKey) {
-        this.deviceId = checkNotNull(deviceId);
-        this.tableId = checkNotNull(tableId);
-        this.matchKey = checkNotNull(matchKey);
-    }
-
-    /**
-     * Returns the device ID of this table entry reference.
-     *
-     * @return a device ID
-     */
-    public DeviceId deviceId() {
-        return deviceId;
-    }
-
-    /**
-     * Returns the table id of this table entry reference.
-     *
-     * @return a table name
-     */
-    public PiTableId tableId() {
-        return tableId;
-    }
-
-    /**
-     * Returns the match key of this table entry reference.
-     *
-     * @return a match key
-     */
-    public PiMatchKey matchKey() {
-        return matchKey;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(deviceId, tableId, matchKey);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null || getClass() != obj.getClass()) {
-            return false;
-        }
-        final P4RuntimeTableEntryReference other = (P4RuntimeTableEntryReference) obj;
-        return Objects.equal(this.deviceId, other.deviceId)
-                && Objects.equal(this.tableId, other.tableId)
-                && Objects.equal(this.matchKey, other.matchKey);
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(this)
-                .add("deviceId", deviceId)
-                .add("tableId", tableId)
-                .add("matchKey", matchKey)
-                .toString();
-    }
-}
diff --git a/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GeneralDeviceProvider.java b/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GeneralDeviceProvider.java
index a77dc33..824ec79 100644
--- a/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GeneralDeviceProvider.java
+++ b/providers/general/device/src/main/java/org/onosproject/provider/general/device/impl/GeneralDeviceProvider.java
@@ -30,7 +30,11 @@
 import org.onlab.util.ItemNotFoundException;
 import org.onlab.util.Tools;
 import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.cluster.NodeId;
 import org.onosproject.core.CoreService;
+import org.onosproject.mastership.MastershipService;
 import org.onosproject.net.AnnotationKeys;
 import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.Device;
@@ -110,6 +114,10 @@
 public class GeneralDeviceProvider extends AbstractProvider
         implements DeviceProvider {
     public static final String DRIVER = "driver";
+    public static final int REACHABILITY_TIMEOUT = 10;
+    public static final String DEPLOY = "deploy-";
+    public static final String PIPECONF_TOPIC = "-pipeconf";
+
     private final Logger log = getLogger(getClass());
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -131,8 +139,17 @@
     protected DriverService driverService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected PiPipeconfService piPipeconfService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LeadershipService leadershipService;
+
     private static final int DEFAULT_POLL_FREQUENCY_SECONDS = 10;
     @Property(name = "pollFrequency", intValue = DEFAULT_POLL_FREQUENCY_SECONDS,
             label = "Configure poll frequency for port status and statistics; " +
@@ -257,17 +274,26 @@
 
     @Override
     public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
-        log.debug("Received role {} for device {}", newRole, deviceId);
+        log.info("Received role {} for device {}", newRole, deviceId);
         CompletableFuture<MastershipRole> roleReply = getHandshaker(deviceId).roleChanged(newRole);
-        roleReply.thenAcceptAsync(mastership -> providerService.receivedRoleReply(deviceId, newRole, mastership));
+        roleReply.thenAcceptAsync(mastership -> {
+            providerService.receivedRoleReply(deviceId, newRole, mastership);
+            if (!mastership.equals(MastershipRole.MASTER) && scheduledTasks.get(deviceId) != null) {
+                scheduledTasks.get(deviceId).cancel(false);
+                scheduledTasks.remove(deviceId);
+            } else if (mastership.equals(MastershipRole.MASTER) && scheduledTasks.get(deviceId) == null) {
+                scheduledTasks.put(deviceId, schedulePolling(deviceId, false));
+                updatePortStatistics(deviceId);
+            }
+        });
     }
 
     @Override
     public boolean isReachable(DeviceId deviceId) {
-        log.debug("Testing rechability for device {}", deviceId);
+        log.debug("Testing reachability for device {}", deviceId);
         CompletableFuture<Boolean> reachable = getHandshaker(deviceId).isReachable();
         try {
-            return reachable.get(10, TimeUnit.SECONDS);
+            return reachable.get(REACHABILITY_TIMEOUT, TimeUnit.SECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException e) {
             log.error("Device {} is not reachable", deviceId, e);
             return false;
@@ -368,15 +394,7 @@
                 return;
             }
 
-            //Storing deviceKeyId and all other config values
-            // as data in the driver with protocol_<info>
-            // name as the key. e.g protocol_ip
-            providerConfig.protocolsInfo()
-                    .forEach((protocol, deviceInfoConfig) -> {
-                        deviceInfoConfig.configValues()
-                                .forEach((k, v) -> driverData.set(protocol + "_" + k, v));
-                        driverData.set(protocol + "_key", deviceInfoConfig.deviceKeyId());
-                    });
+            addConfigData(providerConfig, driverData);
 
             //Connecting to the device
             CompletableFuture<Boolean> connected = handshaker.connect();
@@ -409,7 +427,7 @@
                         ports = deviceDiscovery.discoverPortDetails();
                     }
 
-                    if (!handlePipeconf(deviceId, driver, driverData)) {
+                    if (!handlePipeconf(deviceId, driver, driverData, true)) {
                         // Something went wrong during handling of pipeconf.
                         // We already logged the error.
                         handshaker.disconnect();
@@ -425,11 +443,37 @@
         }
     }
 
+    private void connectStandbyDevice(DeviceId deviceId) {
+
+        //if device is pipeline programmable we merge pipeconf + base driver for every other role
+        GeneralProviderDeviceConfig providerConfig =
+                cfgService.getConfig(deviceId, GeneralProviderDeviceConfig.class);
+
+        Driver driver = getDriver(deviceId);
+
+
+        DriverData driverData = new DefaultDriverData(driver, deviceId);
+        DeviceHandshaker handshaker = getBehaviour(driver, DeviceHandshaker.class, driverData);
+        if (handshaker == null) {
+            log.error("Device {}, with driver {} does not support DeviceHandshaker " +
+                    "behaviour, supported behaviours={}", deviceId, driver.name(), driver.behaviours());
+            return;
+        }
+        addConfigData(providerConfig, driverData);
+
+        //Connecting to the device
+        handshaker.connect().thenAcceptAsync(result -> {
+            if (result) {
+                handlePipeconf(deviceId, driver, driverData, false);
+            }
+        });
+    }
+
     /**
      * Handles the case of a device that is pipeline programmable. Returns true if the operation wa successful and the
      * device can be registered to the core, false otherwise.
      */
-    private boolean handlePipeconf(DeviceId deviceId, Driver driver, DriverData driverData) {
+    private boolean handlePipeconf(DeviceId deviceId, Driver driver, DriverData driverData, boolean deployPipeconf) {
 
         PiPipelineProgrammable pipelineProg = getBehaviour(driver, PiPipelineProgrammable.class,
                 driverData);
@@ -439,6 +483,42 @@
             return true;
         }
 
+        PiPipeconf pipeconf = getPipeconf(deviceId, pipelineProg);
+
+        if (pipeconf != null) {
+
+            PiPipeconfId pipeconfId = pipeconf.id();
+
+            try {
+                if (deployPipeconf) {
+                    if (!pipelineProg.deployPipeconf(pipeconf).get()) {
+                        log.error("Unable to deploy pipeconf {} to {}, aborting device discovery",
+                                pipeconfId, deviceId);
+                        return false;
+                    }
+                }
+            } catch (InterruptedException | ExecutionException e) {
+                log.warn("Exception occurred while deploying pipeconf {} to device {}", pipeconf.id(), deviceId, e);
+                return false;
+            }
+            try {
+                if (!piPipeconfService.bindToDevice(pipeconfId, deviceId).get()) {
+                    log.error("Unable to merge driver {} for device {} with pipeconf {}, aborting device discovery",
+                            driver.name(), deviceId, pipeconfId);
+                    return false;
+                }
+            } catch (InterruptedException | ExecutionException e) {
+                log.warn("Exception occurred while binding pipeconf {} to device {}", pipeconf.id(), deviceId, e);
+                return false;
+            }
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+
+    private PiPipeconf getPipeconf(DeviceId deviceId, PiPipelineProgrammable pipelineProg) {
         PiPipeconfId pipeconfId = piPipeconfService.ofDevice(deviceId).orElseGet(() -> {
             // No pipeconf has been associated with this device.
             // Check if device driver provides a default one.
@@ -453,33 +533,16 @@
 
         if (pipeconfId == null) {
             log.warn("Device {} is pipeline programmable but no pipeconf can be associated to it.", deviceId);
-            return false;
+            return null;
         }
 
         if (!piPipeconfService.getPipeconf(pipeconfId).isPresent()) {
             log.warn("Pipeconf {} is not registered", pipeconfId);
-            return false;
+            return null;
         }
 
 
-        PiPipeconf pipeconf = piPipeconfService.getPipeconf(pipeconfId).get();
-
-        try {
-            if (!pipelineProg.deployPipeconf(pipeconf).get()) {
-                log.error("Unable to deploy pipeconf {} to {}, aborting device discovery", pipeconfId, deviceId);
-                return false;
-            }
-
-            if (!piPipeconfService.bindToDevice(pipeconfId, deviceId).get()) {
-                log.error("Unable to merge driver {} for device {} with pipeconf {}, aborting device discovery",
-                        driver.name(), deviceId, pipeconfId);
-                return false;
-            }
-        } catch (InterruptedException | ExecutionException e) {
-            throw new IllegalStateException(e);
-        }
-
-        return true;
+        return piPipeconfService.getPipeconf(pipeconfId).get();
     }
 
     private void advertiseDevice(DeviceId deviceId, DeviceDescription description, List<PortDescription> ports) {
@@ -492,7 +555,6 @@
         DeviceHandshaker handshaker = getHandshaker(deviceId);
         if (handshaker != null) {
             CompletableFuture<Boolean> disconnect = handshaker.disconnect();
-
             disconnect.thenAcceptAsync(result -> {
                 if (result) {
                     log.info("Disconnected device {}", deviceId);
@@ -560,6 +622,28 @@
                 log.info("Device {} is already connected to ONOS and is available", deviceId);
                 return;
             }
+            NodeId leaderNodeId = leadershipService.runForLeadership(DEPLOY + deviceId.toString() + PIPECONF_TOPIC)
+                    .leader().nodeId();
+            NodeId localNodeId = clusterService.getLocalNode().id();
+            if (localNodeId.equals(leaderNodeId)) {
+                if (processEvent(event, deviceId)) {
+                    log.debug("{} is leader for {}, initiating the connection and deploying pipeline", leaderNodeId,
+                            deviceId);
+                    checkAndSubmitDeviceTask(deviceId);
+                }
+            } else {
+                if (processEvent(event, deviceId)) {
+                    log.debug("{} is not leader for {}, initiating connection but not deploying pipeline, {} is LEADER",
+                            localNodeId, deviceId, leaderNodeId);
+                    connectionExecutor.submit(exceptionSafe(() -> connectStandbyDevice(deviceId)));
+                    //FIXME this will be removed when config is synced
+                    cleanUpConfigInfo(deviceId);
+                }
+            }
+
+        }
+
+        private boolean processEvent(NetworkConfigEvent event, DeviceId deviceId) {
             //FIXME to be removed when netcfg will issue device events in a bundle or
             // ensure all configuration needed is present
             Lock lock = ENTRY_LOCKS.computeIfAbsent(deviceId, key -> new ReentrantLock());
@@ -590,7 +674,7 @@
                 // in the pipelineConfigured
                 if (deviceConfigured.contains(deviceId) && driverConfigured.contains(deviceId)
                         && pipelineConfigured.contains(deviceId)) {
-                    checkAndSubmitDeviceTask(deviceId);
+                    return true;
                 } else {
                     if (deviceConfigured.contains(deviceId) && driverConfigured.contains(deviceId)) {
                         log.debug("Waiting for pipeline configuration for device {}", deviceId);
@@ -604,6 +688,7 @@
                         log.debug("Only device configuration for device {}", deviceId);
                     }
                 }
+                return false;
             } finally {
                 lock.unlock();
             }
@@ -622,10 +707,26 @@
     private void checkAndSubmitDeviceTask(DeviceId deviceId) {
         connectionExecutor.submit(exceptionSafe(() -> connectDevice(deviceId)));
         //FIXME this will be removed when configuration is synced.
+        cleanUpConfigInfo(deviceId);
+
+    }
+
+    private void addConfigData(GeneralProviderDeviceConfig providerConfig, DriverData driverData) {
+        //Storing deviceKeyId and all other config values
+        // as data in the driver with protocol_<info>
+        // name as the key. e.g protocol_ip
+        providerConfig.protocolsInfo()
+                .forEach((protocol, deviceInfoConfig) -> {
+                    deviceInfoConfig.configValues()
+                            .forEach((k, v) -> driverData.set(protocol + "_" + k, v));
+                    driverData.set(protocol + "_key", deviceInfoConfig.deviceKeyId());
+                });
+    }
+
+    private void cleanUpConfigInfo(DeviceId deviceId) {
         deviceConfigured.remove(deviceId);
         driverConfigured.remove(deviceId);
         pipelineConfigured.remove(deviceId);
-
     }
 
     private ScheduledFuture<?> schedulePolling(DeviceId deviceId, boolean randomize) {
@@ -650,10 +751,13 @@
 
                 //For now this is scheduled periodically, when streaming API will
                 // be available we check and base it on the streaming API (e.g. gNMI)
-                scheduledTasks.put(deviceId, schedulePolling(deviceId, false));
-                updatePortStatistics(deviceId);
+                if (mastershipService.isLocalMaster(deviceId)) {
+                    scheduledTasks.put(deviceId, schedulePolling(deviceId, false));
+                    updatePortStatistics(deviceId);
+                }
 
             } else if (type.equals(Type.DEVICE_REMOVED)) {
+
                 connectionExecutor.submit(exceptionSafe(() ->
                         disconnectDevice(deviceId)));
             }
diff --git a/providers/p4runtime/packet/src/main/java/org/onosproject/provider/p4runtime/packet/impl/P4RuntimePacketProvider.java b/providers/p4runtime/packet/src/main/java/org/onosproject/provider/p4runtime/packet/impl/P4RuntimePacketProvider.java
index fb4a97a..8693eff 100644
--- a/providers/p4runtime/packet/src/main/java/org/onosproject/provider/p4runtime/packet/impl/P4RuntimePacketProvider.java
+++ b/providers/p4runtime/packet/src/main/java/org/onosproject/provider/p4runtime/packet/impl/P4RuntimePacketProvider.java
@@ -22,6 +22,7 @@
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onosproject.mastership.MastershipService;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.device.DeviceService;
@@ -67,6 +68,9 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DeviceService deviceService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
     private PacketProviderService providerService;
 
     private InternalPacketListener packetListener = new InternalPacketListener();
@@ -98,7 +102,7 @@
         if (packet != null) {
             DeviceId deviceId = packet.sendThrough();
             Device device = deviceService.getDevice(deviceId);
-            if (device.is(PacketProgrammable.class)) {
+            if (device.is(PacketProgrammable.class) && mastershipService.isLocalMaster(deviceId)) {
                 PacketProgrammable packetProgrammable = device.as(PacketProgrammable.class);
                 packetProgrammable.emit(packet);
             } else {
@@ -148,7 +152,10 @@
 
         @Override
         public void event(P4RuntimeEvent event) {
-            if (event.type() != P4RuntimeEvent.Type.PACKET_IN) {
+            //Masterhip message is sent to everybody but picked up only by master.
+            //FIXME we need the device ID into p4RuntimeEvnetSubject to check for mastsership
+            if (!(event.subject() instanceof P4RuntimePacketIn) || event.type() != P4RuntimeEvent.Type.PACKET_IN) {
+                log.debug("Event type {}", event.type());
                 // Not a packet-in event, ignore it.
                 return;
             }
@@ -163,7 +170,7 @@
 
             if (!device.is(PiPipelineInterpreter.class)) {
                 log.warn("Unable to process packet-in from {}, device has no PiPipelineInterpreter behaviour",
-                         deviceId);
+                        deviceId);
                 return;
             }
 
@@ -184,7 +191,7 @@
             log.debug("Processing inbound packet: {}", inPkt.toString());
 
             OutboundPacket outPkt = new DefaultOutboundPacket(eventSubject.deviceId(), null,
-                                                              operation.data().asReadOnlyBuffer());
+                    operation.data().asReadOnlyBuffer());
             PacketContext pktCtx = new P4RuntimePacketContext(System.currentTimeMillis(), inPkt, outPkt, false);
 
             // Pushing the packet context up for processing.
diff --git a/tools/dev/mininet/bmv2.py b/tools/dev/mininet/bmv2.py
index f218257..7066f61 100644
--- a/tools/dev/mininet/bmv2.py
+++ b/tools/dev/mininet/bmv2.py
@@ -35,7 +35,8 @@
     def config(self, **params):
         r = super(Host, self).config(**params)
         for off in ["rx", "tx", "sg"]:
-            cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf(), off)
+            cmd = "/sbin/ethtool --offload %s %s off"\
+                  % (self.defaultIntf(), off)
             self.cmd(cmd)
         # disable IPv6
         self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
@@ -52,7 +53,8 @@
 
     def __init__(self, name, json=None, debugger=False, loglevel="warn", elogger=False,
                  persistent=False, grpcPort=None, thriftPort=None, netcfg=True, dryrun=False,
-                 pipeconfId="", pktdump=False, valgrind=False, **kwargs):
+                 pipeconfId="", pktdump=False, valgrind=False, netcfgDelay=0,
+                 **kwargs):
         Switch.__init__(self, name, **kwargs)
         self.grpcPort = ONOSBmv2Switch.pickUnusedPort() if not grpcPort else grpcPort
         self.thriftPort = ONOSBmv2Switch.pickUnusedPort() if not thriftPort else thriftPort
@@ -71,6 +73,7 @@
         self.netcfg = parseBoolean(netcfg)
         self.dryrun = parseBoolean(dryrun)
         self.valgrind = parseBoolean(valgrind)
+        self.netcfgDelay = netcfgDelay
         self.netcfgfile = '/tmp/bmv2-%d-netcfg.json' % self.deviceId
         self.pipeconfId = pipeconfId
         if persistent:
@@ -229,11 +232,13 @@
             out = self.cmd(cmdStr)
             if out:
                 print out
-            if self.netcfg and self.valgrind:
-                # With valgrind, it takes some time before the gRPC server is available.
-                # Wait before pushing the netcfg.
-                info("\n*** Waiting %d seconds before pushing the config to ONOS...\n" % VALGRIND_SLEEP)
-                time.sleep(VALGRIND_SLEEP)
+            if self.netcfg:
+                if self.valgrind:
+                    # With valgrind, it takes some time before the gRPC server is available.
+                    # Wait before pushing the netcfg.
+                    info("\n*** Waiting %d seconds before pushing the config to ONOS...\n" % VALGRIND_SLEEP)
+                    time.sleep(VALGRIND_SLEEP)
+                time.sleep(self.netcfgDelay)
 
         try:  # onos.py
             clist = controllers[0].nodes()
diff --git a/tools/dev/mininet/onos.py b/tools/dev/mininet/onos.py
index 9b59cc7..2e7b6f7 100755
--- a/tools/dev/mininet/onos.py
+++ b/tools/dev/mininet/onos.py
@@ -364,14 +364,11 @@
             time.sleep( 1 )
         info( ' ssh-port' )
         waitListening( server=self, port=KarafPort, callback=self.sanityCheck )
-        info( ' openflow-port' )
-        waitListening( server=self, port=OpenFlowPort,
-                       callback=self.sanityCheck )
-        info( ' client' )
+        info( ' protocol' )
         while True:
             result = quietRun( '%s -h %s "apps -a"' %
                                ( self.client, self.IP() ), shell=True )
-            if 'openflow' in result:
+            if 'openflow' in result or 'p4runtime' in result:
                 break
             info( '.' )
             self.sanityCheck()
@@ -380,7 +377,7 @@
         while True:
             result = quietRun( '%s -h %s "nodes"' %
                                ( self.client, self.IP() ), shell=True )
-            nodeStr = 'id=%s, address=%s:%s, state=READY, updated' %\
+            nodeStr = 'id=%s, address=%s:%s, state=READY' %\
                       ( self.IP(), self.IP(), CopycatPort )
             if nodeStr in result:
                 break
diff --git a/tools/test/topos/bmv2-demo.py b/tools/test/topos/bmv2-demo.py
index 9bccc5c..ef0a124 100755
--- a/tools/test/topos/bmv2-demo.py
+++ b/tools/test/topos/bmv2-demo.py
@@ -42,8 +42,12 @@
 from mininet.topo import Topo
 
 
+def getCmdBg(cmd, logfile="/dev/null"):
+    return "{} > {} 2>&1 &".format(cmd, logfile)
+
+
 class ClosTopo(Topo):
-    "2 stage Clos topology"
+    """2 stage Clos topology"""
 
     def __init__(self, args, **opts):
         # Initialize topology and default options
@@ -58,16 +62,17 @@
 
         for switchId in bmv2SwitchIds:
             deviceId = int(switchId[1:])
-            # Use first number in device id to calculate latitude (row number)
+            # Use first number in device id to calculate latitude (row number),
+            # use second to calculate longitude (column number)
             latitude = SWITCH_BASE_LATITUDE + (deviceId // 10) * BASE_SHIFT
-
-            # Use second number in device id to calculate longitude (column number)
             longitude = BASE_LONGITUDE + (deviceId % 10) * BASE_SHIFT
+
             bmv2Switches[switchId] = self.addSwitch(switchId,
                                                     cls=ONOSBmv2Switch,
-                                                    loglevel="warn",
+                                                    loglevel=args.log_level,
                                                     deviceId=deviceId,
-                                                    netcfg=False,
+                                                    netcfg=True,
+                                                    netcfgDelay=0.5,
                                                     longitude=longitude,
                                                     latitude=latitude,
                                                     pipeconfId=args.pipeconf_id)
@@ -75,14 +80,17 @@
         for i in range(1, args.size + 1):
             for j in range(1, args.size + 1):
                 if i == j:
-                    # 2 links
-                    self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
+                    self.addLink(bmv2Switches["s1%d" % i],
+                                 bmv2Switches["s2%d" % j],
                                  cls=TCLink, bw=DEFAULT_SW_BW)
                     if args.with_imbalanced_striping:
-                        self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
+                        # 2 links
+                        self.addLink(bmv2Switches["s1%d" % i],
+                                     bmv2Switches["s2%d" % j],
                                      cls=TCLink, bw=DEFAULT_SW_BW)
                 else:
-                    self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
+                    self.addLink(bmv2Switches["s1%d" % i],
+                                 bmv2Switches["s2%d" % j],
                                  cls=TCLink, bw=DEFAULT_SW_BW)
 
         for hostId in range(1, args.size + 1):
@@ -90,11 +98,12 @@
                                 cls=DemoHost,
                                 ip="10.0.0.%d/24" % hostId,
                                 mac='00:00:00:00:00:%02x' % hostId)
-            self.addLink(host, bmv2Switches["s1%d" % hostId], cls=TCLink, bw=DEFAULT_HOST_BW)
+            self.addLink(host, bmv2Switches["s1%d" % hostId],
+                         cls=TCLink, bw=DEFAULT_HOST_BW)
 
 
 class DemoHost(ONOSHost):
-    "Demo host"
+    """Demo host"""
 
     def __init__(self, name, **params):
         ONOSHost.__init__(self, name, **params)
@@ -109,10 +118,11 @@
         self.cmd(self.getInfiniteCmdBg("iperf -s -u"))
 
     def startIperfClient(self, h, flowBw="512k", numFlows=5, duration=5):
-        iperfCmd = "iperf -c{} -u -b{} -P{} -t{}".format(h.IP(), flowBw, numFlows, duration)
-        self.cmd(self.getInfiniteCmdBg(iperfCmd, sleep=0))
+        iperfCmd = "iperf -c{} -u -b{} -P{} -t{}".format(
+            h.IP(), flowBw, numFlows, duration)
+        self.cmd(self.getInfiniteCmdBg(iperfCmd, delay=0))
 
-    def stop(self):
+    def stop(self, **kwargs):
         self.cmd("killall iperf")
         self.cmd("killall ping")
         self.cmd("killall arping")
@@ -127,28 +137,27 @@
         )
         print "**********"
 
-    def getInfiniteCmdBg(self, cmd, logfile="/dev/null", sleep=1):
+    def getInfiniteCmdBg(self, cmd, logfile="/dev/null", delay=1):
         return "(while [ -e {} ]; " \
                "do {}; " \
                "sleep {}; " \
-               "done;) > {} 2>&1 &".format(self.exectoken, cmd, sleep, logfile)
-
-    def getCmdBg(self, cmd, logfile="/dev/null"):
-        return "{} > {} 2>&1 &".format(cmd, logfile)
+               "done;) > {} 2>&1 &".format(self.exectoken, cmd, delay, logfile)
 
 
 def generateNetcfg(onosIp, net, args):
     netcfg = OrderedDict()
+
+    netcfg['hosts'] = {}
     netcfg['devices'] = {}
     netcfg['links'] = {}
-    netcfg['hosts'] = {}
-    # Device configs
-    for sw in net.switches:
-        srcIp = sw.getSourceIp(onosIp)
-        netcfg['devices'][sw.onosDeviceId] = sw.getDeviceConfig(srcIp)
+
+    if args.full_netcfg:
+        # Device configs
+        for sw in net.switches:
+            srcIp = sw.getSourceIp(onosIp)
+            netcfg['devices'][sw.onosDeviceId] = sw.getDeviceConfig(srcIp)
 
     hostLocations = {}
-    # Link configs
     for link in net.links:
         switchPort = link.intf1.name.split('-')
         sw1Name = switchPort[0]  # s11
@@ -172,14 +181,18 @@
             hostLocations[sw2.name] = '%s/%s' % (sw1.onosDeviceId, port1)
             continue
 
-        for linkId in ('%s/%s-%s/%s' % (sw1.onosDeviceId, port1, sw2.onosDeviceId, port2),
-                       '%s/%s-%s/%s' % (sw2.onosDeviceId, port2, sw1.onosDeviceId, port1)):
-            netcfg['links'][linkId] = {
-                'basic': {
-                    'type': 'DIRECT',
-                    'bandwidth': DEFAULT_SW_BW
+        if args.full_netcfg:
+            # Link configs
+            for linkId in ('%s/%s-%s/%s' % (sw1.onosDeviceId, port1,
+                                            sw2.onosDeviceId, port2),
+                           '%s/%s-%s/%s' % (sw2.onosDeviceId, port2,
+                                            sw1.onosDeviceId, port1)):
+                netcfg['links'][linkId] = {
+                    'basic': {
+                        'type': 'DIRECT',
+                        'bandwidth': DEFAULT_SW_BW
+                    }
                 }
-            }
 
     # Host configs
     longitude = BASE_LONGITUDE
@@ -203,13 +216,14 @@
         }
         netcfg['hosts'][hostId] = hostConfig
 
-    netcfg["apps"] = {
-        "org.onosproject.core": {
-            "core": {
-                "linkDiscoveryMode": "STRICT"
+    if args.full_netcfg:
+        netcfg["apps"] = {
+            "org.onosproject.core": {
+                "core": {
+                    "linkDiscoveryMode": "STRICT"
+                }
             }
         }
-    }
 
     print "Writing network config to %s" % TEMP_NETCFG_FILE
     with open(TEMP_NETCFG_FILE, 'w') as tempFile:
@@ -233,7 +247,7 @@
 
     print "Network started"
 
-    # Generate background traffic.
+    # Always generate background pings.
     sleep(3)
     for (h1, h2) in combinations(net.hosts, 2):
         h1.startPingBg(h2)
@@ -246,14 +260,17 @@
 
     print "Iperf servers started"
 
-    # sleep(4)
-    # print "Starting traffic from h1 to h3..."
-    # net.hosts[0].startIperfClient(net.hosts[-1], flowBw="200k", numFlows=100, duration=10)
+    if args.bg_traffic:
+        sleep(4)
+        print "Starting iperf clients..."
+        net.hosts[0].startIperfClient(net.hosts[-1], flowBw="400k",
+                                      numFlows=50, duration=10)
 
     generateNetcfg(onosIp, net, args)
 
     if args.netcfg_sleep > 0:
-        print "Waiting %d seconds before pushing config to ONOS..." % args.netcfg_sleep
+        print "Waiting %d seconds before pushing config to ONOS..." \
+              % args.netcfg_sleep
         sleep(args.netcfg_sleep)
 
     print "Pushing config to ONOS..."
@@ -275,12 +292,25 @@
                         type=str, action="store", required=False)
     parser.add_argument('--size', help='Number of leaf/spine switches',
                         type=int, action="store", required=False, default=2)
-    parser.add_argument('--with-imbalanced-striping', help='Topology with imbalanced striping',
-                        type=bool, action="store", required=False, default=False)
+    parser.add_argument('--with-imbalanced-striping',
+                        help='Topology with imbalanced striping',
+                        type=bool, action="store", required=False,
+                        default=False)
     parser.add_argument('--pipeconf-id', help='Pipeconf ID for switches',
                         type=str, action="store", required=False, default='')
-    parser.add_argument('--netcfg-sleep', help='Seconds to wait before pushing config to ONOS',
+    parser.add_argument('--netcfg-sleep',
+                        help='Seconds to wait before pushing config to ONOS',
                         type=int, action="store", required=False, default=5)
-    args = parser.parse_args()
+    parser.add_argument('--log-level', help='BMv2 log level',
+                        type=str, action="store", required=False,
+                        default='warn')
+    parser.add_argument('--full-netcfg',
+                        help='Generate full netcfg JSON with links and devices',
+                        type=bool, action="store", required=False,
+                        default=False)
+    parser.add_argument('--bg-traffic',
+                        help='Starts background traffic',
+                        type=bool, action="store", required=False,
+                        default=False)
     setLogLevel('info')
-    main(args)
+    main(parser.parse_args())