Refactored PI-ECMP app to use action profiles of basic.p4

Also removed obsolete ecmp.p4-related code.

Change-Id: Idaca90becfff5fc312de2530bf7924ccd502e076
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
+}