Refactored sr app to enable rest api.

The segment routing application structure has been refactored
to the following structure :
	sr
	 ---- app
 	 ---- web

web folder contains the rest cli implementations while app folder
contains everything else. Originally I tried to split the application
in api / app / web, where api would contain all the interfaces. However,
that was not possible due to the fact that most of the classes do not implement
any interfaces and app -- api would result in circular dependencies and we would
not be able to build them.

Change-Id: Ifaaeefe2c5061c8457924ccd01678fb18966c44f
diff --git a/apps/segmentrouting/app/BUCK b/apps/segmentrouting/app/BUCK
new file mode 100644
index 0000000..9674f76
--- /dev/null
+++ b/apps/segmentrouting/app/BUCK
@@ -0,0 +1,22 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:JACKSON',
+    '//lib:KRYO',
+    '//lib:org.apache.karaf.shell.console',
+    '//cli:onos-cli',
+    '//core/common:onos-core-common',
+    '//core/store/serializers:onos-core-serializers',
+    '//incubator/api:onos-incubator-api',
+    '//apps/route-service/api:onos-apps-route-service-api',
+]
+
+TEST_DEPS = [
+    '//lib:TEST_ADAPTERS',
+    '//incubator/api:onos-incubator-api-tests',
+    '//apps/route-service/api:onos-apps-route-service-api-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java
new file mode 100644
index 0000000..f54ee9e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/AppConfigHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Handles Segment Routing app config events.
+ */
+public class AppConfigHandler {
+    private static final Logger log = LoggerFactory.getLogger(AppConfigHandler.class);
+    private final SegmentRoutingManager srManager;
+    private final DeviceService deviceService;
+
+    /**
+     * Constructs Segment Routing App Config Handler.
+     *
+     * @param srManager instance of {@link SegmentRoutingManager}
+     */
+    public AppConfigHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.deviceService = srManager.deviceService;
+    }
+
+    /**
+     * Processes Segment Routing App Config added event.
+     *
+     * @param event network config added event
+     */
+    protected void processAppConfigAdded(NetworkConfigEvent event) {
+        log.info("Processing AppConfig CONFIG_ADDED");
+        SegmentRoutingAppConfig config = (SegmentRoutingAppConfig) event.config().get();
+        deviceService.getAvailableDevices().forEach(device -> {
+            populateVRouter(device.id(), getMacAddresses(config));
+        });
+    }
+
+    /**
+     * Processes Segment Routing App Config updated event.
+     *
+     * @param event network config updated event
+     */
+    protected void processAppConfigUpdated(NetworkConfigEvent event) {
+        log.info("Processing AppConfig CONFIG_UPDATED");
+        SegmentRoutingAppConfig config = (SegmentRoutingAppConfig) event.config().get();
+        SegmentRoutingAppConfig prevConfig = (SegmentRoutingAppConfig) event.prevConfig().get();
+        deviceService.getAvailableDevices().forEach(device -> {
+            Set<MacAddress> macAddresses = new HashSet<>(getMacAddresses(config));
+            Set<MacAddress> prevMacAddresses = new HashSet<>(getMacAddresses(prevConfig));
+            // Avoid removing and re-adding unchanged MAC addresses since
+            // FlowObjective does not guarantee the execution order.
+            Set<MacAddress> sameMacAddresses = new HashSet<>(macAddresses);
+            sameMacAddresses.retainAll(prevMacAddresses);
+            macAddresses.removeAll(sameMacAddresses);
+            prevMacAddresses.removeAll(sameMacAddresses);
+
+            revokeVRouter(device.id(), prevMacAddresses);
+            populateVRouter(device.id(), macAddresses);
+        });
+
+    }
+
+    /**
+     * Processes Segment Routing App Config removed event.
+     *
+     * @param event network config removed event
+     */
+    protected void processAppConfigRemoved(NetworkConfigEvent event) {
+        log.info("Processing AppConfig CONFIG_REMOVED");
+        SegmentRoutingAppConfig prevConfig = (SegmentRoutingAppConfig) event.prevConfig().get();
+        deviceService.getAvailableDevices().forEach(device -> {
+            revokeVRouter(device.id(), getMacAddresses(prevConfig));
+        });
+    }
+
+    /**
+     * Populates initial vRouter rules.
+     *
+     * @param deviceId device ID
+     */
+    public void init(DeviceId deviceId) {
+        SegmentRoutingAppConfig config =
+                srManager.cfgService.getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        populateVRouter(deviceId, getMacAddresses(config));
+    }
+
+    private void populateVRouter(DeviceId deviceId, Set<MacAddress> pendingAdd) {
+        if (!isEdge(deviceId)) {
+            return;
+        }
+        getVRouterFlowObjBuilders(pendingAdd).forEach(foBuilder -> {
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("vRouterMac filter for {} populated", pendingAdd),
+                    (objective, error) ->
+                            log.warn("Failed to populate vRouterMac filter for {}: {}", pendingAdd, error));
+            srManager.flowObjectiveService.filter(deviceId, foBuilder.add(context));
+        });
+    }
+
+    private void revokeVRouter(DeviceId deviceId, Set<MacAddress> pendingRemove) {
+        if (!isEdge(deviceId)) {
+            return;
+        }
+        getVRouterFlowObjBuilders(pendingRemove).forEach(foBuilder -> {
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("vRouterMac filter for {} revoked", pendingRemove),
+                    (objective, error) ->
+                            log.warn("Failed to revoke vRouterMac filter for {}: {}", pendingRemove, error));
+            srManager.flowObjectiveService.filter(deviceId, foBuilder.remove(context));
+        });
+    }
+
+    private Set<FilteringObjective.Builder> getVRouterFlowObjBuilders(Set<MacAddress> macAddresses) {
+        ImmutableSet.Builder<FilteringObjective.Builder> setBuilder = ImmutableSet.builder();
+        macAddresses.forEach(macAddress -> {
+            FilteringObjective.Builder fobuilder = DefaultFilteringObjective.builder();
+            fobuilder.withKey(Criteria.matchInPort(PortNumber.ANY))
+                    .addCondition(Criteria.matchEthDst(macAddress))
+                    .permit()
+                    .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+                    .fromApp(srManager.appId);
+            setBuilder.add(fobuilder);
+        });
+        return setBuilder.build();
+    }
+
+    private Set<MacAddress> getMacAddresses(SegmentRoutingAppConfig config) {
+        if (config == null) {
+            return ImmutableSet.of();
+        }
+        return ImmutableSet.copyOf(config.vRouterMacs());
+    }
+
+    private boolean isEdge(DeviceId deviceId) {
+        try {
+            if (srManager.deviceConfiguration.isEdgeDevice(deviceId)) {
+                return true;
+            }
+        } catch (DeviceConfigNotFoundException e) { }
+        return false;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java
new file mode 100644
index 0000000..b3a0aca
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/ArpHandler.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.packet.ARP;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.host.HostService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.neighbour.NeighbourMessageType.REQUEST;
+
+/**
+ * Handler of ARP packets that responses or forwards ARP packets that
+ * are sent to the controller.
+ */
+public class ArpHandler extends SegmentRoutingNeighbourHandler {
+
+    private static Logger log = LoggerFactory.getLogger(ArpHandler.class);
+
+    /**
+     * Creates an ArpHandler object.
+     *
+     * @param srManager SegmentRoutingManager object
+     */
+    public ArpHandler(SegmentRoutingManager srManager) {
+        super(srManager);
+    }
+
+    /**
+     * Processes incoming ARP packets.
+     *
+     * If it is an ARP request to router itself or known hosts,
+     * then it sends ARP response.
+     * If it is an ARP request to unknown hosts in its own subnet,
+     * then it flood the ARP request to the ports.
+     * If it is an ARP response, then set a flow rule for the host
+     * and forward any IP packets to the host in the packet buffer to the host.
+     * <p>
+     * Note: We handles all ARP packet in, even for those ARP packets between
+     * hosts in the same subnet.
+     * For an ARP packet with broadcast destination MAC,
+     * some switches pipelines will send it to the controller due to table miss,
+     * other switches will flood the packets directly in the data plane without
+     * packet in.
+     * We can deal with both cases.
+     *
+     * @param pkt incoming ARP packet and context information
+     * @param hostService the host service
+     */
+    public void processPacketIn(NeighbourMessageContext pkt, HostService hostService) {
+
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(pkt.inPort())) {
+            // Ignore ARP packets come from suppressed ports
+            pkt.drop();
+            return;
+        }
+
+        if (!validateArpSpa(pkt)) {
+            log.debug("Ignore ARP packet discovered on {} with unexpected src protocol address {}.",
+                    pkt.inPort(), pkt.sender().getIp4Address());
+            pkt.drop();
+            return;
+        }
+
+        if (pkt.type() == REQUEST) {
+            handleArpRequest(pkt, hostService);
+        } else {
+            handleArpReply(pkt, hostService);
+        }
+    }
+
+    private void handleArpRequest(NeighbourMessageContext pkt, HostService hostService) {
+        // ARP request for router. Send ARP reply.
+        if (isArpForRouter(pkt)) {
+            MacAddress targetMac = config.getRouterMacForAGatewayIp(pkt.target().getIp4Address());
+            sendResponse(pkt, targetMac, hostService);
+        } else {
+            // NOTE: Ignore ARP packets except those target for the router
+            //       We will reconsider enabling this when we have host learning support
+            /*
+            Set<Host> hosts = hostService.getHostsByIp(pkt.target());
+            if (hosts.size() > 1) {
+                log.warn("More than one host with the same ip {}", pkt.target());
+            }
+            Host targetHost = hosts.stream().findFirst().orElse(null);
+            // ARP request for known hosts. Send proxy ARP reply on behalf of the target.
+            if (targetHost != null) {
+                pkt.forward(targetHost.location());
+            // ARP request for unknown host in the subnet. Flood in the subnet.
+            } else {
+                flood(pkt);
+            }
+            */
+        }
+    }
+
+    private void handleArpReply(NeighbourMessageContext pkt, HostService hostService) {
+        // ARP reply for router. Process all pending IP packets.
+        if (isArpForRouter(pkt)) {
+            Ip4Address hostIpAddress = pkt.sender().getIp4Address();
+            srManager.ipHandler.forwardPackets(pkt.inPort().deviceId(), hostIpAddress);
+        } else {
+            // NOTE: Ignore ARP packets except those target for the router
+            //       We will reconsider enabling this when we have host learning support
+            /*
+            HostId targetHostId = HostId.hostId(pkt.dstMac(), pkt.vlan());
+            Host targetHost = hostService.getHost(targetHostId);
+            // ARP reply for known hosts. Forward to the host.
+            if (targetHost != null) {
+                pkt.forward(targetHost.location());
+            // ARP reply for unknown host, Flood in the subnet.
+            } else {
+                // Don't flood to non-edge ports
+                if (pkt.vlan().equals(SegmentRoutingManager.INTERNAL_VLAN)) {
+                    return;
+                }
+                flood(pkt);
+            }
+            */
+        }
+    }
+
+    /**
+     * Check if the source protocol address of an ARP packet belongs to the same
+     * subnet configured on the port it is seen.
+     *
+     * @param pkt ARP packet and context information
+     * @return true if the source protocol address belongs to the configured subnet
+     */
+    private boolean validateArpSpa(NeighbourMessageContext pkt) {
+        Ip4Address spa = pkt.sender().getIp4Address();
+        Set<IpPrefix> subnet = config.getPortSubnets(pkt.inPort().deviceId(), pkt.inPort().port())
+                .stream()
+                .filter(ipPrefix -> ipPrefix.isIp4() && ipPrefix.contains(spa))
+                .collect(Collectors.toSet());
+        return !subnet.isEmpty();
+    }
+
+
+    private boolean isArpForRouter(NeighbourMessageContext pkt) {
+        Ip4Address targetProtocolAddress = pkt.target().getIp4Address();
+        Set<IpAddress> gatewayIpAddresses = null;
+        try {
+            if (targetProtocolAddress.equals(config.getRouterIpv4(pkt.inPort().deviceId()))) {
+                return true;
+            }
+            gatewayIpAddresses = config.getPortIPs(pkt.inPort().deviceId());
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting check for router IP in processing arp");
+        }
+        if (gatewayIpAddresses != null &&
+                gatewayIpAddresses.contains(targetProtocolAddress)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Sends an APR request for the target IP address to all ports except in-port.
+     *
+     * @param deviceId Switch device ID
+     * @param targetAddress target IP address for ARP
+     * @param inPort in-port
+     */
+    public void sendArpRequest(DeviceId deviceId, IpAddress targetAddress, ConnectPoint inPort) {
+        byte[] senderMacAddress = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+        byte[] senderIpAddress = new byte[Ip4Address.BYTE_LENGTH];
+        /*
+         * Retrieves device info.
+         */
+        if (!getSenderInfo(senderMacAddress, senderIpAddress, deviceId, targetAddress)) {
+            log.warn("Aborting sendArpRequest, we cannot get all the information needed");
+            return;
+        }
+        /*
+         * Creates the request.
+         */
+        Ethernet arpRequest = ARP.buildArpRequest(
+                senderMacAddress,
+                senderIpAddress,
+                targetAddress.toOctets(),
+                VlanId.NO_VID
+        );
+        flood(arpRequest, inPort, targetAddress);
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
new file mode 100644
index 0000000..5baa6a9
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultRoutingHandler.java
@@ -0,0 +1,1612 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Default routing handler that is responsible for route computing and
+ * routing rule population.
+ */
+public class DefaultRoutingHandler {
+    private static final int MAX_CONSTANT_RETRY_ATTEMPTS = 5;
+    private static final long RETRY_INTERVAL_MS = 250L;
+    private static final int RETRY_INTERVAL_SCALE = 1;
+    private static final long STABLITY_THRESHOLD = 10; //secs
+    private static Logger log = LoggerFactory.getLogger(DefaultRoutingHandler.class);
+
+    private SegmentRoutingManager srManager;
+    private RoutingRulePopulator rulePopulator;
+    private HashMap<DeviceId, EcmpShortestPathGraph> currentEcmpSpgMap;
+    private HashMap<DeviceId, EcmpShortestPathGraph> updatedEcmpSpgMap;
+    private DeviceConfiguration config;
+    private final Lock statusLock = new ReentrantLock();
+    private volatile Status populationStatus;
+    private ScheduledExecutorService executorService
+        = newScheduledThreadPool(1, groupedThreads("retryftr", "retry-%d", log));
+    private Instant lastRoutingChange;
+
+    /**
+     * Represents the default routing population status.
+     */
+    public enum Status {
+        // population process is not started yet.
+        IDLE,
+
+        // population process started.
+        STARTED,
+
+        // population process was aborted due to errors, mostly for groups not
+        // found.
+        ABORTED,
+
+        // population process was finished successfully.
+        SUCCEEDED
+    }
+
+    /**
+     * Creates a DefaultRoutingHandler object.
+     *
+     * @param srManager SegmentRoutingManager object
+     */
+    public DefaultRoutingHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.rulePopulator = checkNotNull(srManager.routingRulePopulator);
+        this.config = checkNotNull(srManager.deviceConfiguration);
+        this.populationStatus = Status.IDLE;
+        this.currentEcmpSpgMap = Maps.newHashMap();
+    }
+
+    /**
+     * Returns an immutable copy of the current ECMP shortest-path graph as
+     * computed by this controller instance.
+     *
+     * @return immutable copy of the current ECMP graph
+     */
+    public ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEmcpSpgMap() {
+        Builder<DeviceId, EcmpShortestPathGraph> builder = ImmutableMap.builder();
+        currentEcmpSpgMap.entrySet().forEach(entry -> {
+            if (entry.getValue() != null) {
+                builder.put(entry.getKey(), entry.getValue());
+            }
+        });
+        return builder.build();
+    }
+
+    /**
+     * Acquires the lock used when making routing changes.
+     */
+    public void acquireRoutingLock() {
+        statusLock.lock();
+    }
+
+    /**
+     * Releases the lock used when making routing changes.
+     */
+    public void releaseRoutingLock() {
+        statusLock.unlock();
+    }
+
+    /**
+    * Determines if routing in the network has been stable in the last
+    * STABLITY_THRESHOLD seconds, by comparing the current time to the last
+    * routing change timestamp.
+    *
+    * @return true if stable
+    */
+   public boolean isRoutingStable() {
+       long last = (long) (lastRoutingChange.toEpochMilli() / 1000.0);
+       long now = (long) (Instant.now().toEpochMilli() / 1000.0);
+       log.trace("Routing stable since {}s", now - last);
+       return (now - last) > STABLITY_THRESHOLD;
+   }
+
+
+    //////////////////////////////////////
+    //  Route path handling
+    //////////////////////////////////////
+
+    /* The following three methods represent the three major ways in which
+     * route-path handling is triggered in the network
+     *      a) due to configuration change
+     *      b) due to route-added event
+     *      c) due to change in the topology
+     */
+
+    /**
+     * Populates all routing rules to all switches. Typically triggered at
+     * startup or after a configuration event.
+     */
+    public void populateAllRoutingRules() {
+        lastRoutingChange = Instant.now();
+        statusLock.lock();
+        try {
+            if (populationStatus == Status.STARTED) {
+                log.warn("Previous rule population is not finished. Cannot"
+                        + " proceed with populateAllRoutingRules");
+                return;
+            }
+
+            populationStatus = Status.STARTED;
+            rulePopulator.resetCounter();
+            log.info("Starting to populate all routing rules");
+            log.debug("populateAllRoutingRules: populationStatus is STARTED");
+
+            // take a snapshot of the topology
+            updatedEcmpSpgMap = new HashMap<>();
+            Set<EdgePair> edgePairs = new HashSet<>();
+            Set<ArrayList<DeviceId>> routeChanges = new HashSet<>();
+            for (DeviceId dstSw : srManager.deviceConfiguration.getRouters()) {
+                EcmpShortestPathGraph ecmpSpgUpdated =
+                        new EcmpShortestPathGraph(dstSw, srManager);
+                updatedEcmpSpgMap.put(dstSw, ecmpSpgUpdated);
+                DeviceId pairDev = getPairDev(dstSw);
+                if (pairDev != null) {
+                    // pairDev may not be available yet, but we still need to add
+                    ecmpSpgUpdated = new EcmpShortestPathGraph(pairDev, srManager);
+                    updatedEcmpSpgMap.put(pairDev, ecmpSpgUpdated);
+                    edgePairs.add(new EdgePair(dstSw, pairDev));
+                }
+                DeviceId ret = shouldHandleRouting(dstSw);
+                if (ret == null) {
+                    continue;
+                }
+                Set<DeviceId> devsToProcess = Sets.newHashSet(dstSw, ret);
+                // To do a full reroute, assume all routes have changed
+                for (DeviceId dev : devsToProcess) {
+                    for (DeviceId targetSw : srManager.deviceConfiguration.getRouters()) {
+                        if (targetSw.equals(dev)) {
+                            continue;
+                        }
+                        routeChanges.add(Lists.newArrayList(targetSw, dev));
+                    }
+                }
+            }
+
+            if (!redoRouting(routeChanges, edgePairs, null)) {
+                log.debug("populateAllRoutingRules: populationStatus is ABORTED");
+                populationStatus = Status.ABORTED;
+                log.warn("Failed to repopulate all routing rules.");
+                return;
+            }
+
+            log.debug("populateAllRoutingRules: populationStatus is SUCCEEDED");
+            populationStatus = Status.SUCCEEDED;
+            log.info("Completed all routing rule population. Total # of rules pushed : {}",
+                    rulePopulator.getCounter());
+            return;
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    /**
+     * Populate rules from all other edge devices to the connect-point(s)
+     * specified for the given subnets.
+     *
+     * @param cpts connect point(s) of the subnets being added
+     * @param subnets subnets being added
+     */
+    // XXX refactor
+    protected void populateSubnet(Set<ConnectPoint> cpts, Set<IpPrefix> subnets) {
+        if (cpts == null || cpts.size() < 1 || cpts.size() > 2) {
+            log.warn("Skipping populateSubnet due to illegal size of connect points. {}", cpts);
+            return;
+        }
+
+        lastRoutingChange = Instant.now();
+        statusLock.lock();
+        try {
+           if (populationStatus == Status.STARTED) {
+                log.warn("Previous rule population is not finished. Cannot"
+                        + " proceed with routing rules for added routes");
+                return;
+            }
+            populationStatus = Status.STARTED;
+            rulePopulator.resetCounter();
+            log.info("Starting to populate routing rules for added routes, subnets={}, cpts={}",
+                    subnets, cpts);
+            // In principle an update to a subnet/prefix should not require a
+            // new ECMPspg calculation as it is not a topology event. As a
+            // result, we use the current/existing ECMPspg in the updated map
+            // used by the redoRouting method.
+            if (updatedEcmpSpgMap == null) {
+                updatedEcmpSpgMap = new HashMap<>();
+            }
+            currentEcmpSpgMap.entrySet().forEach(entry -> {
+                updatedEcmpSpgMap.put(entry.getKey(), entry.getValue());
+                if (log.isTraceEnabled()) {
+                    log.trace("Root switch: {}", entry.getKey());
+                    log.trace("  Current/Existing SPG: {}", entry.getValue());
+                }
+            });
+            Set<EdgePair> edgePairs = new HashSet<>();
+            Set<ArrayList<DeviceId>> routeChanges = new HashSet<>();
+            boolean handleRouting = false;
+
+            if (cpts.size() == 2) {
+                // ensure connect points are edge-pairs
+                Iterator<ConnectPoint> iter = cpts.iterator();
+                DeviceId dev1 = iter.next().deviceId();
+                DeviceId pairDev = getPairDev(dev1);
+                if (iter.next().deviceId().equals(pairDev)) {
+                    edgePairs.add(new EdgePair(dev1, pairDev));
+                } else {
+                    log.warn("Connectpoints {} for subnets {} not on "
+                            + "pair-devices.. aborting populateSubnet", cpts, subnets);
+                    populationStatus = Status.ABORTED;
+                    return;
+                }
+                for (ConnectPoint cp : cpts) {
+                    if (updatedEcmpSpgMap.get(cp.deviceId()) == null) {
+                        EcmpShortestPathGraph ecmpSpgUpdated =
+                            new EcmpShortestPathGraph(cp.deviceId(), srManager);
+                        updatedEcmpSpgMap.put(cp.deviceId(), ecmpSpgUpdated);
+                        log.warn("populateSubnet: no updated graph for dev:{}"
+                                + " ... creating", cp.deviceId());
+                    }
+                    DeviceId retId = shouldHandleRouting(cp.deviceId());
+                    if (retId == null) {
+                        continue;
+                    }
+                    handleRouting = true;
+                }
+            } else {
+                // single connect point
+                DeviceId dstSw = cpts.iterator().next().deviceId();
+                if (updatedEcmpSpgMap.get(dstSw) == null) {
+                    EcmpShortestPathGraph ecmpSpgUpdated =
+                        new EcmpShortestPathGraph(dstSw, srManager);
+                    updatedEcmpSpgMap.put(dstSw, ecmpSpgUpdated);
+                    log.warn("populateSubnet: no updated graph for dev:{}"
+                            + " ... creating", dstSw);
+                }
+                if (srManager.mastershipService.isLocalMaster(dstSw)) {
+                    handleRouting = true;
+                }
+            }
+
+            if (!handleRouting) {
+                log.debug("This instance is not handling ecmp routing to the "
+                        + "connectPoint(s) {}", cpts);
+                populationStatus = Status.ABORTED;
+                return;
+            }
+
+            // if it gets here, this instance should handle routing for the
+            // connectpoint(s). Assume all route-paths have to be updated to
+            // the connectpoint(s) with the following exceptions
+            // 1. if target is non-edge no need for routing rules
+            // 2. if target is one of the connectpoints
+            for (ConnectPoint cp : cpts) {
+                DeviceId dstSw = cp.deviceId();
+                for (Device targetSw : srManager.deviceService.getDevices()) {
+                    boolean isEdge = false;
+                    try {
+                        isEdge = config.isEdgeDevice(targetSw.id());
+                    } catch (DeviceConfigNotFoundException e) {
+                        log.warn(e.getMessage() + "aborting populateSubnet on targetSw {}", targetSw.id());
+                        continue;
+                    }
+                    if (dstSw.equals(targetSw.id()) || !isEdge ||
+                            (cpts.size() == 2 &&
+                                targetSw.id().equals(getPairDev(dstSw)))) {
+                        continue;
+                    }
+                    routeChanges.add(Lists.newArrayList(targetSw.id(), dstSw));
+                }
+            }
+
+            if (!redoRouting(routeChanges, edgePairs, subnets)) {
+                log.debug("populateSubnet: populationStatus is ABORTED");
+                populationStatus = Status.ABORTED;
+                log.warn("Failed to repopulate the rules for subnet.");
+                return;
+            }
+
+            log.debug("populateSubnet: populationStatus is SUCCEEDED");
+            populationStatus = Status.SUCCEEDED;
+            log.info("Completed subnet population. Total # of rules pushed : {}",
+                    rulePopulator.getCounter());
+            return;
+
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    /**
+     * Populates the routing rules or makes hash group changes according to the
+     * route-path changes due to link failure, switch failure or link up. This
+     * method should only be called for one of these three possible event-types.
+     * Note that when a switch goes away, all of its links fail as well,
+     * but this is handled as a single switch removal event.
+     *
+     * @param linkDown the single failed link, or null for other conditions
+     *                  such as link-up or a removed switch
+     * @param linkUp the single link up, or null for other conditions such as
+     *                  link-down or a removed switch
+     * @param switchDown the removed switch, or null for other conditions such as
+     *                  link-down or link-up
+     */ // refactor
+    public void populateRoutingRulesForLinkStatusChange(Link linkDown,
+                                                           Link linkUp,
+                                                           DeviceId switchDown) {
+        if ((linkDown != null && (linkUp != null || switchDown != null)) ||
+                (linkUp != null && (linkDown != null || switchDown != null)) ||
+                (switchDown != null && (linkUp != null || linkDown != null))) {
+            log.warn("Only one event can be handled for link status change .. aborting");
+            return;
+        }
+        lastRoutingChange = Instant.now();
+        statusLock.lock();
+        try {
+
+            if (populationStatus == Status.STARTED) {
+                log.warn("Previous rule population is not finished. Cannot"
+                        + " proceeed with routingRules for Topology change");
+                return;
+            }
+
+            // Take snapshots of the topology
+            updatedEcmpSpgMap = new HashMap<>();
+            Set<EdgePair> edgePairs = new HashSet<>();
+            for (Device sw : srManager.deviceService.getDevices()) {
+                EcmpShortestPathGraph ecmpSpgUpdated =
+                        new EcmpShortestPathGraph(sw.id(), srManager);
+                updatedEcmpSpgMap.put(sw.id(), ecmpSpgUpdated);
+                DeviceId pairDev = getPairDev(sw.id());
+                if (pairDev != null) {
+                    // pairDev may not be available yet, but we still need to add
+                    ecmpSpgUpdated = new EcmpShortestPathGraph(pairDev, srManager);
+                    updatedEcmpSpgMap.put(pairDev, ecmpSpgUpdated);
+                    edgePairs.add(new EdgePair(sw.id(), pairDev));
+                }
+            }
+
+            log.info("Starting to populate routing rules from Topology change");
+
+            Set<ArrayList<DeviceId>> routeChanges;
+            log.debug("populateRoutingRulesForLinkStatusChange: "
+                    + "populationStatus is STARTED");
+            populationStatus = Status.STARTED;
+            rulePopulator.resetCounter(); //XXX maybe useful to have a rehash ctr
+            boolean hashGroupsChanged = false;
+            // try optimized re-routing
+            if (linkDown == null) {
+                // either a linkUp or a switchDown - compute all route changes by
+                // comparing all routes of existing ECMP SPG to new ECMP SPG
+                routeChanges = computeRouteChange();
+
+                // deal with linkUp of a seen-before link
+                if (linkUp != null && srManager.linkHandler.isSeenLink(linkUp)) {
+                    if (!srManager.linkHandler.isBidirectional(linkUp)) {
+                        log.warn("Not a bidirectional link yet .. not "
+                                + "processing link {}", linkUp);
+                        srManager.linkHandler.updateSeenLink(linkUp, true);
+                        populationStatus = Status.ABORTED;
+                        return;
+                    }
+                    // link previously seen before
+                    // do hash-bucket changes instead of a re-route
+                    processHashGroupChange(routeChanges, false, null);
+                    // clear out routesChanges so a re-route is not attempted
+                    routeChanges = ImmutableSet.of();
+                    hashGroupsChanged = true;
+                }
+                // for a linkUp of a never-seen-before link
+                // let it fall through to a reroute of the routeChanges
+
+                // now that we are past the check for a previously seen link
+                // it is safe to update the store for the linkUp
+                if (linkUp != null) {
+                    srManager.linkHandler.updateSeenLink(linkUp, true);
+                }
+
+                //deal with switchDown
+                if (switchDown != null) {
+                    processHashGroupChange(routeChanges, true, switchDown);
+                    // clear out routesChanges so a re-route is not attempted
+                    routeChanges = ImmutableSet.of();
+                    hashGroupsChanged = true;
+                }
+            } else {
+                // link has gone down
+                // Compare existing ECMP SPG only with the link that went down
+                routeChanges = computeDamagedRoutes(linkDown);
+                if (routeChanges != null) {
+                    processHashGroupChange(routeChanges, true, null);
+                    // clear out routesChanges so a re-route is not attempted
+                    routeChanges = ImmutableSet.of();
+                    hashGroupsChanged = true;
+                }
+            }
+
+            // do full re-routing if optimized routing returns null routeChanges
+            if (routeChanges == null) {
+                log.warn("Optimized routing failed... opting for full reroute");
+                populationStatus = Status.ABORTED;
+                populateAllRoutingRules();
+                return;
+            }
+
+            if (routeChanges.isEmpty()) {
+                if (hashGroupsChanged) {
+                    log.info("Hash-groups changed for link status change");
+                } else {
+                    log.info("No re-route or re-hash attempted for the link"
+                            + " status change");
+                    updatedEcmpSpgMap.keySet().forEach(devId -> {
+                        currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+                        log.debug("Updating ECMPspg for remaining dev:{}", devId);
+                    });
+                }
+                log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is SUCCEEDED");
+                populationStatus = Status.SUCCEEDED;
+                return;
+            }
+
+            // reroute of routeChanges
+            if (redoRouting(routeChanges, edgePairs, null)) {
+                log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is SUCCEEDED");
+                populationStatus = Status.SUCCEEDED;
+                log.info("Completed repopulation of rules for link-status change."
+                        + " # of rules populated : {}", rulePopulator.getCounter());
+                return;
+            } else {
+                log.debug("populateRoutingRulesForLinkStatusChange: populationStatus is ABORTED");
+                populationStatus = Status.ABORTED;
+                log.warn("Failed to repopulate the rules for link status change.");
+                return;
+            }
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    /**
+     * Processes a set a route-path changes by reprogramming routing rules and
+     * creating new hash-groups or editing them if necessary. This method also
+     * determines the next-hops for the route-path from the src-switch (target)
+     * of the path towards the dst-switch of the path.
+     *
+     * @param routeChanges a set of route-path changes, where each route-path is
+     *                     a list with its first element the src-switch (target)
+     *                     of the path, and the second element the dst-switch of
+     *                     the path.
+     * @param edgePairs a set of edge-switches that are paired by configuration
+     * @param subnets  a set of prefixes that need to be populated in the routing
+     *                 table of the target switch in the route-path. Can be null,
+     *                 in which case all the prefixes belonging to the dst-switch
+     *                 will be populated in the target switch
+     * @return true if successful in repopulating all routes
+     */
+    private boolean redoRouting(Set<ArrayList<DeviceId>> routeChanges,
+                                Set<EdgePair> edgePairs, Set<IpPrefix> subnets) {
+        // first make every entry two-elements
+        Set<ArrayList<DeviceId>> changedRoutes = new HashSet<>();
+        for (ArrayList<DeviceId> route : routeChanges) {
+            if (route.size() == 1) {
+                DeviceId dstSw = route.get(0);
+                EcmpShortestPathGraph ec = updatedEcmpSpgMap.get(dstSw);
+                if (ec == null) {
+                    log.warn("No graph found for {} .. aborting redoRouting", dstSw);
+                    return false;
+                }
+                ec.getAllLearnedSwitchesAndVia().keySet().forEach(key -> {
+                    ec.getAllLearnedSwitchesAndVia().get(key).keySet().forEach(target -> {
+                        changedRoutes.add(Lists.newArrayList(target, dstSw));
+                    });
+                });
+            } else {
+                DeviceId targetSw = route.get(0);
+                DeviceId dstSw = route.get(1);
+                changedRoutes.add(Lists.newArrayList(targetSw, dstSw));
+            }
+        }
+
+        // now process changedRoutes according to edgePairs
+        if (!redoRoutingEdgePairs(edgePairs, subnets, changedRoutes)) {
+            return false; //abort routing and fail fast
+        }
+
+        // whatever is left in changedRoutes is now processed for individual dsts.
+        Set<DeviceId> updatedDevices = Sets.newHashSet();
+        if (!redoRoutingIndividualDests(subnets, changedRoutes,
+                                        updatedDevices)) {
+            return false; //abort routing and fail fast
+        }
+
+        // update ecmpSPG for all edge-pairs
+        for (EdgePair ep : edgePairs) {
+            currentEcmpSpgMap.put(ep.dev1, updatedEcmpSpgMap.get(ep.dev1));
+            currentEcmpSpgMap.put(ep.dev2, updatedEcmpSpgMap.get(ep.dev2));
+            log.debug("Updating ECMPspg for edge-pair:{}-{}", ep.dev1, ep.dev2);
+        }
+
+        // here is where we update all devices not touched by this instance
+        updatedEcmpSpgMap.keySet().stream()
+            .filter(devId -> !edgePairs.stream().anyMatch(ep -> ep.includes(devId)))
+            .filter(devId -> !updatedDevices.contains(devId))
+            .forEach(devId -> {
+                currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+                log.debug("Updating ECMPspg for remaining dev:{}", devId);
+            });
+        return true;
+    }
+
+    /**
+     * Programs targetSw in the changedRoutes for given prefixes reachable by
+     * an edgePair. If no prefixes are given, the method will use configured
+     * subnets/prefixes. If some configured subnets belong only to a specific
+     * destination in the edgePair, then the target switch will be programmed
+     * only to that destination.
+     *
+     * @param edgePairs set of edge-pairs for which target will be programmed
+     * @param subnets a set of prefixes that need to be populated in the routing
+     *                 table of the target switch in the changedRoutes. Can be null,
+     *                 in which case all the configured prefixes belonging to the
+     *                 paired switches will be populated in the target switch
+     * @param changedRoutes a set of route-path changes, where each route-path is
+     *                     a list with its first element the src-switch (target)
+     *                     of the path, and the second element the dst-switch of
+     *                     the path.
+     * @return true if successful
+     */
+    private boolean redoRoutingEdgePairs(Set<EdgePair> edgePairs,
+                                      Set<IpPrefix> subnets,
+                                      Set<ArrayList<DeviceId>> changedRoutes) {
+        for (EdgePair ep : edgePairs) {
+            // temp store for a target's changedRoutes to this edge-pair
+            Map<DeviceId, Set<ArrayList<DeviceId>>> targetRoutes = new HashMap<>();
+            Iterator<ArrayList<DeviceId>> i = changedRoutes.iterator();
+            while (i.hasNext()) {
+                ArrayList<DeviceId> route = i.next();
+                DeviceId dstSw = route.get(1);
+                if (ep.includes(dstSw)) {
+                    // routeChange for edge pair found
+                    // sort by target iff target is edge and remove from changedRoutes
+                    DeviceId targetSw = route.get(0);
+                    try {
+                        if (!srManager.deviceConfiguration.isEdgeDevice(targetSw)) {
+                            continue;
+                        }
+                    } catch (DeviceConfigNotFoundException e) {
+                        log.warn(e.getMessage() + "aborting redoRouting");
+                        return false;
+                    }
+                    // route is from another edge to this edge-pair
+                    if (targetRoutes.containsKey(targetSw)) {
+                        targetRoutes.get(targetSw).add(route);
+                    } else {
+                        Set<ArrayList<DeviceId>> temp = new HashSet<>();
+                        temp.add(route);
+                        targetRoutes.put(targetSw, temp);
+                    }
+                    i.remove();
+                }
+            }
+            // so now for this edgepair we have a per target set of routechanges
+            // process target->edgePair route
+            for (Map.Entry<DeviceId, Set<ArrayList<DeviceId>>> entry :
+                            targetRoutes.entrySet()) {
+                log.debug("* redoRoutingDstPair Target:{} -> edge-pair {}",
+                          entry.getKey(), ep);
+                DeviceId targetSw = entry.getKey();
+                Map<DeviceId, Set<DeviceId>> perDstNextHops = new HashMap<>();
+                entry.getValue().forEach(route -> {
+                    Set<DeviceId> nhops = getNextHops(route.get(0), route.get(1));
+                    log.debug("route: target {} -> dst {} found with next-hops {}",
+                              route.get(0), route.get(1), nhops);
+                    perDstNextHops.put(route.get(1), nhops);
+                });
+                Set<IpPrefix> ipDev1 = (subnets == null) ? config.getSubnets(ep.dev1)
+                                                         : subnets;
+                Set<IpPrefix> ipDev2 = (subnets == null) ? config.getSubnets(ep.dev2)
+                                                         : subnets;
+                ipDev1 = (ipDev1 == null) ? Sets.newHashSet() : ipDev1;
+                ipDev2 = (ipDev2 == null) ? Sets.newHashSet() : ipDev2;
+                Set<DeviceId> nhDev1 = perDstNextHops.get(ep.dev1);
+                Set<DeviceId> nhDev2 = perDstNextHops.get(ep.dev2);
+                // handle routing to subnets common to edge-pair
+                // only if the targetSw is not part of the edge-pair and there
+                // exists a next hop to at least one of the devices in the edge-pair
+                if (!ep.includes(targetSw)
+                        && ((nhDev1 != null && !nhDev1.isEmpty())
+                                || (nhDev2 != null && !nhDev2.isEmpty()))) {
+                    if (!populateEcmpRoutingRulePartial(
+                             targetSw,
+                             ep.dev1, ep.dev2,
+                             perDstNextHops,
+                             Sets.intersection(ipDev1, ipDev2))) {
+                        return false; // abort everything and fail fast
+                    }
+                }
+                // handle routing to subnets that only belong to dev1 only if
+                // a next-hop exists from the target to dev1
+                Set<IpPrefix> onlyDev1Subnets = Sets.difference(ipDev1, ipDev2);
+                if (!onlyDev1Subnets.isEmpty()
+                        && nhDev1 != null  && !nhDev1.isEmpty()) {
+                    Map<DeviceId, Set<DeviceId>> onlyDev1NextHops = new HashMap<>();
+                    onlyDev1NextHops.put(ep.dev1, nhDev1);
+                    if (!populateEcmpRoutingRulePartial(
+                            targetSw,
+                            ep.dev1, null,
+                            onlyDev1NextHops,
+                            onlyDev1Subnets)) {
+                        return false; // abort everything and fail fast
+                    }
+                }
+                // handle routing to subnets that only belong to dev2 only if
+                // a next-hop exists from the target to dev2
+                Set<IpPrefix> onlyDev2Subnets = Sets.difference(ipDev2, ipDev1);
+                if (!onlyDev2Subnets.isEmpty()
+                        && nhDev2 != null && !nhDev2.isEmpty()) {
+                    Map<DeviceId, Set<DeviceId>> onlyDev2NextHops = new HashMap<>();
+                    onlyDev2NextHops.put(ep.dev2, nhDev2);
+                    if (!populateEcmpRoutingRulePartial(
+                            targetSw,
+                            ep.dev2, null,
+                            onlyDev2NextHops,
+                            onlyDev2Subnets)) {
+                        return false; // abort everything and fail fast
+                    }
+                }
+            }
+            // if it gets here it has succeeded for all targets to this edge-pair
+        }
+        return true;
+    }
+
+    /**
+     * Programs targetSw in the changedRoutes for given prefixes reachable by
+     * a destination switch that is not part of an edge-pair.
+     * If no prefixes are given, the method will use configured subnets/prefixes.
+     *
+     * @param subnets a set of prefixes that need to be populated in the routing
+     *                 table of the target switch in the changedRoutes. Can be null,
+     *                 in which case all the configured prefixes belonging to the
+     *                 paired switches will be populated in the target switch
+     * @param changedRoutes a set of route-path changes, where each route-path is
+     *                     a list with its first element the src-switch (target)
+     *                     of the path, and the second element the dst-switch of
+     *                     the path.
+     * @return true if successful
+     */
+    private boolean redoRoutingIndividualDests(Set<IpPrefix> subnets,
+                                               Set<ArrayList<DeviceId>> changedRoutes,
+                                               Set<DeviceId> updatedDevices) {
+        // aggregate route-path changes for each dst device
+        HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> routesBydevice =
+                new HashMap<>();
+        for (ArrayList<DeviceId> route: changedRoutes) {
+            DeviceId dstSw = route.get(1);
+            ArrayList<ArrayList<DeviceId>> deviceRoutes =
+                    routesBydevice.get(dstSw);
+            if (deviceRoutes == null) {
+                deviceRoutes = new ArrayList<>();
+                routesBydevice.put(dstSw, deviceRoutes);
+            }
+            deviceRoutes.add(route);
+        }
+        for (DeviceId impactedDstDevice : routesBydevice.keySet()) {
+            ArrayList<ArrayList<DeviceId>> deviceRoutes =
+                    routesBydevice.get(impactedDstDevice);
+            for (ArrayList<DeviceId> route: deviceRoutes) {
+                log.debug("* redoRoutingIndiDst Target: {} -> dst: {}",
+                          route.get(0), route.get(1));
+                DeviceId targetSw = route.get(0);
+                DeviceId dstSw = route.get(1); // same as impactedDstDevice
+                Set<DeviceId> nextHops = getNextHops(targetSw, dstSw);
+                if (nextHops.isEmpty()) {
+                    log.warn("Could not find next hop from target:{} --> dst {} "
+                            + "skipping this route", targetSw, dstSw);
+                    continue;
+                }
+                Map<DeviceId, Set<DeviceId>> nhops = new HashMap<>();
+                nhops.put(dstSw, nextHops);
+                if (!populateEcmpRoutingRulePartial(targetSw, dstSw, null, nhops,
+                         (subnets == null) ? Sets.newHashSet() : subnets)) {
+                    return false; // abort routing and fail fast
+                }
+                log.debug("Populating flow rules from target: {} to dst: {}"
+                        + " is successful", targetSw, dstSw);
+            }
+            //Only if all the flows for all impacted routes to a
+            //specific target are pushed successfully, update the
+            //ECMP graph for that target. Or else the next event
+            //would not see any changes in the ECMP graphs.
+            //In another case, the target switch has gone away, so
+            //routes can't be installed. In that case, the current map
+            //is updated here, without any flows being pushed.
+            currentEcmpSpgMap.put(impactedDstDevice,
+                                  updatedEcmpSpgMap.get(impactedDstDevice));
+            updatedDevices.add(impactedDstDevice);
+            log.debug("Updating ECMPspg for impacted dev:{}", impactedDstDevice);
+        }
+        return true;
+    }
+
+    /**
+     * Populate ECMP rules for subnets from target to destination via nexthops.
+     *
+     * @param targetSw Device ID of target switch in which rules will be programmed
+     * @param destSw1 Device ID of final destination switch to which the rules will forward
+     * @param destSw2 Device ID of paired destination switch to which the rules will forward
+     *                A null deviceId indicates packets should only be sent to destSw1
+     * @param nextHops Map indication a list of next hops per destSw
+     * @param subnets Subnets to be populated. If empty, populate all configured subnets.
+     * @return true if it succeeds in populating rules
+     */ // refactor
+    private boolean populateEcmpRoutingRulePartial(DeviceId targetSw,
+                                                   DeviceId destSw1,
+                                                   DeviceId destSw2,
+                                                   Map<DeviceId, Set<DeviceId>> nextHops,
+                                                   Set<IpPrefix> subnets) {
+        boolean result;
+        // If both target switch and dest switch are edge routers, then set IP
+        // rule for both subnet and router IP.
+        boolean targetIsEdge;
+        boolean dest1IsEdge;
+        Ip4Address dest1RouterIpv4, dest2RouterIpv4 = null;
+        Ip6Address dest1RouterIpv6, dest2RouterIpv6 = null;
+
+        try {
+            targetIsEdge = config.isEdgeDevice(targetSw);
+            dest1IsEdge = config.isEdgeDevice(destSw1);
+            dest1RouterIpv4 = config.getRouterIpv4(destSw1);
+            dest1RouterIpv6 = config.getRouterIpv6(destSw1);
+            if (destSw2 != null) {
+                dest2RouterIpv4 = config.getRouterIpv4(destSw2);
+                dest2RouterIpv6 = config.getRouterIpv6(destSw2);
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting populateEcmpRoutingRulePartial.");
+            return false;
+        }
+
+        if (targetIsEdge && dest1IsEdge) {
+            subnets = (subnets != null && !subnets.isEmpty())
+                            ? Sets.newHashSet(subnets)
+                            : Sets.newHashSet(config.getSubnets(destSw1));
+            // XXX -  Rethink this
+            /*subnets.add(dest1RouterIpv4.toIpPrefix());
+            if (dest1RouterIpv6 != null) {
+                subnets.add(dest1RouterIpv6.toIpPrefix());
+            }
+            if (destSw2 != null && dest2RouterIpv4 != null) {
+                subnets.add(dest2RouterIpv4.toIpPrefix());
+                if (dest2RouterIpv6 != null) {
+                    subnets.add(dest2RouterIpv6.toIpPrefix());
+                }
+            }*/
+            log.debug(". populateEcmpRoutingRulePartial in device {} towards {} {} "
+                    + "for subnets {}", targetSw, destSw1,
+                                        (destSw2 != null) ? ("& " + destSw2) : "",
+                                        subnets);
+            result = rulePopulator.populateIpRuleForSubnet(targetSw, subnets,
+                                                           destSw1, destSw2,
+                                                           nextHops);
+            if (!result) {
+                return false;
+            }
+            /* XXX rethink this
+            IpPrefix routerIpPrefix = destRouterIpv4.toIpPrefix();
+            log.debug("* populateEcmpRoutingRulePartial in device {} towards {} "
+                    + "for router IP {}", targetSw, destSw, routerIpPrefix);
+            result = rulePopulator.populateIpRuleForRouter(targetSw, routerIpPrefix,
+                                                           destSw, nextHops);
+            if (!result) {
+                return false;
+            }
+            // If present we deal with IPv6 loopback.
+            if (destRouterIpv6 != null) {
+                routerIpPrefix = destRouterIpv6.toIpPrefix();
+                log.debug("* populateEcmpRoutingRulePartial in device {} towards {}"
+                        + " for v6 router IP {}", targetSw, destSw, routerIpPrefix);
+                result = rulePopulator.populateIpRuleForRouter(targetSw, routerIpPrefix,
+                                                               destSw, nextHops);
+                if (!result) {
+                    return false;
+                }
+            }*/
+        }
+
+        if (!targetIsEdge && dest1IsEdge) {
+            // MPLS rules in all non-edge target devices. These rules are for
+            // individual destinations, even if the dsts are part of edge-pairs.
+            log.debug(". populateEcmpRoutingRulePartial in device{} towards {} for "
+                    + "all MPLS rules", targetSw, destSw1);
+            result = rulePopulator.populateMplsRule(targetSw, destSw1,
+                                                    nextHops.get(destSw1),
+                                                    dest1RouterIpv4);
+            if (!result) {
+                return false;
+            }
+            if (dest1RouterIpv6 != null) {
+                result = rulePopulator.populateMplsRule(targetSw, destSw1,
+                                                        nextHops.get(destSw1),
+                                                        dest1RouterIpv6);
+                if (!result) {
+                    return false;
+                }
+            }
+        }
+
+        // To save on ECMP groups
+        // avoid MPLS rules in non-edge-devices to non-edge-devices
+        // avoid MPLS transit rules in edge-devices
+        // avoid loopback IP rules in edge-devices to non-edge-devices
+        return true;
+    }
+
+    /**
+     * Processes a set a route-path changes by editing hash groups.
+     *
+     * @param routeChanges a set of route-path changes, where each route-path is
+     *                     a list with its first element the src-switch of the path
+     *                     and the second element the dst-switch of the path.
+     * @param linkOrSwitchFailed true if the route changes are for a failed
+     *                           switch or linkDown event
+     * @param failedSwitch the switchId if the route changes are for a failed switch,
+     *                     otherwise null
+     */
+    private void processHashGroupChange(Set<ArrayList<DeviceId>> routeChanges,
+                                        boolean linkOrSwitchFailed,
+                                        DeviceId failedSwitch) {
+        Set<ArrayList<DeviceId>> changedRoutes = new HashSet<>();
+        // first, ensure each routeChanges entry has two elements
+        for (ArrayList<DeviceId> route : routeChanges) {
+            if (route.size() == 1) {
+                // route-path changes are from everyone else to this switch
+                DeviceId dstSw = route.get(0);
+                srManager.deviceService.getAvailableDevices().forEach(sw -> {
+                    if (!sw.id().equals(dstSw)) {
+                        changedRoutes.add(Lists.newArrayList(sw.id(), dstSw));
+                    }
+                });
+            } else {
+                changedRoutes.add(route);
+            }
+        }
+        boolean someFailed = false;
+        Set<DeviceId> updatedDevices = Sets.newHashSet();
+        for (ArrayList<DeviceId> route : changedRoutes) {
+            DeviceId targetSw = route.get(0);
+            DeviceId dstSw = route.get(1);
+            if (linkOrSwitchFailed) {
+                boolean success = fixHashGroupsForRoute(route, true);
+                // it's possible that we cannot fix hash groups for a route
+                // if the target switch has failed. Nevertheless the ecmp graph
+                // for the impacted switch must still be updated.
+                if (!success && failedSwitch != null && targetSw.equals(failedSwitch)) {
+                    currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+                    currentEcmpSpgMap.remove(targetSw);
+                    log.debug("Updating ECMPspg for dst:{} removing failed switch "
+                            + "target:{}", dstSw, targetSw);
+                    updatedDevices.add(targetSw);
+                    updatedDevices.add(dstSw);
+                    continue;
+                }
+                //linkfailed - update both sides
+                if (success) {
+                    currentEcmpSpgMap.put(targetSw, updatedEcmpSpgMap.get(targetSw));
+                    currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+                    log.debug("Updating ECMPspg for dst:{} and target:{} for linkdown"
+                            + " or switchdown", dstSw, targetSw);
+                    updatedDevices.add(targetSw);
+                    updatedDevices.add(dstSw);
+                } else {
+                    someFailed = true;
+                }
+            } else {
+                //linkup of seen before link
+                boolean success = fixHashGroupsForRoute(route, false);
+                if (success) {
+                    currentEcmpSpgMap.put(targetSw, updatedEcmpSpgMap.get(targetSw));
+                    currentEcmpSpgMap.put(dstSw, updatedEcmpSpgMap.get(dstSw));
+                    log.debug("Updating ECMPspg for target:{} and dst:{} for linkup",
+                              targetSw, dstSw);
+                    updatedDevices.add(targetSw);
+                    updatedDevices.add(dstSw);
+                } else {
+                    someFailed = true;
+                }
+            }
+        }
+        if (!someFailed) {
+            // here is where we update all devices not touched by this instance
+            updatedEcmpSpgMap.keySet().stream()
+                .filter(devId -> !updatedDevices.contains(devId))
+                .forEach(devId -> {
+                    currentEcmpSpgMap.put(devId, updatedEcmpSpgMap.get(devId));
+                    log.debug("Updating ECMPspg for remaining dev:{}", devId);
+            });
+        }
+    }
+
+    /**
+     * Edits hash groups in the src-switch (targetSw) of a route-path by
+     * calling the groupHandler to either add or remove buckets in an existing
+     * hash group.
+     *
+     * @param route a single list representing a route-path where the first element
+     *                  is the src-switch (targetSw) of the route-path and the
+     *                  second element is the dst-switch
+     * @param revoke true if buckets in the hash-groups need to be removed;
+     *              false if buckets in the hash-groups need to be added
+     * @return true if the hash group editing is successful
+     */
+    private boolean fixHashGroupsForRoute(ArrayList<DeviceId> route,
+                                          boolean revoke) {
+        DeviceId targetSw = route.get(0);
+        if (route.size() < 2) {
+            log.warn("Cannot fixHashGroupsForRoute - no dstSw in route {}", route);
+            return false;
+        }
+        DeviceId destSw = route.get(1);
+        log.debug("* processing fixHashGroupsForRoute: Target {} -> Dest {}",
+                  targetSw, destSw);
+        // figure out the new next hops at the targetSw towards the destSw
+        Set<DeviceId> nextHops = getNextHops(targetSw, destSw);
+        // call group handler to change hash group at targetSw
+        DefaultGroupHandler grpHandler = srManager.getGroupHandler(targetSw);
+        if (grpHandler == null) {
+            log.warn("Cannot find grouphandler for dev:{} .. aborting"
+                    + " {} hash group buckets for route:{} ", targetSw,
+                    (revoke) ? "revoke" : "repopulate", route);
+            return false;
+        }
+        log.debug("{} hash-groups buckets For Route {} -> {} to next-hops {}",
+                  (revoke) ? "revoke" : "repopulating",
+                  targetSw, destSw, nextHops);
+        return (revoke) ? grpHandler.fixHashGroups(targetSw, nextHops,
+                                                       destSw, true)
+                            : grpHandler.fixHashGroups(targetSw, nextHops,
+                                                       destSw, false);
+    }
+
+    /**
+     * Start the flow rule population process if it was never started. The
+     * process finishes successfully when all flow rules are set and stops with
+     * ABORTED status when any groups required for flows is not set yet.
+     */
+    public void startPopulationProcess() {
+        statusLock.lock();
+        try {
+            if (populationStatus == Status.IDLE
+                    || populationStatus == Status.SUCCEEDED
+                    || populationStatus == Status.ABORTED) {
+                populateAllRoutingRules();
+            } else {
+                log.warn("Not initiating startPopulationProcess as populationStatus is {}",
+                         populationStatus);
+            }
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    /**
+     * Revoke rules of given subnet in all edge switches.
+     *
+     * @param subnets subnet being removed
+     * @return true if succeed
+     */
+    protected boolean revokeSubnet(Set<IpPrefix> subnets) {
+        statusLock.lock();
+        try {
+            return srManager.routingRulePopulator.revokeIpRuleForSubnet(subnets);
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    /**
+     * Populates IP rules for a route that has direct connection to the switch
+     * if the current instance is the master of the switch.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param hostVlanId Vlan ID of the nexthop
+     * @param outPort port where the next hop attaches to
+     */
+    void populateRoute(DeviceId deviceId, IpPrefix prefix,
+                       MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        if (srManager.mastershipService.isLocalMaster(deviceId)) {
+            srManager.routingRulePopulator.populateRoute(deviceId, prefix, hostMac, hostVlanId, outPort);
+        }
+    }
+
+    /**
+     * Removes IP rules for a route when the next hop is gone.
+     * if the current instance is the master of the switch.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param hostVlanId Vlan ID of the nexthop
+     * @param outPort port that next hop attaches to
+     */
+    void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+                     MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        if (srManager.mastershipService.isLocalMaster(deviceId)) {
+            srManager.routingRulePopulator.revokeRoute(deviceId, prefix, hostMac, hostVlanId, outPort);
+        }
+    }
+
+    /**
+     * Remove ECMP graph entry for the given device. Typically called when
+     * device is no longer available.
+     *
+     * @param deviceId the device for which graphs need to be purged
+     */
+    protected void purgeEcmpGraph(DeviceId deviceId) {
+        statusLock.lock();
+        try {
+
+            if (populationStatus == Status.STARTED) {
+                log.warn("Previous rule population is not finished. Cannot"
+                        + " proceeed with purgeEcmpGraph for {}", deviceId);
+                return;
+            }
+            log.debug("Updating ECMPspg for unavailable dev:{}", deviceId);
+            currentEcmpSpgMap.remove(deviceId);
+            if (updatedEcmpSpgMap != null) {
+                updatedEcmpSpgMap.remove(deviceId);
+            }
+        } finally {
+            statusLock.unlock();
+        }
+    }
+
+    //////////////////////////////////////
+    //  Routing helper methods and classes
+    //////////////////////////////////////
+
+    /**
+     * Computes set of affected routes due to failed link. Assumes
+     * previous ecmp shortest-path graph exists for a switch in order to compute
+     * affected routes. If such a graph does not exist, the method returns null.
+     *
+     * @param linkFail the failed link
+     * @return the set of affected routes which may be empty if no routes were
+     *         affected, or null if no previous ecmp spg was found for comparison
+     */
+    private Set<ArrayList<DeviceId>> computeDamagedRoutes(Link linkFail) {
+        Set<ArrayList<DeviceId>> routes = new HashSet<>();
+
+        for (Device sw : srManager.deviceService.getDevices()) {
+            log.debug("Computing the impacted routes for device {} due to link fail",
+                      sw.id());
+            DeviceId retId = shouldHandleRouting(sw.id());
+            if (retId == null) {
+                continue;
+            }
+            Set<DeviceId> devicesToProcess = Sets.newHashSet(retId, sw.id());
+            for (DeviceId rootSw : devicesToProcess) {
+                EcmpShortestPathGraph ecmpSpg = currentEcmpSpgMap.get(rootSw);
+                if (ecmpSpg == null) {
+                    log.warn("No existing ECMP graph for switch {}. Aborting optimized"
+                            + " rerouting and opting for full-reroute", rootSw);
+                    return null;
+                }
+                if (log.isDebugEnabled()) {
+                    log.debug("Root switch: {}", rootSw);
+                    log.debug("  Current/Existing SPG: {}", ecmpSpg);
+                    log.debug("       New/Updated SPG: {}", updatedEcmpSpgMap.get(rootSw));
+                }
+                HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>>
+                    switchVia = ecmpSpg.getAllLearnedSwitchesAndVia();
+                // figure out if the broken link affected any route-paths in this graph
+                for (Integer itrIdx : switchVia.keySet()) {
+                    log.trace("Current/Exiting SPG Iterindex# {}", itrIdx);
+                    HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+                            switchVia.get(itrIdx);
+                    for (DeviceId targetSw : swViaMap.keySet()) {
+                        log.trace("TargetSwitch {} --> RootSwitch {}",
+                                  targetSw, rootSw);
+                        for (ArrayList<DeviceId> via : swViaMap.get(targetSw)) {
+                            log.trace(" Via:");
+                            via.forEach(e -> log.trace("  {}", e));
+                        }
+                        Set<ArrayList<DeviceId>> subLinks =
+                                computeLinks(targetSw, rootSw, swViaMap);
+                        for (ArrayList<DeviceId> alink: subLinks) {
+                            if ((alink.get(0).equals(linkFail.src().deviceId()) &&
+                                    alink.get(1).equals(linkFail.dst().deviceId()))
+                                    ||
+                                    (alink.get(0).equals(linkFail.dst().deviceId()) &&
+                                         alink.get(1).equals(linkFail.src().deviceId()))) {
+                                log.debug("Impacted route:{}->{}", targetSw, rootSw);
+                                ArrayList<DeviceId> aRoute = new ArrayList<>();
+                                aRoute.add(targetSw); // switch with rules to populate
+                                aRoute.add(rootSw); // towards this destination
+                                routes.add(aRoute);
+                                break;
+                            }
+                        }
+                    }
+                }
+
+            }
+
+        }
+        return routes;
+    }
+
+    /**
+     * Computes set of affected routes due to new links or failed switches.
+     *
+     * @return the set of affected routes which may be empty if no routes were
+     *         affected
+     */
+    private Set<ArrayList<DeviceId>> computeRouteChange() {
+        ImmutableSet.Builder<ArrayList<DeviceId>> changedRtBldr =
+                ImmutableSet.builder();
+
+        for (Device sw : srManager.deviceService.getDevices()) {
+            log.debug("Computing the impacted routes for device {}", sw.id());
+            DeviceId retId = shouldHandleRouting(sw.id());
+            if (retId == null) {
+                continue;
+            }
+            Set<DeviceId> devicesToProcess = Sets.newHashSet(retId, sw.id());
+            for (DeviceId rootSw : devicesToProcess) {
+                if (log.isTraceEnabled()) {
+                    log.trace("Device links for dev: {}", rootSw);
+                    for (Link link: srManager.linkService.getDeviceLinks(rootSw)) {
+                        log.trace("{} -> {} ", link.src().deviceId(),
+                                  link.dst().deviceId());
+                    }
+                }
+                EcmpShortestPathGraph currEcmpSpg = currentEcmpSpgMap.get(rootSw);
+                if (currEcmpSpg == null) {
+                    log.debug("No existing ECMP graph for device {}.. adding self as "
+                            + "changed route", rootSw);
+                    changedRtBldr.add(Lists.newArrayList(rootSw));
+                    continue;
+                }
+                EcmpShortestPathGraph newEcmpSpg = updatedEcmpSpgMap.get(rootSw);
+                if (log.isDebugEnabled()) {
+                    log.debug("Root switch: {}", rootSw);
+                    log.debug("  Current/Existing SPG: {}", currEcmpSpg);
+                    log.debug("       New/Updated SPG: {}", newEcmpSpg);
+                }
+                // first use the updated/new map to compare to current/existing map
+                // as new links may have come up
+                changedRtBldr.addAll(compareGraphs(newEcmpSpg, currEcmpSpg, rootSw));
+                // then use the current/existing map to compare to updated/new map
+                // as switch may have been removed
+                changedRtBldr.addAll(compareGraphs(currEcmpSpg, newEcmpSpg, rootSw));
+            }
+        }
+
+        Set<ArrayList<DeviceId>> changedRoutes = changedRtBldr.build();
+        for (ArrayList<DeviceId> route: changedRoutes) {
+            log.debug("Route changes Target -> Root");
+            if (route.size() == 1) {
+                log.debug(" : all -> {}", route.get(0));
+            } else {
+                log.debug(" : {} -> {}", route.get(0), route.get(1));
+            }
+        }
+        return changedRoutes;
+    }
+
+    /**
+     * For the root switch, searches all the target nodes reachable in the base
+     * graph, and compares paths to the ones in the comp graph.
+     *
+     * @param base the graph that is indexed for all reachable target nodes
+     *             from the root node
+     * @param comp the graph that the base graph is compared to
+     * @param rootSw  both ecmp graphs are calculated for the root node
+     * @return all the routes that have changed in the base graph
+     */
+    private Set<ArrayList<DeviceId>> compareGraphs(EcmpShortestPathGraph base,
+                                                   EcmpShortestPathGraph comp,
+                                                   DeviceId rootSw) {
+        ImmutableSet.Builder<ArrayList<DeviceId>> changedRoutesBuilder =
+                ImmutableSet.builder();
+        HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> baseMap =
+                base.getAllLearnedSwitchesAndVia();
+        HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> compMap =
+                comp.getAllLearnedSwitchesAndVia();
+        for (Integer itrIdx : baseMap.keySet()) {
+            HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> baseViaMap =
+                    baseMap.get(itrIdx);
+            for (DeviceId targetSw : baseViaMap.keySet()) {
+                ArrayList<ArrayList<DeviceId>> basePath = baseViaMap.get(targetSw);
+                ArrayList<ArrayList<DeviceId>> compPath = getVia(compMap, targetSw);
+                if ((compPath == null) || !basePath.equals(compPath)) {
+                    log.trace("Impacted route:{} -> {}", targetSw, rootSw);
+                    ArrayList<DeviceId> route = new ArrayList<>();
+                    route.add(targetSw); // switch with rules to populate
+                    route.add(rootSw); // towards this destination
+                    changedRoutesBuilder.add(route);
+                }
+            }
+        }
+        return changedRoutesBuilder.build();
+    }
+
+    /**
+     * Returns the ECMP paths traversed to reach the target switch.
+     *
+     * @param switchVia a per-iteration view of the ECMP graph for a root switch
+     * @param targetSw the switch to reach from the root switch
+     * @return the nodes traversed on ECMP paths to the target switch
+     */
+    private ArrayList<ArrayList<DeviceId>> getVia(HashMap<Integer, HashMap<DeviceId,
+            ArrayList<ArrayList<DeviceId>>>> switchVia, DeviceId targetSw) {
+        for (Integer itrIdx : switchVia.keySet()) {
+            HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+                    switchVia.get(itrIdx);
+            if (swViaMap.get(targetSw) == null) {
+                continue;
+            } else {
+                return swViaMap.get(targetSw);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Utility method to break down a path from src to dst device into a collection
+     * of links.
+     *
+     * @param src src device of the path
+     * @param dst dst device of the path
+     * @param viaMap path taken from src to dst device
+     * @return collection of links in the path
+     */
+    private Set<ArrayList<DeviceId>> computeLinks(DeviceId src,
+                                                  DeviceId dst,
+                       HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> viaMap) {
+        Set<ArrayList<DeviceId>> subLinks = Sets.newHashSet();
+        for (ArrayList<DeviceId> via : viaMap.get(src)) {
+            DeviceId linkSrc = src;
+            DeviceId linkDst = dst;
+            for (DeviceId viaDevice: via) {
+                ArrayList<DeviceId> link = new ArrayList<>();
+                linkDst = viaDevice;
+                link.add(linkSrc);
+                link.add(linkDst);
+                subLinks.add(link);
+                linkSrc = viaDevice;
+            }
+            ArrayList<DeviceId> link = new ArrayList<>();
+            link.add(linkSrc);
+            link.add(dst);
+            subLinks.add(link);
+        }
+
+        return subLinks;
+    }
+
+    /**
+     * Determines whether this controller instance should handle routing for the
+     * given {@code deviceId}, based on mastership and pairDeviceId if one exists.
+     * Returns null if this instance should not handle routing for given {@code deviceId}.
+     * Otherwise the returned value could be the given deviceId itself, or the
+     * deviceId for the paired edge device. In the latter case, this instance
+     * should handle routing for both the given device and the paired device.
+     *
+     * @param deviceId device identifier to consider for routing
+     * @return null or deviceId which could be the same as the given deviceId
+     *          or the deviceId of a paired edge device
+     */
+    private DeviceId shouldHandleRouting(DeviceId deviceId) {
+        if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not master for dev:{} .. skipping routing, may get handled "
+                    + "elsewhere as part of paired devices", deviceId);
+            return null;
+        }
+        NodeId myNode = srManager.mastershipService.getMasterFor(deviceId);
+        DeviceId pairDev = getPairDev(deviceId);
+
+        if (pairDev != null) {
+            if (!srManager.deviceService.isAvailable(pairDev)) {
+                log.warn("pairedDev {} not available .. routing this dev:{} "
+                        + "without mastership check",
+                          pairDev, deviceId);
+                return pairDev; // handle both temporarily
+            }
+            NodeId pairMasterNode = srManager.mastershipService.getMasterFor(pairDev);
+            if (myNode.compareTo(pairMasterNode) <= 0) {
+                log.debug("Handling routing for both dev:{} pair-dev:{}; myNode: {}"
+                        + " pairMaster:{} compare:{}", deviceId, pairDev,
+                        myNode, pairMasterNode,
+                        myNode.compareTo(pairMasterNode));
+                return pairDev; // handle both
+            } else {
+                log.debug("PairDev node: {} should handle routing for dev:{} and "
+                        + "pair-dev:{}", pairMasterNode, deviceId, pairDev);
+                return null; // handle neither
+            }
+        }
+        return deviceId; // not paired, just handle given device
+    }
+
+    /**
+     * Returns the configured paired DeviceId for the given Device, or null
+     * if no such paired device has been configured.
+     *
+     * @param deviceId
+     * @return configured pair deviceId or null
+     */
+    private DeviceId getPairDev(DeviceId deviceId) {
+        DeviceId pairDev;
+        try {
+            pairDev = srManager.deviceConfiguration.getPairDeviceId(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " .. cannot continue routing for dev: {}");
+            return null;
+        }
+        return pairDev;
+    }
+
+    /**
+     * Returns the set of deviceIds which are the next hops from the targetSw
+     * to the dstSw according to the latest ECMP spg.
+     *
+     * @param targetSw the switch for which the next-hops are desired
+     * @param dstSw the switch to which the next-hops lead to from the targetSw
+     * @return set of next hop deviceIds, could be empty if no next hops are found
+     */
+    private Set<DeviceId> getNextHops(DeviceId targetSw, DeviceId dstSw) {
+        boolean targetIsEdge = false;
+        try {
+            targetIsEdge = srManager.deviceConfiguration.isEdgeDevice(targetSw);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + "Cannot determine if targetIsEdge {}.. "
+                    + "continuing to getNextHops", targetSw);
+        }
+
+        EcmpShortestPathGraph ecmpSpg = updatedEcmpSpgMap.get(dstSw);
+        if (ecmpSpg == null) {
+            log.debug("No ecmpSpg found for dstSw: {}", dstSw);
+            return ImmutableSet.of();
+        }
+        HashMap<Integer,
+            HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> switchVia =
+                ecmpSpg.getAllLearnedSwitchesAndVia();
+        for (Integer itrIdx : switchVia.keySet()) {
+            HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swViaMap =
+                    switchVia.get(itrIdx);
+            for (DeviceId target : swViaMap.keySet()) {
+                if (!target.equals(targetSw)) {
+                    continue;
+                }
+                if (!targetIsEdge && itrIdx > 1) {
+                    // optimization for spines to not use other leaves to get
+                    // to a leaf to avoid loops
+                    log.debug("Avoiding {} hop path for non-edge targetSw:{}"
+                            + " --> dstSw:{}", itrIdx, targetSw, dstSw);
+                    break;
+                }
+                Set<DeviceId> nextHops = new HashSet<>();
+                for (ArrayList<DeviceId> via : swViaMap.get(targetSw)) {
+                    if (via.isEmpty()) {
+                        // the dstSw is the next-hop from the targetSw
+                        nextHops.add(dstSw);
+                    } else {
+                        // first elem is next-hop in each ECMP path
+                        nextHops.add(via.get(0));
+                    }
+                }
+                return nextHops;
+            }
+        }
+        return ImmutableSet.of(); //no next-hops found
+    }
+
+    /**
+     * Represents two devices that are paired by configuration. An EdgePair for
+     * (dev1, dev2) is the same as as EdgePair for (dev2, dev1)
+     */
+    protected final class EdgePair {
+        DeviceId dev1;
+        DeviceId dev2;
+
+        EdgePair(DeviceId dev1, DeviceId dev2) {
+            this.dev1 = dev1;
+            this.dev2 = dev2;
+        }
+
+        boolean includes(DeviceId dev) {
+            return dev1.equals(dev) || dev2.equals(dev);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof EdgePair)) {
+                return false;
+            }
+            EdgePair that = (EdgePair) o;
+            return ((this.dev1.equals(that.dev1) && this.dev2.equals(that.dev2)) ||
+                    (this.dev1.equals(that.dev2) && this.dev2.equals(that.dev1)));
+        }
+
+        @Override
+        public int hashCode() {
+            if (dev1.toString().compareTo(dev2.toString()) <= 0) {
+                return Objects.hash(dev1, dev2);
+            } else {
+                return Objects.hash(dev2, dev1);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return toStringHelper(this)
+                    .add("Dev1", dev1)
+                    .add("Dev2", dev2)
+                    .toString();
+        }
+    }
+
+    //////////////////////////////////////
+    //  Filtering rule creation
+    //////////////////////////////////////
+
+    /**
+     * Populates filtering rules for port, and punting rules
+     * for gateway IPs, loopback IPs and arp/ndp traffic.
+     * Should only be called by the master instance for this device/port.
+     *
+     * @param deviceId Switch ID to set the rules
+     */
+    public void populatePortAddressingRules(DeviceId deviceId) {
+        // Although device is added, sometimes device store does not have the
+        // ports for this device yet. It results in missing filtering rules in the
+        // switch. We will attempt it a few times. If it still does not work,
+        // user can manually repopulate using CLI command sr-reroute-network
+        PortFilterInfo firstRun = rulePopulator.populateVlanMacFilters(deviceId);
+        if (firstRun == null) {
+            firstRun = new PortFilterInfo(0, 0, 0);
+        }
+        executorService.schedule(new RetryFilters(deviceId, firstRun),
+                                 RETRY_INTERVAL_MS, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Utility class used to temporarily store information about the ports on a
+     * device processed for filtering objectives.
+     */
+    public final class PortFilterInfo {
+        int disabledPorts = 0, errorPorts = 0, filteredPorts = 0;
+
+        public PortFilterInfo(int disabledPorts, int errorPorts,
+                           int filteredPorts) {
+            this.disabledPorts = disabledPorts;
+            this.filteredPorts = filteredPorts;
+            this.errorPorts = errorPorts;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(disabledPorts, filteredPorts, errorPorts);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if ((obj == null) || (!(obj instanceof PortFilterInfo))) {
+                return false;
+            }
+            PortFilterInfo other = (PortFilterInfo) obj;
+            return ((disabledPorts == other.disabledPorts) &&
+                    (filteredPorts == other.filteredPorts) &&
+                    (errorPorts == other.errorPorts));
+        }
+
+        @Override
+        public String toString() {
+            MoreObjects.ToStringHelper helper = toStringHelper(this)
+                    .add("disabledPorts", disabledPorts)
+                    .add("errorPorts", errorPorts)
+                    .add("filteredPorts", filteredPorts);
+            return helper.toString();
+        }
+    }
+
+    /**
+     * RetryFilters populates filtering objectives for a device and keeps retrying
+     * till the number of ports filtered are constant for a predefined number
+     * of attempts.
+     */
+    protected final class RetryFilters implements Runnable {
+        int constantAttempts = MAX_CONSTANT_RETRY_ATTEMPTS;
+        DeviceId devId;
+        int counter;
+        PortFilterInfo prevRun;
+
+        private RetryFilters(DeviceId deviceId, PortFilterInfo previousRun) {
+            devId = deviceId;
+            prevRun = previousRun;
+            counter = 0;
+        }
+
+        @Override
+        public void run() {
+            log.debug("RETRY FILTER ATTEMPT {} ** dev:{}", ++counter, devId);
+            PortFilterInfo thisRun = rulePopulator.populateVlanMacFilters(devId);
+            boolean sameResult = prevRun.equals(thisRun);
+            log.debug("dev:{} prevRun:{} thisRun:{} sameResult:{}", devId, prevRun,
+                      thisRun, sameResult);
+            if (thisRun == null || !sameResult || (sameResult && --constantAttempts > 0)) {
+                // exponentially increasing intervals for retries
+                executorService.schedule(this,
+                    RETRY_INTERVAL_MS * (int) Math.pow(counter, RETRY_INTERVAL_SCALE),
+                    TimeUnit.MILLISECONDS);
+                if (!sameResult) {
+                    constantAttempts = MAX_CONSTANT_RETRY_ATTEMPTS; //reset
+                }
+            }
+            prevRun = (thisRun == null) ? prevRun : thisRun;
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java
new file mode 100644
index 0000000..0fd7c077
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/DefaultTunnel.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import java.util.List;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default Tunnel class.
+ */
+public class DefaultTunnel implements Tunnel {
+
+    private final String id;
+    private final List<Integer> labelIds;
+
+    private int groupId;
+    private boolean allowedToRemoveGroup;
+
+    /**
+     * Creates a Tunnel reference.
+     *
+     * @param tid  Tunnel ID
+     * @param labelIds Label stack of the tunnel
+     */
+    public DefaultTunnel(String tid, List<Integer> labelIds) {
+        this.id = checkNotNull(tid);
+        this.labelIds = labelIds;
+        //TODO: need to register the class in Kryo for this
+        //this.labelIds = Collections.unmodifiableList(labelIds);
+        this.groupId = -1;
+    }
+
+    /**
+     * Creates a new DefaultTunnel reference using the tunnel reference.
+     *
+     * @param tunnel DefaultTunnel reference
+     */
+    public DefaultTunnel(DefaultTunnel tunnel) {
+        this.id = tunnel.id;
+        this.labelIds = tunnel.labelIds;
+        this.groupId = tunnel.groupId;
+    }
+
+    @Override
+    public String id() {
+        return this.id;
+    }
+
+    @Override
+    public List<Integer> labelIds() {
+        return this.labelIds;
+    }
+
+    @Override
+    public int groupId() {
+        return this.groupId;
+    }
+
+    @Override
+    public void setGroupId(int id) {
+        this.groupId = id;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof DefaultTunnel) {
+            DefaultTunnel tunnel = (DefaultTunnel) o;
+            // We compare only the tunnel paths.
+            if (tunnel.labelIds.equals(this.labelIds)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return labelIds.hashCode();
+    }
+
+    @Override
+    public boolean isAllowedToRemoveGroup() {
+        return this.allowedToRemoveGroup;
+    }
+
+    @Override
+    public void allowToRemoveGroup(boolean b) {
+        this.allowedToRemoveGroup = b;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java
new file mode 100644
index 0000000..c9bd596
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/EcmpShortestPathGraph.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.graph.ScalarWeight;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DefaultPath;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.provider.ProviderId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Set;
+
+/**
+ * This class creates breadth-first-search (BFS) tree for a given root device
+ * and returns paths from the root Device to leaf Devices (target devices).
+ * The paths are snapshot paths at the point of the class instantiation.
+ */
+public class EcmpShortestPathGraph {
+    LinkedList<DeviceId> deviceQueue = new LinkedList<>();
+    LinkedList<Integer> distanceQueue = new LinkedList<>();
+    HashMap<DeviceId, Integer> deviceSearched = new HashMap<>();
+    HashMap<DeviceId, ArrayList<Link>> upstreamLinks = new HashMap<>();
+    HashMap<DeviceId, ArrayList<Path>> paths = new HashMap<>();
+    HashMap<Integer, ArrayList<DeviceId>> distanceDeviceMap = new HashMap<>();
+    DeviceId rootDevice;
+    private SegmentRoutingManager srManager;
+    private static final Logger log = LoggerFactory.getLogger(EcmpShortestPathGraph.class);
+
+    /**
+     * Constructor.
+     *
+     * @param rootDevice root of the BFS tree
+     * @param srManager SegmentRoutingManager object
+     */
+    public EcmpShortestPathGraph(DeviceId rootDevice, SegmentRoutingManager srManager) {
+        this.rootDevice = rootDevice;
+        this.srManager = srManager;
+        calcECMPShortestPathGraph();
+    }
+
+    /**
+     * Calculates the BFS tree.
+     */
+   private void calcECMPShortestPathGraph() {
+        deviceQueue.add(rootDevice);
+        int currDistance = 0;
+        distanceQueue.add(currDistance);
+        deviceSearched.put(rootDevice, currDistance);
+        while (!deviceQueue.isEmpty()) {
+            DeviceId sw = deviceQueue.poll();
+            Set<DeviceId> prevSw = Sets.newHashSet();
+            currDistance = distanceQueue.poll();
+
+            for (Link link : srManager.linkService.getDeviceEgressLinks(sw)) {
+                if (srManager.linkHandler.avoidLink(link)) {
+                    continue;
+                }
+                DeviceId reachedDevice = link.dst().deviceId();
+                if (prevSw.contains(reachedDevice)) {
+                    // Ignore LAG links between the same set of Devices
+                    continue;
+                } else  {
+                    prevSw.add(reachedDevice);
+                }
+
+                Integer distance = deviceSearched.get(reachedDevice);
+                if ((distance != null) && (distance < (currDistance + 1))) {
+                    continue;
+                }
+                if (distance == null) {
+                    // First time visiting this Device node
+                    deviceQueue.add(reachedDevice);
+                    distanceQueue.add(currDistance + 1);
+                    deviceSearched.put(reachedDevice, currDistance + 1);
+
+                    ArrayList<DeviceId> distanceSwArray = distanceDeviceMap
+                            .get(currDistance + 1);
+                    if (distanceSwArray == null) {
+                        distanceSwArray = new ArrayList<>();
+                        distanceSwArray.add(reachedDevice);
+                        distanceDeviceMap.put(currDistance + 1, distanceSwArray);
+                    } else {
+                        distanceSwArray.add(reachedDevice);
+                    }
+                }
+
+                ArrayList<Link> upstreamLinkArray =
+                        upstreamLinks.get(reachedDevice);
+                if (upstreamLinkArray == null) {
+                    upstreamLinkArray = new ArrayList<>();
+                    upstreamLinkArray.add(copyDefaultLink(link));
+                    //upstreamLinkArray.add(link);
+                    upstreamLinks.put(reachedDevice, upstreamLinkArray);
+                } else {
+                    // ECMP links
+                    upstreamLinkArray.add(copyDefaultLink(link));
+                }
+            }
+        }
+    }
+
+    private void getDFSPaths(DeviceId dstDeviceDeviceId, Path path, ArrayList<Path> paths) {
+        DeviceId rootDeviceDeviceId = rootDevice;
+        for (Link upstreamLink : upstreamLinks.get(dstDeviceDeviceId)) {
+            /* Deep clone the path object */
+            Path sofarPath;
+            ArrayList<Link> sofarLinks = new ArrayList<>();
+            if (path != null && !path.links().isEmpty()) {
+                sofarLinks.addAll(path.links());
+            }
+            sofarLinks.add(upstreamLink);
+            sofarPath = new DefaultPath(ProviderId.NONE, sofarLinks, ScalarWeight.toWeight(0));
+            if (upstreamLink.src().deviceId().equals(rootDeviceDeviceId)) {
+                paths.add(sofarPath);
+                return;
+            } else {
+                getDFSPaths(upstreamLink.src().deviceId(), sofarPath, paths);
+            }
+        }
+    }
+
+    /**
+     * Return root Device for the graph.
+     *
+     * @return root Device
+     */
+    public DeviceId getRootDevice() {
+        return rootDevice;
+    }
+
+    /**
+     * Return the computed ECMP paths from the root Device to a given Device in
+     * the network.
+     *
+     * @param targetDevice the target Device
+     * @return the list of ECMP Paths from the root Device to the target Device
+     */
+    public ArrayList<Path> getECMPPaths(DeviceId targetDevice) {
+        ArrayList<Path> pathArray = paths.get(targetDevice);
+        if (pathArray == null && deviceSearched.containsKey(
+                targetDevice)) {
+            pathArray = new ArrayList<>();
+            DeviceId sw = targetDevice;
+            getDFSPaths(sw, null, pathArray);
+            paths.put(targetDevice, pathArray);
+        }
+        return pathArray;
+    }
+
+    /**
+     * Return the complete info of the computed ECMP paths for each Device
+     * learned in multiple iterations from the root Device.
+     *
+     * @return the hash table of Devices learned in multiple Dijkstra
+     *         iterations and corresponding ECMP paths to it from the root
+     *         Device
+     */
+    public HashMap<Integer, HashMap<DeviceId,
+            ArrayList<Path>>> getCompleteLearnedDeviceesAndPaths() {
+
+        HashMap<Integer, HashMap<DeviceId, ArrayList<Path>>> pathGraph = new HashMap<>();
+
+        for (Integer itrIndx : distanceDeviceMap.keySet()) {
+            HashMap<DeviceId, ArrayList<Path>> swMap = new HashMap<>();
+            for (DeviceId sw : distanceDeviceMap.get(itrIndx)) {
+                swMap.put(sw, getECMPPaths(sw));
+            }
+            pathGraph.put(itrIndx, swMap);
+        }
+
+        return pathGraph;
+    }
+
+    /**
+     * Returns the complete info of the computed ECMP paths for each target device
+     * learned in multiple iterations from the root Device. The computed info
+     * returned is per iteration (Integer key of outer HashMap). In each
+     * iteration, for the target devices reached (DeviceId key of inner HashMap),
+     * the ECMP paths are detailed (2D array).
+     *
+     * @return the hash table of target Devices learned in multiple Dijkstra
+     *         iterations and corresponding ECMP paths in terms of Devices to
+     *         be traversed (via) from the root Device to the target Device
+     */
+    public HashMap<Integer, HashMap<DeviceId,
+            ArrayList<ArrayList<DeviceId>>>> getAllLearnedSwitchesAndVia() {
+
+        HashMap<Integer, HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>>> deviceViaMap = new HashMap<>();
+
+        for (Integer itrIndx : distanceDeviceMap.keySet()) {
+            HashMap<DeviceId, ArrayList<ArrayList<DeviceId>>> swMap = new HashMap<>();
+
+            for (DeviceId sw : distanceDeviceMap.get(itrIndx)) {
+                ArrayList<ArrayList<DeviceId>> swViaArray = new ArrayList<>();
+                for (Path path : getECMPPaths(sw)) {
+                    ArrayList<DeviceId> swVia = new ArrayList<>();
+                    for (Link link : path.links()) {
+                        if (link.src().deviceId().equals(rootDevice)) {
+                            /* No need to add the root Device again in
+                             * the Via list
+                             */
+                            continue;
+                        }
+                        swVia.add(link.src().deviceId());
+                    }
+                    swViaArray.add(swVia);
+                }
+                swMap.put(sw, swViaArray);
+            }
+            deviceViaMap.put(itrIndx, swMap);
+        }
+        return deviceViaMap;
+    }
+
+
+    private Link copyDefaultLink(Link link) {
+        DefaultLink src = (DefaultLink) link;
+        DefaultLink defaultLink = DefaultLink.builder()
+                .providerId(src.providerId())
+                .src(src.src())
+                .dst(src.dst())
+                .type(src.type())
+                .annotations(src.annotations())
+                .build();
+
+        return defaultLink;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sBuilder = new StringBuilder();
+        for (Device device: srManager.deviceService.getDevices()) {
+            if (!device.id().equals(rootDevice)) {
+                sBuilder.append("\r\n  Paths from " + rootDevice + " to "
+                                + device.id());
+                ArrayList<Path> paths = getECMPPaths(device.id());
+                if (paths != null) {
+                    for (Path path : paths) {
+                        sBuilder.append("\r\n       == "); // equal cost paths delimiter
+                        for (int i = path.links().size() - 1; i >= 0; i--) {
+                            Link link = path.links().get(i);
+                            sBuilder.append(" : " + link.src() + " -> " + link.dst());
+                        }
+                    }
+                } else {
+                    sBuilder.append("\r\n       == no paths");
+                }
+            }
+        }
+        return sBuilder.toString();
+    }
+}
+
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
new file mode 100644
index 0000000..908a339
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/HostHandler.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostLocationProbingService.ProbeMode;
+import org.onosproject.net.host.HostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Handles host-related events.
+ */
+public class HostHandler {
+    private static final Logger log = LoggerFactory.getLogger(HostHandler.class);
+    static final int HOST_MOVED_DELAY_MS = 1000;
+
+    protected final SegmentRoutingManager srManager;
+    private HostService hostService;
+    private FlowObjectiveService flowObjectiveService;
+
+    /**
+     * Constructs the HostHandler.
+     *
+     * @param srManager Segment Routing manager
+     */
+    HostHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        hostService = srManager.hostService;
+        flowObjectiveService = srManager.flowObjectiveService;
+    }
+
+    protected void init(DeviceId devId) {
+        hostService.getHosts().forEach(host ->
+            host.locations().stream()
+                    .filter(location -> location.deviceId().equals(devId) ||
+                            location.deviceId().equals(srManager.getPairDeviceId(devId).orElse(null)))
+                    .forEach(location -> processHostAddedAtLocation(host, location))
+        );
+    }
+
+    void processHostAddedEvent(HostEvent event) {
+        processHostAdded(event.subject());
+    }
+
+    private void processHostAdded(Host host) {
+        host.locations().forEach(location -> processHostAddedAtLocation(host, location));
+        // ensure dual-homed host locations have viable uplinks
+        if (host.locations().size() > 1) {
+            host.locations().forEach(loc -> {
+                if (srManager.mastershipService.isLocalMaster(loc.deviceId())) {
+                    srManager.linkHandler.checkUplinksForDualHomedHosts(loc);
+                }
+            });
+        }
+    }
+
+    void processHostAddedAtLocation(Host host, HostLocation location) {
+        checkArgument(host.locations().contains(location), "{} is not a location of {}", location, host);
+
+        MacAddress hostMac = host.mac();
+        VlanId hostVlanId = host.vlan();
+        Set<HostLocation> locations = host.locations();
+        Set<IpAddress> ips = host.ipAddresses();
+        log.info("Host {}/{} is added at {}", hostMac, hostVlanId, locations);
+
+        if (srManager.isMasterOf(location)) {
+            processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, false);
+            ips.forEach(ip ->
+                    processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
+            );
+        }
+
+        // Use the pair link temporarily before the second location of a dual-homed host shows up.
+        // This do not affect single-homed hosts since the flow will be blocked in
+        // processBridgingRule or processRoutingRule due to VLAN or IP mismatch respectively
+        srManager.getPairDeviceId(location.deviceId()).ifPresent(pairDeviceId -> {
+            if (srManager.mastershipService.isLocalMaster(pairDeviceId) &&
+                    host.locations().stream().noneMatch(l -> l.deviceId().equals(pairDeviceId))) {
+                srManager.getPairLocalPorts(pairDeviceId).ifPresent(pairRemotePort -> {
+                    // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                    //       when the host is untagged
+                    VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+                    processBridgingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, false);
+                    ips.forEach(ip -> processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId,
+                                    ip, false));
+
+                    if (srManager.activeProbing) {
+                        probe(host, location, pairDeviceId, pairRemotePort);
+                    }
+                });
+            }
+        });
+    }
+
+    void processHostRemovedEvent(HostEvent event) {
+        processHostRemoved(event.subject());
+    }
+
+    private void processHostRemoved(Host host) {
+        MacAddress hostMac = host.mac();
+        VlanId hostVlanId = host.vlan();
+        Set<HostLocation> locations = host.locations();
+        Set<IpAddress> ips = host.ipAddresses();
+        log.info("Host {}/{} is removed from {}", hostMac, hostVlanId, locations);
+
+        locations.forEach(location -> {
+            if (srManager.isMasterOf(location)) {
+                processBridgingRule(location.deviceId(), location.port(), hostMac, hostVlanId, true);
+                ips.forEach(ip ->
+                        processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
+                );
+            }
+
+            // Also remove redirection flows on the pair device if exists.
+            Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
+            Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(location.deviceId());
+            if (pairDeviceId.isPresent() && pairLocalPort.isPresent() &&
+                    srManager.mastershipService.isLocalMaster(pairDeviceId.get())) {
+                // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                //       when the host is untagged
+                VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+                processBridgingRule(pairDeviceId.get(), pairLocalPort.get(), hostMac, vlanId, true);
+                ips.forEach(ip ->
+                        processRoutingRule(pairDeviceId.get(), pairLocalPort.get(), hostMac, vlanId,
+                                ip, true));
+            }
+        });
+    }
+
+    void processHostMovedEvent(HostEvent event) {
+        MacAddress hostMac = event.subject().mac();
+        VlanId hostVlanId = event.subject().vlan();
+        Set<HostLocation> prevLocations = event.prevSubject().locations();
+        Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
+        Set<HostLocation> newLocations = event.subject().locations();
+        Set<IpAddress> newIps = event.subject().ipAddresses();
+
+        // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+        //        The permanent solution would be introducing CompletableFuture and wait for it
+        if (prevLocations.size() == 1 && newLocations.size() == 2) {
+            log.debug("Delay event handling when host {}/{} moves from 1 to 2 locations", hostMac, hostVlanId);
+            ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+            executorService.schedule(() ->
+                    processHostMoved(hostMac, hostVlanId, prevLocations, prevIps, newLocations, newIps),
+                    HOST_MOVED_DELAY_MS, TimeUnit.MILLISECONDS);
+        } else {
+            processHostMoved(hostMac, hostVlanId, prevLocations, prevIps, newLocations, newIps);
+        }
+    }
+
+    private void processHostMoved(MacAddress hostMac, VlanId hostVlanId, Set<HostLocation> prevLocations,
+                                  Set<IpAddress> prevIps, Set<HostLocation> newLocations, Set<IpAddress> newIps) {
+        log.info("Host {}/{} is moved from {} to {}", hostMac, hostVlanId, prevLocations, newLocations);
+        Set<DeviceId> newDeviceIds = newLocations.stream().map(HostLocation::deviceId)
+                .collect(Collectors.toSet());
+
+        // For each old location
+        Sets.difference(prevLocations, newLocations).stream().filter(srManager::isMasterOf)
+                .forEach(prevLocation -> {
+            // Remove routing rules for old IPs
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                    processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+                            ip, true)
+            );
+
+            // Redirect the flows to pair link if configured
+            // Note: Do not continue removing any rule
+            Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(prevLocation.deviceId());
+            Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(prevLocation.deviceId());
+            if (pairDeviceId.isPresent() && pairLocalPort.isPresent() && newLocations.stream()
+                    .anyMatch(location -> location.deviceId().equals(pairDeviceId.get()))) {
+                // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                //       when the host is untagged
+                VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(prevLocation)).orElse(hostVlanId);
+
+                processBridgingRule(prevLocation.deviceId(), pairLocalPort.get(), hostMac, vlanId, false);
+                newIps.forEach(ip ->
+                        processRoutingRule(prevLocation.deviceId(), pairLocalPort.get(), hostMac, vlanId,
+                            ip, false));
+                return;
+            }
+
+            // Remove bridging rule and routing rules for unchanged IPs if the host moves from a switch to another.
+            // Otherwise, do not remove and let the adding part update the old flow
+            if (!newDeviceIds.contains(prevLocation.deviceId())) {
+                processBridgingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId, true);
+                Sets.intersection(prevIps, newIps).forEach(ip ->
+                        processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+                                ip, true)
+                );
+            }
+
+            // Remove bridging rules if new interface vlan is different from old interface vlan
+            // Otherwise, do not remove and let the adding part update the old flow
+            if (newLocations.stream().noneMatch(newLocation -> {
+                VlanId oldAssignedVlan = srManager.getInternalVlanId(prevLocation);
+                VlanId newAssignedVlan = srManager.getInternalVlanId(newLocation);
+                // Host is tagged and the new location has the host vlan in vlan-tagged
+                return srManager.getTaggedVlanId(newLocation).contains(hostVlanId) ||
+                        (oldAssignedVlan != null && newAssignedVlan != null &&
+                        // Host is untagged and the new location has the same assigned vlan
+                        oldAssignedVlan.equals(newAssignedVlan));
+            })) {
+                processBridgingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId, true);
+            }
+
+            // Remove routing rules for unchanged IPs if none of the subnet of new location contains
+            // the IP. Otherwise, do not remove and let the adding part update the old flow
+            Sets.intersection(prevIps, newIps).forEach(ip -> {
+                if (newLocations.stream().noneMatch(newLocation ->
+                        srManager.deviceConfiguration.inSameSubnet(newLocation, ip))) {
+                    processRoutingRule(prevLocation.deviceId(), prevLocation.port(), hostMac, hostVlanId,
+                            ip, true);
+                }
+            });
+        });
+
+        // For each new location, add all new IPs.
+        Sets.difference(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+                .forEach(newLocation -> {
+            processBridgingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId, false);
+            newIps.forEach(ip ->
+                    processRoutingRule(newLocation.deviceId(), newLocation.port(), hostMac, hostVlanId,
+                            ip, false)
+            );
+        });
+
+        // For each unchanged location, add new IPs and remove old IPs.
+        Sets.intersection(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+                .forEach(unchangedLocation -> {
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
+                            hostVlanId, ip, true)
+            );
+
+            Sets.difference(newIps, prevIps).forEach(ip ->
+                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), hostMac,
+                        hostVlanId, ip, false)
+            );
+        });
+
+        // ensure dual-homed host locations have viable uplinks
+        if (newLocations.size() > prevLocations.size()) {
+            newLocations.forEach(loc -> {
+                if (srManager.mastershipService.isLocalMaster(loc.deviceId())) {
+                    srManager.linkHandler.checkUplinksForDualHomedHosts(loc);
+                }
+            });
+        }
+    }
+
+    void processHostUpdatedEvent(HostEvent event) {
+        Host host = event.subject();
+        MacAddress hostMac = host.mac();
+        VlanId hostVlanId = host.vlan();
+        Set<HostLocation> locations = host.locations();
+        Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
+        Set<IpAddress> newIps = host.ipAddresses();
+        log.info("Host {}/{} is updated", hostMac, hostVlanId);
+
+        locations.stream().filter(srManager::isMasterOf).forEach(location -> {
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                    processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, true)
+            );
+            Sets.difference(newIps, prevIps).forEach(ip ->
+                    processRoutingRule(location.deviceId(), location.port(), hostMac, hostVlanId, ip, false)
+            );
+        });
+
+        // Use the pair link temporarily before the second location of a dual-homed host shows up.
+        // This do not affect single-homed hosts since the flow will be blocked in
+        // processBridgingRule or processRoutingRule due to VLAN or IP mismatch respectively
+        locations.forEach(location ->
+            srManager.getPairDeviceId(location.deviceId()).ifPresent(pairDeviceId -> {
+                if (srManager.mastershipService.isLocalMaster(pairDeviceId) &&
+                        locations.stream().noneMatch(l -> l.deviceId().equals(pairDeviceId))) {
+                    Set<IpAddress> ipsToAdd = Sets.difference(newIps, prevIps);
+                    Set<IpAddress> ipsToRemove = Sets.difference(prevIps, newIps);
+
+                    srManager.getPairLocalPorts(pairDeviceId).ifPresent(pairRemotePort -> {
+                        // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                        //       when the host is untagged
+                        VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(hostVlanId);
+
+                        ipsToRemove.forEach(ip ->
+                                processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, ip, true)
+                        );
+                        ipsToAdd.forEach(ip ->
+                                processRoutingRule(pairDeviceId, pairRemotePort, hostMac, vlanId, ip, false)
+                        );
+
+                        if (srManager.activeProbing) {
+                            probe(host, location, pairDeviceId, pairRemotePort);
+                        }
+                    });
+                }
+            })
+        );
+    }
+
+    /**
+     * When a non-pair port comes up, probe each host on the pair device if
+     * (1) the host is tagged and the tagged vlan of current port contains host vlan; or
+     * (2) the host is untagged and the internal vlan is the same on the host port and current port.
+     *
+     * @param cp connect point
+     */
+    void processPortUp(ConnectPoint cp) {
+        if (cp.port().equals(srManager.getPairLocalPorts(cp.deviceId()).orElse(null))) {
+            return;
+        }
+        if (srManager.activeProbing) {
+            srManager.getPairDeviceId(cp.deviceId())
+                    .ifPresent(pairDeviceId -> srManager.hostService.getConnectedHosts(pairDeviceId).stream()
+                            .filter(host -> isHostInVlanOfPort(host, pairDeviceId, cp))
+                            .forEach(host -> srManager.probingService.probeHostLocation(host, cp, ProbeMode.DISCOVER))
+                    );
+        }
+    }
+
+    /**
+     * Checks if given host located on given device id matches VLAN config of current port.
+     *
+     * @param host host to check
+     * @param deviceId device id to check
+     * @param cp current connect point
+     * @return true if the host located at deviceId matches the VLAN config on cp
+     */
+    private boolean isHostInVlanOfPort(Host host, DeviceId deviceId, ConnectPoint cp) {
+        VlanId internalVlan = srManager.getInternalVlanId(cp);
+        Set<VlanId> taggedVlan = srManager.getTaggedVlanId(cp);
+
+        return taggedVlan.contains(host.vlan()) ||
+                (internalVlan != null && host.locations().stream()
+                        .filter(l -> l.deviceId().equals(deviceId))
+                        .map(srManager::getInternalVlanId)
+                        .anyMatch(internalVlan::equals));
+    }
+
+    /**
+     * Send a probe on all locations with the same VLAN on pair device, excluding pair port.
+     *
+     * @param host host to probe
+     * @param location newly discovered host location
+     * @param pairDeviceId pair device id
+     * @param pairRemotePort pair remote port
+     */
+    private void probe(Host host, ConnectPoint location, DeviceId pairDeviceId, PortNumber pairRemotePort) {
+        VlanId vlanToProbe = host.vlan().equals(VlanId.NONE) ?
+                srManager.getInternalVlanId(location) : host.vlan();
+        srManager.interfaceService.getInterfaces().stream()
+                .filter(i -> i.vlanTagged().contains(vlanToProbe) ||
+                        i.vlanUntagged().equals(vlanToProbe) ||
+                        i.vlanNative().equals(vlanToProbe))
+                .filter(i -> i.connectPoint().deviceId().equals(pairDeviceId))
+                .filter(i -> !i.connectPoint().port().equals(pairRemotePort))
+                .forEach(i -> {
+                    log.debug("Probing host {} on pair device {}", host.id(), i.connectPoint());
+                    srManager.probingService.probeHostLocation(host, i.connectPoint(), ProbeMode.DISCOVER);
+                });
+    }
+
+    /**
+     * Generates a forwarding objective builder for bridging rules.
+     * <p>
+     * The forwarding objective bridges packets destined to a given MAC to
+     * given port on given device.
+     *
+     * @param deviceId Device that host attaches to
+     * @param mac MAC address of the host
+     * @param hostVlanId VLAN ID of the host
+     * @param outport Port that host attaches to
+     * @param revoke true if forwarding objective is meant to revoke forwarding rule
+     * @return Forwarding objective builder
+     */
+    ForwardingObjective.Builder bridgingFwdObjBuilder(
+            DeviceId deviceId, MacAddress mac, VlanId hostVlanId,
+            PortNumber outport, boolean revoke) {
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, outport);
+        VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+        Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+        VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+        // Create host selector
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchEthDst(mac);
+
+        // Create host treatment
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.immediate().setOutput(outport);
+
+        // Create host meta
+        TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+        // Adjust the selector, treatment and meta according to VLAN configuration
+        if (taggedVlans.contains(hostVlanId)) {
+            sbuilder.matchVlanId(hostVlanId);
+            mbuilder.matchVlanId(hostVlanId);
+        } else if (hostVlanId.equals(VlanId.NONE)) {
+            if (untaggedVlan != null) {
+                sbuilder.matchVlanId(untaggedVlan);
+                mbuilder.matchVlanId(untaggedVlan);
+                tbuilder.immediate().popVlan();
+            } else if (nativeVlan != null) {
+                sbuilder.matchVlanId(nativeVlan);
+                mbuilder.matchVlanId(nativeVlan);
+                tbuilder.immediate().popVlan();
+            } else {
+                log.warn("Untagged host {}/{} is not allowed on {} without untagged or native" +
+                        "vlan config", mac, hostVlanId, connectPoint);
+                return null;
+            }
+        } else {
+            log.warn("Tagged host {}/{} is not allowed on {} without VLAN listed in tagged vlan",
+                    mac, hostVlanId, connectPoint);
+            return null;
+        }
+
+        // All forwarding is via Groups. Drivers can re-purpose to flow-actions if needed.
+        // If the objective is to revoke an existing rule, and for some reason
+        // the next-objective does not exist, then a new one should not be created
+        int portNextObjId = srManager.getPortNextObjectiveId(deviceId, outport,
+                tbuilder.build(), mbuilder.build(), !revoke);
+        if (portNextObjId == -1) {
+            // Warning log will come from getPortNextObjective method
+            return null;
+        }
+
+        return DefaultForwardingObjective.builder()
+                .withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .withSelector(sbuilder.build())
+                .nextStep(portNextObjId)
+                .withPriority(100)
+                .fromApp(srManager.appId)
+                .makePermanent();
+    }
+
+    /**
+     * Populate or revoke a bridging rule on given deviceId that matches given mac, given vlan and
+     * output to given port.
+     *
+     * @param deviceId device ID
+     * @param port port
+     * @param mac mac address
+     * @param vlanId VLAN ID
+     * @param revoke true to revoke the rule; false to populate
+     */
+    private void processBridgingRule(DeviceId deviceId, PortNumber port, MacAddress mac,
+                                     VlanId vlanId, boolean revoke) {
+        log.debug("{} bridging entry for host {}/{} at {}:{}", revoke ? "Revoking" : "Populating",
+                mac, vlanId, deviceId, port);
+
+        ForwardingObjective.Builder fob = bridgingFwdObjBuilder(deviceId, mac, vlanId, port, revoke);
+        if (fob == null) {
+            log.warn("Fail to build fwd obj for host {}/{}. Abort.", mac, vlanId);
+            return;
+        }
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Brigding rule for {}/{} {}", mac, vlanId,
+                        revoke ? "revoked" : "populated"),
+                (objective, error) -> log.warn("Failed to {} bridging rule for {}/{}: {}",
+                        revoke ? "revoked" : "populated", mac, vlanId, error));
+        flowObjectiveService.forward(deviceId, revoke ? fob.remove(context) : fob.add(context));
+    }
+
+    /**
+     * Populate or revoke a routing rule on given deviceId that matches given ip,
+     * set destination mac to given mac, set vlan to given vlan and output to given port.
+     *
+     * @param deviceId device ID
+     * @param port port
+     * @param mac mac address
+     * @param vlanId VLAN ID
+     * @param ip IP address
+     * @param revoke true to revoke the rule; false to populate
+     */
+    private void processRoutingRule(DeviceId deviceId, PortNumber port, MacAddress mac,
+                                    VlanId vlanId, IpAddress ip, boolean revoke) {
+        ConnectPoint location = new ConnectPoint(deviceId, port);
+        if (!srManager.deviceConfiguration.inSameSubnet(location, ip)) {
+            log.info("{} is not included in the subnet config of {}/{}. Ignored.", ip, deviceId, port);
+            return;
+        }
+
+        log.info("{} routing rule for {} at {}", revoke ? "Revoking" : "Populating", ip, location);
+        if (revoke) {
+            srManager.defaultRoutingHandler.revokeRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
+        } else {
+            srManager.defaultRoutingHandler.populateRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
+        }
+    }
+
+    /**
+     * Populate or revoke a bridging rule on given deviceId that matches given vlanId,
+     * and hostMAC connected to given port, and output to given port only when
+     * vlan information is valid.
+     *
+     * @param deviceId device ID that host attaches to
+     * @param portNum port number that host attaches to
+     * @param hostMac mac address of the host connected to the switch port
+     * @param vlanId Vlan ID configured on the switch port
+     * @param popVlan true to pop Vlan tag at TrafficTreatment, false otherwise
+     * @param install true to populate the objective, false to revoke
+     */
+    private void updateBridgingRule(DeviceId deviceId, PortNumber portNum, MacAddress hostMac,
+                            VlanId vlanId, boolean popVlan, boolean install) {
+        // Create host selector
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchEthDst(hostMac);
+
+        // Create host meta
+        TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+        sbuilder.matchVlanId(vlanId);
+        mbuilder.matchVlanId(vlanId);
+
+        // Create host treatment
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.immediate().setOutput(portNum);
+
+        if (popVlan) {
+            tbuilder.immediate().popVlan();
+        }
+
+        int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNum,
+                                                             tbuilder.build(), mbuilder.build(), install);
+        if (portNextObjId != -1) {
+            ForwardingObjective.Builder fob = DefaultForwardingObjective.builder()
+                    .withFlag(ForwardingObjective.Flag.SPECIFIC)
+                    .withSelector(sbuilder.build())
+                    .nextStep(portNextObjId)
+                    .withPriority(100)
+                    .fromApp(srManager.appId)
+                    .makePermanent();
+
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("Brigding rule for {}/{} {}", hostMac, vlanId,
+                                             install ? "populated" : "revoked"),
+                    (objective, error) -> log.warn("Failed to {} bridging rule for {}/{}: {}",
+                                                   install ? "populate" : "revoke", hostMac, vlanId, error));
+            flowObjectiveService.forward(deviceId, install ? fob.add(context) : fob.remove(context));
+        } else {
+            log.warn("Failed to retrieve next objective for {}/{}", hostMac, vlanId);
+        }
+    }
+
+    /**
+     * Update forwarding objective for unicast bridging and unicast routing.
+     * Also check the validity of updated interface configuration on VLAN.
+     *
+     * @param deviceId device ID that host attaches to
+     * @param portNum port number that host attaches to
+     * @param vlanId Vlan ID configured on the switch port
+     * @param popVlan true to pop Vlan tag at TrafficTreatment, false otherwise
+     * @param install true to populate the objective, false to revoke
+     */
+    void processIntfVlanUpdatedEvent(DeviceId deviceId, PortNumber portNum, VlanId vlanId,
+                                 boolean popVlan, boolean install) {
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, portNum);
+        Set<Host> hosts = hostService.getConnectedHosts(connectPoint);
+
+        if (hosts == null || hosts.size() == 0) {
+            return;
+        }
+
+        hosts.forEach(host -> {
+            MacAddress mac = host.mac();
+            VlanId hostVlanId = host.vlan();
+
+            // Check whether the host vlan is valid for new interface configuration
+            if ((!popVlan && hostVlanId.equals(vlanId)) ||
+                    (popVlan && hostVlanId.equals(VlanId.NONE))) {
+                updateBridgingRule(deviceId, portNum, mac, vlanId, popVlan, install);
+                // Update Forwarding objective and corresponding simple Next objective
+                // for each host and IP address connected to given port
+                host.ipAddresses().forEach(ipAddress ->
+                    srManager.routingRulePopulator.updateFwdObj(deviceId, portNum, ipAddress.toIpPrefix(),
+                                                                mac, vlanId, popVlan, install)
+                );
+            }
+        });
+    }
+
+    /**
+     * Populate or revoke routing rule for each host, according to the updated
+     * subnet configuration on the interface.
+     * @param cp connect point of the updated interface
+     * @param ipPrefixSet IP Prefixes added or removed
+     * @param install true if IP Prefixes added, false otherwise
+     */
+    void processIntfIpUpdatedEvent(ConnectPoint cp, Set<IpPrefix> ipPrefixSet, boolean install) {
+        Set<Host> hosts = hostService.getConnectedHosts(cp);
+
+        if (hosts == null || hosts.size() == 0) {
+            log.warn("processIntfIpUpdatedEvent: No hosts connected to {}", cp);
+            return;
+        }
+
+        // Check whether the host IP address is in the interface's subnet
+        hosts.forEach(host ->
+            host.ipAddresses().forEach(hostIpAddress -> {
+                ipPrefixSet.forEach(ipPrefix -> {
+                    if (install && ipPrefix.contains(hostIpAddress)) {
+                            srManager.routingRulePopulator.populateRoute(cp.deviceId(), hostIpAddress.toIpPrefix(),
+                                                                         host.mac(), host.vlan(), cp.port());
+                    } else if (!install && ipPrefix.contains(hostIpAddress)) {
+                            srManager.routingRulePopulator.revokeRoute(cp.deviceId(), hostIpAddress.toIpPrefix(),
+                                                                       host.mac(), host.vlan(), cp.port());
+                    }
+                });
+            }));
+    }
+
+    /**
+     * Returns the set of portnumbers on the given device that are part of the
+     * locations for dual-homed hosts.
+     *
+     * @param deviceId the given deviceId
+     * @return set of port numbers on given device that are dual-homed host
+     *         locations. May be empty if no dual homed hosts are connected to
+     *         the given device
+     */
+    Set<PortNumber> getDualHomedHostPorts(DeviceId deviceId) {
+        Set<PortNumber> dualHomedLocations = new HashSet<>();
+        srManager.hostService.getConnectedHosts(deviceId).stream()
+            .filter(host -> host.locations().size() == 2)
+            .forEach(host -> host.locations().stream()
+                     .filter(loc -> loc.deviceId().equals(deviceId))
+                        .forEach(loc -> dualHomedLocations.add(loc.port())));
+        return dualHomedLocations;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
new file mode 100644
index 0000000..8228bb5
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP;
+import org.onlab.packet.ICMP6;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MPLS;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.packet.ndp.NeighborSolicitation;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.neighbour.NeighbourMessageType;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Handler of ICMP packets that responses or forwards ICMP packets that
+ * are sent to the controller.
+ */
+public class IcmpHandler extends SegmentRoutingNeighbourHandler {
+
+    private static Logger log = LoggerFactory.getLogger(IcmpHandler.class);
+
+    /**
+     * Creates an IcmpHandler object.
+     *
+     * @param srManager SegmentRoutingManager object
+     */
+    public IcmpHandler(SegmentRoutingManager srManager) {
+        super(srManager);
+    }
+
+    /**
+     * Utility function to send packet out.
+     *
+     * @param outport the output port
+     * @param payload the packet to send
+     * @param sid the segment id
+     * @param destIpAddress the destination ip address
+     * @param allowedHops the hop limit/ttl
+     */
+    private void sendPacketOut(ConnectPoint outport,
+                               Ethernet payload,
+                               int sid,
+                               IpAddress destIpAddress,
+                               byte allowedHops) {
+        int destSid;
+        if (destIpAddress.isIp4()) {
+            destSid = config.getIPv4SegmentId(payload.getDestinationMAC());
+        } else {
+            destSid = config.getIPv6SegmentId(payload.getDestinationMAC());
+        }
+
+        if (sid == -1 || destSid == sid ||
+                config.inSameSubnet(outport.deviceId(), destIpAddress)) {
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder().
+                    setOutput(outport.port()).build();
+            OutboundPacket packet = new DefaultOutboundPacket(outport.deviceId(),
+                                                              treatment, ByteBuffer.wrap(payload.serialize()));
+            srManager.packetService.emit(packet);
+        } else {
+            log.trace("Send a MPLS packet as a ICMP response");
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .setOutput(outport.port())
+                    .build();
+
+            payload.setEtherType(Ethernet.MPLS_UNICAST);
+            MPLS mplsPkt = new MPLS();
+            mplsPkt.setLabel(sid);
+            mplsPkt.setTtl(allowedHops);
+            mplsPkt.setPayload(payload.getPayload());
+            payload.setPayload(mplsPkt);
+
+            OutboundPacket packet = new DefaultOutboundPacket(outport.deviceId(),
+                                                              treatment, ByteBuffer.wrap(payload.serialize()));
+
+            srManager.packetService.emit(packet);
+        }
+    }
+
+    //////////////////////////////////////
+    //     ICMP Echo/Reply Protocol     //
+    //////////////////////////////////////
+
+    /**
+     * Process incoming ICMP packet.
+     * If it is an ICMP request to router, then sends an ICMP response.
+     * Otherwise ignore the packet.
+     *
+     * @param eth inbound ICMP packet
+     * @param inPort the input port
+     */
+    public void processIcmp(Ethernet eth, ConnectPoint inPort) {
+        DeviceId deviceId = inPort.deviceId();
+        IPv4 ipv4Packet = (IPv4) eth.getPayload();
+        Ip4Address destinationAddress = Ip4Address.valueOf(ipv4Packet.getDestinationAddress());
+        Set<IpAddress> gatewayIpAddresses = config.getPortIPs(deviceId);
+        IpAddress routerIp;
+        try {
+            routerIp = config.getRouterIpv4(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting processPacketIn.");
+            return;
+        }
+        // ICMP to the router IP or gateway IP
+        if (((ICMP) ipv4Packet.getPayload()).getIcmpType() == ICMP.TYPE_ECHO_REQUEST &&
+                (destinationAddress.equals(routerIp.getIp4Address()) ||
+                        gatewayIpAddresses.contains(destinationAddress))) {
+            sendIcmpResponse(eth, inPort);
+        } else {
+            log.trace("Ignore ICMP that targets for {}", destinationAddress);
+        }
+        // We remove the packet from the queue
+        srManager.ipHandler.dequeuePacket(ipv4Packet, destinationAddress);
+    }
+
+    /**
+     * Sends an ICMP reply message.
+     *
+     * @param icmpRequest the original ICMP request
+     * @param outport the output port where the ICMP reply should be sent to
+     */
+    private void sendIcmpResponse(Ethernet icmpRequest, ConnectPoint outport) {
+        Ethernet icmpReplyEth = ICMP.buildIcmpReply(icmpRequest);
+        IPv4 icmpRequestIpv4 = (IPv4) icmpRequest.getPayload();
+        IPv4 icmpReplyIpv4 = (IPv4) icmpReplyEth.getPayload();
+        Ip4Address destIpAddress = Ip4Address.valueOf(icmpRequestIpv4.getSourceAddress());
+        Ip4Address destRouterAddress = config.getRouterIpAddressForASubnetHost(destIpAddress);
+
+        // Note: Source IP of the ICMP request doesn't belong to any configured subnet.
+        //       The source might be an indirectly attached host (e.g. behind a router)
+        //       Lookup the route store for the nexthop instead.
+        if (destRouterAddress == null) {
+            Optional<DeviceId> deviceId = srManager.routeService
+                    .longestPrefixLookup(destIpAddress).map(srManager::nextHopLocations)
+                    .flatMap(locations -> locations.stream().findFirst())
+                    .map(ConnectPoint::deviceId);
+            if (deviceId.isPresent()) {
+                try {
+                    destRouterAddress = config.getRouterIpv4(deviceId.get());
+                } catch (DeviceConfigNotFoundException e) {
+                    log.warn("Device config for {} not found. Abort ICMP processing", deviceId);
+                    return;
+                }
+            }
+        }
+
+        int destSid = config.getIPv4SegmentId(destRouterAddress);
+        if (destSid < 0) {
+            log.warn("Failed to lookup SID of the switch that {} attaches to. " +
+                    "Unable to process ICMP request.", destIpAddress);
+            return;
+        }
+        sendPacketOut(outport, icmpReplyEth, destSid, destIpAddress, icmpReplyIpv4.getTtl());
+    }
+
+    ///////////////////////////////////////////
+    //      ICMPv6 Echo/Reply Protocol       //
+    ///////////////////////////////////////////
+
+    /**
+     * Process incoming ICMPv6 packet.
+     * If it is an ICMPv6 request to router, then sends an ICMPv6 response.
+     * Otherwise ignore the packet.
+     *
+     * @param eth the incoming ICMPv6 packet
+     * @param inPort the input port
+     */
+    public void processIcmpv6(Ethernet eth, ConnectPoint inPort) {
+        DeviceId deviceId = inPort.deviceId();
+        IPv6 ipv6Packet = (IPv6) eth.getPayload();
+        ICMP6 icmp6 = (ICMP6) ipv6Packet.getPayload();
+        Ip6Address destinationAddress = Ip6Address.valueOf(ipv6Packet.getDestinationAddress());
+        Set<IpAddress> gatewayIpAddresses = config.getPortIPs(deviceId);
+        IpAddress routerIp;
+
+        // Only proceed with echo request
+        if (icmp6.getIcmpType() != ICMP6.ECHO_REQUEST) {
+            return;
+        }
+
+        try {
+            routerIp = config.getRouterIpv6(deviceId);
+
+            Optional<Ip6Address> linkLocalIp = srManager.interfaceService.getInterfacesByPort(inPort)
+                    .stream()
+                    .map(Interface::mac)
+                    .map(MacAddress::toBytes)
+                    .map(IPv6::getLinkLocalAddress)
+                    .map(Ip6Address::valueOf)
+                    .findFirst();
+
+            // Ensure ICMP to the router IP, EUI-64 link-local IP, or gateway IP
+            if (destinationAddress.equals(routerIp.getIp6Address()) ||
+                    (linkLocalIp.isPresent() && destinationAddress.equals(linkLocalIp.get())) ||
+                    gatewayIpAddresses.contains(destinationAddress)) {
+                sendIcmpv6Response(eth, inPort);
+            } else {
+                log.trace("Ignore ICMPv6 that targets for {}", destinationAddress);
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Ignore ICMPv6 that targets to {}.", destinationAddress);
+        }
+    }
+
+    /**
+     * Sends an ICMPv6 reply message.
+     *
+     * Note: we assume that packets sending from the edge switches to the hosts
+     * have untagged VLAN.
+     * @param ethRequest the original ICMP request
+     * @param outport the output port where the ICMP reply should be sent to
+     */
+    private void sendIcmpv6Response(Ethernet ethRequest, ConnectPoint outport) {
+        // Note: We assume that packets arrive at the edge switches have untagged VLAN.
+        Ethernet ethReply = ICMP6.buildIcmp6Reply(ethRequest);
+        IPv6 icmpRequestIpv6 = (IPv6) ethRequest.getPayload();
+        IPv6 icmpReplyIpv6 = (IPv6) ethRequest.getPayload();
+        Ip6Address destIpAddress = Ip6Address.valueOf(icmpRequestIpv6.getSourceAddress());
+        Ip6Address destRouterAddress = config.getRouterIpAddressForASubnetHost(destIpAddress);
+
+        // Note: Source IP of the ICMP request doesn't belong to any configured subnet.
+        //       The source might be an indirect host behind a router.
+        //       Lookup the route store for the nexthop instead.
+        if (destRouterAddress == null) {
+            Optional<DeviceId> deviceId = srManager.routeService
+                    .longestPrefixLookup(destIpAddress).map(srManager::nextHopLocations)
+                    .flatMap(locations -> locations.stream().findFirst())
+                    .map(ConnectPoint::deviceId);
+            if (deviceId.isPresent()) {
+                try {
+                    destRouterAddress = config.getRouterIpv6(deviceId.get());
+                } catch (DeviceConfigNotFoundException e) {
+                    log.warn("Device config for {} not found. Abort ICMPv6 processing", deviceId);
+                    return;
+                }
+            }
+        }
+
+        // Search SID only if store lookup is success otherwise proceed with "sid=-1"
+        int sid = -1;
+        if (destRouterAddress !=  null) {
+            sid = config.getIPv6SegmentId(destRouterAddress);
+            if (sid < 0) {
+                log.warn("Failed to lookup SID of the switch that {} attaches to. " +
+                        "Unable to process ICMPv6 request.", destIpAddress);
+                return;
+            }
+        }
+        sendPacketOut(outport, ethReply, sid, destIpAddress, icmpReplyIpv6.getHopLimit());
+    }
+
+    ///////////////////////////////////////////
+    //  ICMPv6 Neighbour Discovery Protocol  //
+    ///////////////////////////////////////////
+
+    /**
+     * Process incoming NDP packet.
+     *
+     * If it is an NDP request for the router or for the gateway, then sends a NDP reply.
+     * If it is an NDP request to unknown host flood in the subnet.
+     * If it is an NDP packet to known host forward the packet to the host.
+     *
+     * FIXME If the NDP packets use link local addresses we fail.
+     *
+     * @param pkt inbound packet
+     * @param hostService the host service
+     */
+    public void processPacketIn(NeighbourMessageContext pkt, HostService hostService) {
+        // First we validate the ndp packet
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(pkt.inPort())) {
+            // Ignore NDP packets come from suppressed ports
+            pkt.drop();
+            return;
+        }
+
+        if (pkt.type() == NeighbourMessageType.REQUEST) {
+            handleNdpRequest(pkt, hostService);
+        } else {
+            handleNdpReply(pkt, hostService);
+        }
+
+    }
+
+    /**
+     * Helper method to handle the ndp requests.
+     * @param pkt the ndp packet request and context information
+     * @param hostService the host service
+     */
+    private void handleNdpRequest(NeighbourMessageContext pkt, HostService hostService) {
+        // ND request for the gateway. We have to reply on behalf of the gateway.
+        if (isNdpForGateway(pkt)) {
+            log.trace("Sending NDP reply on behalf of gateway IP for pkt: {}", pkt.target());
+            MacAddress routerMac = config.getRouterMacForAGatewayIp(pkt.target());
+            sendResponse(pkt, routerMac, hostService);
+        } else {
+
+            // Process NDP targets towards EUI-64 address.
+            try {
+                DeviceId deviceId = pkt.inPort().deviceId();
+
+                Optional<Ip6Address> linkLocalIp = srManager.interfaceService.getInterfacesByPort(pkt.inPort())
+                        .stream()
+                        .map(Interface::mac)
+                        .map(MacAddress::toBytes)
+                        .map(IPv6::getLinkLocalAddress)
+                        .map(Ip6Address::valueOf)
+                        .findFirst();
+
+                if (linkLocalIp.isPresent() && pkt.target().equals(linkLocalIp.get())) {
+                    MacAddress routerMac = config.getDeviceMac(deviceId);
+                    sendResponse(pkt, routerMac, hostService);
+                }
+            } catch (DeviceConfigNotFoundException e) {
+                log.warn(e.getMessage() + " Unable to handle NDP packet to {}. Aborting.", pkt.target());
+                return;
+            }
+
+            // NOTE: Ignore NDP packets except those target for the router
+            //       We will reconsider enabling this when we have host learning support
+            /*
+            // ND request for an host. We do a search by Ip.
+            Set<Host> hosts = hostService.getHostsByIp(pkt.target());
+            // Possible misconfiguration ? In future this case
+            // should be handled we can have same hosts in different VLANs.
+            if (hosts.size() > 1) {
+                log.warn("More than one host with IP {}", pkt.target());
+            }
+            Host targetHost = hosts.stream().findFirst().orElse(null);
+            // If we know the host forward to its attachment point.
+            if (targetHost != null) {
+                log.debug("Forward NDP request to the target host");
+                pkt.forward(targetHost.location());
+            } else {
+                // Flood otherwise.
+                log.debug("Flood NDP request to the target subnet");
+                flood(pkt);
+            }
+            */
+        }
+    }
+
+    /**
+     * Helper method to handle the ndp replies.
+     *
+     * @param pkt the ndp packet reply and context information
+     * @param hostService the host service
+     */
+    private void handleNdpReply(NeighbourMessageContext pkt, HostService hostService) {
+        if (isNdpForGateway(pkt)) {
+            log.debug("Forwarding all the ip packets we stored");
+            Ip6Address hostIpAddress = pkt.sender().getIp6Address();
+            srManager.ipHandler.forwardPackets(pkt.inPort().deviceId(), hostIpAddress);
+        } else {
+            // NOTE: Ignore NDP packets except those target for the router
+            //       We will reconsider enabling this when we have host learning support
+            /*
+            HostId hostId = HostId.hostId(pkt.dstMac(), pkt.vlan());
+            Host targetHost = hostService.getHost(hostId);
+            if (targetHost != null) {
+                log.debug("Forwarding the reply to the host");
+                pkt.forward(targetHost.location());
+            } else {
+                // We don't have to flood towards spine facing ports.
+                if (pkt.vlan().equals(SegmentRoutingManager.INTERNAL_VLAN)) {
+                    return;
+                }
+                log.debug("Flooding the reply to the subnet");
+                flood(pkt);
+            }
+            */
+        }
+    }
+
+    /**
+     * Utility to verify if the ND are for the gateway.
+     *
+     * @param pkt the ndp packet
+     * @return true if the ndp is for the gateway. False otherwise
+     */
+    private boolean isNdpForGateway(NeighbourMessageContext pkt) {
+        DeviceId deviceId = pkt.inPort().deviceId();
+        Set<IpAddress> gatewayIpAddresses = null;
+
+        try {
+            if (pkt.target().equals(config.getRouterIpv6(deviceId))) {
+                return true;
+            }
+            gatewayIpAddresses = config.getPortIPs(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting check for router IP in processing ndp");
+            return false;
+        }
+        return gatewayIpAddresses != null && gatewayIpAddresses.stream()
+                .filter(IpAddress::isIp6)
+                .anyMatch(gatewayIp -> gatewayIp.equals(pkt.target()) ||
+                        Arrays.equals(IPv6.getSolicitNodeAddress(gatewayIp.toOctets()),
+                                pkt.target().toOctets()));
+    }
+
+    /**
+     * Sends a NDP request for the target IP address to all ports except in-port.
+     *
+     * @param deviceId Switch device ID
+     * @param targetAddress target IP address for ARP
+     * @param inPort in-port
+     */
+    public void sendNdpRequest(DeviceId deviceId, IpAddress targetAddress, ConnectPoint inPort) {
+        byte[] senderMacAddress = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+        byte[] senderIpAddress = new byte[Ip6Address.BYTE_LENGTH];
+        // Retrieves device info.
+        if (!getSenderInfo(senderMacAddress, senderIpAddress, deviceId, targetAddress)) {
+            log.warn("Aborting sendNdpRequest, we cannot get all the information needed");
+            return;
+        }
+        // We have to compute the dst mac address and dst ip address.
+        byte[] dstIp = IPv6.getSolicitNodeAddress(targetAddress.toOctets());
+        byte[] dstMac = IPv6.getMCastMacAddress(dstIp);
+        // Creates the request.
+        Ethernet ndpRequest = NeighborSolicitation.buildNdpSolicit(
+                targetAddress.toOctets(),
+                senderIpAddress,
+                dstIp,
+                senderMacAddress,
+                dstMac,
+                VlanId.NONE
+        );
+        flood(ndpRequest, inPort, targetAddress);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java
new file mode 100644
index 0000000..c445ac4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/IpHandler.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IP;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.IpAddress.Version.INET6;
+
+/**
+ * Handler of IP packets that forwards IP packets that are sent to the controller,
+ * except the ICMP packets which are processed by @link{IcmpHandler}.
+ */
+public class IpHandler {
+
+    private static Logger log = LoggerFactory.getLogger(IpHandler.class);
+    private SegmentRoutingManager srManager;
+    private DeviceConfiguration config;
+    private ConcurrentHashMap<IpAddress, ConcurrentLinkedQueue<IP>> ipPacketQueue;
+
+    /**
+     * Creates an IpHandler object.
+     *
+     * @param srManager SegmentRoutingManager object
+     */
+    public IpHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.config = checkNotNull(srManager.deviceConfiguration);
+        ipPacketQueue = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Enqueues the packet using the destination address as key.
+     *
+     * @param ipPacket the ip packet to store
+     * @param destinationAddress the destination address
+     */
+    private void enqueuePacket(IP ipPacket, IpAddress destinationAddress) {
+
+        ipPacketQueue
+            .computeIfAbsent(destinationAddress, a -> new ConcurrentLinkedQueue<>())
+            .add(ipPacket);
+
+    }
+
+    /**
+     * Dequeues the packet using the destination address as key.
+     *
+     * @param ipPacket the ip packet to remove
+     * @param destinationAddress the destination address
+     */
+    public void dequeuePacket(IP ipPacket, IpAddress destinationAddress) {
+
+        if (ipPacketQueue.get(destinationAddress) == null) {
+            return;
+        }
+        ipPacketQueue.get(destinationAddress).remove(ipPacket);
+    }
+
+    /**
+     * Forwards the packet to a given host and deque the packet.
+     *
+     * @param deviceId the target device
+     * @param eth the packet to send
+     * @param dest the target host
+     */
+    private void forwardToHost(DeviceId deviceId, Ethernet eth, Host dest) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder().
+                setOutput(dest.location().port()).build();
+        OutboundPacket packet = new DefaultOutboundPacket(deviceId,
+                                                          treatment, ByteBuffer.wrap(eth.serialize()));
+        srManager.packetService.emit(packet);
+    }
+
+    //////////////////////
+    //  IPv4 Handling  //
+    ////////////////////
+
+    /**
+     * Processes incoming IP packets.
+     *
+     * If it is an IP packet for known host, then forward it to the host.
+     * If it is an IP packet for unknown host in subnet, then send an ARP request
+     * to the subnet.
+     *
+     * @param pkt incoming packet
+     * @param connectPoint the target device
+     */
+    public void processPacketIn(IPv4 pkt, ConnectPoint connectPoint) {
+
+        DeviceId deviceId = connectPoint.deviceId();
+        Ip4Address destinationAddress = Ip4Address.valueOf(pkt.getDestinationAddress());
+
+        // IP packet for know hosts
+        if (!srManager.hostService.getHostsByIp(destinationAddress).isEmpty()) {
+            forwardPackets(deviceId, destinationAddress);
+
+        // IP packet for unknown host in one of the configured subnets of the router
+        } else if (config.inSameSubnet(deviceId, destinationAddress)) {
+            srManager.arpHandler.sendArpRequest(deviceId, destinationAddress, connectPoint);
+
+        // IP packets for unknown host
+        } else {
+            log.debug("IPv4 packet for unknown host {} which is not in the subnet",
+                    destinationAddress);
+            // Do nothing
+        }
+    }
+
+    /**
+     * Adds the IP packet to a buffer.
+     * The packets are forwarded to corresponding destination when the destination
+     * MAC address is known via ARP response.
+     *
+     * @param ipPacket IP packet to add to the buffer
+     */
+    public void addToPacketBuffer(IPv4 ipPacket) {
+
+        // Better not buffer TCP packets due to out-of-order packet transfer
+        if (ipPacket.getProtocol() == IPv4.PROTOCOL_TCP) {
+            return;
+        }
+        IpAddress destIpAddress = IpAddress.valueOf(ipPacket.getDestinationAddress());
+        enqueuePacket(ipPacket, destIpAddress);
+    }
+
+    /**
+     * Forwards IP packets in the buffer to the destination IP address.
+     * It is called when the controller finds the destination MAC address
+     * via ARP responses.
+     *
+     * @param deviceId switch device ID
+     * @param destIpAddress destination IP address
+     */
+    public void forwardPackets(DeviceId deviceId, Ip4Address destIpAddress) {
+        if (ipPacketQueue.get(destIpAddress) == null) {
+            return;
+        }
+        for (IP ipPacket : ipPacketQueue.get(destIpAddress)) {
+            if (ipPacket.getVersion() == ((byte) 4)) {
+                IPv4 ipv4Packet = (IPv4) ipPacket;
+                Ip4Address destAddress = Ip4Address.valueOf(ipv4Packet.getDestinationAddress());
+                if (config.inSameSubnet(deviceId, destAddress)) {
+                    ipv4Packet.setTtl((byte) (ipv4Packet.getTtl() - 1));
+                    ipv4Packet.setChecksum((short) 0);
+                    for (Host dest : srManager.hostService.getHostsByIp(destIpAddress)) {
+                        Ethernet eth = new Ethernet();
+                        eth.setDestinationMACAddress(dest.mac());
+                        try {
+                            eth.setSourceMACAddress(config.getDeviceMac(deviceId));
+                        } catch (DeviceConfigNotFoundException e) {
+                            log.warn(e.getMessage()
+                                             + " Skipping forwardPackets for this destination.");
+                            continue;
+                        }
+                        eth.setEtherType(Ethernet.TYPE_IPV4);
+                        eth.setPayload(ipv4Packet);
+                        forwardToHost(deviceId, eth, dest);
+                        ipPacketQueue.get(destIpAddress).remove(ipPacket);
+                    }
+                    ipPacketQueue.get(destIpAddress).remove(ipPacket);
+                }
+            }
+        }
+    }
+
+    //////////////////////
+    //  IPv6 Handling  //
+    ////////////////////
+
+    /**
+     * Processes incoming IPv6 packets.
+     *
+     * If it is an IPv6 packet for known host, then forward it to the host.
+     * If it is an IPv6 packet for unknown host in subnet, then send an NDP request
+     * to the subnet.
+     *
+     * @param pkt incoming packet
+     * @param connectPoint the target device
+     */
+    public void processPacketIn(IPv6 pkt, ConnectPoint connectPoint) {
+
+        DeviceId deviceId = connectPoint.deviceId();
+        Ip6Address destinationAddress = Ip6Address.valueOf(pkt.getDestinationAddress());
+
+        // IPv6 packet for know hosts
+        if (!srManager.hostService.getHostsByIp(destinationAddress).isEmpty()) {
+            forwardPackets(deviceId, destinationAddress);
+
+            // IPv6 packet for unknown host in one of the configured subnets of the router
+        } else if (config.inSameSubnet(deviceId, destinationAddress)) {
+            srManager.icmpHandler.sendNdpRequest(deviceId, destinationAddress, connectPoint);
+
+            // IPv6 packets for unknown host
+        } else {
+            log.debug("IPv6 packet for unknown host {} which is not in the subnet",
+                      destinationAddress);
+        }
+    }
+
+    /**
+     * Adds the IPv6 packet to a buffer.
+     * The packets are forwarded to corresponding destination when the destination
+     * MAC address is known via NDP response.
+     *
+     * @param ipPacket IP packet to add to the buffer
+     */
+    public void addToPacketBuffer(IPv6 ipPacket) {
+
+        // Better not buffer TCP packets due to out-of-order packet transfer
+        if (ipPacket.getNextHeader() == IPv6.PROTOCOL_TCP) {
+            return;
+        }
+        IpAddress destIpAddress = IpAddress.valueOf(INET6, ipPacket.getDestinationAddress());
+        enqueuePacket(ipPacket, destIpAddress);
+    }
+
+    /**
+     * Forwards IP packets in the buffer to the destination IP address.
+     * It is called when the controller finds the destination MAC address
+     * via NDP replies.
+     *
+     * @param deviceId the target device
+     * @param destIpAddress the destination ip address
+     */
+    public void forwardPackets(DeviceId deviceId, Ip6Address destIpAddress) {
+        if (ipPacketQueue.get(destIpAddress) == null) {
+            return;
+        }
+        for (IP ipPacket : ipPacketQueue.get(destIpAddress)) {
+            if (ipPacket.getVersion() == ((byte) 6)) {
+                IPv6 ipv6Packet = (IPv6) ipPacket;
+                Ip6Address destAddress = Ip6Address.valueOf(ipv6Packet.getDestinationAddress());
+                if (config.inSameSubnet(deviceId, destAddress)) {
+                    ipv6Packet.setHopLimit((byte) (ipv6Packet.getHopLimit() - 1));
+                    for (Host dest : srManager.hostService.getHostsByIp(destIpAddress)) {
+                        Ethernet eth = new Ethernet();
+                        eth.setDestinationMACAddress(dest.mac());
+                        try {
+                            eth.setSourceMACAddress(config.getDeviceMac(deviceId));
+                        } catch (DeviceConfigNotFoundException e) {
+                            log.warn(e.getMessage()
+                                             + " Skipping forwardPackets for this destination.");
+                            continue;
+                        }
+                        eth.setEtherType(Ethernet.TYPE_IPV6);
+                        eth.setPayload(ipv6Packet);
+                        forwardToHost(deviceId, eth, dest);
+                        ipPacketQueue.get(destIpAddress).remove(ipPacket);
+                    }
+                    ipPacketQueue.get(destIpAddress).remove(ipPacket);
+                }
+            }
+            ipPacketQueue.get(destIpAddress).remove(ipPacket);
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java
new file mode 100644
index 0000000..c643991
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/LinkHandler.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright 2018-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.segmentrouting;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapBuilder;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
+public class LinkHandler {
+    private static final Logger log = LoggerFactory.getLogger(LinkHandler.class);
+    protected final SegmentRoutingManager srManager;
+    protected LinkService linkService;
+
+    // Local store for all links seen and their present status, used for
+    // optimized routing. The existence of the link in the keys is enough to know
+    // if the link has been "seen-before" by this instance of the controller.
+    // The boolean value indicates if the link is currently up or not.
+    // Currently the optimized routing logic depends on "forgetting" a link
+    // when a switch goes down, but "remembering" it when only the link goes down.
+    private Map<Link, Boolean> seenLinks = new ConcurrentHashMap<>();
+
+    private EventuallyConsistentMap<DeviceId, Set<PortNumber>> downedPortStore = null;
+
+    /**
+     * Constructs the LinkHandler.
+     *
+     * @param srManager Segment Routing manager
+     */
+    LinkHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        linkService = srManager.linkService;
+        log.debug("Creating EC map downedportstore");
+        EventuallyConsistentMapBuilder<DeviceId, Set<PortNumber>> downedPortsMapBuilder
+                = srManager.storageService.eventuallyConsistentMapBuilder();
+        downedPortStore = downedPortsMapBuilder.withName("downedportstore")
+                .withSerializer(srManager.createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+        log.trace("Current size {}", downedPortStore.size());
+        init();
+    }
+
+    /**
+     * Constructs the LinkHandler for unit-testing.
+     *
+     * @param srManager SegmentRoutingManager
+     * @param linkService LinkService
+     */
+    LinkHandler(SegmentRoutingManager srManager, LinkService linkService) {
+        this.srManager = srManager;
+        this.linkService = linkService;
+    }
+
+    /**
+     * Initialize LinkHandler.
+     */
+    private void init() {
+        log.info("Loading stored links");
+        srManager.linkService.getActiveLinks()
+                .forEach(link -> processLinkAdded(link));
+    }
+
+    /**
+     * Preprocessing of added link before being sent for route-path handling.
+     * Also performs post processing of link.
+     *
+     * @param link the link to be processed
+     */
+    void processLinkAdded(Link link) {
+        log.info("** LINK ADDED {}", link.toString());
+        if (!isLinkValid(link)) {
+            return;
+        }
+        if (srManager.deviceConfiguration == null ||
+                !srManager.deviceConfiguration.isConfigured(link.src().deviceId())) {
+            updateSeenLink(link, true);
+            // XXX revisit - what about devicePortMap
+            log.warn("Source device of this link is not configured.. "
+                    + "not processing further");
+            return;
+        }
+
+        // Irrespective of whether the local is a MASTER or not for this device,
+        // create group handler instance and push default TTP flow rules if needed,
+        // as in a multi-instance setup, instances can initiate groups for any
+        // device.
+        DefaultGroupHandler groupHandler = srManager.groupHandlerMap
+                .get(link.src().deviceId());
+        if (groupHandler != null) {
+            groupHandler.portUpForLink(link);
+        } else {
+            // XXX revisit/cleanup
+            Device device = srManager.deviceService.getDevice(link.src().deviceId());
+            if (device != null) {
+                log.warn("processLinkAdded: Link Added "
+                        + "Notification without Device Added "
+                        + "event, still handling it");
+                srManager.processDeviceAdded(device);
+                groupHandler = srManager.groupHandlerMap.get(link.src().deviceId());
+                groupHandler.portUpForLink(link);
+            }
+        }
+
+        /*
+         // process link only if it is bidirectional
+         if (!isBidirectional(link)) {
+            log.debug("Link not bidirectional.. waiting for other direction " +
+                      "src {} --> dst {} ", link.dst(), link.src());
+            // note that if we are not processing for routing, it should at least
+            // be considered a seen-link
+            updateSeenLink(link, true); return;
+          }
+         //TODO ensure that rehash is still done correctly even if link is not processed for
+         //rerouting - perhaps rehash in both directions when it ultimately becomes bidi?
+         */
+
+        log.debug("Starting optimized route-path processing for added link "
+                + "{} --> {}", link.src(), link.dst());
+        boolean seenBefore = isSeenLink(link);
+        // seenLink updates will be done after route-path changes
+        srManager.defaultRoutingHandler
+                .populateRoutingRulesForLinkStatusChange(null, link, null);
+
+        if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
+            // handle edge-ports for dual-homed hosts
+            updateDualHomedHostPorts(link, true);
+
+            // It's possible that linkUp causes no route-path change as ECMP graph does
+            // not change if the link is a parallel link (same src-dst as
+            // another link). However we still need to update ECMP hash groups to include new buckets
+            // for the link that has come up.
+            if (groupHandler != null) {
+                if (!seenBefore && isParallelLink(link)) {
+                    // if link seen first time, we need to ensure hash-groups have
+                    // all ports
+                    log.debug("Attempting retryHash for paralled first-time link {}",
+                            link);
+                    groupHandler.retryHash(link, false, true);
+                } else {
+                    // seen before-link
+                    if (isParallelLink(link)) {
+                        log.debug("Attempting retryHash for paralled seen-before "
+                                + "link {}", link);
+                        groupHandler.retryHash(link, false, false);
+                    }
+                }
+            }
+        }
+
+        srManager.mcastHandler.init();
+    }
+
+    /**
+     * Preprocessing of removed link before being sent for route-path handling.
+     * Also performs post processing of link.
+     *
+     * @param link the link to be processed
+     */
+    void processLinkRemoved(Link link) {
+        log.info("** LINK REMOVED {}", link.toString());
+        if (!isLinkValid(link)) {
+            return;
+        }
+        // when removing links, update seen links first, before doing route-path
+        // changes
+        updateSeenLink(link, false);
+        // handle edge-ports for dual-homed hosts
+        if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
+            updateDualHomedHostPorts(link, false);
+        }
+
+        // device availability check helps to ensure that multiple link-removed
+        // events are actually treated as a single switch removed event.
+        // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
+        // when switch comes back.
+        if (link.src().elementId() instanceof DeviceId
+                && !srManager.deviceService.isAvailable(link.src().deviceId())) {
+            purgeSeenLink(link);
+            return;
+        }
+        if (link.dst().elementId() instanceof DeviceId
+                && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
+            purgeSeenLink(link);
+            return;
+        }
+
+        log.debug("Starting optimized route-path processing for removed link "
+                + "{} --> {}", link.src(), link.dst());
+        srManager.defaultRoutingHandler
+                .populateRoutingRulesForLinkStatusChange(link, null, null);
+
+        // update local groupHandler stores
+        DefaultGroupHandler groupHandler = srManager.groupHandlerMap
+                .get(link.src().deviceId());
+        if (groupHandler != null) {
+            if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
+                    && isParallelLink(link)) {
+                log.debug("* retrying hash for parallel link removed:{}", link);
+                groupHandler.retryHash(link, true, false);
+            } else {
+                log.debug("Not attempting retry-hash for link removed: {} .. {}",
+                          link,
+                          (srManager.mastershipService.isLocalMaster(link.src()
+                                  .deviceId())) ? "not parallel"
+                                                : "not master");
+            }
+            // ensure local stores are updated
+            groupHandler.portDown(link.src().port());
+        } else {
+            log.warn("group handler not found for dev:{} when removing link: {}",
+                     link.src().deviceId(), link);
+        }
+
+        srManager.mcastHandler.processLinkDown(link);
+    }
+
+    /**
+     * Checks validity of link. Examples of invalid links include
+     * indirect-links, links between ports on the same switch, and more.
+     *
+     * @param link the link to be processed
+     * @return true if valid link
+     */
+    private boolean isLinkValid(Link link) {
+        if (link.type() != Link.Type.DIRECT) {
+            // NOTE: A DIRECT link might be transiently marked as INDIRECT
+            // if BDDP is received before LLDP. We can safely ignore that
+            // until the LLDP is received and the link is marked as DIRECT.
+            log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
+                     link.src(), link.dst(), link.type());
+            return false;
+        }
+        DeviceId srcId = link.src().deviceId();
+        DeviceId dstId = link.dst().deviceId();
+        if (srcId.equals(dstId)) {
+            log.warn("Links between ports on the same switch are not "
+                    + "allowed .. ignoring link {}", link);
+            return false;
+        }
+        DeviceConfiguration devConfig = srManager.deviceConfiguration;
+        if (devConfig == null) {
+            log.warn("Cannot check validity of link without device config");
+            return true;
+        }
+        try {
+            if (!devConfig.isEdgeDevice(srcId)
+                    && !devConfig.isEdgeDevice(dstId)) {
+                // ignore links between spines
+                // XXX revisit when handling multi-stage fabrics
+                log.warn("Links between spines not allowed...ignoring "
+                        + "link {}", link);
+                return false;
+            }
+            if (devConfig.isEdgeDevice(srcId)
+                    && devConfig.isEdgeDevice(dstId)) {
+                // ignore links between leaves if they are not pair-links
+                // XXX revisit if removing pair-link config or allowing more than
+                // one pair-link
+                if (devConfig.getPairDeviceId(srcId).equals(dstId)
+                        && devConfig.getPairLocalPort(srcId)
+                                .equals(link.src().port())
+                        && devConfig.getPairLocalPort(dstId)
+                                .equals(link.dst().port())) {
+                    // found pair link - allow it
+                    return true;
+                } else {
+                    log.warn("Links between leaves other than pair-links are "
+                            + "not allowed...ignoring link {}", link);
+                    return false;
+                }
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            // We still want to count the links in seenLinks even though there
+            // is no config. So we let it return true
+            log.warn("Could not check validity of link {} as subtending devices "
+                    + "are not yet configured", link);
+        }
+        return true;
+    }
+
+    /**
+     * Administratively enables or disables edge ports if the link that was
+     * added or removed was the only uplink port from an edge device. Only edge
+     * ports that belong to dual-homed hosts are considered.
+     *
+     * @param link the link to be processed
+     * @param added true if link was added, false if link was removed
+     */
+    private void updateDualHomedHostPorts(Link link, boolean added) {
+        if (!onlyUplink(link)) {
+            return;
+        }
+        if (added) {
+            // re-enable previously disabled ports on this dev
+            Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
+            if (p != null) {
+                log.warn("Link src {} -->dst {} added is the first uplink, "
+                        + "enabling dual homed ports: {}", link.src().deviceId(),
+                     link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
+                p.forEach(pnum -> srManager.deviceAdminService
+                        .changePortState(link.src().deviceId(), pnum, true));
+            }
+        } else {
+            // find dual homed hosts on this dev to disable
+            Set<PortNumber> dhp = srManager.hostHandler
+                    .getDualHomedHostPorts(link.src().deviceId());
+            log.warn("Link src {} -->dst {} removed was the last uplink, "
+                    + "disabling  dual homed ports:  {}", link.src().deviceId(),
+                     link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
+            dhp.forEach(pnum -> srManager.deviceAdminService
+                        .changePortState(link.src().deviceId(), pnum, false));
+            if (!dhp.isEmpty()) {
+                // update global store
+                Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
+                if (p == null) {
+                    p = dhp;
+                } else {
+                    p.addAll(dhp);
+                }
+                downedPortStore.put(link.src().deviceId(), p);
+            }
+        }
+    }
+
+    /**
+     * Returns true if given link is the only active uplink from src-device of
+     * link. An uplink is defined as a unidirectional link with src as
+     * edgeRouter and dst as non-edgeRouter.
+     *
+     * @param link
+     * @return true if given link is-the-first/was-the-last uplink from the src
+     *         device
+     */
+    private boolean onlyUplink(Link link) {
+        DeviceConfiguration devConfig = srManager.deviceConfiguration;
+        try {
+            if (!devConfig.isEdgeDevice(link.src().deviceId())
+                    || devConfig.isEdgeDevice(link.dst().deviceId())) {
+                return false;
+            }
+            // note that using linkservice here would cause race conditions as
+            // more links can show up while the app is still processing the first one
+            Set<Link> devLinks = seenLinks.entrySet().stream()
+                    .filter(entry -> entry.getKey().src().deviceId()
+                            .equals(link.src().deviceId()))
+                    .filter(entry -> entry.getValue())
+                    .filter(entry -> !entry.getKey().equals(link))
+                    .map(entry -> entry.getKey())
+                    .collect(Collectors.toSet());
+
+            for (Link l : devLinks) {
+                if (devConfig.isEdgeDevice(l.dst().deviceId())) {
+                    continue;
+                }
+                log.debug("Link {} is not the only active uplink. Found another"
+                        + "link {}", link, l);
+                return false;
+            }
+            log.debug("Link {} is the only uplink", link);
+            return true;
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn("Unable to determine if link is only uplink"
+                    + e.getMessage());
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this controller instance has seen this link before. The
+     * link may not be currently up, but as long as the link had been seen
+     * before this method will return true. The one exception is when the link
+     * was indeed seen before, but this controller instance was forced to forget
+     * it by a call to purgeSeenLink method.
+     *
+     * @param link the infrastructure link being queried
+     * @return true if this controller instance has seen this link before
+     */
+    boolean isSeenLink(Link link) {
+        return seenLinks.containsKey(link);
+    }
+
+    /**
+     * Updates the seen link store. Updates can be for links that are currently
+     * available or not.
+     *
+     * @param link the link to update in the seen-link local store
+     * @param up the status of the link, true if up, false if down
+     */
+    void updateSeenLink(Link link, boolean up) {
+        seenLinks.put(link, up);
+    }
+
+    /**
+     * Returns the status of a seen-link (up or down). If the link has not been
+     * seen-before, a null object is returned.
+     *
+     * @param link the infrastructure link being queried
+     * @return null if the link was not seen-before; true if the seen-link is
+     *         up; false if the seen-link is down
+     */
+    private Boolean isSeenLinkUp(Link link) {
+        return seenLinks.get(link);
+    }
+
+    /**
+     * Makes this controller instance forget a previously seen before link.
+     *
+     * @param link the infrastructure link to purge
+     */
+    private void purgeSeenLink(Link link) {
+        seenLinks.remove(link);
+    }
+
+    /**
+     * Returns the status of a link as parallel link. A parallel link is defined
+     * as a link which has common src and dst switches as another seen-link that
+     * is currently enabled. It is not necessary for the link being queried to
+     * be a seen-link.
+     *
+     * @param link the infrastructure link being queried
+     * @return true if a seen-link exists that is up, and shares the same src
+     *         and dst switches as the link being queried
+     */
+    private boolean isParallelLink(Link link) {
+        for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
+            Link seenLink = seen.getKey();
+            if (seenLink.equals(link)) {
+                continue;
+            }
+            if (seenLink.src().deviceId().equals(link.src().deviceId())
+                    && seenLink.dst().deviceId().equals(link.dst().deviceId())
+                    && seen.getValue()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the link being queried is a bidirectional link. A bidi
+     * link is defined as a link, whose reverse link - ie. the link in the
+     * reverse direction - has been seen-before and is up. It is not necessary
+     * for the link being queried to be a seen-link.
+     *
+     * @param link the infrastructure link being queried
+     * @return true if another unidirectional link exists in the reverse
+     *         direction, has been seen-before and is up
+     */
+    boolean isBidirectional(Link link) {
+        Link reverseLink = linkService.getLink(link.dst(), link.src());
+        if (reverseLink == null) {
+            return false;
+        }
+        Boolean result = isSeenLinkUp(reverseLink);
+        if (result == null) {
+            return false;
+        }
+        return result.booleanValue();
+    }
+
+    /**
+     * Determines if the given link should be avoided in routing calculations by
+     * policy or design.
+     *
+     * @param link the infrastructure link being queried
+     * @return true if link should be avoided
+     */
+    boolean avoidLink(Link link) {
+        // XXX currently only avoids all pair-links. In the future can be
+        // extended to avoid any generic link
+        DeviceId src = link.src().deviceId();
+        PortNumber srcPort = link.src().port();
+        DeviceConfiguration devConfig = srManager.deviceConfiguration;
+        if (devConfig == null || !devConfig.isConfigured(src)) {
+            log.warn("Device {} not configured..cannot avoid link {}", src,
+                     link);
+            return false;
+        }
+        DeviceId pairDev;
+        PortNumber pairLocalPort, pairRemotePort = null;
+        try {
+            pairDev = devConfig.getPairDeviceId(src);
+            pairLocalPort = devConfig.getPairLocalPort(src);
+            if (pairDev != null) {
+                pairRemotePort = devConfig
+                        .getPairLocalPort(pairDev);
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
+                     src, link);
+            return false;
+        }
+
+        return srcPort.equals(pairLocalPort)
+                && link.dst().deviceId().equals(pairDev)
+                && link.dst().port().equals(pairRemotePort);
+    }
+
+    /**
+     * Cleans up internal LinkHandler stores.
+     *
+     * @param device the device that has been removed
+     */
+    void processDeviceRemoved(Device device) {
+        seenLinks.keySet()
+                .removeIf(key -> key.src().deviceId().equals(device.id())
+                        || key.dst().deviceId().equals(device.id()));
+    }
+
+    /**
+     * Administratively disables the host location switchport if the edge device
+     * has no viable uplinks.
+     *
+     * @param loc one of the locations of the dual-homed host
+     */
+    void checkUplinksForDualHomedHosts(HostLocation loc) {
+        try {
+            for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
+                if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
+                        || l.state() == Link.State.INACTIVE) {
+                    continue;
+                }
+                // found valid uplink - so, nothing to do
+                return;
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn("Could not check for valid uplinks due to missing device"
+                    + "config " + e.getMessage());
+            return;
+        }
+        log.warn("Dual homed host location {} has no valid uplinks; "
+                + "disabling  dual homed port", loc);
+        srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
+                                                     false);
+        Set<PortNumber> p = downedPortStore.get(loc.deviceId());
+        if (p == null) {
+            p = Sets.newHashSet(loc.port());
+        } else {
+            p.add(loc.port());
+        }
+        downedPortStore.put(loc.deviceId(), p);
+    }
+
+    ImmutableMap<Link, Boolean> getSeenLinks() {
+        return ImmutableMap.copyOf(seenLinks);
+    }
+
+    ImmutableMap<DeviceId, Set<PortNumber>> getDownedPorts() {
+        return ImmutableMap.copyOf(downedPortStore.entrySet());
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java
new file mode 100644
index 0000000..fff2c92
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/McastHandler.java
@@ -0,0 +1,1453 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.config.basics.McastConfig;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastRoute;
+import org.onosproject.net.mcast.McastRouteInfo;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+
+/**
+ * Handles multicast-related events.
+ */
+public class McastHandler {
+    private static final Logger log = LoggerFactory.getLogger(McastHandler.class);
+    private final SegmentRoutingManager srManager;
+    private final ApplicationId coreAppId;
+    private final StorageService storageService;
+    private final TopologyService topologyService;
+    private final ConsistentMap<McastStoreKey, NextObjective> mcastNextObjStore;
+    private final KryoNamespace.Builder mcastKryo;
+    private final ConsistentMap<McastStoreKey, McastRole> mcastRoleStore;
+
+    // Mcast lock to serialize local operations
+    private final Lock mcastLock = new ReentrantLock();
+
+    /**
+     * Acquires the lock used when making mcast changes.
+     */
+    private void mcastLock() {
+        mcastLock.lock();
+    }
+
+    /**
+     * Releases the lock used when making mcast changes.
+     */
+    private void mcastUnlock() {
+        mcastLock.unlock();
+    }
+
+    // Stability threshold for Mcast. Seconds
+    private static final long MCAST_STABLITY_THRESHOLD = 5;
+    // Last change done
+    private Instant lastMcastChange = Instant.now();
+
+    /**
+     * Determines if mcast in the network has been stable in the last
+     * MCAST_STABLITY_THRESHOLD seconds, by comparing the current time
+     * to the last mcast change timestamp.
+     *
+     * @return true if stable
+     */
+    private boolean isMcastStable() {
+        long last = (long) (lastMcastChange.toEpochMilli() / 1000.0);
+        long now = (long) (Instant.now().toEpochMilli() / 1000.0);
+        log.debug("Mcast stable since {}s", now - last);
+        return (now - last) > MCAST_STABLITY_THRESHOLD;
+    }
+
+    // Verify interval for Mcast
+    private static final long MCAST_VERIFY_INTERVAL = 30;
+
+    // Executor for mcast bucket corrector
+    private ScheduledExecutorService executorService
+            = newScheduledThreadPool(1, groupedThreads("mcastBktCorrector", "mcastbktC-%d", log));
+
+    /**
+     * Role in the multicast tree.
+     */
+    public enum McastRole {
+        /**
+         * The device is the ingress device of this group.
+         */
+        INGRESS,
+        /**
+         * The device is the transit device of this group.
+         */
+        TRANSIT,
+        /**
+         * The device is the egress device of this group.
+         */
+        EGRESS
+    }
+
+    /**
+     * Constructs the McastEventHandler.
+     *
+     * @param srManager Segment Routing manager
+     */
+    public McastHandler(SegmentRoutingManager srManager) {
+        coreAppId = srManager.coreService.getAppId(CoreService.CORE_APP_NAME);
+        this.srManager = srManager;
+        this.storageService = srManager.storageService;
+        this.topologyService = srManager.topologyService;
+        mcastKryo = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(McastStoreKey.class)
+                .register(McastRole.class);
+        mcastNextObjStore = storageService
+                .<McastStoreKey, NextObjective>consistentMapBuilder()
+                .withName("onos-mcast-nextobj-store")
+                .withSerializer(Serializer.using(mcastKryo.build("McastHandler-NextObj")))
+                .build();
+        mcastRoleStore = storageService
+                .<McastStoreKey, McastRole>consistentMapBuilder()
+                .withName("onos-mcast-role-store")
+                .withSerializer(Serializer.using(mcastKryo.build("McastHandler-Role")))
+                .build();
+        // Init the executor service and the buckets corrector
+        executorService.scheduleWithFixedDelay(new McastBucketCorrector(), 10,
+                                               MCAST_VERIFY_INTERVAL,
+                                               TimeUnit.SECONDS);
+    }
+
+    /**
+     * Read initial multicast from mcast store.
+     */
+    protected void init() {
+        srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
+            ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
+            Set<ConnectPoint> sinks = srManager.multicastRouteService.fetchSinks(mcastRoute);
+            sinks.forEach(sink -> {
+                processSinkAddedInternal(source, sink, mcastRoute.group());
+            });
+        });
+    }
+
+    /**
+     * Clean up when deactivating the application.
+     */
+    protected void terminate() {
+        executorService.shutdown();
+    }
+
+    /**
+     * Processes the SOURCE_ADDED event.
+     *
+     * @param event McastEvent with SOURCE_ADDED type
+     */
+    protected void processSourceAdded(McastEvent event) {
+        log.info("processSourceAdded {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        Set<ConnectPoint> sinks = mcastRouteInfo.sinks();
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+
+        sinks.forEach(sink -> processSinkAddedInternal(source, sink, mcastIp));
+    }
+
+    /**
+     * Processes the SINK_ADDED event.
+     *
+     * @param event McastEvent with SINK_ADDED type
+     */
+    protected void processSinkAdded(McastEvent event) {
+        log.info("processSinkAdded {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+
+        processSinkAddedInternal(source, sink, mcastIp);
+    }
+
+    /**
+     * Processes the SINK_REMOVED event.
+     *
+     * @param event McastEvent with SINK_REMOVED type
+     */
+    protected void processSinkRemoved(McastEvent event) {
+        log.info("processSinkRemoved {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.isComplete()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        ConnectPoint source = mcastRouteInfo.source().orElse(null);
+        ConnectPoint sink = mcastRouteInfo.sink().orElse(null);
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+
+        processSinkRemovedInternal(source, sink, mcastIp);
+    }
+
+    /**
+     * Processes the ROUTE_REMOVED event.
+     *
+     * @param event McastEvent with ROUTE_REMOVED type
+     */
+    protected void processRouteRemoved(McastEvent event) {
+        log.info("processRouteRemoved {}", event);
+        McastRouteInfo mcastRouteInfo = event.subject();
+        if (!mcastRouteInfo.source().isPresent()) {
+            log.info("Incompleted McastRouteInfo. Abort.");
+            return;
+        }
+        // Get group ip and ingress connect point
+        IpAddress mcastIp = mcastRouteInfo.route().group();
+        ConnectPoint source = mcastRouteInfo.source().get();
+
+        processRouteRemovedInternal(source, mcastIp);
+    }
+
+    /**
+     * Removes the entire mcast tree related to this group.
+     *
+     * @param mcastIp multicast group IP address
+     */
+    private void processRouteRemovedInternal(ConnectPoint source, IpAddress mcastIp) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            log.debug("Processing route down for group {}", mcastIp);
+
+            // Find out the ingress, transit and egress device of the affected group
+            DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                    .stream().findAny().orElse(null);
+            DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+                    .stream().findAny().orElse(null);
+            Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+
+            // Verify leadership on the operation
+            if (!isLeader(source)) {
+                log.debug("Skip {} due to lack of leadership", mcastIp);
+                return;
+            }
+
+            // If there are egress devices, sinks could be only on the ingress
+            if (!egressDevices.isEmpty()) {
+                egressDevices.forEach(
+                        deviceId -> removeGroupFromDevice(deviceId, mcastIp, assignedVlan(null))
+                );
+            }
+            // Transit could be null
+            if (transitDevice != null) {
+                removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+            }
+            // Ingress device should be not null
+            if (ingressDevice != null) {
+                removeGroupFromDevice(ingressDevice, mcastIp, assignedVlan(source));
+            }
+
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    /**
+     * Removes a path from source to sink for given multicast group.
+     *
+     * @param source connect point of the multicast source
+     * @param sink connection point of the multicast sink
+     * @param mcastIp multicast group IP address
+     */
+    private void processSinkRemovedInternal(ConnectPoint source, ConnectPoint sink,
+                                          IpAddress mcastIp) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            // Verify leadership on the operation
+            if (!isLeader(source)) {
+                log.debug("Skip {} due to lack of leadership", mcastIp);
+                return;
+            }
+
+            // When source and sink are on the same device
+            if (source.deviceId().equals(sink.deviceId())) {
+                // Source and sink are on even the same port. There must be something wrong.
+                if (source.port().equals(sink.port())) {
+                    log.warn("Skip {} since sink {} is on the same port of source {}. Abort",
+                             mcastIp, sink, source);
+                    return;
+                }
+                removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(source));
+                return;
+            }
+
+            // Process the egress device
+            boolean isLast = removePortFromDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(null));
+            if (isLast) {
+                mcastRoleStore.remove(new McastStoreKey(mcastIp, sink.deviceId()));
+            }
+
+            // If this is the last sink on the device, also update upstream
+            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+            if (mcastPath.isPresent()) {
+                List<Link> links = Lists.newArrayList(mcastPath.get().links());
+                Collections.reverse(links);
+                for (Link link : links) {
+                    if (isLast) {
+                        isLast = removePortFromDevice(
+                                link.src().deviceId(),
+                                link.src().port(),
+                                mcastIp,
+                                assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null)
+                        );
+                        mcastRoleStore.remove(new McastStoreKey(mcastIp, link.src().deviceId()));
+                    }
+                }
+            }
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    /**
+     * Establishes a path from source to sink for given multicast group.
+     *
+     * @param source connect point of the multicast source
+     * @param sink connection point of the multicast sink
+     * @param mcastIp multicast group IP address
+     */
+    private void processSinkAddedInternal(ConnectPoint source, ConnectPoint sink,
+            IpAddress mcastIp) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            // Continue only when this instance is the master of source device
+            if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+                log.debug("Skip {} due to lack of mastership of the source device {}",
+                          mcastIp, source.deviceId());
+                return;
+            }
+
+            // Process the ingress device
+            addFilterToDevice(source.deviceId(), source.port(), assignedVlan(source), mcastIp);
+
+            // When source and sink are on the same device
+            if (source.deviceId().equals(sink.deviceId())) {
+                // Source and sink are on even the same port. There must be something wrong.
+                if (source.port().equals(sink.port())) {
+                    log.warn("Skip {} since sink {} is on the same port of source {}. Abort",
+                             mcastIp, sink, source);
+                    return;
+                }
+                addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(source));
+                mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()), McastRole.INGRESS);
+                return;
+            }
+
+            // Find a path. If present, create/update groups and flows for each hop
+            Optional<Path> mcastPath = getPath(source.deviceId(), sink.deviceId(), mcastIp);
+            if (mcastPath.isPresent()) {
+                List<Link> links = mcastPath.get().links();
+                checkState(links.size() == 2,
+                           "Path in leaf-spine topology should always be two hops: ", links);
+
+                links.forEach(link -> {
+                    addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp,
+                                    assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null));
+                    addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan(null), mcastIp);
+                });
+
+                // Process the egress device
+                addPortToDevice(sink.deviceId(), sink.port(), mcastIp, assignedVlan(null));
+
+                // Setup mcast roles
+                mcastRoleStore.put(new McastStoreKey(mcastIp, source.deviceId()),
+                                   McastRole.INGRESS);
+                mcastRoleStore.put(new McastStoreKey(mcastIp, links.get(0).dst().deviceId()),
+                                   McastRole.TRANSIT);
+                mcastRoleStore.put(new McastStoreKey(mcastIp, sink.deviceId()),
+                                   McastRole.EGRESS);
+            } else {
+                log.warn("Unable to find a path from {} to {}. Abort sinkAdded",
+                         source.deviceId(), sink.deviceId());
+            }
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    /**
+     * Processes the LINK_DOWN event.
+     *
+     * @param affectedLink Link that is going down
+     */
+    protected void processLinkDown(Link affectedLink) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            // Get groups affected by the link down event
+            getAffectedGroups(affectedLink).forEach(mcastIp -> {
+                // TODO Optimize when the group editing is in place
+                log.debug("Processing link down {} for group {}",
+                          affectedLink, mcastIp);
+
+                // Find out the ingress, transit and egress device of affected group
+                DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                        .stream().findAny().orElse(null);
+                DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+                        .stream().findAny().orElse(null);
+                Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+                ConnectPoint source = getSource(mcastIp);
+
+                // Do not proceed if any of these info is missing
+                if (ingressDevice == null || transitDevice == null
+                        || egressDevices == null || source == null) {
+                    log.warn("Missing ingress {}, transit {}, egress {} devices or source {}",
+                             ingressDevice, transitDevice, egressDevices, source);
+                    return;
+                }
+
+                // Continue only when this instance is the master of source device
+                if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+                    log.debug("Skip {} due to lack of mastership of the source device {}",
+                             source.deviceId());
+                    return;
+                }
+
+                // Remove entire transit
+                removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+
+                // Remove transit-facing port on ingress device
+                PortNumber ingressTransitPort = ingressTransitPort(mcastIp);
+                if (ingressTransitPort != null) {
+                    removePortFromDevice(ingressDevice, ingressTransitPort, mcastIp, assignedVlan(source));
+                    mcastRoleStore.remove(new McastStoreKey(mcastIp, transitDevice));
+                }
+
+                // Construct a new path for each egress device
+                egressDevices.forEach(egressDevice -> {
+                    Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+                    if (mcastPath.isPresent()) {
+                        installPath(mcastIp, source, mcastPath.get());
+                    } else {
+                        log.warn("Fail to recover egress device {} from link failure {}",
+                                 egressDevice, affectedLink);
+                        removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null));
+                    }
+                });
+            });
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    /**
+     * Process the DEVICE_DOWN event.
+     *
+     * @param deviceDown device going down
+     */
+    protected void processDeviceDown(DeviceId deviceDown) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            // Get the mcast groups affected by the device going down
+            getAffectedGroups(deviceDown).forEach(mcastIp -> {
+                // TODO Optimize when the group editing is in place
+                log.debug("Processing device down {} for group {}",
+                          deviceDown, mcastIp);
+
+                // Find out the ingress, transit and egress device of affected group
+                DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                        .stream().findAny().orElse(null);
+                DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+                        .stream().findAny().orElse(null);
+                Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+                ConnectPoint source = getSource(mcastIp);
+
+                // Do not proceed if ingress device or source of this group are missing
+                // If sinks are in other leafs, we have ingress, transit, egress, and source
+                // If sinks are in the same leaf, we have just ingress and source
+                if (ingressDevice == null || source == null) {
+                    log.warn("Missing ingress {} or source {} for group {}",
+                             ingressDevice, source, mcastIp);
+                    return;
+                }
+
+                // Verify leadership on the operation
+                if (!isLeader(source)) {
+                    log.debug("Skip {} due to lack of leadership", mcastIp);
+                    return;
+                }
+
+                // If it exists, we have to remove it in any case
+                if (transitDevice != null) {
+                    // Remove entire transit
+                    removeGroupFromDevice(transitDevice, mcastIp, assignedVlan(null));
+                }
+                // If the ingress is down
+                if (ingressDevice.equals(deviceDown)) {
+                    // Remove entire ingress
+                    removeGroupFromDevice(ingressDevice, mcastIp, assignedVlan(source));
+                    // If other sinks different from the ingress exist
+                    if (!egressDevices.isEmpty()) {
+                        // Remove all the remaining egress
+                        egressDevices.forEach(
+                                egressDevice -> removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null))
+                        );
+                    }
+                } else {
+                    // Egress or transit could be down at this point
+                    // Get the ingress-transit port if it exists
+                    PortNumber ingressTransitPort = ingressTransitPort(mcastIp);
+                    if (ingressTransitPort != null) {
+                        // Remove transit-facing port on ingress device
+                        removePortFromDevice(ingressDevice, ingressTransitPort, mcastIp, assignedVlan(source));
+                    }
+                    // One of the egress device is down
+                    if (egressDevices.contains(deviceDown)) {
+                        // Remove entire device down
+                        removeGroupFromDevice(deviceDown, mcastIp, assignedVlan(null));
+                        // Remove the device down from egress
+                        egressDevices.remove(deviceDown);
+                        // If there are no more egress and ingress does not have sinks
+                        if (egressDevices.isEmpty() && !hasSinks(ingressDevice, mcastIp)) {
+                            // Remove entire ingress
+                            mcastRoleStore.remove(new McastStoreKey(mcastIp, ingressDevice));
+                            // We have done
+                            return;
+                        }
+                    }
+                    // Construct a new path for each egress device
+                    egressDevices.forEach(egressDevice -> {
+                        Optional<Path> mcastPath = getPath(ingressDevice, egressDevice, mcastIp);
+                        // If there is a new path
+                        if (mcastPath.isPresent()) {
+                            // Let's install the new mcast path for this egress
+                            installPath(mcastIp, source, mcastPath.get());
+                        } else {
+                            // We were not able to find an alternative path for this egress
+                            log.warn("Fail to recover egress device {} from device down {}",
+                                     egressDevice, deviceDown);
+                            removeGroupFromDevice(egressDevice, mcastIp, assignedVlan(null));
+                        }
+                    });
+                }
+            });
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    /**
+     * Adds filtering objective for given device and port.
+     *
+     * @param deviceId device ID
+     * @param port ingress port number
+     * @param assignedVlan assigned VLAN ID
+     */
+    private void addFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan, IpAddress mcastIp) {
+        // Do nothing if the port is configured as suppressed
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+            log.info("Ignore suppressed port {}", connectPoint);
+            return;
+        }
+
+        FilteringObjective.Builder filtObjBuilder =
+                filterObjBuilder(deviceId, port, assignedVlan, mcastIp);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully add filter on {}/{}, vlan {}",
+                        deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to add filter on {}/{}, vlan {}: {}",
+                                deviceId, port.toLong(), assignedVlan, error));
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add(context));
+    }
+
+    /**
+     * Adds a port to given multicast group on given device. This involves the
+     * update of L3 multicast group and multicast routing table entry.
+     *
+     * @param deviceId device ID
+     * @param port port to be added
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     */
+    private void addPortToDevice(DeviceId deviceId, PortNumber port,
+            IpAddress mcastIp, VlanId assignedVlan) {
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
+        ImmutableSet.Builder<PortNumber> portBuilder = ImmutableSet.builder();
+        NextObjective newNextObj;
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+            // First time someone request this mcast group via this device
+            portBuilder.add(port);
+            // New nextObj
+            newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+                                        portBuilder.build(), null).add();
+            // Store the new port
+            mcastNextObjStore.put(mcastStoreKey, newNextObj);
+        } else {
+            // This device already serves some subscribers of this mcast group
+            NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+            // Stop if the port is already in the nextobj
+            Set<PortNumber> existingPorts = getPorts(nextObj.next());
+            if (existingPorts.contains(port)) {
+                log.info("NextObj for {}/{} already exists. Abort", deviceId, port);
+                return;
+            }
+            // Let's add the port and reuse the previous one
+            portBuilder.addAll(existingPorts).add(port);
+            // Reuse previous nextObj
+            newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+                                        portBuilder.build(), nextObj.id()).addToExisting();
+            // Store the final next objective and send only the difference to the driver
+            mcastNextObjStore.put(mcastStoreKey, newNextObj);
+            // Add just the new port
+            portBuilder = ImmutableSet.builder();
+            portBuilder.add(port);
+            newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+                                        portBuilder.build(), nextObj.id()).addToExisting();
+        }
+        // Create, store and apply the new nextObj and fwdObj
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully add {} on {}/{}, vlan {}",
+                        mcastIp, deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to add {} on {}/{}, vlan {}: {}",
+                                mcastIp, deviceId, port.toLong(), assignedVlan, error));
+        ForwardingObjective fwdObj =
+                fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add(context);
+        srManager.flowObjectiveService.next(deviceId, newNextObj);
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+    }
+
+    /**
+     * Removes a port from given multicast group on given device.
+     * This involves the update of L3 multicast group and multicast routing
+     * table entry.
+     *
+     * @param deviceId device ID
+     * @param port port to be added
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @return true if this is the last sink on this device
+     */
+    private boolean removePortFromDevice(DeviceId deviceId, PortNumber port,
+            IpAddress mcastIp, VlanId assignedVlan) {
+        McastStoreKey mcastStoreKey =
+                new McastStoreKey(mcastIp, deviceId);
+        // This device is not serving this multicast group
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+            log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+            return false;
+        }
+        NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+
+        Set<PortNumber> existingPorts = getPorts(nextObj.next());
+        // This port does not serve this multicast group
+        if (!existingPorts.contains(port)) {
+            log.warn("{} is not serving {} on port {}. Abort.", deviceId, mcastIp, port);
+            return false;
+        }
+        // Copy and modify the ImmutableSet
+        existingPorts = Sets.newHashSet(existingPorts);
+        existingPorts.remove(port);
+
+        NextObjective newNextObj;
+        ObjectiveContext context;
+        ForwardingObjective fwdObj;
+        if (existingPorts.isEmpty()) {
+            // If this is the last sink, remove flows and last bucket
+            // NOTE: Rely on GroupStore garbage collection rather than explicitly
+            //       remove L3MG since there might be other flows/groups refer to
+            //       the same L2IG
+            context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("Successfully remove {} on {}/{}, vlan {}",
+                            mcastIp, deviceId, port.toLong(), assignedVlan),
+                    (objective, error) ->
+                            log.warn("Failed to remove {} on {}/{}, vlan {}: {}",
+                                    mcastIp, deviceId, port.toLong(), assignedVlan, error));
+            fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+            mcastNextObjStore.remove(mcastStoreKey);
+        } else {
+            // If this is not the last sink, update flows and groups
+            context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("Successfully update {} on {}/{}, vlan {}",
+                            mcastIp, deviceId, port.toLong(), assignedVlan),
+                    (objective, error) ->
+                            log.warn("Failed to update {} on {}/{}, vlan {}: {}",
+                                    mcastIp, deviceId, port.toLong(), assignedVlan, error));
+            // Here we store the next objective with the remaining port
+            newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+                                        existingPorts, nextObj.id()).removeFromExisting();
+            fwdObj = fwdObjBuilder(mcastIp, assignedVlan, newNextObj.id()).add(context);
+            mcastNextObjStore.put(mcastStoreKey, newNextObj);
+        }
+        // Let's modify the next objective removing the bucket
+        newNextObj = nextObjBuilder(mcastIp, assignedVlan,
+                                    ImmutableSet.of(port), nextObj.id()).removeFromExisting();
+        srManager.flowObjectiveService.next(deviceId, newNextObj);
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        return existingPorts.isEmpty();
+    }
+
+    /**
+     * Removes entire group on given device.
+     *
+     * @param deviceId device ID
+     * @param mcastIp multicast group to be removed
+     * @param assignedVlan assigned VLAN ID
+     */
+    private void removeGroupFromDevice(DeviceId deviceId, IpAddress mcastIp,
+            VlanId assignedVlan) {
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, deviceId);
+        // This device is not serving this multicast group
+        if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+            log.warn("{} is not serving {}. Abort.", deviceId, mcastIp);
+            return;
+        }
+        NextObjective nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+        // NOTE: Rely on GroupStore garbage collection rather than explicitly
+        //       remove L3MG since there might be other flows/groups refer to
+        //       the same L2IG
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully remove {} on {}, vlan {}",
+                        mcastIp, deviceId, assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to remove {} on {}, vlan {}: {}",
+                                mcastIp, deviceId, assignedVlan, error));
+        ForwardingObjective fwdObj = fwdObjBuilder(mcastIp, assignedVlan, nextObj.id()).remove(context);
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+        mcastNextObjStore.remove(mcastStoreKey);
+        mcastRoleStore.remove(mcastStoreKey);
+    }
+
+    private void installPath(IpAddress mcastIp, ConnectPoint source, Path mcastPath) {
+        // Get Links
+        List<Link> links = mcastPath.links();
+        // For each link, modify the next on the source device adding the src port
+        // and a new filter objective on the destination port
+        links.forEach(link -> {
+            addPortToDevice(link.src().deviceId(), link.src().port(), mcastIp,
+                            assignedVlan(link.src().deviceId().equals(source.deviceId()) ? source : null));
+            addFilterToDevice(link.dst().deviceId(), link.dst().port(), assignedVlan(null),
+                              mcastIp);
+        });
+        // Setup new transit mcast role
+        mcastRoleStore.put(new McastStoreKey(mcastIp, links.get(0).dst().deviceId()),
+                           McastRole.TRANSIT);
+    }
+
+    /**
+     * Creates a next objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param outPorts set of output port numbers
+     * @return next objective builder
+     */
+    private NextObjective.Builder nextObjBuilder(IpAddress mcastIp,
+            VlanId assignedVlan, Set<PortNumber> outPorts, Integer nextId) {
+        // If nextId is null allocate a new one
+        if (nextId == null) {
+            nextId = srManager.flowObjectiveService.allocateNextId();
+        }
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder()
+                        .matchVlanId(assignedVlan)
+                        .matchIPDst(mcastIp.toIpPrefix())
+                        .build();
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(srManager.appId)
+                .withMeta(metadata);
+
+        outPorts.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            if (egressVlan().equals(VlanId.NONE)) {
+                tBuilder.popVlan();
+            }
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+
+        return nextObjBuilder;
+    }
+
+    /**
+     * Creates a forwarding objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param nextId next ID of the L3 multicast group
+     * @return forwarding objective builder
+     */
+    private ForwardingObjective.Builder fwdObjBuilder(IpAddress mcastIp,
+            VlanId assignedVlan, int nextId) {
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        IpPrefix mcastPrefix = mcastIp.toIpPrefix();
+
+        if (mcastIp.isIp4()) {
+            sbuilder.matchEthType(Ethernet.TYPE_IPV4);
+            sbuilder.matchIPDst(mcastPrefix);
+        } else {
+            sbuilder.matchEthType(Ethernet.TYPE_IPV6);
+            sbuilder.matchIPv6Dst(mcastPrefix);
+        }
+
+
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+        metabuilder.matchVlanId(assignedVlan);
+
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective.builder();
+        fwdBuilder.withSelector(sbuilder.build())
+                .withMeta(metabuilder.build())
+                .nextStep(nextId)
+                .withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .fromApp(srManager.appId)
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        return fwdBuilder;
+    }
+
+    /**
+     * Creates a filtering objective builder for multicast.
+     *
+     * @param deviceId Device ID
+     * @param ingressPort ingress port of the multicast stream
+     * @param assignedVlan assigned VLAN ID
+     * @return filtering objective builder
+     */
+    private FilteringObjective.Builder filterObjBuilder(DeviceId deviceId, PortNumber ingressPort,
+            VlanId assignedVlan, IpAddress mcastIp) {
+        FilteringObjective.Builder filtBuilder = DefaultFilteringObjective.builder();
+
+        if (mcastIp.isIp4()) {
+            filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+            .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV4_MULTICAST,
+                    MacAddress.IPV4_MULTICAST_MASK))
+            .addCondition(Criteria.matchVlanId(egressVlan()))
+            .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        } else {
+            filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+            .addCondition(Criteria.matchEthDstMasked(MacAddress.IPV6_MULTICAST,
+                     MacAddress.IPV6_MULTICAST_MASK))
+            .addCondition(Criteria.matchVlanId(egressVlan()))
+            .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        }
+        TrafficTreatment tt = DefaultTrafficTreatment.builder()
+                .pushVlan().setVlanId(assignedVlan).build();
+        filtBuilder.withMeta(tt);
+
+        return filtBuilder.permit().fromApp(srManager.appId);
+    }
+
+    /**
+     * Gets output ports information from treatments.
+     *
+     * @param treatments collection of traffic treatments
+     * @return set of output port numbers
+     */
+    private Set<PortNumber> getPorts(Collection<TrafficTreatment> treatments) {
+        ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+        treatments.forEach(treatment -> {
+            treatment.allInstructions().stream()
+                    .filter(instr -> instr instanceof OutputInstruction)
+                    .forEach(instr -> {
+                        builder.add(((OutputInstruction) instr).port());
+                    });
+        });
+        return builder.build();
+    }
+
+    /**
+     * Gets a path from src to dst.
+     * If a path was allocated before, returns the allocated path.
+     * Otherwise, randomly pick one from available paths.
+     *
+     * @param src source device ID
+     * @param dst destination device ID
+     * @param mcastIp multicast group
+     * @return an optional path from src to dst
+     */
+    private Optional<Path> getPath(DeviceId src, DeviceId dst, IpAddress mcastIp) {
+        List<Path> allPaths = Lists.newArrayList(
+                topologyService.getPaths(topologyService.currentTopology(), src, dst));
+        log.debug("{} path(s) found from {} to {}", allPaths.size(), src, dst);
+        if (allPaths.isEmpty()) {
+            return Optional.empty();
+        }
+
+        // Create a map index of suitablity-to-list of paths. For example
+        // a path in the list associated to the index 1 shares only the
+        // first hop and it is less suitable of a path belonging to the index
+        // 2 that shares leaf-spine.
+        Map<Integer, List<Path>> eligiblePaths = Maps.newHashMap();
+        // Some init steps
+        int nhop;
+        McastStoreKey mcastStoreKey;
+        Link hop;
+        PortNumber srcPort;
+        Set<PortNumber> existingPorts;
+        NextObjective nextObj;
+        // Iterate over paths looking for eligible paths
+        for (Path path : allPaths) {
+            // Unlikely, it will happen...
+            if (!src.equals(path.links().get(0).src().deviceId())) {
+                continue;
+            }
+            nhop = 0;
+            // Iterate over the links
+            while (nhop < path.links().size()) {
+                // Get the link and verify if a next related
+                // to the src device exist in the store
+                hop = path.links().get(nhop);
+                mcastStoreKey = new McastStoreKey(mcastIp, hop.src().deviceId());
+                // It does not exist in the store, exit
+                if (!mcastNextObjStore.containsKey(mcastStoreKey)) {
+                    break;
+                }
+                // Get the output ports on the next
+                nextObj = mcastNextObjStore.get(mcastStoreKey).value();
+                existingPorts = getPorts(nextObj.next());
+                // And the src port on the link
+                srcPort = hop.src().port();
+                // the src port is not used as output, exit
+                if (!existingPorts.contains(srcPort)) {
+                    break;
+                }
+                nhop++;
+            }
+            // n_hop defines the index
+            if (nhop > 0) {
+                eligiblePaths.compute(nhop, (index, paths) -> {
+                    paths = paths == null ? Lists.newArrayList() : paths;
+                    paths.add(path);
+                    return paths;
+                });
+            }
+        }
+
+        // No suitable paths
+        if (eligiblePaths.isEmpty()) {
+            log.debug("No eligiblePath(s) found from {} to {}", src, dst);
+            // Otherwise, randomly pick a path
+            Collections.shuffle(allPaths);
+            return allPaths.stream().findFirst();
+        }
+
+        // Let's take the best ones
+        Integer bestIndex = eligiblePaths.keySet()
+                .stream()
+                .sorted(Comparator.reverseOrder())
+                .findFirst().orElse(null);
+        List<Path> bestPaths = eligiblePaths.get(bestIndex);
+        log.debug("{} eligiblePath(s) found from {} to {}",
+                  bestPaths.size(), src, dst);
+        // randomly pick a path on the highest index
+        Collections.shuffle(bestPaths);
+        return bestPaths.stream().findFirst();
+    }
+
+    /**
+     * Gets device(s) of given role in given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @param role multicast role
+     * @return set of device ID or empty set if not found
+     */
+    private Set<DeviceId> getDevice(IpAddress mcastIp, McastRole role) {
+        return mcastRoleStore.entrySet().stream()
+                .filter(entry -> entry.getKey().mcastIp().equals(mcastIp) &&
+                        entry.getValue().value() == role)
+                .map(Map.Entry::getKey).map(McastStoreKey::deviceId)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Gets source connect point of given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @return source connect point or null if not found
+     */
+    private ConnectPoint getSource(IpAddress mcastIp) {
+        return srManager.multicastRouteService.getRoutes().stream()
+                .filter(mcastRoute -> mcastRoute.group().equals(mcastIp))
+                .map(mcastRoute -> srManager.multicastRouteService.fetchSource(mcastRoute))
+                .findAny().orElse(null);
+    }
+
+    /**
+     * Gets groups which is affected by the link down event.
+     *
+     * @param link link going down
+     * @return a set of multicast IpAddress
+     */
+    private Set<IpAddress> getAffectedGroups(Link link) {
+        DeviceId deviceId = link.src().deviceId();
+        PortNumber port = link.src().port();
+        return mcastNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId) &&
+                        getPorts(entry.getValue().value().next()).contains(port))
+                .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Gets groups which are affected by the device down event.
+     *
+     * @param deviceId device going down
+     * @return a set of multicast IpAddress
+     */
+    private Set<IpAddress> getAffectedGroups(DeviceId deviceId) {
+        return mcastNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                .map(Map.Entry::getKey).map(McastStoreKey::mcastIp)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Gets egress VLAN from McastConfig.
+     *
+     * @return egress VLAN or VlanId.NONE if not configured
+     */
+    private VlanId egressVlan() {
+        McastConfig mcastConfig =
+                srManager.cfgService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.egressVlan() : VlanId.NONE;
+    }
+
+    /**
+     * Gets assigned VLAN according to the value of egress VLAN.
+     * If connect point is specified, try to reuse the assigned VLAN on the connect point.
+     *
+     * @param cp connect point; Can be null if not specified
+     * @return assigned VLAN ID
+     */
+    private VlanId assignedVlan(ConnectPoint cp) {
+        // Use the egressVlan if it is tagged
+        if (!egressVlan().equals(VlanId.NONE)) {
+            return egressVlan();
+        }
+        // Reuse unicast VLAN if the port has subnet configured
+        if (cp != null) {
+            VlanId untaggedVlan = srManager.getInternalVlanId(cp);
+            return (untaggedVlan != null) ? untaggedVlan : INTERNAL_VLAN;
+        }
+        // Use DEFAULT_VLAN if none of the above matches
+        return SegmentRoutingManager.INTERNAL_VLAN;
+    }
+
+    /**
+     * Gets the spine-facing port on ingress device of given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @return spine-facing port on ingress device
+     */
+    private PortNumber ingressTransitPort(IpAddress mcastIp) {
+        DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                .stream().findAny().orElse(null);
+        if (ingressDevice != null) {
+            NextObjective nextObj = mcastNextObjStore
+                    .get(new McastStoreKey(mcastIp, ingressDevice)).value();
+            Set<PortNumber> ports = getPorts(nextObj.next());
+
+            for (PortNumber port : ports) {
+                // Spine-facing port should have no subnet and no xconnect
+                if (srManager.deviceConfiguration != null &&
+                        srManager.deviceConfiguration.getPortSubnets(ingressDevice, port).isEmpty() &&
+                        !srManager.xConnectHandler.hasXConnect(new ConnectPoint(ingressDevice, port))) {
+                    return port;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Verify if the given device has sinks
+     * for the multicast group.
+     *
+     * @param deviceId device Id
+     * @param mcastIp multicast IP
+     * @return true if the device has sink for the group.
+     * False otherwise.
+     */
+    private boolean hasSinks(DeviceId deviceId, IpAddress mcastIp) {
+        if (deviceId != null) {
+            // Get the nextobjective
+            Versioned<NextObjective> versionedNextObj = mcastNextObjStore.get(
+                    new McastStoreKey(mcastIp, deviceId)
+            );
+            // If it exists
+            if (versionedNextObj != null) {
+                NextObjective nextObj = versionedNextObj.value();
+                // Retrieves all the output ports
+                Set<PortNumber> ports = getPorts(nextObj.next());
+                // Tries to find at least one port that is not spine-facing
+                for (PortNumber port : ports) {
+                    // Spine-facing port should have no subnet and no xconnect
+                    if (srManager.deviceConfiguration != null &&
+                            (!srManager.deviceConfiguration.getPortSubnets(deviceId, port).isEmpty() ||
+                            srManager.xConnectHandler.hasXConnect(new ConnectPoint(deviceId, port)))) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Removes filtering objective for given device and port.
+     *
+     * @param deviceId device ID
+     * @param port ingress port number
+     * @param assignedVlan assigned VLAN ID
+     * @param mcastIp multicast IP address
+     */
+    private void removeFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan, IpAddress mcastIp) {
+        // Do nothing if the port is configured as suppressed
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+            log.info("Ignore suppressed port {}", connectPoint);
+            return;
+        }
+
+        FilteringObjective.Builder filtObjBuilder =
+                filterObjBuilder(deviceId, port, assignedVlan, mcastIp);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully removed filter on {}/{}, vlan {}",
+                                         deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to remove filter on {}/{}, vlan {}: {}",
+                                 deviceId, port.toLong(), assignedVlan, error));
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.remove(context));
+    }
+
+    /**
+     * Updates filtering objective for given device and port.
+     * It is called in general when the mcast config has been
+     * changed.
+     *
+     * @param deviceId device ID
+     * @param portNum ingress port number
+     * @param vlanId assigned VLAN ID
+     * @param install true to add, false to remove
+     */
+    protected void updateFilterToDevice(DeviceId deviceId, PortNumber portNum,
+                                        VlanId vlanId, boolean install) {
+        lastMcastChange = Instant.now();
+        mcastLock();
+        try {
+            // Iterates over the route and updates properly the filtering objective
+            // on the source device.
+            srManager.multicastRouteService.getRoutes().forEach(mcastRoute -> {
+                ConnectPoint source = srManager.multicastRouteService.fetchSource(mcastRoute);
+                if (source.deviceId().equals(deviceId) && source.port().equals(portNum)) {
+                    if (install) {
+                        addFilterToDevice(deviceId, portNum, vlanId, mcastRoute.group());
+                    } else {
+                        removeFilterToDevice(deviceId, portNum, vlanId, mcastRoute.group());
+                    }
+                }
+            });
+        } finally {
+            mcastUnlock();
+        }
+    }
+
+    private boolean isLeader(ConnectPoint source) {
+        // Continue only when we have the mastership on the operation
+        if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+            // When the source is available we just check the mastership
+            if (srManager.deviceService.isAvailable(source.deviceId())) {
+                return false;
+            }
+            // Fallback with Leadership service
+            // source id is used a topic
+            NodeId leader = srManager.leadershipService.runForLeadership(
+                    source.deviceId().toString()).leaderNodeId();
+            // Verify if this node is the leader
+            if (!srManager.clusterService.getLocalNode().id().equals(leader)) {
+                return false;
+            }
+        }
+        // Done
+        return true;
+    }
+
+    /**
+     * Performs bucket verification operation for all mcast groups in the devices.
+     * Firstly, it verifies that mcast is stable before trying verification operation.
+     * Verification consists in creating new nexts with VERIFY operation. Actually,
+     * the operation is totally delegated to the driver.
+     */
+     private final class McastBucketCorrector implements Runnable {
+
+        @Override
+        public void run() {
+            // Verify if the Mcast has been stable for MCAST_STABLITY_THRESHOLD
+            if (!isMcastStable()) {
+                return;
+            }
+            // Acquires lock
+            mcastLock();
+            try {
+                // Iterates over the routes and verify the related next objectives
+                srManager.multicastRouteService.getRoutes()
+                    .stream()
+                    .map(McastRoute::group)
+                    .forEach(mcastIp -> {
+                        log.trace("Running mcast buckets corrector for mcast group: {}",
+                                  mcastIp);
+
+                        // For each group we get current information in the store
+                        // and issue a check of the next objectives in place
+                        DeviceId ingressDevice = getDevice(mcastIp, McastRole.INGRESS)
+                                .stream().findAny().orElse(null);
+                        DeviceId transitDevice = getDevice(mcastIp, McastRole.TRANSIT)
+                                .stream().findAny().orElse(null);
+                        Set<DeviceId> egressDevices = getDevice(mcastIp, McastRole.EGRESS);
+                        ConnectPoint source = getSource(mcastIp);
+
+                        // Do not proceed if ingress device or source of this group are missing
+                        if (ingressDevice == null || source == null) {
+                            log.warn("Unable to run buckets corrector. " +
+                                             "Missing ingress {} or source {} for group {}",
+                                     ingressDevice, source, mcastIp);
+                            return;
+                        }
+
+                        // Continue only when this instance is the master of source device
+                        if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+                            log.trace("Unable to run buckets corrector. " +
+                                             "Skip {} due to lack of mastership " +
+                                             "of the source device {}",
+                                     mcastIp, source.deviceId());
+                            return;
+                        }
+
+                        // Create the set of the devices to be processed
+                        ImmutableSet.Builder<DeviceId> devicesBuilder = ImmutableSet.builder();
+                        devicesBuilder.add(ingressDevice);
+                        if (transitDevice != null) {
+                            devicesBuilder.add(transitDevice);
+                        }
+                        if (!egressDevices.isEmpty()) {
+                            devicesBuilder.addAll(egressDevices);
+                        }
+                        Set<DeviceId> devicesToProcess = devicesBuilder.build();
+
+                        // Iterate over the devices
+                        devicesToProcess.forEach(deviceId -> {
+                            McastStoreKey currentKey = new McastStoreKey(mcastIp, deviceId);
+                            // If next exists in our store verify related next objective
+                            if (mcastNextObjStore.containsKey(currentKey)) {
+                                NextObjective currentNext = mcastNextObjStore.get(currentKey).value();
+                                // Get current ports
+                                Set<PortNumber> currentPorts = getPorts(currentNext.next());
+                                // Rebuild the next objective
+                                currentNext = nextObjBuilder(
+                                        mcastIp,
+                                        assignedVlan(deviceId.equals(source.deviceId()) ? source : null),
+                                        currentPorts,
+                                        currentNext.id()
+                                ).verify();
+                                // Send to the flowobjective service
+                                srManager.flowObjectiveService.next(deviceId, currentNext);
+                            } else {
+                                log.warn("Unable to run buckets corrector." +
+                                                 "Missing next for {} and group {}",
+                                         deviceId, mcastIp);
+                            }
+                        });
+
+                    });
+            } finally {
+                // Finally, it releases the lock
+                mcastUnlock();
+            }
+
+        }
+    }
+
+    public Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp) {
+        // If mcast ip is present
+        if (mcastIp != null) {
+            return mcastNextObjStore.entrySet().stream()
+                    .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
+                    .collect(Collectors.toMap(Map.Entry::getKey,
+                                              entry -> entry.getValue().value().id()));
+        }
+        // Otherwise take all the groups
+        return mcastNextObjStore.entrySet().stream()
+                .collect(Collectors.toMap(Map.Entry::getKey,
+                                          entry -> entry.getValue().value().id()));
+    }
+
+    public Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp) {
+        // If mcast ip is present
+        if (mcastIp != null) {
+            return mcastRoleStore.entrySet().stream()
+                    .filter(mcastEntry -> mcastIp.equals(mcastEntry.getKey().mcastIp()))
+                    .collect(Collectors.toMap(Map.Entry::getKey,
+                                              entry -> entry.getValue().value()));
+        }
+        // Otherwise take all the groups
+        return mcastRoleStore.entrySet().stream()
+                .collect(Collectors.toMap(Map.Entry::getKey,
+                                          entry -> entry.getValue().value()));
+    }
+
+    public Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp) {
+        Map<ConnectPoint, List<ConnectPoint>> mcastPaths = Maps.newHashMap();
+        // Get the source
+        ConnectPoint source = getSource(mcastIp);
+        // Source cannot be null, we don't know the starting point
+        if (source != null) {
+            // Init steps
+            Set<DeviceId> visited = Sets.newHashSet();
+            List<ConnectPoint> currentPath = Lists.newArrayList(
+                    source
+            );
+            // Build recursively the mcast paths
+            buildMcastPaths(source.deviceId(), visited, mcastPaths, currentPath, mcastIp);
+        }
+        return mcastPaths;
+    }
+
+    private void buildMcastPaths(DeviceId toVisit, Set<DeviceId> visited,
+                                 Map<ConnectPoint, List<ConnectPoint>> mcastPaths,
+                                 List<ConnectPoint> currentPath, IpAddress mcastIp) {
+        // If we have visited the node to visit
+        // there is a loop
+        if (visited.contains(toVisit)) {
+            return;
+        }
+        // Visit next-hop
+        visited.add(toVisit);
+        McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, toVisit);
+        // Looking for next-hops
+        if (mcastNextObjStore.containsKey(mcastStoreKey)) {
+            // Build egress connectpoints
+            NextObjective nextObjective = mcastNextObjStore.get(mcastStoreKey).value();
+            // Get Ports
+            Set<PortNumber> outputPorts = getPorts(nextObjective.next());
+            // Build relative cps
+            ImmutableSet.Builder<ConnectPoint> cpBuilder = ImmutableSet.builder();
+            outputPorts.forEach(portNumber -> cpBuilder.add(new ConnectPoint(toVisit, portNumber)));
+            Set<ConnectPoint> egressPoints = cpBuilder.build();
+            // Define other variables for the next steps
+            Set<Link> egressLinks;
+            List<ConnectPoint> newCurrentPath;
+            Set<DeviceId> newVisited;
+            DeviceId newToVisit;
+            for (ConnectPoint egressPoint : egressPoints) {
+                egressLinks = srManager.linkService.getEgressLinks(egressPoint);
+                // If it does not have egress links, stop
+                if (egressLinks.isEmpty()) {
+                    // Add the connect points to the path
+                    newCurrentPath = Lists.newArrayList(currentPath);
+                    newCurrentPath.add(0, egressPoint);
+                    // Save in the map
+                    mcastPaths.put(egressPoint, newCurrentPath);
+                } else {
+                    newVisited = Sets.newHashSet(visited);
+                    // Iterate over the egress links for the next hops
+                    for (Link egressLink : egressLinks) {
+                        // Update to visit
+                        newToVisit = egressLink.dst().deviceId();
+                        // Add the connect points to the path
+                        newCurrentPath = Lists.newArrayList(currentPath);
+                        newCurrentPath.add(0, egressPoint);
+                        newCurrentPath.add(0, egressLink.dst());
+                        // Go to the next hop
+                        buildMcastPaths(newToVisit, newVisited, mcastPaths, newCurrentPath, mcastIp);
+                    }
+                }
+            }
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java
new file mode 100644
index 0000000..c9baf93
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Policy.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+/**
+ * Interface for Segment Routing Policy.
+ */
+public interface Policy {
+    /**
+     * Enums for policy type.
+     */
+    enum Type {
+        /**
+         * Tunnel flow policy type.
+         */
+        TUNNEL_FLOW,
+
+        /**
+         * Load balancing policy type.
+         */
+        LOADBALANCE,
+
+        /**
+         * policy to avoid specific routers or links.
+         */
+        AVOID,
+
+        /**
+         * Access Control policy type.
+         */
+        DENY
+    }
+
+    /**
+     * Returns the policy ID.
+     *
+     * @return policy ID
+     */
+    String id();
+
+    /**
+     * Returns the priority of the policy.
+     *
+     * @return priority
+     */
+    int priority();
+
+    /**
+     * Returns the policy type.
+     *
+     * @return policy type
+     */
+    Type type();
+
+    /**
+     * Returns the source IP address of the policy.
+     *
+     * @return source IP address
+     */
+    String srcIp();
+
+    /**
+     * Returns the destination IP address of the policy.
+     *
+     * @return destination IP address
+     */
+    String dstIp();
+
+    /**
+     * Returns the IP protocol of the policy.
+     *
+     * @return IP protocol
+     */
+    String ipProto();
+
+    /**
+     * Returns the source port of the policy.
+     *
+     * @return source port
+     */
+    short srcPort();
+
+    /**
+     * Returns the destination of the policy.
+     *
+     * @return destination port
+     */
+    short dstPort();
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java
new file mode 100644
index 0000000..a341bb1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PolicyHandler.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.TpPort;
+import org.onosproject.cli.net.IpProtocol;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Segment Routing Policy Handler.
+ */
+public class PolicyHandler {
+
+    protected final Logger log = getLogger(getClass());
+
+    private ApplicationId appId;
+    private DeviceConfiguration deviceConfiguration;
+    private FlowObjectiveService flowObjectiveService;
+    private TunnelHandler tunnelHandler;
+    private final EventuallyConsistentMap<String, Policy> policyStore;
+    /**
+     * Result of policy creation.
+     */
+    public enum Result {
+        /**
+         * Success.
+         */
+        SUCCESS,
+
+        /**
+         * The same policy exists already.
+         */
+        POLICY_EXISTS,
+
+        /**
+         * The policy ID exists already.
+         */
+        ID_EXISTS,
+
+        /**
+         * Cannot find associated tunnel.
+         */
+        TUNNEL_NOT_FOUND,
+
+        /**
+         * Policy was not found.
+         */
+        POLICY_NOT_FOUND,
+
+        /**
+         * Policy type {} is not supported yet.
+         */
+        UNSUPPORTED_TYPE
+    }
+
+    /**
+     * Constructs policy handler.
+     *
+     * @param appId                segment routing application ID
+     * @param deviceConfiguration  DeviceConfiguration reference
+     * @param flowObjectiveService FlowObjectiveService reference
+     * @param tunnelHandler        tunnel handler reference
+     * @param policyStore          policy store
+     */
+    public PolicyHandler(ApplicationId appId,
+                         DeviceConfiguration deviceConfiguration,
+                         FlowObjectiveService flowObjectiveService,
+                         TunnelHandler tunnelHandler,
+                         EventuallyConsistentMap<String, Policy> policyStore) {
+        this.appId = appId;
+        this.deviceConfiguration = deviceConfiguration;
+        this.flowObjectiveService = flowObjectiveService;
+        this.tunnelHandler = tunnelHandler;
+        this.policyStore = policyStore;
+    }
+
+    /**
+     * Returns the policies.
+     *
+     * @return policy list
+     */
+    public List<Policy> getPolicies() {
+        return policyStore.values()
+                .stream()
+                .filter(policy -> policy instanceof TunnelPolicy)
+                .map(policy -> new TunnelPolicy((TunnelPolicy) policy))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Creates a policy using the policy information given.
+     *  @param policy policy reference to create
+     *  @return ID_EXISTS if the same policy ID exists,
+     *  POLICY_EXISTS if the same policy exists, TUNNEL_NOT_FOUND if the tunnel
+     *  does not exists, UNSUPPORTED_TYPE if the policy type is not supported,
+     *  SUCCESS if the policy is created successfully
+     */
+    public Result createPolicy(Policy policy) {
+
+        if (policyStore.containsKey(policy.id())) {
+            log.warn("The policy id {} exists already", policy.id());
+            return Result.ID_EXISTS;
+        }
+
+        if (policyStore.containsValue(policy)) {
+            log.warn("The same policy exists already");
+            return Result.POLICY_EXISTS;
+        }
+
+        if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+
+            TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+            Tunnel tunnel = tunnelHandler.getTunnel(tunnelPolicy.tunnelId());
+            if (tunnel == null) {
+                return Result.TUNNEL_NOT_FOUND;
+            }
+
+            ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+                    .builder()
+                    .fromApp(appId)
+                    .makePermanent()
+                    .nextStep(tunnel.groupId())
+                    .withPriority(tunnelPolicy.priority())
+                    .withSelector(buildSelector(policy))
+                    .withFlag(ForwardingObjective.Flag.VERSATILE);
+
+            DeviceId source = deviceConfiguration.getDeviceId(tunnel.labelIds().get(0));
+            flowObjectiveService.forward(source, fwdBuilder.add());
+
+        } else {
+            log.warn("Policy type {} is not supported yet.", policy.type());
+            return Result.UNSUPPORTED_TYPE;
+        }
+
+        policyStore.put(policy.id(), policy);
+
+        return Result.SUCCESS;
+    }
+
+    /**
+     * Removes the policy given.
+     *
+     * @param policyInfo policy information to remove
+     * @return POLICY_NOT_FOUND if the policy to remove does not exists,
+     * SUCCESS if it is removed successfully
+     */
+    public Result removePolicy(Policy policyInfo) {
+
+        if (policyStore.get(policyInfo.id()) != null) {
+            Policy policy = policyStore.get(policyInfo.id());
+            if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+                TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+                Tunnel tunnel = tunnelHandler.getTunnel(tunnelPolicy.tunnelId());
+
+                ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+                        .builder()
+                        .fromApp(appId)
+                        .makePermanent()
+                        .withSelector(buildSelector(policy))
+                        .withPriority(tunnelPolicy.priority())
+                        .nextStep(tunnel.groupId())
+                        .withFlag(ForwardingObjective.Flag.VERSATILE);
+
+                DeviceId source = deviceConfiguration.getDeviceId(tunnel.labelIds().get(0));
+                flowObjectiveService.forward(source, fwdBuilder.remove());
+
+                policyStore.remove(policyInfo.id());
+            }
+        } else {
+            log.warn("Policy {} was not found", policyInfo.id());
+            return Result.POLICY_NOT_FOUND;
+        }
+
+        return Result.SUCCESS;
+    }
+
+
+    private TrafficSelector buildSelector(Policy policy) {
+
+        TrafficSelector.Builder tsb = DefaultTrafficSelector.builder();
+        tsb.matchEthType(Ethernet.TYPE_IPV4);
+        if (policy.dstIp() != null && !policy.dstIp().isEmpty()) {
+            tsb.matchIPDst(IpPrefix.valueOf(policy.dstIp()));
+        }
+        if (policy.srcIp() != null && !policy.srcIp().isEmpty()) {
+            tsb.matchIPSrc(IpPrefix.valueOf(policy.srcIp()));
+        }
+        if (policy.ipProto() != null && !policy.ipProto().isEmpty()) {
+            Short ipProto = IpProtocol.valueOf(policy.ipProto()).value();
+            tsb.matchIPProtocol(ipProto.byteValue());
+            if (IpProtocol.valueOf(policy.ipProto()).equals(IpProtocol.TCP)) {
+                if (policy.srcPort() != 0) {
+                    tsb.matchTcpSrc(TpPort.tpPort(policy.srcPort()));
+                }
+                if (policy.dstPort() != 0) {
+                    tsb.matchTcpDst(TpPort.tpPort(policy.dstPort()));
+                }
+            } else if (IpProtocol.valueOf(policy.ipProto()).equals(IpProtocol.UDP)) {
+                if (policy.srcPort() != 0) {
+                    tsb.matchUdpSrc(TpPort.tpPort(policy.srcPort()));
+                }
+                if (policy.dstPort() != 0) {
+                    tsb.matchUdpDst(TpPort.tpPort(policy.dstPort()));
+                }
+            }
+        }
+
+        return tsb.build();
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java
new file mode 100644
index 0000000..2127019
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/PortAuthTracker.java
@@ -0,0 +1,357 @@
+/*
+ * 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.segmentrouting;
+
+
+import com.google.common.base.Objects;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.config.BlockedPortsConfig;
+import org.onosproject.utils.Comparators;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Keeps track of ports that have been configured for blocking,
+ * and their current authentication state.
+ */
+public class PortAuthTracker {
+
+    private static final Logger log = getLogger(PortAuthTracker.class);
+
+    private Map<DeviceId, Map<PortNumber, BlockState>> blockedPorts = new HashMap<>();
+    private Map<DeviceId, Map<PortNumber, BlockState>> oldMap;
+
+    @Override
+    public String toString() {
+        return "PortAuthTracker{entries = " + blockedPorts.size() + "}";
+    }
+
+    /**
+     * Changes the state of the given device id / port number pair to the
+     * specified state.
+     *
+     * @param d        device identifier
+     * @param p        port number
+     * @param newState the updated state
+     * @return true, if the state changed from what was previously mapped
+     */
+    private boolean changeStateTo(DeviceId d, PortNumber p, BlockState newState) {
+        Map<PortNumber, BlockState> portMap =
+                blockedPorts.computeIfAbsent(d, k -> new HashMap<>());
+        BlockState oldState =
+                portMap.computeIfAbsent(p, k -> BlockState.UNCHECKED);
+        portMap.put(p, newState);
+        return (oldState != newState);
+    }
+
+    /**
+     * Radius has authorized the supplicant at this connect point. If
+     * we are tracking this port, clear the blocking flow and mark the
+     * port as authorized.
+     *
+     * @param connectPoint supplicant connect point
+     */
+    void radiusAuthorize(ConnectPoint connectPoint) {
+        DeviceId d = connectPoint.deviceId();
+        PortNumber p = connectPoint.port();
+        if (configured(d, p)) {
+            clearBlockingFlow(d, p);
+            markAsAuthenticated(d, p);
+        }
+    }
+
+    /**
+     * Supplicant at specified connect point has logged off Radius. If
+     * we are tracking this port, install a blocking flow and mark the
+     * port as blocked.
+     *
+     * @param connectPoint supplicant connect point
+     */
+    void radiusLogoff(ConnectPoint connectPoint) {
+        DeviceId d = connectPoint.deviceId();
+        PortNumber p = connectPoint.port();
+        if (configured(d, p)) {
+            installBlockingFlow(d, p);
+            markAsBlocked(d, p);
+        }
+    }
+
+    /**
+     * Marks the specified device/port as blocked.
+     *
+     * @param d device id
+     * @param p port number
+     * @return true if the state changed (was not already blocked)
+     */
+    private boolean markAsBlocked(DeviceId d, PortNumber p) {
+        return changeStateTo(d, p, BlockState.BLOCKED);
+    }
+
+    /**
+     * Marks the specified device/port as authenticated.
+     *
+     * @param d device id
+     * @param p port number
+     * @return true if the state changed (was not already authenticated)
+     */
+    private boolean markAsAuthenticated(DeviceId d, PortNumber p) {
+        return changeStateTo(d, p, BlockState.AUTHENTICATED);
+    }
+
+    /**
+     * Returns true if the given device/port are configured for blocking.
+     *
+     * @param d device id
+     * @param p port number
+     * @return true if this device/port configured for blocking
+     */
+    private boolean configured(DeviceId d, PortNumber p) {
+        Map<PortNumber, BlockState> portMap = blockedPorts.get(d);
+        return portMap != null && portMap.get(p) != null;
+    }
+
+    private BlockState whatState(DeviceId d, PortNumber p,
+                                 Map<DeviceId, Map<PortNumber, BlockState>> m) {
+        Map<PortNumber, BlockState> portMap = m.get(d);
+        if (portMap == null) {
+            return BlockState.UNCHECKED;
+        }
+        BlockState state = portMap.get(p);
+        if (state == null) {
+            return BlockState.UNCHECKED;
+        }
+        return state;
+    }
+
+    /**
+     * Returns the current state of the given device/port.
+     *
+     * @param d device id
+     * @param p port number
+     * @return current block-state
+     */
+    BlockState currentState(DeviceId d, PortNumber p) {
+        return whatState(d, p, blockedPorts);
+    }
+
+    /**
+     * Returns the current state of the given connect point.
+     *
+     * @param cp connect point
+     * @return current block-state
+     */
+
+    BlockState currentState(ConnectPoint cp) {
+        return whatState(cp.deviceId(), cp.port(), blockedPorts);
+    }
+
+    /**
+     * Returns the number of entries being tracked.
+     *
+     * @return the number of tracked entries
+     */
+    int entryCount() {
+        int count = 0;
+        for (Map<PortNumber, BlockState> m : blockedPorts.values()) {
+            count += m.size();
+        }
+        return count;
+    }
+
+    /**
+     * Returns the previously recorded state of the given device/port.
+     *
+     * @param d device id
+     * @param p port number
+     * @return previous block-state
+     */
+    private BlockState oldState(DeviceId d, PortNumber p) {
+        return whatState(d, p, oldMap);
+    }
+
+    private void configurePort(DeviceId d, PortNumber p) {
+        boolean alreadyAuthenticated =
+                oldState(d, p) == BlockState.AUTHENTICATED;
+
+        if (alreadyAuthenticated) {
+            clearBlockingFlow(d, p);
+            markAsAuthenticated(d, p);
+        } else {
+            installBlockingFlow(d, p);
+            markAsBlocked(d, p);
+        }
+        log.info("Configuring port {}/{} as {}", d, p,
+                 alreadyAuthenticated ? "AUTHENTICATED" : "BLOCKED");
+    }
+
+    private boolean notInMap(DeviceId deviceId, PortNumber portNumber) {
+        Map<PortNumber, BlockState> m = blockedPorts.get(deviceId);
+        return m == null || m.get(portNumber) == null;
+    }
+
+    private void logPortsNoLongerBlocked() {
+        for (Map.Entry<DeviceId, Map<PortNumber, BlockState>> entry :
+                oldMap.entrySet()) {
+            DeviceId d = entry.getKey();
+            Map<PortNumber, BlockState> portMap = entry.getValue();
+
+            for (PortNumber p : portMap.keySet()) {
+                if (notInMap(d, p)) {
+                    clearBlockingFlow(d, p);
+                    log.info("De-configuring port {}/{} (UNCHECKED)", d, p);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Reconfigures the port tracker using the supplied configuration.
+     *
+     * @param cfg the new configuration
+     */
+    void configurePortBlocking(BlockedPortsConfig cfg) {
+        // remember the old map; prepare a new map
+        oldMap = blockedPorts;
+        blockedPorts = new HashMap<>();
+
+        // for each configured device, add configured ports to map
+        for (String devId : cfg.deviceIds()) {
+            cfg.portIterator(devId)
+                    .forEachRemaining(p -> configurePort(deviceId(devId),
+                                                         portNumber(p)));
+        }
+
+        // have we de-configured any ports?
+        logPortsNoLongerBlocked();
+
+        // allow old map to be garbage collected
+        oldMap = null;
+    }
+
+    private List<PortAuthState> reportPortsAuthState() {
+        List<PortAuthState> result = new ArrayList<>();
+
+        for (Map.Entry<DeviceId, Map<PortNumber, BlockState>> entry :
+                blockedPorts.entrySet()) {
+            DeviceId d = entry.getKey();
+            Map<PortNumber, BlockState> portMap = entry.getValue();
+
+            for (PortNumber p : portMap.keySet()) {
+                result.add(new PortAuthState(d, p, portMap.get(p)));
+            }
+        }
+        Collections.sort(result);
+        return result;
+    }
+
+    /**
+     * Installs a "blocking" flow for device/port specified.
+     *
+     * @param d device id
+     * @param p port number
+     */
+    void installBlockingFlow(DeviceId d, PortNumber p) {
+        log.debug("Installing Blocking Flow at {}/{}", d, p);
+        // TODO: invoke SegmentRoutingService.block(...) appropriately
+        log.info("TODO >> Installing Blocking Flow at {}/{}", d, p);
+    }
+
+    /**
+     * Removes the "blocking" flow from device/port specified.
+     *
+     * @param d device id
+     * @param p port number
+     */
+    void clearBlockingFlow(DeviceId d, PortNumber p) {
+        log.debug("Clearing Blocking Flow from {}/{}", d, p);
+        // TODO: invoke SegmentRoutingService.block(...) appropriately
+        log.info("TODO >> Clearing Blocking Flow from {}/{}", d, p);
+    }
+
+
+    /**
+     * Designates the state of a given port. One of:
+     * <ul>
+     * <li> UNCHECKED: not configured for blocking </li>
+     * <li> BLOCKED: configured for blocking, and not yet authenticated </li>
+     * <li> AUTHENTICATED: configured for blocking, but authenticated </li>
+     * </ul>
+     */
+    public enum BlockState {
+        UNCHECKED,
+        BLOCKED,
+        AUTHENTICATED
+    }
+
+    /**
+     * A simple DTO binding of device identifier, port number, and block state.
+     */
+    public static final class PortAuthState implements Comparable<PortAuthState> {
+        private final DeviceId d;
+        private final PortNumber p;
+        private final BlockState s;
+
+        private PortAuthState(DeviceId d, PortNumber p, BlockState s) {
+            this.d = d;
+            this.p = p;
+            this.s = s;
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(d) + "/" + p + " -- " + s;
+        }
+
+        @Override
+        public int compareTo(PortAuthState o) {
+            // NOTE: only compare against "deviceid/port"
+            int result = Comparators.ELEMENT_ID_COMPARATOR.compare(d, o.d);
+            return (result != 0) ? result : Long.signum(p.toLong() - o.p.toLong());
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final PortAuthTracker.PortAuthState that = (PortAuthTracker.PortAuthState) obj;
+
+            return Comparators.ELEMENT_ID_COMPARATOR.compare(this.d, that.d) == 0 &&
+                    p.toLong() == that.p.toLong();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(this.d, this.p);
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
new file mode 100644
index 0000000..21870ee
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RouteHandler.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalCause;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.Sets;
+
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.net.DeviceId;
+import org.onosproject.routeservice.RouteInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Handles RouteEvent and manages routing entries.
+ */
+public class RouteHandler {
+    private static final Logger log = LoggerFactory.getLogger(RouteHandler.class);
+    private final SegmentRoutingManager srManager;
+
+    private static final int WAIT_TIME_MS = 1000;
+    /**
+     * The routeEventCache is implemented to avoid race condition by giving more time to the
+     * underlying flow subsystem to process previous populateSubnet call.
+     */
+    private Cache<IpPrefix, RouteEvent> routeEventCache = CacheBuilder.newBuilder()
+            .expireAfterWrite(WAIT_TIME_MS, TimeUnit.MILLISECONDS)
+            .removalListener((RemovalNotification<IpPrefix, RouteEvent> notification) -> {
+                IpPrefix prefix = notification.getKey();
+                RouteEvent routeEvent = notification.getValue();
+                RemovalCause cause = notification.getCause();
+                log.debug("routeEventCache removal event. prefix={}, routeEvent={}, cause={}",
+                        prefix, routeEvent, cause);
+
+                switch (notification.getCause()) {
+                    case REPLACED:
+                    case EXPIRED:
+                        dequeueRouteEvent(routeEvent);
+                        break;
+                    default:
+                        break;
+                }
+            }).build();
+
+    RouteHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+
+        Executors.newSingleThreadScheduledExecutor()
+                .scheduleAtFixedRate(routeEventCache::cleanUp, 0, WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+    }
+
+    protected void init(DeviceId deviceId) {
+        srManager.routeService.getRouteTables().forEach(routeTableId ->
+            srManager.routeService.getRoutes(routeTableId).forEach(routeInfo ->
+                routeInfo.allRoutes().forEach(resolvedRoute ->
+                    srManager.nextHopLocations(resolvedRoute).stream()
+                            .filter(location -> deviceId.equals(location.deviceId()))
+                            .forEach(location -> processRouteAddedInternal(resolvedRoute)
+                    )
+                )
+            )
+        );
+    }
+
+    void processRouteAdded(RouteEvent event) {
+        enqueueRouteEvent(event);
+    }
+
+    private void processRouteAddedInternal(ResolvedRoute route) {
+        processRouteAddedInternal(Sets.newHashSet(route));
+    }
+
+    private void processRouteAddedInternal(Collection<ResolvedRoute> routes) {
+        if (!isReady()) {
+            log.info("System is not ready. Skip adding route for {}", routes);
+            return;
+        }
+
+        log.info("processRouteAddedInternal. routes={}", routes);
+
+        Set<ConnectPoint> allLocations = Sets.newHashSet();
+        Set<IpPrefix> allPrefixes = Sets.newHashSet();
+        routes.forEach(route -> {
+            allLocations.addAll(srManager.nextHopLocations(route));
+            allPrefixes.add(route.prefix());
+        });
+        log.debug("RouteAdded. populateSubnet {}, {}", allLocations, allPrefixes);
+        srManager.defaultRoutingHandler.populateSubnet(allLocations, allPrefixes);
+
+        routes.forEach(route -> {
+            IpPrefix prefix = route.prefix();
+            MacAddress nextHopMac = route.nextHopMac();
+            VlanId nextHopVlan = route.nextHopVlan();
+            Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+            locations.forEach(location -> {
+                log.debug("RouteAdded. addSubnet {}, {}", location, prefix);
+                srManager.deviceConfiguration.addSubnet(location, prefix);
+                log.debug("RouteAdded populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+                srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
+                        nextHopMac, nextHopVlan, location.port());
+            });
+        });
+    }
+
+    void processRouteUpdated(RouteEvent event) {
+        enqueueRouteEvent(event);
+    }
+
+    void processAlternativeRoutesChanged(RouteEvent event) {
+        enqueueRouteEvent(event);
+    }
+
+    private void processRouteUpdatedInternal(Set<ResolvedRoute> routes, Set<ResolvedRoute> oldRoutes) {
+        if (!isReady()) {
+            log.info("System is not ready. Skip updating route for {} -> {}", oldRoutes, routes);
+            return;
+        }
+
+        log.info("processRouteUpdatedInternal. routes={}, oldRoutes={}", routes, oldRoutes);
+
+        Set<ConnectPoint> allLocations = Sets.newHashSet();
+        Set<IpPrefix> allPrefixes = Sets.newHashSet();
+        routes.forEach(route -> {
+            allLocations.addAll(srManager.nextHopLocations(route));
+            allPrefixes.add(route.prefix());
+        });
+        log.debug("RouteUpdated. populateSubnet {}, {}", allLocations, allPrefixes);
+        srManager.defaultRoutingHandler.populateSubnet(allLocations, allPrefixes);
+
+        Set<ResolvedRoute> toBeRemoved = Sets.difference(oldRoutes, routes).immutableCopy();
+        Set<ResolvedRoute> toBeAdded = Sets.difference(routes, oldRoutes).immutableCopy();
+
+        toBeRemoved.forEach(route -> {
+            srManager.nextHopLocations(route).forEach(oldLocation -> {
+                if (toBeAdded.stream().map(srManager::nextHopLocations)
+                        .flatMap(Set::stream).map(ConnectPoint::deviceId)
+                        .noneMatch(deviceId -> deviceId.equals(oldLocation.deviceId()))) {
+                    IpPrefix prefix = route.prefix();
+                    log.debug("RouteUpdated. removeSubnet {}, {}", oldLocation, prefix);
+                    srManager.deviceConfiguration.removeSubnet(oldLocation, prefix);
+                    // We don't remove the flow on the old location in occasion of two next hops becoming one
+                    // since the populateSubnet will point the old location to the new location via spine.
+                }
+            });
+        });
+
+        toBeAdded.forEach(route -> {
+            IpPrefix prefix = route.prefix();
+            MacAddress nextHopMac = route.nextHopMac();
+            VlanId nextHopVlan = route.nextHopVlan();
+            Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+            locations.forEach(location -> {
+                log.debug("RouteUpdated. addSubnet {}, {}", location, prefix);
+                srManager.deviceConfiguration.addSubnet(location, prefix);
+                log.debug("RouteUpdated. populateRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+                srManager.defaultRoutingHandler.populateRoute(location.deviceId(), prefix,
+                        nextHopMac, nextHopVlan, location.port());
+            });
+        });
+    }
+
+    void processRouteRemoved(RouteEvent event) {
+        enqueueRouteEvent(event);
+    }
+
+    private void processRouteRemovedInternal(Collection<ResolvedRoute> routes) {
+        if (!isReady()) {
+            log.info("System is not ready. Skip removing route for {}", routes);
+            return;
+        }
+
+        log.info("processRouteRemovedInternal. routes={}", routes);
+
+        Set<IpPrefix> allPrefixes = Sets.newHashSet();
+        routes.forEach(route -> {
+            allPrefixes.add(route.prefix());
+        });
+        log.debug("RouteRemoved. revokeSubnet {}", allPrefixes);
+        srManager.defaultRoutingHandler.revokeSubnet(allPrefixes);
+
+        routes.forEach(route -> {
+            IpPrefix prefix = route.prefix();
+            MacAddress nextHopMac = route.nextHopMac();
+            VlanId nextHopVlan = route.nextHopVlan();
+            Set<ConnectPoint> locations = srManager.nextHopLocations(route);
+
+            locations.forEach(location -> {
+                log.debug("RouteRemoved. removeSubnet {}, {}", location, prefix);
+                srManager.deviceConfiguration.removeSubnet(location, prefix);
+                log.debug("RouteRemoved. revokeRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+                srManager.defaultRoutingHandler.revokeRoute(location.deviceId(), prefix,
+                        nextHopMac, nextHopVlan, location.port());
+
+                // Also remove redirection flows on the pair device if exists.
+                Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(location.deviceId());
+                Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(location.deviceId());
+                if (pairDeviceId.isPresent() && pairLocalPort.isPresent()) {
+                    // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                    //       when the host is untagged
+                    VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(location)).orElse(nextHopVlan);
+
+                    log.debug("RouteRemoved. revokeRoute {}, {}, {}, {}", location, prefix, nextHopMac, nextHopVlan);
+                    srManager.defaultRoutingHandler.revokeRoute(pairDeviceId.get(), prefix,
+                            nextHopMac, vlanId, pairLocalPort.get());
+                }
+            });
+        });
+    }
+
+    void processHostMovedEvent(HostEvent event) {
+        log.info("processHostMovedEvent {}", event);
+        MacAddress hostMac = event.subject().mac();
+        VlanId hostVlanId = event.subject().vlan();
+
+        affectedRoutes(hostMac, hostVlanId).forEach(affectedRoute -> {
+            IpPrefix prefix = affectedRoute.prefix();
+            Set<HostLocation> prevLocations = event.prevSubject().locations();
+            Set<HostLocation> newLocations = event.subject().locations();
+
+            // For each old location
+            Sets.difference(prevLocations, newLocations).stream().filter(srManager::isMasterOf)
+                    .forEach(prevLocation -> {
+                // Redirect the flows to pair link if configured
+                // Note: Do not continue removing any rule
+                Optional<DeviceId> pairDeviceId = srManager.getPairDeviceId(prevLocation.deviceId());
+                Optional<PortNumber> pairLocalPort = srManager.getPairLocalPorts(prevLocation.deviceId());
+                if (pairDeviceId.isPresent() && pairLocalPort.isPresent() && newLocations.stream()
+                        .anyMatch(location -> location.deviceId().equals(pairDeviceId.get()))) {
+                    // NOTE: Since the pairLocalPort is trunk port, use assigned vlan of original port
+                    //       when the host is untagged
+                    VlanId vlanId = Optional.ofNullable(srManager.getInternalVlanId(prevLocation)).orElse(hostVlanId);
+                    log.debug("HostMoved. populateRoute {}, {}, {}, {}", prevLocation, prefix, hostMac, vlanId);
+                    srManager.defaultRoutingHandler.populateRoute(prevLocation.deviceId(), prefix,
+                            hostMac, vlanId, pairLocalPort.get());
+                    return;
+                }
+
+                // No pair information supplied. Remove route
+                log.debug("HostMoved. revokeRoute {}, {}, {}, {}", prevLocation, prefix, hostMac, hostVlanId);
+                srManager.defaultRoutingHandler.revokeRoute(prevLocation.deviceId(), prefix,
+                        hostMac, hostVlanId, prevLocation.port());
+            });
+
+            // For each new location, add all new IPs.
+            Sets.difference(newLocations, prevLocations).stream().filter(srManager::isMasterOf)
+                    .forEach(newLocation -> {
+                log.debug("HostMoved. populateRoute {}, {}, {}, {}", newLocation, prefix, hostMac, hostVlanId);
+                srManager.defaultRoutingHandler.populateRoute(newLocation.deviceId(), prefix,
+                        hostMac, hostVlanId, newLocation.port());
+            });
+
+        });
+    }
+
+    private Set<ResolvedRoute> affectedRoutes(MacAddress mac, VlanId vlanId) {
+        return srManager.routeService.getRouteTables().stream()
+                .map(routeTableId -> srManager.routeService.getRoutes(routeTableId))
+                .flatMap(Collection::stream)
+                .map(RouteInfo::allRoutes)
+                .flatMap(Collection::stream)
+                .filter(resolvedRoute -> mac.equals(resolvedRoute.nextHopMac()) &&
+                        vlanId.equals(resolvedRoute.nextHopVlan())).collect(Collectors.toSet());
+    }
+
+    private boolean isReady() {
+        return Objects.nonNull(srManager.deviceConfiguration) &&
+                Objects.nonNull(srManager.defaultRoutingHandler);
+    }
+
+    void enqueueRouteEvent(RouteEvent routeEvent) {
+        log.debug("Enqueue routeEvent {}", routeEvent);
+        routeEventCache.put(routeEvent.subject().prefix(), routeEvent);
+    }
+
+    void dequeueRouteEvent(RouteEvent routeEvent) {
+        log.debug("Dequeue routeEvent {}", routeEvent);
+        switch (routeEvent.type()) {
+            case ROUTE_ADDED:
+                processRouteAddedInternal(routeEvent.alternatives());
+                break;
+            case ROUTE_REMOVED:
+                processRouteRemovedInternal(routeEvent.alternatives());
+                break;
+            case ROUTE_UPDATED:
+            case ALTERNATIVE_ROUTES_CHANGED:
+                processRouteUpdatedInternal(Sets.newHashSet(routeEvent.alternatives()),
+                        Sets.newHashSet(routeEvent.prevAlternatives()));
+                break;
+            default:
+                break;
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
new file mode 100644
index 0000000..f37caea
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/RoutingRulePopulator.java
@@ -0,0 +1,1250 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.onlab.packet.EthType;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.segmentrouting.DefaultRoutingHandler.PortFilterInfo;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective.Builder;
+import org.onosproject.net.flowobjective.ForwardingObjective.Flag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.Ethernet.TYPE_ARP;
+import static org.onlab.packet.Ethernet.TYPE_IPV6;
+import static org.onlab.packet.ICMP6.NEIGHBOR_ADVERTISEMENT;
+import static org.onlab.packet.ICMP6.NEIGHBOR_SOLICITATION;
+import static org.onlab.packet.ICMP6.ROUTER_ADVERTISEMENT;
+import static org.onlab.packet.ICMP6.ROUTER_SOLICITATION;
+import static org.onlab.packet.IPv6.PROTOCOL_ICMP6;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+
+/**
+ * Populator of segment routing flow rules.
+ */
+public class RoutingRulePopulator {
+    private static final Logger log = LoggerFactory.getLogger(RoutingRulePopulator.class);
+
+    private static final int ARP_NDP_PRIORITY = 30000;
+
+    private AtomicLong rulePopulationCounter;
+    private SegmentRoutingManager srManager;
+    private DeviceConfiguration config;
+
+    /**
+     * Creates a RoutingRulePopulator object.
+     *
+     * @param srManager segment routing manager reference
+     */
+    RoutingRulePopulator(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.config = checkNotNull(srManager.deviceConfiguration);
+        this.rulePopulationCounter = new AtomicLong(0);
+    }
+
+    /**
+     * Resets the population counter.
+     */
+    void resetCounter() {
+        rulePopulationCounter.set(0);
+    }
+
+    /**
+     * Returns the number of rules populated.
+     *
+     * @return number of rules
+     */
+    long getCounter() {
+        return rulePopulationCounter.get();
+    }
+
+    /**
+     * Populates IP rules for a route that has direct connection to the
+     * switch.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param hostVlanId Vlan ID of the nexthop
+     * @param outPort port where the next hop attaches to
+     */
+    void populateRoute(DeviceId deviceId, IpPrefix prefix,
+                              MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        log.debug("Populate direct routing entry for route {} at {}:{}",
+                prefix, deviceId, outPort);
+        ForwardingObjective.Builder fwdBuilder;
+        try {
+            fwdBuilder = routingFwdObjBuilder(deviceId, prefix, hostMac,
+                                              hostVlanId, outPort, false);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting direct populateRoute");
+            return;
+        }
+        if (fwdBuilder == null) {
+            log.warn("Aborting host routing table entry due "
+                    + "to error for dev:{} route:{}", deviceId, prefix);
+            return;
+        }
+
+        int nextId = fwdBuilder.add().nextId();
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Direct routing rule for route {} populated. nextId={}",
+                                         prefix, nextId),
+                (objective, error) ->
+                        log.warn("Failed to populate direct routing rule for route {}: {}",
+                                 prefix, error));
+        srManager.flowObjectiveService.forward(deviceId, fwdBuilder.add(context));
+        rulePopulationCounter.incrementAndGet();
+    }
+
+    /**
+     * Removes IP rules for a route when the next hop is gone.
+     *
+     * @param deviceId device ID of the device that next hop attaches to
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param hostVlanId Vlan ID of the nexthop
+     * @param outPort port that next hop attaches to
+     */
+    void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+            MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        log.debug("Revoke IP table entry for route {} at {}:{}",
+                prefix, deviceId, outPort);
+        ForwardingObjective.Builder fwdBuilder;
+        try {
+            fwdBuilder = routingFwdObjBuilder(deviceId, prefix, hostMac,
+                                              hostVlanId, outPort, true);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting revokeIpRuleForHost.");
+            return;
+        }
+        if (fwdBuilder == null) {
+            log.warn("Aborting host routing table entries due "
+                    + "to error for dev:{} route:{}", deviceId, prefix);
+            return;
+        }
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("IP rule for route {} revoked", prefix),
+                (objective, error) ->
+                        log.warn("Failed to revoke IP rule for route {}: {}", prefix, error));
+        srManager.flowObjectiveService.forward(deviceId, fwdBuilder.remove(context));
+    }
+
+    /**
+     * Returns a forwarding objective builder for routing rules.
+     * <p>
+     * The forwarding objective routes packets destined to a given prefix to
+     * given port on given device with given destination MAC.
+     *
+     * @param deviceId device ID
+     * @param prefix prefix that need to be routed
+     * @param hostMac MAC address of the nexthop
+     * @param hostVlanId Vlan ID of the nexthop
+     * @param outPort port where the nexthop attaches to
+     * @param revoke true if forwarding objective is meant to revoke forwarding rule
+     * @return forwarding objective builder
+     * @throws DeviceConfigNotFoundException if given device is not configured
+     */
+    private ForwardingObjective.Builder routingFwdObjBuilder(
+            DeviceId deviceId, IpPrefix prefix,
+            MacAddress hostMac, VlanId hostVlanId, PortNumber outPort,
+            boolean revoke)
+            throws DeviceConfigNotFoundException {
+        MacAddress deviceMac;
+        deviceMac = config.getDeviceMac(deviceId);
+
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, outPort);
+        VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+        Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+        VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+        // Create route selector
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(prefix);
+
+        // Create route treatment
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.deferred()
+                .setEthDst(hostMac)
+                .setEthSrc(deviceMac)
+                .setOutput(outPort);
+
+        // Create route meta
+        TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+        // Adjust the meta according to VLAN configuration
+        if (taggedVlans.contains(hostVlanId)) {
+            tbuilder.setVlanId(hostVlanId);
+        } else if (hostVlanId.equals(VlanId.NONE)) {
+            if (untaggedVlan != null) {
+                mbuilder.matchVlanId(untaggedVlan);
+            } else if (nativeVlan != null) {
+                mbuilder.matchVlanId(nativeVlan);
+            } else {
+                log.warn("Untagged nexthop {}/{} is not allowed on {} without untagged or native vlan",
+                        hostMac, hostVlanId, connectPoint);
+                return null;
+            }
+        } else {
+            log.warn("Tagged nexthop {}/{} is not allowed on {} without VLAN listed"
+                    + " in tagged vlan", hostMac, hostVlanId, connectPoint);
+            return null;
+        }
+        // if the objective is to revoke an existing rule, and for some reason
+        // the next-objective does not exist, then a new one should not be created
+        int portNextObjId = srManager.getPortNextObjectiveId(deviceId, outPort,
+                                          tbuilder.build(), mbuilder.build(), !revoke);
+        if (portNextObjId == -1) {
+            // Warning log will come from getPortNextObjective method
+            return null;
+        }
+
+        return DefaultForwardingObjective.builder()
+                .withSelector(sbuilder.build())
+                .nextStep(portNextObjId)
+                .fromApp(srManager.appId).makePermanent()
+                .withPriority(getPriorityFromPrefix(prefix))
+                .withFlag(ForwardingObjective.Flag.SPECIFIC);
+    }
+
+    /**
+     * Populates IP flow rules for all the given prefixes reachable from the
+     * destination switch(es).
+     *
+     * @param targetSw switch where rules are to be programmed
+     * @param subnets subnets/prefixes being added
+     * @param destSw1 destination switch where the prefixes are reachable
+     * @param destSw2 paired destination switch if one exists for the subnets/prefixes.
+     *                Should be null if there is no paired destination switch (by config)
+     *                or if the given prefixes are reachable only via destSw1
+     * @param nextHops a map containing a set of next-hops for each destination switch.
+     *                 If destSw2 is not null, then this map must contain an
+     *                 entry for destSw2 with its next-hops from the targetSw
+     *                 (although the next-hop set may be empty in certain scenarios).
+     *                 If destSw2 is null, there should not be an entry in this
+     *                 map for destSw2.
+     * @return true if all rules are set successfully, false otherwise
+     */
+    boolean populateIpRuleForSubnet(DeviceId targetSw, Set<IpPrefix> subnets,
+            DeviceId destSw1, DeviceId destSw2, Map<DeviceId, Set<DeviceId>> nextHops) {
+        for (IpPrefix subnet : subnets) {
+            if (!populateIpRuleForRouter(targetSw, subnet, destSw1, destSw2, nextHops)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Revokes IP flow rules for the subnets in each edge switch.
+     *
+     * @param subnets subnet being removed
+     * @return true if all rules are removed successfully, false otherwise
+     */
+    boolean revokeIpRuleForSubnet(Set<IpPrefix> subnets) {
+        for (IpPrefix subnet : subnets) {
+            if (!revokeIpRuleForRouter(subnet)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Populates IP flow rules for an IP prefix in the target device. The prefix
+     * is reachable via destination device(s).
+     *
+     * @param targetSw target device ID to set the rules
+     * @param ipPrefix the IP prefix
+     * @param destSw1 destination switch where the prefixes are reachable
+     * @param destSw2 paired destination switch if one exists for the subnets/prefixes.
+     *                Should be null if there is no paired destination switch (by config)
+     *                or if the given prefixes are reachable only via destSw1
+     * @param nextHops map of destination switches and their next-hops.
+     *                  Should only contain destination switches that are
+     *                  actually meant to be routed to. If destSw2 is null, there
+     *                  should not be an entry for destSw2 in this map.
+     * @return true if all rules are set successfully, false otherwise
+     */
+    private boolean populateIpRuleForRouter(DeviceId targetSw,
+                                           IpPrefix ipPrefix, DeviceId destSw1,
+                                           DeviceId destSw2,
+                                           Map<DeviceId, Set<DeviceId>> nextHops) {
+        int segmentId1, segmentId2 = -1;
+        try {
+            if (ipPrefix.isIp4()) {
+                segmentId1 = config.getIPv4SegmentId(destSw1);
+                if (destSw2 != null) {
+                    segmentId2 = config.getIPv4SegmentId(destSw2);
+                }
+            } else {
+                segmentId1 = config.getIPv6SegmentId(destSw1);
+                if (destSw2 != null) {
+                    segmentId2 = config.getIPv6SegmentId(destSw2);
+                }
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting populateIpRuleForRouter.");
+            return false;
+        }
+
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(ipPrefix);
+        TrafficSelector selector = sbuilder.build();
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        DestinationSet ds;
+        TrafficTreatment treatment;
+
+        // If the next hop is the same as the final destination, then MPLS label
+        // is not set.
+        /*if (nextHops.size() == 1 && nextHops.toArray()[0].equals(destSw)) {
+            tbuilder.immediate().decNwTtl();
+            ds = new DestinationSet(false, destSw);
+            treatment = tbuilder.build();
+        } else {
+            ds = new DestinationSet(false, segmentId, destSw);
+            treatment = null;
+        }*/
+        if (destSw2 == null) {
+            // single dst - create destination set based on next-hop
+            Set<DeviceId> nhd1 = nextHops.get(destSw1);
+            if (nhd1.size() == 1 && nhd1.iterator().next().equals(destSw1)) {
+                tbuilder.immediate().decNwTtl();
+                ds = new DestinationSet(false, destSw1);
+                treatment = tbuilder.build();
+            } else {
+                ds = new DestinationSet(false, segmentId1, destSw1);
+                treatment = null;
+            }
+        } else {
+            // dst pair - IP rules for dst-pairs are always from other edge nodes
+            // the destination set needs to have both destinations, even if there
+            // are no next hops to one of them
+            ds = new DestinationSet(false, segmentId1, destSw1, segmentId2, destSw2);
+            treatment = null;
+        }
+
+        // setup metadata to pass to nextObjective - indicate the vlan on egress
+        // if needed by the switch pipeline. Since neighbor sets are always to
+        // other neighboring routers, there is no subnet assigned on those ports.
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder(selector);
+        metabuilder.matchVlanId(SegmentRoutingManager.INTERNAL_VLAN);
+        DefaultGroupHandler grpHandler = srManager.getGroupHandler(targetSw);
+        if (grpHandler == null) {
+            log.warn("populateIPRuleForRouter: groupHandler for device {} "
+                    + "not found", targetSw);
+            return false;
+        }
+
+        int nextId = grpHandler.getNextObjectiveId(ds, nextHops,
+                                                   metabuilder.build(), true);
+        if (nextId <= 0) {
+            log.warn("No next objective in {} for ds: {}", targetSw, ds);
+            return false;
+        }
+
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+                .builder()
+                .fromApp(srManager.appId)
+                .makePermanent()
+                .nextStep(nextId)
+                .withSelector(selector)
+                .withPriority(getPriorityFromPrefix(ipPrefix))
+                .withFlag(ForwardingObjective.Flag.SPECIFIC);
+        if (treatment != null) {
+            fwdBuilder.withTreatment(treatment);
+        }
+        log.debug("Installing IPv4 forwarding objective for router IP/subnet {} "
+                + "in switch {} with nextId: {}", ipPrefix, targetSw, nextId);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("IP rule for router {} populated in dev:{}",
+                                         ipPrefix, targetSw),
+                (objective, error) ->
+                        log.warn("Failed to populate IP rule for router {}: {} in dev:{}",
+                                 ipPrefix, error, targetSw));
+        srManager.flowObjectiveService.forward(targetSw, fwdBuilder.add(context));
+        rulePopulationCounter.incrementAndGet();
+
+        return true;
+    }
+
+    /**
+     * Revokes IP flow rules for the router IP address.
+     *
+     * @param ipPrefix the IP address of the destination router
+     * @return true if all rules are removed successfully, false otherwise
+     */
+    private boolean revokeIpRuleForRouter(IpPrefix ipPrefix) {
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(ipPrefix);
+        TrafficSelector selector = sbuilder.build();
+        TrafficTreatment dummyTreatment = DefaultTrafficTreatment.builder().build();
+
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+                .builder()
+                .fromApp(srManager.appId)
+                .makePermanent()
+                .withSelector(selector)
+                .withTreatment(dummyTreatment)
+                .withPriority(getPriorityFromPrefix(ipPrefix))
+                .withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+        srManager.deviceService.getAvailableDevices().forEach(device -> {
+            if (srManager.mastershipService.isLocalMaster(device.id())) {
+                ObjectiveContext context = new DefaultObjectiveContext(
+                        (objective) -> log.debug("IP rule for router {} revoked from {}", ipPrefix, device.id()),
+                        (objective, error) -> log.warn("Failed to revoke IP rule for router {} from {}: {}",
+                                ipPrefix, device.id(), error));
+                srManager.flowObjectiveService.forward(device.id(), fwdBuilder.remove(context));
+            } else {
+                log.debug("Not the master of {}. Abort route {} removal", device.id(), ipPrefix);
+            }
+        });
+
+        return true;
+    }
+
+    /**
+     * Deals with !MPLS Bos use case.
+     *
+     * @param targetSwId the target sw
+     * @param destSwId the destination sw
+     * @param nextHops the set of next hops
+     * @param segmentId the segmentId to match
+     * @param routerIp the router ip
+     * @return a collection of fwdobjective
+     */
+    private Collection<ForwardingObjective> handleMpls(
+                                        DeviceId targetSwId,
+                                        DeviceId destSwId,
+                                        Set<DeviceId> nextHops,
+                                        int segmentId,
+                                        IpAddress routerIp,
+                                        boolean isMplsBos) {
+
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        List<ForwardingObjective.Builder> fwdObjBuilders = Lists.newArrayList();
+        // For the transport of Pwaas we can use two or three MPLS label
+        sbuilder.matchEthType(Ethernet.MPLS_UNICAST);
+        sbuilder.matchMplsLabel(MplsLabel.mplsLabel(segmentId));
+        sbuilder.matchMplsBos(isMplsBos);
+        TrafficSelector selector = sbuilder.build();
+
+        // setup metadata to pass to nextObjective - indicate the vlan on egress
+        // if needed by the switch pipeline. Since mpls next-hops are always to
+        // other neighboring routers, there is no subnet assigned on those ports.
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder(selector);
+        metabuilder.matchVlanId(SegmentRoutingManager.INTERNAL_VLAN);
+
+        if (nextHops.size() == 1 && destSwId.equals(nextHops.toArray()[0])) {
+            // If the next hop is the destination router for the segment, do pop
+            log.debug("populateMplsRule: Installing MPLS forwarding objective for "
+                    + "label {} in switch {} with pop to next-hops {}",
+                    segmentId, targetSwId, nextHops);
+            // Not-bos pop case (php for the current label). If MPLS-ECMP
+            // has been configured, the application we will request the
+            // installation for an MPLS-ECMP group.
+            ForwardingObjective.Builder fwdObjNoBosBuilder =
+                    getMplsForwardingObjective(targetSwId,
+                                               nextHops,
+                                               true,
+                                               isMplsBos,
+                                               metabuilder.build(),
+                                               routerIp,
+                                               destSwId);
+            // Error case, we cannot handle, exit.
+            if (fwdObjNoBosBuilder == null) {
+                return Collections.emptyList();
+            }
+            fwdObjBuilders.add(fwdObjNoBosBuilder);
+
+        } else {
+            // next hop is not destination, SR CONTINUE case (swap with self)
+            log.debug("Installing MPLS forwarding objective for "
+                    + "label {} in switch {} without pop to next-hops {}",
+                    segmentId, targetSwId, nextHops);
+            // Not-bos pop case. If MPLS-ECMP has been configured, the
+            // application we will request the installation for an MPLS-ECMP
+            // group.
+            ForwardingObjective.Builder fwdObjNoBosBuilder =
+                    getMplsForwardingObjective(targetSwId,
+                                               nextHops,
+                                               false,
+                                               isMplsBos,
+                                               metabuilder.build(),
+                                               routerIp,
+                                               destSwId);
+            // Error case, we cannot handle, exit.
+            if (fwdObjNoBosBuilder == null) {
+                return Collections.emptyList();
+            }
+            fwdObjBuilders.add(fwdObjNoBosBuilder);
+
+        }
+
+        List<ForwardingObjective> fwdObjs = Lists.newArrayList();
+        // We add the final property to the fwdObjs.
+        for (ForwardingObjective.Builder fwdObjBuilder : fwdObjBuilders) {
+
+            ((Builder) ((Builder) fwdObjBuilder
+                    .fromApp(srManager.appId)
+                    .makePermanent())
+                    .withSelector(selector)
+                    .withPriority(SegmentRoutingService.DEFAULT_PRIORITY))
+                    .withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) ->
+                            log.debug("MPLS rule {} for SID {} populated in dev:{} ",
+                                      objective.id(), segmentId, targetSwId),
+                    (objective, error) ->
+                            log.warn("Failed to populate MPLS rule {} for SID {}: {} in dev:{}",
+                                     objective.id(), segmentId, error, targetSwId));
+
+            ForwardingObjective fob = fwdObjBuilder.add(context);
+            fwdObjs.add(fob);
+
+        }
+
+        return fwdObjs;
+    }
+
+    /**
+     * Populates MPLS flow rules in the target device to point towards the
+     * destination device.
+     *
+     * @param targetSwId target device ID of the switch to set the rules
+     * @param destSwId destination switch device ID
+     * @param nextHops next hops switch ID list
+     * @param routerIp the router ip
+     * @return true if all rules are set successfully, false otherwise
+     */
+    boolean populateMplsRule(DeviceId targetSwId, DeviceId destSwId,
+                                    Set<DeviceId> nextHops,
+                                    IpAddress routerIp) {
+
+        int segmentId;
+        try {
+            if (routerIp.isIp4()) {
+                segmentId = config.getIPv4SegmentId(destSwId);
+            } else {
+                segmentId = config.getIPv6SegmentId(destSwId);
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting populateMplsRule.");
+            return false;
+        }
+
+        List<ForwardingObjective> fwdObjs = new ArrayList<>();
+        Collection<ForwardingObjective> fwdObjsMpls;
+        // Generates the transit rules used by the standard "routing".
+        fwdObjsMpls = handleMpls(targetSwId, destSwId, nextHops, segmentId, routerIp, true);
+        if (fwdObjsMpls.isEmpty()) {
+            return false;
+        }
+        fwdObjs.addAll(fwdObjsMpls);
+
+        // Generates the transit rules used by the MPLS Pwaas.
+        int pwSrLabel;
+        try {
+            pwSrLabel = config.getPWRoutingLabel(destSwId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting populateMplsRule. No label for PseudoWire traffic.");
+            return false;
+        }
+        fwdObjsMpls = handleMpls(targetSwId, destSwId, nextHops, pwSrLabel, routerIp, false);
+        if (fwdObjsMpls.isEmpty()) {
+            return false;
+        }
+        fwdObjs.addAll(fwdObjsMpls);
+
+        for (ForwardingObjective fwdObj : fwdObjs) {
+            log.debug("Sending MPLS fwd obj {} for SID {}-> next {} in sw: {}",
+                      fwdObj.id(), segmentId, fwdObj.nextId(), targetSwId);
+            srManager.flowObjectiveService.forward(targetSwId, fwdObj);
+            rulePopulationCounter.incrementAndGet();
+        }
+
+        return true;
+    }
+
+    private ForwardingObjective.Builder getMplsForwardingObjective(
+                                             DeviceId targetSw,
+                                             Set<DeviceId> nextHops,
+                                             boolean phpRequired,
+                                             boolean isBos,
+                                             TrafficSelector meta,
+                                             IpAddress routerIp,
+                                             DeviceId destSw) {
+
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective
+                .builder().withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+
+        if (phpRequired) {
+            // php case - pop should always be flow-action
+            log.debug("getMplsForwardingObjective: php required");
+            tbuilder.deferred().copyTtlIn();
+            if (isBos) {
+                if (routerIp.isIp4()) {
+                    tbuilder.deferred().popMpls(EthType.EtherType.IPV4.ethType());
+                } else {
+                    tbuilder.deferred().popMpls(EthType.EtherType.IPV6.ethType());
+                }
+                tbuilder.decNwTtl();
+            } else {
+                tbuilder.deferred().popMpls(EthType.EtherType.MPLS_UNICAST.ethType())
+                    .decMplsTtl();
+            }
+        } else {
+            // swap with self case - SR CONTINUE
+            log.debug("getMplsForwardingObjective: php not required");
+            tbuilder.deferred().decMplsTtl();
+        }
+
+        fwdBuilder.withTreatment(tbuilder.build());
+        // if MPLS-ECMP == True we will build a standard NeighborSet.
+        // Otherwise a RandomNeighborSet.
+        DestinationSet ns = DestinationSet.destinationSet(false, false, destSw);
+        if (!isBos && this.srManager.getMplsEcmp()) {
+            ns = DestinationSet.destinationSet(false, true, destSw);
+        } else if (!isBos && !this.srManager.getMplsEcmp()) {
+            ns = DestinationSet.destinationSet(true, true, destSw);
+        }
+
+        log.debug("Trying to get a nextObjId for mpls rule on device:{} to ns:{}",
+                  targetSw, ns);
+        DefaultGroupHandler gh = srManager.getGroupHandler(targetSw);
+        if (gh == null) {
+            log.warn("getNextObjectiveId query - groupHandler for device {} "
+                    + "not found", targetSw);
+            return null;
+        }
+        // If BoS == True, all forwarding is via L3 ECMP group.
+        // If Bos == False, the forwarding can be via MPLS-ECMP group or through
+        // MPLS-Interface group. This depends on the configuration of the option
+        // MPLS-ECMP.
+        // The metadata informs the driver that the next-Objective will be used
+        // by MPLS flows and if Bos == False the driver will use MPLS groups.
+        Map<DeviceId, Set<DeviceId>> dstNextHops = new HashMap<>();
+        dstNextHops.put(destSw, nextHops);
+        int nextId = gh.getNextObjectiveId(ns, dstNextHops, meta, isBos);
+        if (nextId <= 0) {
+            log.warn("No next objective in {} for ns: {}", targetSw, ns);
+            return null;
+        } else {
+            log.debug("nextObjId found:{} for mpls rule on device:{} to ns:{}",
+                      nextId, targetSw, ns);
+        }
+
+        fwdBuilder.nextStep(nextId);
+        return fwdBuilder;
+    }
+
+    /**
+     * Creates a filtering objective to permit all untagged packets with a
+     * dstMac corresponding to the router's MAC address. For those pipelines
+     * that need to internally assign vlans to untagged packets, this method
+     * provides per-subnet vlan-ids as metadata.
+     * <p>
+     * Note that the vlan assignment and filter programming should only be done by
+     * the master for a switch. This method is typically called at deviceAdd and
+     * programs filters only for the enabled ports of the device. For port-updates,
+     * that enable/disable ports after device add, singlePortFilter methods should
+     * be called.
+     *
+     * @param deviceId  the switch dpid for the router
+     * @return PortFilterInfo information about the processed ports
+     */
+    PortFilterInfo populateVlanMacFilters(DeviceId deviceId) {
+        log.debug("Installing per-port filtering objective for untagged "
+                + "packets in device {}", deviceId);
+
+        List<Port> devPorts = srManager.deviceService.getPorts(deviceId);
+        if (devPorts == null || devPorts.isEmpty()) {
+            log.warn("Device {} ports not available. Unable to add MacVlan filters",
+                     deviceId);
+            return null;
+        }
+        int disabledPorts = 0, errorPorts = 0, filteredPorts = 0;
+        for (Port port : devPorts) {
+            if (!port.isEnabled()) {
+                disabledPorts++;
+                continue;
+            }
+            if (processSinglePortFilters(deviceId, port.number(), true)) {
+                filteredPorts++;
+            } else {
+                errorPorts++;
+            }
+        }
+        log.debug("Filtering on dev:{}, disabledPorts:{}, errorPorts:{}, filteredPorts:{}",
+                  deviceId, disabledPorts, errorPorts, filteredPorts);
+        return srManager.defaultRoutingHandler.new PortFilterInfo(disabledPorts,
+                                                       errorPorts, filteredPorts);
+    }
+
+    /**
+     * Creates or removes filtering objectives for a single port. Should only be
+     * called by the master for a switch.
+     *
+     * @param deviceId device identifier
+     * @param portnum  port identifier for port to be filtered
+     * @param install true to install the filtering objective, false to remove
+     * @return true if no errors occurred during the build of the filtering objective
+     */
+    boolean processSinglePortFilters(DeviceId deviceId, PortNumber portnum, boolean install) {
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, portnum);
+        VlanId untaggedVlan = srManager.getUntaggedVlanId(connectPoint);
+        Set<VlanId> taggedVlans = srManager.getTaggedVlanId(connectPoint);
+        VlanId nativeVlan = srManager.getNativeVlanId(connectPoint);
+
+        if (taggedVlans.size() != 0) {
+            // Filter for tagged vlans
+            if (!srManager.getTaggedVlanId(connectPoint).stream().allMatch(taggedVlanId ->
+                    processSinglePortFiltersInternal(deviceId, portnum, false, taggedVlanId, install))) {
+                return false;
+            }
+            if (nativeVlan != null) {
+                // Filter for native vlan
+                if (!processSinglePortFiltersInternal(deviceId, portnum, true, nativeVlan, install)) {
+                    return false;
+                }
+            }
+        } else if (untaggedVlan != null) {
+            // Filter for untagged vlan
+            if (!processSinglePortFiltersInternal(deviceId, portnum, true, untaggedVlan, install)) {
+                return false;
+            }
+        } else {
+            // Unconfigured port, use INTERNAL_VLAN
+            if (!processSinglePortFiltersInternal(deviceId, portnum, true, INTERNAL_VLAN, install)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Updates filtering objectives for a single port. Should only be called by
+     * the master for a switch
+     * @param deviceId device identifier
+     * @param portNum  port identifier for port to be filtered
+     * @param pushVlan true to push vlan, false otherwise
+     * @param vlanId vlan identifier
+     * @param install true to install the filtering objective, false to remove
+     */
+    void updateSinglePortFilters(DeviceId deviceId, PortNumber portNum,
+                                 boolean pushVlan, VlanId vlanId, boolean install) {
+        if (!processSinglePortFiltersInternal(deviceId, portNum, pushVlan, vlanId, install)) {
+            log.warn("Failed to update FilteringObjective for {}/{} with vlan {}",
+                     deviceId, portNum, vlanId);
+        }
+    }
+
+    private boolean processSinglePortFiltersInternal(DeviceId deviceId, PortNumber portnum,
+                                                      boolean pushVlan, VlanId vlanId, boolean install) {
+        FilteringObjective.Builder fob = buildFilteringObjective(deviceId, portnum, pushVlan, vlanId);
+        if (fob == null) {
+            // error encountered during build
+            return false;
+        }
+        log.debug("{} filtering objectives for dev/port: {}/{}",
+                 install ? "Installing" : "Removing", deviceId, portnum);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Filter for {}/{} {}", deviceId, portnum,
+                        install ? "installed" : "removed"),
+                (objective, error) -> log.warn("Failed to {} filter for {}/{}: {}",
+                        install ? "install" : "remove", deviceId, portnum, error));
+        if (install) {
+            srManager.flowObjectiveService.filter(deviceId, fob.add(context));
+        } else {
+            srManager.flowObjectiveService.filter(deviceId, fob.remove(context));
+        }
+        return true;
+    }
+
+    private FilteringObjective.Builder buildFilteringObjective(DeviceId deviceId, PortNumber portnum,
+                                                               boolean pushVlan, VlanId vlanId) {
+        MacAddress deviceMac;
+        try {
+            deviceMac = config.getDeviceMac(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Processing SinglePortFilters aborted");
+            return null;
+        }
+        FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+        fob.withKey(Criteria.matchInPort(portnum))
+            .addCondition(Criteria.matchEthDst(deviceMac))
+            .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+
+        if (pushVlan) {
+            fob.addCondition(Criteria.matchVlanId(VlanId.NONE));
+            tBuilder.pushVlan().setVlanId(vlanId);
+        } else {
+            fob.addCondition(Criteria.matchVlanId(vlanId));
+        }
+
+        // NOTE: Some switch hardware share the same filtering flow among different ports.
+        //       We use this metadata to let the driver know that there is no more enabled port
+        //       within the same VLAN on this device.
+        boolean noMoreEnabledPort = srManager.interfaceService.getInterfaces().stream()
+                .filter(intf -> intf.connectPoint().deviceId().equals(deviceId))
+                .filter(intf -> intf.vlanTagged().contains(vlanId) ||
+                        intf.vlanUntagged().equals(vlanId) ||
+                        intf.vlanNative().equals(vlanId))
+                .noneMatch(intf -> {
+                    Port port = srManager.deviceService.getPort(intf.connectPoint());
+                    return port != null && port.isEnabled();
+                });
+        if (noMoreEnabledPort) {
+            tBuilder.wipeDeferred();
+        }
+
+        fob.withMeta(tBuilder.build());
+
+        fob.permit().fromApp(srManager.appId);
+        return fob;
+    }
+
+    /**
+     * Creates a forwarding objective to punt all IP packets, destined to the
+     * router's port IP addresses, to the controller. Note that the input
+     * port should not be matched on, as these packets can come from any input.
+     * Furthermore, these are applied only by the master instance.
+     *
+     * @param deviceId the switch dpid for the router
+     */
+    void populateIpPunts(DeviceId deviceId) {
+        Ip4Address routerIpv4, pairRouterIpv4 = null;
+        Ip6Address routerIpv6, routerLinkLocalIpv6, pairRouterIpv6 = null;
+        try {
+            routerIpv4 = config.getRouterIpv4(deviceId);
+            routerIpv6 = config.getRouterIpv6(deviceId);
+            routerLinkLocalIpv6 = Ip6Address.valueOf(
+                    IPv6.getLinkLocalAddress(config.getDeviceMac(deviceId).toBytes()));
+
+            if (config.isPairedEdge(deviceId)) {
+                pairRouterIpv4 = config.getRouterIpv4(config.getPairDeviceId(deviceId));
+                pairRouterIpv6 = config.getRouterIpv6(config.getPairDeviceId(deviceId));
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting populateIpPunts.");
+            return;
+        }
+
+        if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not installing port-IP punts - not the master for dev:{} ",
+                      deviceId);
+            return;
+        }
+        Set<IpAddress> allIps = new HashSet<>(config.getPortIPs(deviceId));
+        allIps.add(routerIpv4);
+        allIps.add(routerLinkLocalIpv6);
+        if (routerIpv6 != null) {
+            allIps.add(routerIpv6);
+        }
+        if (pairRouterIpv4 != null) {
+            allIps.add(pairRouterIpv4);
+        }
+        if (pairRouterIpv6 != null) {
+            allIps.add(pairRouterIpv6);
+        }
+        for (IpAddress ipaddr : allIps) {
+            populateSingleIpPunts(deviceId, ipaddr);
+        }
+    }
+
+    /**
+     * Creates a forwarding objective to punt all IP packets, destined to the
+     * specified IP address, which should be router's port IP address.
+     *
+     * @param deviceId the switch dpid for the router
+     * @param ipAddress the IP address of the router's port
+     */
+    void populateSingleIpPunts(DeviceId deviceId, IpAddress ipAddress) {
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpAddress(ipAddress);
+        Optional<DeviceId> optDeviceId = Optional.of(deviceId);
+
+        srManager.packetService.requestPackets(sbuilder.build(),
+                                               PacketPriority.CONTROL, srManager.appId, optDeviceId);
+    }
+
+    /**
+     * Removes a forwarding objective to punt all IP packets, destined to the
+     * specified IP address, which should be router's port IP address.
+     *
+     * @param deviceId the switch dpid for the router
+     * @param ipAddress the IP address of the router's port
+     */
+    void revokeSingleIpPunts(DeviceId deviceId, IpAddress ipAddress) {
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpAddress(ipAddress);
+        Optional<DeviceId> optDeviceId = Optional.of(deviceId);
+
+        try {
+            if (!ipAddress.equals(config.getRouterIpv4(deviceId)) &&
+                    !ipAddress.equals(config.getRouterIpv6(deviceId))) {
+                srManager.packetService.cancelPackets(sbuilder.build(),
+                                                      PacketPriority.CONTROL, srManager.appId, optDeviceId);
+            }
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting revokeSingleIpPunts");
+        }
+    }
+
+    /**
+     * Method to build IPv4 or IPv6 selector.
+     *
+     * @param addressToMatch the address to match
+     */
+    private TrafficSelector.Builder buildIpSelectorFromIpAddress(IpAddress addressToMatch) {
+        return buildIpSelectorFromIpPrefix(addressToMatch.toIpPrefix());
+    }
+
+    /**
+     * Method to build IPv4 or IPv6 selector.
+     *
+     * @param prefixToMatch the prefix to match
+     */
+    private TrafficSelector.Builder buildIpSelectorFromIpPrefix(IpPrefix prefixToMatch) {
+        TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
+        // If the prefix is IPv4
+        if (prefixToMatch.isIp4()) {
+            selectorBuilder.matchEthType(Ethernet.TYPE_IPV4);
+            selectorBuilder.matchIPDst(prefixToMatch.getIp4Prefix());
+            return selectorBuilder;
+        }
+        // If the prefix is IPv6
+        selectorBuilder.matchEthType(Ethernet.TYPE_IPV6);
+        selectorBuilder.matchIPv6Dst(prefixToMatch.getIp6Prefix());
+        return selectorBuilder;
+    }
+
+    /**
+     * Creates forwarding objectives to punt ARP and NDP packets, to the controller.
+     * Furthermore, these are applied only by the master instance. Deferred actions
+     * are not cleared such that packets can be flooded in the cross connect use case
+     *
+     * @param deviceId the switch dpid for the router
+     */
+    void populateArpNdpPunts(DeviceId deviceId) {
+        // We are not the master just skip.
+        if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+            log.debug("Not installing ARP/NDP punts - not the master for dev:{} ",
+                      deviceId);
+            return;
+        }
+
+        ForwardingObjective fwdObj;
+        // We punt all ARP packets towards the controller.
+        fwdObj = arpFwdObjective(null, true, ARP_NDP_PRIORITY)
+                .add(new ObjectiveContext() {
+                    @Override
+                    public void onError(Objective objective, ObjectiveError error) {
+                        log.warn("Failed to install forwarding objective to punt ARP to {}: {}",
+                                 deviceId, error);
+                    }
+                });
+        srManager.flowObjectiveService.forward(deviceId, fwdObj);
+
+        // We punt all NDP packets towards the controller.
+        ndpFwdObjective(null, true, ARP_NDP_PRIORITY).forEach(builder -> {
+             ForwardingObjective obj = builder.add(new ObjectiveContext() {
+                @Override
+                public void onError(Objective objective, ObjectiveError error) {
+                    log.warn("Failed to install forwarding objective to punt NDP to {}: {}",
+                            deviceId, error);
+                }
+            });
+            srManager.flowObjectiveService.forward(deviceId, obj);
+        });
+
+        srManager.getPairLocalPorts(deviceId).ifPresent(port -> {
+            ForwardingObjective pairFwdObj;
+            // Do not punt ARP packets from pair port
+            pairFwdObj = arpFwdObjective(port, false, PacketPriority.CONTROL.priorityValue() + 1)
+                    .add(new ObjectiveContext() {
+                        @Override
+                        public void onError(Objective objective, ObjectiveError error) {
+                            log.warn("Failed to install forwarding objective to ignore ARP to {}: {}",
+                                    deviceId, error);
+                        }
+                    });
+            srManager.flowObjectiveService.forward(deviceId, pairFwdObj);
+
+            // Do not punt NDP packets from pair port
+            ndpFwdObjective(port, false, PacketPriority.CONTROL.priorityValue() + 1).forEach(builder -> {
+                ForwardingObjective obj = builder.add(new ObjectiveContext() {
+                    @Override
+                    public void onError(Objective objective, ObjectiveError error) {
+                        log.warn("Failed to install forwarding objective to ignore ARP to {}: {}",
+                                deviceId, error);
+                    }
+                });
+                srManager.flowObjectiveService.forward(deviceId, obj);
+            });
+
+            // Do not forward DAD packets from pair port
+            pairFwdObj = dad6FwdObjective(port, PacketPriority.CONTROL.priorityValue() + 2)
+                    .add(new ObjectiveContext() {
+                        @Override
+                        public void onError(Objective objective, ObjectiveError error) {
+                            log.warn("Failed to install forwarding objective to drop DAD to {}: {}",
+                                    deviceId, error);
+                        }
+                    });
+            srManager.flowObjectiveService.forward(deviceId, pairFwdObj);
+        });
+    }
+
+    private ForwardingObjective.Builder fwdObjBuilder(TrafficSelector selector,
+                                                      TrafficTreatment treatment, int priority) {
+        return DefaultForwardingObjective.builder()
+                .withPriority(priority)
+                .withSelector(selector)
+                .fromApp(srManager.appId)
+                .withFlag(ForwardingObjective.Flag.VERSATILE)
+                .withTreatment(treatment)
+                .makePermanent();
+    }
+
+    private ForwardingObjective.Builder arpFwdObjective(PortNumber port, boolean punt, int priority) {
+        TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+        sBuilder.matchEthType(TYPE_ARP);
+        if (port != null) {
+            sBuilder.matchInPort(port);
+        }
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        if (punt) {
+            tBuilder.punt();
+        }
+        return fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority);
+    }
+
+    private Set<ForwardingObjective.Builder> ndpFwdObjective(PortNumber port, boolean punt, int priority) {
+        Set<ForwardingObjective.Builder> result = Sets.newHashSet();
+
+        Lists.newArrayList(NEIGHBOR_SOLICITATION, NEIGHBOR_ADVERTISEMENT, ROUTER_SOLICITATION, ROUTER_ADVERTISEMENT)
+                .forEach(type -> {
+                    TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+                    sBuilder.matchEthType(TYPE_IPV6)
+                            .matchIPProtocol(PROTOCOL_ICMP6)
+                            .matchIcmpv6Type(type);
+                    if (port != null) {
+                        sBuilder.matchInPort(port);
+                    }
+
+                    TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+                    if (punt) {
+                        tBuilder.punt();
+                    }
+
+                    result.add(fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority));
+                });
+
+        return result;
+    }
+
+    private ForwardingObjective.Builder dad6FwdObjective(PortNumber port, int priority) {
+        TrafficSelector.Builder sBuilder = DefaultTrafficSelector.builder();
+        sBuilder.matchEthType(TYPE_IPV6)
+                .matchIPv6Src(Ip6Address.ZERO.toIpPrefix());
+                // TODO CORD-1672 Fix this when OFDPA can distinguish ::/0 and ::/128 correctly
+                // .matchIPProtocol(PROTOCOL_ICMP6)
+                // .matchIcmpv6Type(NEIGHBOR_SOLICITATION);
+        if (port != null) {
+            sBuilder.matchInPort(port);
+        }
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        tBuilder.wipeDeferred();
+        return fwdObjBuilder(sBuilder.build(), tBuilder.build(), priority);
+    }
+
+    /**
+     * Populates a forwarding objective to send packets that miss other high
+     * priority Bridging Table entries to a group that contains all ports of
+     * its subnet.
+     *
+     * @param deviceId switch ID to set the rules
+     */
+    void populateSubnetBroadcastRule(DeviceId deviceId) {
+        srManager.getVlanPortMap(deviceId).asMap().forEach((vlanId, ports) -> {
+            updateSubnetBroadcastRule(deviceId, vlanId, true);
+        });
+    }
+
+    /**
+     * Creates or removes a forwarding objective to broadcast packets to its subnet.
+     * @param deviceId switch ID to set the rule
+     * @param vlanId vlan ID to specify the subnet
+     * @param install true to install the rule, false to revoke the rule
+     */
+    void updateSubnetBroadcastRule(DeviceId deviceId, VlanId vlanId, boolean install) {
+        int nextId = srManager.getVlanNextObjectiveId(deviceId, vlanId);
+
+        if (nextId < 0) {
+            log.error("Cannot install vlan {} broadcast rule in dev:{} due"
+                              + " to vlanId:{} or nextId:{}", vlanId, deviceId, vlanId, nextId);
+            return;
+        }
+
+        // Driver should treat objective with MacAddress.NONE as the
+        // subnet broadcast rule
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchVlanId(vlanId);
+        sbuilder.matchEthDst(MacAddress.NONE);
+
+        ForwardingObjective.Builder fob = DefaultForwardingObjective.builder();
+        fob.withFlag(Flag.SPECIFIC)
+                .withSelector(sbuilder.build())
+                .nextStep(nextId)
+                .withPriority(SegmentRoutingService.FLOOD_PRIORITY)
+                .fromApp(srManager.appId)
+                .makePermanent();
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Vlan broadcast rule for {} populated", vlanId),
+                (objective, error) ->
+                        log.warn("Failed to populate vlan broadcast rule for {}: {}", vlanId, error));
+
+        if (install) {
+            srManager.flowObjectiveService.forward(deviceId, fob.add(context));
+        } else {
+            srManager.flowObjectiveService.forward(deviceId, fob.remove(context));
+        }
+    }
+
+    private int getPriorityFromPrefix(IpPrefix prefix) {
+        return (prefix.isIp4()) ?
+                2000 * prefix.prefixLength() + SegmentRoutingService.MIN_IP_PRIORITY :
+                500 * prefix.prefixLength() + SegmentRoutingService.MIN_IP_PRIORITY;
+    }
+
+    /**
+     * Update Forwarding objective for each host and IP address connected to given port.
+     * And create corresponding Simple Next objective if it does not exist.
+     * Applied only when populating Forwarding objective
+     * @param deviceId switch ID to set the rule
+     * @param portNumber port number
+     * @param prefix IP prefix of the route
+     * @param hostMac MAC address of the next hop
+     * @param vlanId Vlan ID of the port
+     * @param popVlan true to pop vlan tag in TrafficTreatment
+     * @param install true to populate the forwarding objective, false to revoke
+     */
+    void updateFwdObj(DeviceId deviceId, PortNumber portNumber, IpPrefix prefix, MacAddress hostMac,
+                      VlanId vlanId, boolean popVlan, boolean install) {
+        ForwardingObjective.Builder fob;
+        TrafficSelector.Builder sbuilder = buildIpSelectorFromIpPrefix(prefix);
+        MacAddress deviceMac;
+        try {
+            deviceMac = config.getDeviceMac(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting updateFwdObj.");
+            return;
+        }
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.deferred()
+                .setEthDst(hostMac)
+                .setEthSrc(deviceMac)
+                .setOutput(portNumber);
+
+        TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+
+        if (!popVlan) {
+            tbuilder.setVlanId(vlanId);
+        } else {
+            mbuilder.matchVlanId(vlanId);
+        }
+
+        // if the objective is to revoke an existing rule, and for some reason
+        // the next-objective does not exist, then a new one should not be created
+        int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNumber,
+                                                             tbuilder.build(), mbuilder.build(), install);
+        if (portNextObjId == -1) {
+            // Warning log will come from getPortNextObjective method
+            return;
+        }
+
+        fob = DefaultForwardingObjective.builder().withSelector(sbuilder.build())
+                .nextStep(portNextObjId).fromApp(srManager.appId).makePermanent()
+                .withPriority(getPriorityFromPrefix(prefix)).withFlag(ForwardingObjective.Flag.SPECIFIC);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("IP rule for route {} {}", prefix, install ? "installed" : "revoked"),
+                (objective, error) ->
+                        log.warn("Failed to {} IP rule for route {}: {}",
+                                 install ? "install" : "revoke", prefix, error));
+        srManager.flowObjectiveService.forward(deviceId, install ? fob.add(context) : fob.remove(context));
+
+        if (!install) {
+            DefaultGroupHandler grpHandler = srManager.getGroupHandler(deviceId);
+            if (grpHandler == null) {
+                log.warn("updateFwdObj: groupHandler for device {} not found", deviceId);
+            } else {
+                // Remove L3UG for the given port and host
+                grpHandler.removeGroupFromPort(portNumber, tbuilder.build(), mbuilder.build());
+            }
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
new file mode 100644
index 0000000..d951b53
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -0,0 +1,1727 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+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.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP6;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.Event;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.ConfigException;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.config.basics.InterfaceConfig;
+import org.onosproject.net.config.basics.McastConfig;
+import org.onosproject.net.config.basics.SubjectFactories;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostLocationProbingService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.mcast.McastEvent;
+import org.onosproject.net.mcast.McastListener;
+import org.onosproject.net.mcast.MulticastRouteService;
+import org.onosproject.net.neighbour.NeighbourResolutionService;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.topology.TopologyService;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteListener;
+import org.onosproject.routeservice.RouteService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+import org.onosproject.segmentrouting.config.XConnectConfig;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapBuilder;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.onlab.packet.Ethernet.TYPE_ARP;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_REGISTERED;
+import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_UNREGISTERED;
+
+/**
+ * Segment routing manager.
+ */
+@Service
+@Component(immediate = true)
+public class SegmentRoutingManager implements SegmentRoutingService {
+
+    private static Logger log = LoggerFactory.getLogger(SegmentRoutingManager.class);
+    private static final String NOT_MASTER = "Current instance is not the master of {}. Ignore.";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private ComponentConfigService compCfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private NeighbourResolutionService neighbourResolutionService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    HostLocationProbingService probingService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    DeviceAdminService deviceAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public FlowObjectiveService flowObjectiveService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    MulticastRouteService multicastRouteService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public TopologyService topologyService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    RouteService routeService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public LeadershipService leadershipService;
+
+    @Property(name = "activeProbing", boolValue = true,
+            label = "Enable active probing to discover dual-homed hosts.")
+    boolean activeProbing = true;
+
+    ArpHandler arpHandler = null;
+    IcmpHandler icmpHandler = null;
+    IpHandler ipHandler = null;
+    RoutingRulePopulator routingRulePopulator = null;
+    ApplicationId appId;
+    DeviceConfiguration deviceConfiguration = null;
+
+    DefaultRoutingHandler defaultRoutingHandler = null;
+    private TunnelHandler tunnelHandler = null;
+    private PolicyHandler policyHandler = null;
+    private InternalPacketProcessor processor = null;
+    private InternalLinkListener linkListener = null;
+    private InternalDeviceListener deviceListener = null;
+    private AppConfigHandler appCfgHandler = null;
+    XConnectHandler xConnectHandler = null;
+    McastHandler mcastHandler = null;
+    HostHandler hostHandler = null;
+    private RouteHandler routeHandler = null;
+    LinkHandler linkHandler = null;
+    private SegmentRoutingNeighbourDispatcher neighbourHandler = null;
+    private L2TunnelHandler l2TunnelHandler = null;
+    private InternalEventHandler eventHandler = new InternalEventHandler();
+    private final InternalHostListener hostListener = new InternalHostListener();
+    private final InternalConfigListener cfgListener = new InternalConfigListener(this);
+    private final InternalMcastListener mcastListener = new InternalMcastListener();
+    private final InternalRouteEventListener routeListener = new InternalRouteEventListener();
+
+    private ScheduledExecutorService executorService = Executors
+            .newScheduledThreadPool(1, groupedThreads("SegmentRoutingManager", "event-%d", log));
+
+    @SuppressWarnings("unused")
+    private static ScheduledFuture<?> eventHandlerFuture = null;
+    @SuppressWarnings("rawtypes")
+    private ConcurrentLinkedQueue<Event> eventQueue = new ConcurrentLinkedQueue<>();
+    Map<DeviceId, DefaultGroupHandler> groupHandlerMap =
+            new ConcurrentHashMap<>();
+    /**
+     * Per device next objective ID store with (device id + destination set) as key.
+     * Used to keep track on MPLS group information.
+     */
+    private EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+            dsNextObjStore = null;
+    /**
+     * Per device next objective ID store with (device id + vlanid) as key.
+     * Used to keep track on L2 flood group information.
+     */
+    private EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer>
+            vlanNextObjStore = null;
+    /**
+     * Per device next objective ID store with (device id + port + treatment + meta) as key.
+     * Used to keep track on L2 interface group and L3 unicast group information.
+     */
+    private EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer>
+            portNextObjStore = null;
+
+    private EventuallyConsistentMap<String, Tunnel> tunnelStore = null;
+    private EventuallyConsistentMap<String, Policy> policyStore = null;
+
+    private AtomicBoolean programmingScheduled = new AtomicBoolean();
+
+    private final ConfigFactory<DeviceId, SegmentRoutingDeviceConfig> deviceConfigFactory =
+            new ConfigFactory<DeviceId, SegmentRoutingDeviceConfig>(
+                    SubjectFactories.DEVICE_SUBJECT_FACTORY,
+                    SegmentRoutingDeviceConfig.class, "segmentrouting") {
+                @Override
+                public SegmentRoutingDeviceConfig createConfig() {
+                    return new SegmentRoutingDeviceConfig();
+                }
+            };
+
+    private final ConfigFactory<ApplicationId, SegmentRoutingAppConfig> appConfigFactory =
+            new ConfigFactory<ApplicationId, SegmentRoutingAppConfig>(
+                    SubjectFactories.APP_SUBJECT_FACTORY,
+                    SegmentRoutingAppConfig.class, "segmentrouting") {
+                @Override
+                public SegmentRoutingAppConfig createConfig() {
+                    return new SegmentRoutingAppConfig();
+                }
+            };
+
+    private final ConfigFactory<ApplicationId, XConnectConfig> xConnectConfigFactory =
+            new ConfigFactory<ApplicationId, XConnectConfig>(
+                    SubjectFactories.APP_SUBJECT_FACTORY,
+                    XConnectConfig.class, "xconnect") {
+                @Override
+                public XConnectConfig createConfig() {
+                    return new XConnectConfig();
+                }
+            };
+
+    private ConfigFactory<ApplicationId, McastConfig> mcastConfigFactory =
+            new ConfigFactory<ApplicationId, McastConfig>(
+                    SubjectFactories.APP_SUBJECT_FACTORY,
+                    McastConfig.class, "multicast") {
+                @Override
+                public McastConfig createConfig() {
+                    return new McastConfig();
+                }
+            };
+
+    private final ConfigFactory<ApplicationId, PwaasConfig> pwaasConfigFactory =
+            new ConfigFactory<ApplicationId, PwaasConfig>(
+                    SubjectFactories.APP_SUBJECT_FACTORY,
+                    PwaasConfig.class, "pwaas") {
+                @Override
+                public PwaasConfig createConfig() {
+                    return new PwaasConfig();
+                }
+            };
+
+    private static final Object THREAD_SCHED_LOCK = new Object();
+    private static int numOfEventsQueued = 0;
+    private static int numOfEventsExecuted = 0;
+    private static int numOfHandlerExecution = 0;
+    private static int numOfHandlerScheduled = 0;
+
+    /**
+     * Segment Routing App ID.
+     */
+    public static final String APP_NAME = "org.onosproject.segmentrouting";
+
+    /**
+     * The default VLAN ID assigned to the interfaces without subnet config.
+     */
+    public static final VlanId INTERNAL_VLAN = VlanId.vlanId((short) 4094);
+
+    @Activate
+    protected void activate(ComponentContext context) {
+        appId = coreService.registerApplication(APP_NAME);
+
+        log.debug("Creating EC map nsnextobjectivestore");
+        EventuallyConsistentMapBuilder<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+                nsNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+        dsNextObjStore = nsNextObjMapBuilder
+                .withName("nsnextobjectivestore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+        log.trace("Current size {}", dsNextObjStore.size());
+
+        log.debug("Creating EC map vlannextobjectivestore");
+        EventuallyConsistentMapBuilder<VlanNextObjectiveStoreKey, Integer>
+                vlanNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+        vlanNextObjStore = vlanNextObjMapBuilder
+                .withName("vlannextobjectivestore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+
+        log.debug("Creating EC map subnetnextobjectivestore");
+        EventuallyConsistentMapBuilder<PortNextObjectiveStoreKey, Integer>
+                portNextObjMapBuilder = storageService.eventuallyConsistentMapBuilder();
+        portNextObjStore = portNextObjMapBuilder
+                .withName("portnextobjectivestore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+
+        EventuallyConsistentMapBuilder<String, Tunnel> tunnelMapBuilder =
+                storageService.eventuallyConsistentMapBuilder();
+        tunnelStore = tunnelMapBuilder
+                .withName("tunnelstore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+
+        EventuallyConsistentMapBuilder<String, Policy> policyMapBuilder =
+                storageService.eventuallyConsistentMapBuilder();
+        policyStore = policyMapBuilder
+                .withName("policystore")
+                .withSerializer(createSerializer())
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .build();
+
+        compCfgService.preSetProperty("org.onosproject.net.group.impl.GroupManager",
+                                      "purgeOnDisconnection", "true");
+        compCfgService.preSetProperty("org.onosproject.net.flow.impl.FlowRuleManager",
+                                      "purgeOnDisconnection", "true");
+        compCfgService.preSetProperty("org.onosproject.provider.host.impl.HostLocationProvider",
+                                      "requestInterceptsEnabled", "false");
+        compCfgService.preSetProperty("org.onosproject.net.neighbour.impl.NeighbourResolutionManager",
+                                      "requestInterceptsEnabled", "false");
+        compCfgService.preSetProperty("org.onosproject.dhcprelay.DhcpRelayManager",
+                                      "arpEnabled", "false");
+        compCfgService.preSetProperty("org.onosproject.net.host.impl.HostManager",
+                                      "greedyLearningIpv6", "true");
+        compCfgService.preSetProperty("org.onosproject.routing.cpr.ControlPlaneRedirectManager",
+                                      "forceUnprovision", "true");
+        compCfgService.preSetProperty("org.onosproject.routeservice.store.RouteStoreImpl",
+                                      "distributed", "true");
+        compCfgService.preSetProperty("org.onosproject.provider.host.impl.HostLocationProvider",
+                                      "multihomingEnabled", "true");
+        compCfgService.preSetProperty("org.onosproject.provider.lldp.impl.LldpLinkProvider",
+                                      "staleLinkAge", "15000");
+        compCfgService.preSetProperty("org.onosproject.net.host.impl.HostManager",
+                                      "allowDuplicateIps", "false");
+        compCfgService.registerProperties(getClass());
+        modified(context);
+
+        processor = new InternalPacketProcessor();
+        linkListener = new InternalLinkListener();
+        deviceListener = new InternalDeviceListener();
+        appCfgHandler = new AppConfigHandler(this);
+        xConnectHandler = new XConnectHandler(this);
+        mcastHandler = new McastHandler(this);
+        hostHandler = new HostHandler(this);
+        linkHandler = new LinkHandler(this);
+        routeHandler = new RouteHandler(this);
+        neighbourHandler = new SegmentRoutingNeighbourDispatcher(this);
+        l2TunnelHandler = new DefaultL2TunnelHandler(this);
+
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(deviceConfigFactory);
+        cfgService.registerConfigFactory(appConfigFactory);
+        cfgService.registerConfigFactory(xConnectConfigFactory);
+        cfgService.registerConfigFactory(mcastConfigFactory);
+        cfgService.registerConfigFactory(pwaasConfigFactory);
+        log.info("Configuring network before adding listeners");
+        cfgListener.configureNetwork();
+
+        hostService.addListener(hostListener);
+        packetService.addProcessor(processor, PacketProcessor.director(2));
+        linkService.addListener(linkListener);
+        deviceService.addListener(deviceListener);
+        multicastRouteService.addListener(mcastListener);
+        routeService.addListener(routeListener);
+
+        l2TunnelHandler.init();
+
+        log.info("Started");
+    }
+
+    KryoNamespace.Builder createSerializer() {
+        return new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(DestinationSetNextObjectiveStoreKey.class,
+                          VlanNextObjectiveStoreKey.class,
+                          DestinationSet.class,
+                          NextNeighbors.class,
+                          Tunnel.class,
+                          DefaultTunnel.class,
+                          Policy.class,
+                          TunnelPolicy.class,
+                          Policy.Type.class,
+                          PortNextObjectiveStoreKey.class,
+                          XConnectStoreKey.class,
+                          L2Tunnel.class,
+                          L2TunnelPolicy.class,
+                          DefaultL2Tunnel.class,
+                          DefaultL2TunnelPolicy.class
+                );
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        cfgService.removeListener(cfgListener);
+        cfgService.unregisterConfigFactory(deviceConfigFactory);
+        cfgService.unregisterConfigFactory(appConfigFactory);
+        cfgService.unregisterConfigFactory(xConnectConfigFactory);
+        cfgService.unregisterConfigFactory(mcastConfigFactory);
+        cfgService.unregisterConfigFactory(pwaasConfigFactory);
+        compCfgService.unregisterProperties(getClass(), false);
+
+        hostService.removeListener(hostListener);
+        packetService.removeProcessor(processor);
+        linkService.removeListener(linkListener);
+        deviceService.removeListener(deviceListener);
+        multicastRouteService.removeListener(mcastListener);
+        routeService.removeListener(routeListener);
+
+        neighbourResolutionService.unregisterNeighbourHandlers(appId);
+
+        processor = null;
+        linkListener = null;
+        deviceListener = null;
+        groupHandlerMap.clear();
+
+        dsNextObjStore.destroy();
+        vlanNextObjStore.destroy();
+        portNextObjStore.destroy();
+        tunnelStore.destroy();
+        policyStore.destroy();
+
+        mcastHandler.terminate();
+        log.info("Stopped");
+    }
+
+    @Modified
+    private void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        if (properties == null) {
+            return;
+        }
+
+        String strActiveProving = Tools.get(properties, "activeProbing");
+        boolean expectActiveProbing = Boolean.parseBoolean(strActiveProving);
+
+        if (expectActiveProbing != activeProbing) {
+            activeProbing = expectActiveProbing;
+            log.info("{} active probing", activeProbing ? "Enabling" : "Disabling");
+        }
+    }
+
+    @Override
+    public List<Tunnel> getTunnels() {
+        return tunnelHandler.getTunnels();
+    }
+
+    @Override
+    public TunnelHandler.Result createTunnel(Tunnel tunnel) {
+        return tunnelHandler.createTunnel(tunnel);
+    }
+
+    @Override
+    public TunnelHandler.Result removeTunnel(Tunnel tunnel) {
+        for (Policy policy: policyHandler.getPolicies()) {
+            if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+                TunnelPolicy tunnelPolicy = (TunnelPolicy) policy;
+                if (tunnelPolicy.tunnelId().equals(tunnel.id())) {
+                    log.warn("Cannot remove the tunnel used by a policy");
+                    return TunnelHandler.Result.TUNNEL_IN_USE;
+                }
+            }
+        }
+        return tunnelHandler.removeTunnel(tunnel);
+    }
+
+    @Override
+    public PolicyHandler.Result removePolicy(Policy policy) {
+        return policyHandler.removePolicy(policy);
+    }
+
+    @Override
+    public PolicyHandler.Result createPolicy(Policy policy) {
+        return policyHandler.createPolicy(policy);
+    }
+
+    @Override
+    public List<Policy> getPolicies() {
+        return policyHandler.getPolicies();
+    }
+
+    @Override
+    public List<L2Tunnel> getL2Tunnels() {
+        return l2TunnelHandler.getL2Tunnels();
+    }
+
+    @Override
+    public List<L2TunnelPolicy> getL2Policies() {
+        return l2TunnelHandler.getL2Policies();
+    }
+
+    @Override
+    public L2TunnelHandler.Result addPseudowire(String tunnelId, String pwLabel, String cP1,
+                                                 String cP1InnerVlan, String cP1OuterVlan, String cP2,
+                                                 String cP2InnerVlan, String cP2OuterVlan,
+                                                 String mode, String sdTag) {
+        // Try to inject an empty Pwaas config if it is not found for the first time
+        PwaasConfig config = cfgService.getConfig(appId(), PwaasConfig.class);
+        if (config == null) {
+            log.debug("Pwaas config not found. Try to create an empty one.");
+            cfgService.applyConfig(appId(), PwaasConfig.class, new ObjectMapper().createObjectNode());
+            config = cfgService.getConfig(appId(), PwaasConfig.class);
+        }
+
+        ObjectNode object = config.addPseudowire(tunnelId, pwLabel,
+                                                 cP1, cP1InnerVlan, cP1OuterVlan,
+                                                 cP2, cP2InnerVlan, cP2OuterVlan,
+                                                 mode, sdTag);
+        if (object == null) {
+            log.warn("Could not add pseudowire to the configuration!");
+            return L2TunnelHandler.Result.ADDITION_ERROR;
+        }
+
+        // inform everyone about the valid change in the pw configuration
+        cfgService.applyConfig(appId(), PwaasConfig.class, object);
+        return L2TunnelHandler.Result.SUCCESS;
+    }
+
+    @Override
+    public L2TunnelHandler.Result removePseudowire(String pwId) {
+
+        PwaasConfig config = cfgService.getConfig(appId(), PwaasConfig.class);
+        if (config == null) {
+            log.warn("Configuration for Pwaas class could not be found!");
+            return L2TunnelHandler.Result.CONFIG_NOT_FOUND;
+        }
+
+        ObjectNode object = config.removePseudowire(pwId);
+        if (object == null) {
+            log.warn("Could not delete pseudowire from configuration!");
+            return L2TunnelHandler.Result.REMOVAL_ERROR;
+        }
+
+        // sanity check, this should never fail since we removed a pw
+        // and we always check when we update the configuration
+        config.isValid();
+
+        // inform everyone
+        cfgService.applyConfig(appId(), PwaasConfig.class, object);
+
+        return L2TunnelHandler.Result.SUCCESS;
+    }
+
+    @Override
+    public void rerouteNetwork() {
+        cfgListener.configureNetwork();
+    }
+
+    @Override
+    public Map<DeviceId, Set<IpPrefix>> getDeviceSubnetMap() {
+        Map<DeviceId, Set<IpPrefix>> deviceSubnetMap = Maps.newHashMap();
+        deviceConfiguration.getRouters().forEach(device ->
+            deviceSubnetMap.put(device, deviceConfiguration.getSubnets(device)));
+        return deviceSubnetMap;
+    }
+
+
+    @Override
+    public ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEcmpSpg() {
+        if (defaultRoutingHandler != null) {
+            return defaultRoutingHandler.getCurrentEmcpSpgMap();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public ImmutableMap<DestinationSetNextObjectiveStoreKey, NextNeighbors> getDestinationSet() {
+        if (dsNextObjStore != null) {
+            return ImmutableMap.copyOf(dsNextObjStore.entrySet());
+        } else {
+            return ImmutableMap.of();
+        }
+    }
+
+    @Override
+    public void verifyGroups(DeviceId id) {
+        DefaultGroupHandler gh = groupHandlerMap.get(id);
+        if (gh != null) {
+            gh.triggerBucketCorrector();
+        }
+    }
+
+    @Override
+    public ImmutableMap<Link, Boolean> getSeenLinks() {
+        return linkHandler.getSeenLinks();
+    }
+
+    @Override
+    public ImmutableMap<DeviceId, Set<PortNumber>> getDownedPortState() {
+        return linkHandler.getDownedPorts();
+    }
+
+    @Override
+    public Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp) {
+        return mcastHandler.getMcastNextIds(mcastIp);
+    }
+
+    @Override
+    public Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp) {
+        return mcastHandler.getMcastRoles(mcastIp);
+    }
+
+    @Override
+    public Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp) {
+        return mcastHandler.getMcastPaths(mcastIp);
+    }
+
+    /**
+     * Extracts the application ID from the manager.
+     *
+     * @return application ID
+     */
+    public ApplicationId appId() {
+        return appId;
+    }
+
+    /**
+     * Returns the device configuration.
+     *
+     * @return device configuration
+     */
+    public DeviceConfiguration deviceConfiguration() {
+        return deviceConfiguration;
+    }
+
+    /**
+     * Per device next objective ID store with (device id + destination set) as key.
+     * Used to keep track on MPLS group information.
+     *
+     * @return next objective ID store
+     */
+    public EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+                dsNextObjStore() {
+        return dsNextObjStore;
+    }
+
+    /**
+     * Per device next objective ID store with (device id + vlanid) as key.
+     * Used to keep track on L2 flood group information.
+     *
+     * @return vlan next object store
+     */
+    public EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer> vlanNextObjStore() {
+        return vlanNextObjStore;
+    }
+
+    /**
+     * Per device next objective ID store with (device id + port + treatment + meta) as key.
+     * Used to keep track on L2 interface group and L3 unicast group information.
+     *
+     * @return port next object store.
+     */
+    public EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer> portNextObjStore() {
+        return portNextObjStore;
+    }
+
+    /**
+     * Returns the MPLS-ECMP configuration which indicates whether ECMP on
+     * labeled packets should be programmed or not.
+     *
+     * @return MPLS-ECMP value
+     */
+    public boolean getMplsEcmp() {
+        SegmentRoutingAppConfig segmentRoutingAppConfig = cfgService
+                .getConfig(this.appId, SegmentRoutingAppConfig.class);
+        return segmentRoutingAppConfig != null && segmentRoutingAppConfig.mplsEcmp();
+    }
+
+    /**
+     * Returns the tunnel object with the tunnel ID.
+     *
+     * @param tunnelId Tunnel ID
+     * @return Tunnel reference
+     */
+    public Tunnel getTunnel(String tunnelId) {
+        return tunnelHandler.getTunnel(tunnelId);
+    }
+
+    // TODO Consider moving these to InterfaceService
+    /**
+     * Returns untagged VLAN configured on given connect point.
+     * <p>
+     * Only returns the first match if there are multiple untagged VLAN configured
+     * on the connect point.
+     *
+     * @param connectPoint connect point
+     * @return untagged VLAN or null if not configured
+     */
+    VlanId getUntaggedVlanId(ConnectPoint connectPoint) {
+        return interfaceService.getInterfacesByPort(connectPoint).stream()
+                .filter(intf -> !intf.vlanUntagged().equals(VlanId.NONE))
+                .map(Interface::vlanUntagged)
+                .findFirst().orElse(null);
+    }
+
+    /**
+     * Returns tagged VLAN configured on given connect point.
+     * <p>
+     * Returns all matches if there are multiple tagged VLAN configured
+     * on the connect point.
+     *
+     * @param connectPoint connect point
+     * @return tagged VLAN or empty set if not configured
+     */
+    Set<VlanId> getTaggedVlanId(ConnectPoint connectPoint) {
+        Set<Interface> interfaces = interfaceService.getInterfacesByPort(connectPoint);
+        return interfaces.stream()
+                .map(Interface::vlanTagged)
+                .flatMap(Set::stream)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Returns native VLAN configured on given connect point.
+     * <p>
+     * Only returns the first match if there are multiple native VLAN configured
+     * on the connect point.
+     *
+     * @param connectPoint connect point
+     * @return native VLAN or null if not configured
+     */
+    VlanId getNativeVlanId(ConnectPoint connectPoint) {
+        Set<Interface> interfaces = interfaceService.getInterfacesByPort(connectPoint);
+        return interfaces.stream()
+                .filter(intf -> !intf.vlanNative().equals(VlanId.NONE))
+                .map(Interface::vlanNative)
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Returns internal VLAN for untagged hosts on given connect point.
+     * <p>
+     * The internal VLAN is either vlan-untagged for an access port,
+     * or vlan-native for a trunk port.
+     *
+     * @param connectPoint connect point
+     * @return internal VLAN or null if both vlan-untagged and vlan-native are undefined
+     */
+    VlanId getInternalVlanId(ConnectPoint connectPoint) {
+        VlanId untaggedVlanId = getUntaggedVlanId(connectPoint);
+        VlanId nativeVlanId = getNativeVlanId(connectPoint);
+        return untaggedVlanId != null ? untaggedVlanId : nativeVlanId;
+    }
+
+    /**
+     * Returns optional pair device ID of given device.
+     *
+     * @param deviceId device ID
+     * @return optional pair device ID. Might be empty if pair device is not configured
+     */
+    Optional<DeviceId> getPairDeviceId(DeviceId deviceId) {
+        SegmentRoutingDeviceConfig deviceConfig =
+                cfgService.getConfig(deviceId, SegmentRoutingDeviceConfig.class);
+        return Optional.ofNullable(deviceConfig).map(SegmentRoutingDeviceConfig::pairDeviceId);
+    }
+    /**
+     * Returns optional pair device local port of given device.
+     *
+     * @param deviceId device ID
+     * @return optional pair device ID. Might be empty if pair device is not configured
+     */
+    Optional<PortNumber> getPairLocalPorts(DeviceId deviceId) {
+        SegmentRoutingDeviceConfig deviceConfig =
+                cfgService.getConfig(deviceId, SegmentRoutingDeviceConfig.class);
+        return Optional.ofNullable(deviceConfig).map(SegmentRoutingDeviceConfig::pairLocalPort);
+    }
+
+    /**
+     * Determine if current instance is the master of given connect point.
+     *
+     * @param cp connect point
+     * @return true if current instance is the master of given connect point
+     */
+    public boolean isMasterOf(ConnectPoint cp) {
+        boolean isMaster = mastershipService.isLocalMaster(cp.deviceId());
+        if (!isMaster) {
+            log.debug(NOT_MASTER, cp);
+        }
+        return isMaster;
+    }
+
+    /**
+     * Returns locations of given resolved route.
+     *
+     * @param resolvedRoute resolved route
+     * @return locations of nexthop. Might be empty if next hop is not found
+     */
+    Set<ConnectPoint> nextHopLocations(ResolvedRoute resolvedRoute) {
+        HostId hostId = HostId.hostId(resolvedRoute.nextHopMac(), resolvedRoute.nextHopVlan());
+        return Optional.ofNullable(hostService.getHost(hostId))
+                .map(Host::locations).orElse(Sets.newHashSet())
+                .stream().map(l -> (ConnectPoint) l).collect(Collectors.toSet());
+    }
+
+    /**
+     * Returns vlan port map of given device.
+     *
+     * @param deviceId device id
+     * @return vlan-port multimap
+     */
+    public Multimap<VlanId, PortNumber> getVlanPortMap(DeviceId deviceId) {
+        HashMultimap<VlanId, PortNumber> vlanPortMap = HashMultimap.create();
+
+        interfaceService.getInterfaces().stream()
+                .filter(intf -> intf.connectPoint().deviceId().equals(deviceId))
+                .forEach(intf -> {
+                    vlanPortMap.put(intf.vlanUntagged(), intf.connectPoint().port());
+                    intf.vlanTagged().forEach(vlanTagged ->
+                        vlanPortMap.put(vlanTagged, intf.connectPoint().port())
+                    );
+                    vlanPortMap.put(intf.vlanNative(), intf.connectPoint().port());
+                });
+        vlanPortMap.removeAll(VlanId.NONE);
+
+        return vlanPortMap;
+    }
+
+    /**
+     * Returns the next objective ID for the given vlan id. It is expected
+     * that the next-objective has been pre-created from configuration.
+     *
+     * @param deviceId Device ID
+     * @param vlanId VLAN ID
+     * @return next objective ID or -1 if it was not found
+     */
+    int getVlanNextObjectiveId(DeviceId deviceId, VlanId vlanId) {
+        if (groupHandlerMap.get(deviceId) != null) {
+            log.trace("getVlanNextObjectiveId query in device {}", deviceId);
+            return groupHandlerMap.get(deviceId).getVlanNextObjectiveId(vlanId);
+        } else {
+            log.warn("getVlanNextObjectiveId query - groupHandler for "
+                    + "device {} not found", deviceId);
+            return -1;
+        }
+    }
+
+    /**
+     * Returns the next objective ID for the given portNumber, given the treatment.
+     * There could be multiple different treatments to the same outport, which
+     * would result in different objectives. If the next object does not exist,
+     * and should be created, a new one is created and its id is returned.
+     *
+     * @param deviceId Device ID
+     * @param portNum port number on device for which NextObjective is queried
+     * @param treatment the actions to apply on the packets (should include outport)
+     * @param meta metadata passed into the creation of a Next Objective if necessary
+     * @param createIfMissing true if a next object should be created if not found
+     * @return next objective ID or -1 if an error occurred during retrieval or creation
+     */
+    public int getPortNextObjectiveId(DeviceId deviceId, PortNumber portNum,
+                                      TrafficTreatment treatment,
+                                      TrafficSelector meta,
+                                      boolean createIfMissing) {
+        DefaultGroupHandler ghdlr = groupHandlerMap.get(deviceId);
+        if (ghdlr != null) {
+            return ghdlr.getPortNextObjectiveId(portNum, treatment, meta, createIfMissing);
+        } else {
+            log.warn("getPortNextObjectiveId query - groupHandler for device {}"
+                    + " not found", deviceId);
+            return -1;
+        }
+    }
+
+    /**
+     * Returns the group handler object for the specified device id.
+     *
+     * @param devId the device identifier
+     * @return the groupHandler object for the device id, or null if not found
+     */
+    DefaultGroupHandler getGroupHandler(DeviceId devId) {
+        return groupHandlerMap.get(devId);
+    }
+
+    /**
+     * Returns the default routing handler object.
+     *
+     * @return the default routing handler object
+     */
+    public DefaultRoutingHandler getRoutingHandler() {
+        return defaultRoutingHandler;
+    }
+
+    private class InternalPacketProcessor implements PacketProcessor {
+        @Override
+        public void process(PacketContext context) {
+
+            if (context.isHandled()) {
+                return;
+            }
+
+            InboundPacket pkt = context.inPacket();
+            Ethernet ethernet = pkt.parsed();
+
+            if (ethernet == null) {
+                return;
+            }
+
+            log.trace("Rcvd pktin from {}: {}", context.inPacket().receivedFrom(),
+                      ethernet);
+            if (ethernet.getEtherType() == TYPE_ARP) {
+                log.warn("Received unexpected ARP packet on {}",
+                         context.inPacket().receivedFrom());
+                log.trace("{}", ethernet);
+                return;
+            } else if (ethernet.getEtherType() == Ethernet.TYPE_IPV4) {
+                IPv4 ipv4Packet = (IPv4) ethernet.getPayload();
+                //ipHandler.addToPacketBuffer(ipv4Packet);
+                if (ipv4Packet.getProtocol() == IPv4.PROTOCOL_ICMP) {
+                    icmpHandler.processIcmp(ethernet, pkt.receivedFrom());
+                } else {
+                    // NOTE: We don't support IP learning at this moment so this
+                    //       is not necessary. Also it causes duplication of DHCP packets.
+                    // ipHandler.processPacketIn(ipv4Packet, pkt.receivedFrom());
+                }
+            } else if (ethernet.getEtherType() == Ethernet.TYPE_IPV6) {
+                IPv6 ipv6Packet = (IPv6) ethernet.getPayload();
+                //ipHandler.addToPacketBuffer(ipv6Packet);
+                // We deal with the packet only if the packet is a ICMP6 ECHO/REPLY
+                if (ipv6Packet.getNextHeader() == IPv6.PROTOCOL_ICMP6) {
+                    ICMP6 icmp6Packet = (ICMP6) ipv6Packet.getPayload();
+                    if (icmp6Packet.getIcmpType() == ICMP6.ECHO_REQUEST ||
+                            icmp6Packet.getIcmpType() == ICMP6.ECHO_REPLY) {
+                        icmpHandler.processIcmpv6(ethernet, pkt.receivedFrom());
+                    } else {
+                        log.trace("Received ICMPv6 0x{} - not handled",
+                                Integer.toHexString(icmp6Packet.getIcmpType() & 0xff));
+                    }
+                } else {
+                   // NOTE: We don't support IP learning at this moment so this
+                   //       is not necessary. Also it causes duplication of DHCPv6 packets.
+                   // ipHandler.processPacketIn(ipv6Packet, pkt.receivedFrom());
+                }
+            }
+        }
+    }
+
+    private class InternalLinkListener implements LinkListener {
+        @Override
+        public void event(LinkEvent event) {
+            if (event.type() == LinkEvent.Type.LINK_ADDED ||
+                    event.type() == LinkEvent.Type.LINK_UPDATED ||
+                    event.type() == LinkEvent.Type.LINK_REMOVED) {
+                log.debug("Event {} received from Link Service", event.type());
+                scheduleEventHandlerIfNotScheduled(event);
+            }
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            switch (event.type()) {
+            case DEVICE_ADDED:
+            case PORT_UPDATED:
+            case PORT_ADDED:
+            case DEVICE_UPDATED:
+            case DEVICE_AVAILABILITY_CHANGED:
+                log.trace("Event {} received from Device Service", event.type());
+                scheduleEventHandlerIfNotScheduled(event);
+                break;
+            default:
+            }
+        }
+    }
+
+    @SuppressWarnings("rawtypes")
+    private void scheduleEventHandlerIfNotScheduled(Event event) {
+        synchronized (THREAD_SCHED_LOCK) {
+            eventQueue.add(event);
+            numOfEventsQueued++;
+
+            if ((numOfHandlerScheduled - numOfHandlerExecution) == 0) {
+                //No pending scheduled event handling threads. So start a new one.
+                eventHandlerFuture = executorService
+                        .schedule(eventHandler, 100, TimeUnit.MILLISECONDS);
+                numOfHandlerScheduled++;
+            }
+            log.trace("numOfEventsQueued {}, numOfEventHandlerScheduled {}",
+                      numOfEventsQueued,
+                      numOfHandlerScheduled);
+        }
+    }
+
+    private class InternalEventHandler implements Runnable {
+        @Override
+        public void run() {
+            while (true) {
+                try {
+                    @SuppressWarnings("rawtypes")
+                    Event event;
+                    synchronized (THREAD_SCHED_LOCK) {
+                        if (!eventQueue.isEmpty()) {
+                            event = eventQueue.poll();
+                            numOfEventsExecuted++;
+                        } else {
+                            numOfHandlerExecution++;
+                            log.debug("numOfHandlerExecution {} numOfEventsExecuted {}",
+                                      numOfHandlerExecution, numOfEventsExecuted);
+                            break;
+                        }
+                    }
+                    if (event.type() == LinkEvent.Type.LINK_ADDED ||
+                            event.type() == LinkEvent.Type.LINK_UPDATED) {
+                        linkHandler.processLinkAdded((Link) event.subject());
+                    } else if (event.type() == LinkEvent.Type.LINK_REMOVED) {
+                        linkHandler.processLinkRemoved((Link) event.subject());
+                    } else if (event.type() == DeviceEvent.Type.DEVICE_ADDED ||
+                            event.type() == DeviceEvent.Type.DEVICE_AVAILABILITY_CHANGED ||
+                            event.type() == DeviceEvent.Type.DEVICE_UPDATED) {
+                        DeviceId deviceId = ((Device) event.subject()).id();
+                        if (deviceService.isAvailable(deviceId)) {
+                            log.info("Processing device event {} for available device {}",
+                                     event.type(), ((Device) event.subject()).id());
+                            processDeviceAdded((Device) event.subject());
+                        } else {
+                            log.info("Processing device event {} for unavailable device {}",
+                                     event.type(), ((Device) event.subject()).id());
+                            processDeviceRemoved((Device) event.subject());
+                        }
+                    } else if (event.type() == DeviceEvent.Type.PORT_ADDED) {
+                        // typically these calls come when device is added first time
+                        // so port filtering rules are handled at the device_added event.
+                        // port added calls represent all ports on the device,
+                        // enabled or not.
+                        log.trace("** PORT ADDED {}/{} -> {}",
+                                  ((DeviceEvent) event).subject().id(),
+                                  ((DeviceEvent) event).port().number(),
+                                  event.type());
+                    } else if (event.type() == DeviceEvent.Type.PORT_UPDATED) {
+                        // these calls happen for every subsequent event
+                        // ports enabled, disabled, switch goes away, comes back
+                        log.info("** PORT UPDATED {}/{} -> {}",
+                                 event.subject(),
+                                 ((DeviceEvent) event).port(),
+                                 event.type());
+                        processPortUpdated(((Device) event.subject()),
+                                           ((DeviceEvent) event).port());
+                    } else {
+                        log.warn("Unhandled event type: {}", event.type());
+                    }
+                } catch (Exception e) {
+                    log.error("SegmentRouting event handler thread thrown an exception: {}",
+                              e.getMessage(), e);
+                }
+            }
+        }
+    }
+
+    void processDeviceAdded(Device device) {
+        log.info("** DEVICE ADDED with ID {}", device.id());
+
+        // NOTE: Punt ARP/NDP even when the device is not configured.
+        //       Host learning without network config is required for CORD config generator.
+        routingRulePopulator.populateIpPunts(device.id());
+        routingRulePopulator.populateArpNdpPunts(device.id());
+
+        if (deviceConfiguration == null || !deviceConfiguration.isConfigured(device.id())) {
+            log.warn("Device configuration unavailable. Device {} will be "
+                    + "processed after configuration.", device.id());
+            return;
+        }
+        processDeviceAddedInternal(device.id());
+    }
+
+    private void processDeviceAddedInternal(DeviceId deviceId) {
+        // Irrespective of whether the local is a MASTER or not for this device,
+        // we need to create a SR-group-handler instance. This is because in a
+        // multi-instance setup, any instance can initiate forwarding/next-objectives
+        // for any switch (even if this instance is a SLAVE or not even connected
+        // to the switch). To handle this, a default-group-handler instance is necessary
+        // per switch.
+        log.debug("Current groupHandlerMap devs: {}", groupHandlerMap.keySet());
+        if (groupHandlerMap.get(deviceId) == null) {
+            DefaultGroupHandler groupHandler;
+            try {
+                groupHandler = DefaultGroupHandler.
+                        createGroupHandler(deviceId,
+                                appId,
+                                deviceConfiguration,
+                                linkService,
+                                flowObjectiveService,
+                                this);
+            } catch (DeviceConfigNotFoundException e) {
+                log.warn(e.getMessage() + " Aborting processDeviceAdded.");
+                return;
+            }
+            log.debug("updating groupHandlerMap with new grpHdlr for device: {}",
+                    deviceId);
+            groupHandlerMap.put(deviceId, groupHandler);
+        }
+
+        if (mastershipService.isLocalMaster(deviceId)) {
+            defaultRoutingHandler.populatePortAddressingRules(deviceId);
+            hostHandler.init(deviceId);
+            xConnectHandler.init(deviceId);
+            DefaultGroupHandler groupHandler = groupHandlerMap.get(deviceId);
+            groupHandler.createGroupsFromVlanConfig();
+            routingRulePopulator.populateSubnetBroadcastRule(deviceId);
+        }
+
+        appCfgHandler.init(deviceId);
+        routeHandler.init(deviceId);
+    }
+
+    private void processDeviceRemoved(Device device) {
+        dsNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+                .forEach(entry -> dsNextObjStore.remove(entry.getKey()));
+        vlanNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+                .forEach(entry -> vlanNextObjStore.remove(entry.getKey()));
+        portNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(device.id()))
+                .forEach(entry -> portNextObjStore.remove(entry.getKey()));
+        linkHandler.processDeviceRemoved(device);
+
+        DefaultGroupHandler gh = groupHandlerMap.remove(device.id());
+        if (gh != null) {
+            gh.shutdown();
+        }
+        // Note that a switch going down is associated with all of its links
+        // going down as well, but it is treated as a single switch down event
+        // while the link-downs are ignored.
+        defaultRoutingHandler
+            .populateRoutingRulesForLinkStatusChange(null, null, device.id());
+        defaultRoutingHandler.purgeEcmpGraph(device.id());
+        mcastHandler.processDeviceDown(device.id());
+        xConnectHandler.removeDevice(device.id());
+    }
+
+    private void processPortUpdated(Device device, Port port) {
+        if (deviceConfiguration == null || !deviceConfiguration.isConfigured(device.id())) {
+            log.warn("Device configuration uploading. Not handling port event for"
+                    + "dev: {} port: {}", device.id(), port.number());
+            return;
+        }
+
+        if (!mastershipService.isLocalMaster(device.id()))  {
+            log.debug("Not master for dev:{} .. not handling port updated event"
+                    + "for port {}", device.id(), port.number());
+            return;
+        }
+
+        // first we handle filtering rules associated with the port
+        if (port.isEnabled()) {
+            log.info("Switchport {}/{} enabled..programming filters",
+                     device.id(), port.number());
+            routingRulePopulator.processSinglePortFilters(device.id(), port.number(), true);
+        } else {
+            log.info("Switchport {}/{} disabled..removing filters",
+                     device.id(), port.number());
+            routingRulePopulator.processSinglePortFilters(device.id(), port.number(), false);
+        }
+
+        // portUpdated calls are for ports that have gone down or up. For switch
+        // to switch ports, link-events should take care of any re-routing or
+        // group editing necessary for port up/down. Here we only process edge ports
+        // that are already configured.
+        ConnectPoint cp = new ConnectPoint(device.id(), port.number());
+        VlanId untaggedVlan = getUntaggedVlanId(cp);
+        VlanId nativeVlan = getNativeVlanId(cp);
+        Set<VlanId> taggedVlans = getTaggedVlanId(cp);
+
+        if (untaggedVlan == null && nativeVlan == null && taggedVlans.isEmpty()) {
+            log.debug("Not handling port updated event for non-edge port (unconfigured) "
+                    + "dev/port: {}/{}", device.id(), port.number());
+            return;
+        }
+        if (untaggedVlan != null) {
+            processEdgePort(device, port, untaggedVlan, true);
+        }
+        if (nativeVlan != null) {
+            processEdgePort(device, port, nativeVlan, true);
+        }
+        if (!taggedVlans.isEmpty()) {
+            taggedVlans.forEach(tag -> processEdgePort(device, port, tag, false));
+        }
+    }
+
+    private void processEdgePort(Device device, Port port, VlanId vlanId,
+                                 boolean popVlan) {
+        boolean portUp = port.isEnabled();
+        if (portUp) {
+            log.info("Device:EdgePort {}:{} is enabled in vlan: {}", device.id(),
+                     port.number(), vlanId);
+            hostHandler.processPortUp(new ConnectPoint(device.id(), port.number()));
+        } else {
+            log.info("Device:EdgePort {}:{} is disabled in vlan: {}", device.id(),
+                     port.number(), vlanId);
+        }
+
+        DefaultGroupHandler groupHandler = groupHandlerMap.get(device.id());
+        if (groupHandler != null) {
+            groupHandler.processEdgePort(port.number(), vlanId, popVlan, portUp);
+        } else {
+            log.warn("Group handler not found for dev:{}. Not handling edge port"
+                    + " {} event for port:{}", device.id(),
+                    (portUp) ? "UP" : "DOWN", port.number());
+        }
+    }
+
+    private void createOrUpdateDeviceConfiguration() {
+        if (deviceConfiguration == null) {
+            log.info("Creating new DeviceConfiguration");
+            deviceConfiguration = new DeviceConfiguration(this);
+        } else {
+            log.info("Updating DeviceConfiguration");
+            deviceConfiguration.updateConfig();
+        }
+    }
+
+    /**
+     * Registers the given connect point with the NRS, this is necessary
+     * to receive the NDP and ARP packets from the NRS.
+     *
+     * @param portToRegister connect point to register
+     */
+    public void registerConnectPoint(ConnectPoint portToRegister) {
+        neighbourResolutionService.registerNeighbourHandler(
+                portToRegister,
+                neighbourHandler,
+                appId
+        );
+    }
+
+    private class InternalConfigListener implements NetworkConfigListener {
+        private static final long PROGRAM_DELAY = 2;
+        SegmentRoutingManager srManager;
+
+        /**
+         * Constructs the internal network config listener.
+         *
+         * @param srManager segment routing manager
+         */
+        InternalConfigListener(SegmentRoutingManager srManager) {
+            this.srManager = srManager;
+        }
+
+        /**
+         * Reads network config and initializes related data structure accordingly.
+         */
+        void configureNetwork() {
+            log.info("Configuring network ...");
+            createOrUpdateDeviceConfiguration();
+
+            arpHandler = new ArpHandler(srManager);
+            icmpHandler = new IcmpHandler(srManager);
+            ipHandler = new IpHandler(srManager);
+            routingRulePopulator = new RoutingRulePopulator(srManager);
+            defaultRoutingHandler = new DefaultRoutingHandler(srManager);
+
+            tunnelHandler = new TunnelHandler(linkService, deviceConfiguration,
+                                              groupHandlerMap, tunnelStore);
+            policyHandler = new PolicyHandler(appId, deviceConfiguration,
+                                              flowObjectiveService,
+                                              tunnelHandler, policyStore);
+            // add a small delay to absorb multiple network config added notifications
+            if (!programmingScheduled.get()) {
+                log.info("Buffering config calls for {} secs", PROGRAM_DELAY);
+                programmingScheduled.set(true);
+                executorService.schedule(new ConfigChange(), PROGRAM_DELAY,
+                                         TimeUnit.SECONDS);
+            }
+            mcastHandler.init();
+        }
+
+        @Override
+        public void event(NetworkConfigEvent event) {
+            // TODO move this part to NetworkConfigEventHandler
+            if (event.configClass().equals(SegmentRoutingDeviceConfig.class)) {
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                        log.info("Segment Routing Device Config added for {}", event.subject());
+                        configureNetwork();
+                        break;
+                    case CONFIG_UPDATED:
+                        log.info("Segment Routing Config updated for {}", event.subject());
+                        createOrUpdateDeviceConfiguration();
+                        // TODO support dynamic configuration
+                        break;
+                    default:
+                        break;
+                }
+            } else if (event.configClass().equals(InterfaceConfig.class)) {
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                        log.info("Interface Config added for {}", event.subject());
+                        configureNetwork();
+                        break;
+                    case CONFIG_UPDATED:
+                        log.info("Interface Config updated for {}", event.subject());
+                        createOrUpdateDeviceConfiguration();
+
+                        // Following code will be uncommented when [CORD-634] is fully implemented.
+                        // [CORD-634] Add dynamic config support for interfaces
+                        updateInterface((InterfaceConfig) event.config().get(),
+                                        (InterfaceConfig) event.prevConfig().get());
+                        // TODO support dynamic configuration
+                        break;
+                    default:
+                        break;
+                }
+            } else if (event.configClass().equals(SegmentRoutingAppConfig.class)) {
+                checkState(appCfgHandler != null, "NetworkConfigEventHandler is not initialized");
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                        appCfgHandler.processAppConfigAdded(event);
+                        break;
+                    case CONFIG_UPDATED:
+                        appCfgHandler.processAppConfigUpdated(event);
+                        break;
+                    case CONFIG_REMOVED:
+                        appCfgHandler.processAppConfigRemoved(event);
+                        break;
+                    default:
+                        break;
+                }
+                log.info("App config event .. configuring network");
+                configureNetwork();
+            } else if (event.configClass().equals(XConnectConfig.class)) {
+                checkState(xConnectHandler != null, "XConnectHandler is not initialized");
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                        xConnectHandler.processXConnectConfigAdded(event);
+                        break;
+                    case CONFIG_UPDATED:
+                        xConnectHandler.processXConnectConfigUpdated(event);
+                        break;
+                    case CONFIG_REMOVED:
+                        xConnectHandler.processXConnectConfigRemoved(event);
+                        break;
+                    default:
+                        break;
+                }
+            } else if (event.configClass().equals(PwaasConfig.class)) {
+                checkState(l2TunnelHandler != null, "DefaultL2TunnelHandler is not initialized");
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                        l2TunnelHandler.processPwaasConfigAdded(event);
+                        break;
+                    case CONFIG_UPDATED:
+                        l2TunnelHandler.processPwaasConfigUpdated(event);
+                        break;
+                    case CONFIG_REMOVED:
+                        l2TunnelHandler.processPwaasConfigRemoved(event);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.type() == CONFIG_REGISTERED ||
+                    event.type() == CONFIG_UNREGISTERED) {
+                log.debug("Ignore event {} due to type mismatch", event);
+                return false;
+            }
+
+            if (!event.configClass().equals(SegmentRoutingDeviceConfig.class) &&
+                    !event.configClass().equals(SegmentRoutingAppConfig.class) &&
+                    !event.configClass().equals(InterfaceConfig.class) &&
+                    !event.configClass().equals(XConnectConfig.class) &&
+                    !event.configClass().equals(PwaasConfig.class)) {
+                log.debug("Ignore event {} due to class mismatch", event);
+                return false;
+            }
+
+            return true;
+        }
+
+        private final class ConfigChange implements Runnable {
+            @Override
+            public void run() {
+                programmingScheduled.set(false);
+                log.info("Reacting to config changes after buffer delay");
+                for (Device device : deviceService.getDevices()) {
+                    processDeviceAdded(device);
+                }
+                defaultRoutingHandler.startPopulationProcess();
+            }
+        }
+    }
+
+    private class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            switch (event.type()) {
+                case HOST_ADDED:
+                    hostHandler.processHostAddedEvent(event);
+                    break;
+                case HOST_MOVED:
+                    hostHandler.processHostMovedEvent(event);
+                    routeHandler.processHostMovedEvent(event);
+                    break;
+                case HOST_REMOVED:
+                    hostHandler.processHostRemovedEvent(event);
+                    break;
+                case HOST_UPDATED:
+                    hostHandler.processHostUpdatedEvent(event);
+                    break;
+                default:
+                    log.warn("Unsupported host event type: {}", event.type());
+                    break;
+            }
+        }
+    }
+
+    private class InternalMcastListener implements McastListener {
+        @Override
+        public void event(McastEvent event) {
+            switch (event.type()) {
+                case SOURCE_ADDED:
+                    mcastHandler.processSourceAdded(event);
+                    break;
+                case SINK_ADDED:
+                    mcastHandler.processSinkAdded(event);
+                    break;
+                case SINK_REMOVED:
+                    mcastHandler.processSinkRemoved(event);
+                    break;
+                case ROUTE_REMOVED:
+                    mcastHandler.processRouteRemoved(event);
+                    break;
+                case ROUTE_ADDED:
+                default:
+                    break;
+            }
+        }
+    }
+
+    private class InternalRouteEventListener implements RouteListener {
+        @Override
+        public void event(RouteEvent event) {
+            switch (event.type()) {
+                case ROUTE_ADDED:
+                    routeHandler.processRouteAdded(event);
+                    break;
+                case ROUTE_UPDATED:
+                    routeHandler.processRouteUpdated(event);
+                    break;
+                case ROUTE_REMOVED:
+                    routeHandler.processRouteRemoved(event);
+                    break;
+                case ALTERNATIVE_ROUTES_CHANGED:
+                    routeHandler.processAlternativeRoutesChanged(event);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private void updateInterface(InterfaceConfig conf, InterfaceConfig prevConf) {
+        try {
+            Set<Interface> intfs = conf.getInterfaces();
+            Set<Interface> prevIntfs = prevConf.getInterfaces();
+
+            // Now we only handle one interface config at each port.
+            if (intfs.size() != 1 || prevIntfs.size() != 1) {
+                log.warn("Interface update aborted - one at a time is allowed, " +
+                                 "but {} / {}(prev) received.", intfs.size(), prevIntfs.size());
+                return;
+            }
+
+            Interface intf = intfs.stream().findFirst().get();
+            Interface prevIntf = prevIntfs.stream().findFirst().get();
+
+            DeviceId deviceId = intf.connectPoint().deviceId();
+            PortNumber portNum = intf.connectPoint().port();
+
+            if (!mastershipService.isLocalMaster(deviceId)) {
+                log.debug("CONFIG_UPDATED event for interfaces should be " +
+                                  "handled by master node for device {}", deviceId);
+                return;
+            }
+
+            removeSubnetConfig(prevIntf.connectPoint(),
+                               Sets.difference(new HashSet<>(prevIntf.ipAddressesList()),
+                                               new HashSet<>(intf.ipAddressesList())));
+
+            if (prevIntf.vlanNative() != VlanId.NONE && !intf.vlanNative().equals(prevIntf.vlanNative())) {
+                // RemoveVlanNative
+                updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanNative(), true, false);
+            }
+
+            if (!prevIntf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
+                // RemoveVlanTagged
+                prevIntf.vlanTagged().stream().filter(i -> !intf.vlanTagged().contains(i)).forEach(
+                        vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, false)
+                );
+            }
+
+            if (prevIntf.vlanUntagged() != VlanId.NONE && !intf.vlanUntagged().equals(prevIntf.vlanUntagged())) {
+                // RemoveVlanUntagged
+                updateVlanConfigInternal(deviceId, portNum, prevIntf.vlanUntagged(), true, false);
+            }
+
+            if (intf.vlanNative() != VlanId.NONE && !prevIntf.vlanNative().equals(intf.vlanNative())) {
+                // AddVlanNative
+                updateVlanConfigInternal(deviceId, portNum, intf.vlanNative(), true, true);
+            }
+
+            if (!intf.vlanTagged().isEmpty() && !intf.vlanTagged().equals(prevIntf.vlanTagged())) {
+                // AddVlanTagged
+                intf.vlanTagged().stream().filter(i -> !prevIntf.vlanTagged().contains(i)).forEach(
+                        vlanId -> updateVlanConfigInternal(deviceId, portNum, vlanId, false, true)
+                );
+            }
+
+            if (intf.vlanUntagged() != VlanId.NONE && !prevIntf.vlanUntagged().equals(intf.vlanUntagged())) {
+                // AddVlanUntagged
+                updateVlanConfigInternal(deviceId, portNum, intf.vlanUntagged(), true, true);
+            }
+            addSubnetConfig(prevIntf.connectPoint(),
+                            Sets.difference(new HashSet<>(intf.ipAddressesList()),
+                                            new HashSet<>(prevIntf.ipAddressesList())));
+        } catch (ConfigException e) {
+            log.error("Error in configuration");
+        }
+    }
+
+    private void updateVlanConfigInternal(DeviceId deviceId, PortNumber portNum,
+                                          VlanId vlanId, boolean pushVlan, boolean install) {
+        DefaultGroupHandler grpHandler = getGroupHandler(deviceId);
+        if (grpHandler == null) {
+            log.warn("Failed to retrieve group handler for device {}", deviceId);
+            return;
+        }
+
+        // Update filtering objective for a single port
+        routingRulePopulator.updateSinglePortFilters(deviceId, portNum, pushVlan, vlanId, install);
+
+        // Update filtering objective for multicast ingress port
+        mcastHandler.updateFilterToDevice(deviceId, portNum, vlanId, install);
+
+        int nextId = getVlanNextObjectiveId(deviceId, vlanId);
+
+        if (nextId != -1 && !install) {
+            // Update next objective for a single port as an output port
+            // Remove a single port from L2FG
+            grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+            // Remove L2 Bridging rule and L3 Unicast rule to the host
+            hostHandler.processIntfVlanUpdatedEvent(deviceId, portNum, vlanId, pushVlan, install);
+            // Remove broadcast forwarding rule and corresponding L2FG for VLAN
+            // only if there is no port configured on that VLAN ID
+            if (!getVlanPortMap(deviceId).containsKey(vlanId)) {
+                // Remove broadcast forwarding rule for the VLAN
+                routingRulePopulator.updateSubnetBroadcastRule(deviceId, vlanId, install);
+                // Remove L2FG for VLAN
+                grpHandler.removeBcastGroupFromVlan(deviceId, portNum, vlanId, pushVlan);
+            } else {
+                // Remove L2IG of the port
+                grpHandler.removePortNextObjective(deviceId, portNum, vlanId, pushVlan);
+            }
+        } else if (install) {
+            if (nextId != -1) {
+                // Add a single port to L2FG
+                grpHandler.updateGroupFromVlanConfiguration(portNum, Collections.singleton(vlanId), nextId, install);
+            } else {
+                // Create L2FG for VLAN
+                grpHandler.createBcastGroupFromVlan(vlanId, Collections.singleton(portNum));
+                routingRulePopulator.updateSubnetBroadcastRule(deviceId, vlanId, install);
+            }
+            hostHandler.processIntfVlanUpdatedEvent(deviceId, portNum, vlanId, pushVlan, install);
+        } else {
+            log.warn("Failed to retrieve next objective for vlan {} in device {}:{}", vlanId, deviceId, portNum);
+        }
+    }
+
+    private void removeSubnetConfig(ConnectPoint cp, Set<InterfaceIpAddress> ipAddressSet) {
+        Set<IpPrefix> ipPrefixSet = ipAddressSet.stream().
+                map(InterfaceIpAddress::subnetAddress).collect(Collectors.toSet());
+
+        Set<InterfaceIpAddress> deviceIntfIpAddrs = interfaceService.getInterfaces().stream()
+                .filter(intf -> intf.connectPoint().deviceId().equals(cp.deviceId()))
+                .filter(intf -> !intf.connectPoint().equals(cp))
+                .flatMap(intf -> intf.ipAddressesList().stream())
+                .collect(Collectors.toSet());
+        // 1. Partial subnet population
+        // Remove routing rules for removed subnet from previous configuration,
+        // which does not also exist in other interfaces in the same device
+        Set<IpPrefix> deviceIpPrefixSet = deviceIntfIpAddrs.stream()
+                .map(InterfaceIpAddress::subnetAddress)
+                .collect(Collectors.toSet());
+
+        defaultRoutingHandler.revokeSubnet(
+                ipPrefixSet.stream()
+                        .filter(ipPrefix -> !deviceIpPrefixSet.contains(ipPrefix))
+                        .collect(Collectors.toSet()));
+
+        // 2. Interface IP punts
+        // Remove IP punts for old Intf address
+        Set<IpAddress> deviceIpAddrs = deviceIntfIpAddrs.stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .collect(Collectors.toSet());
+        ipAddressSet.stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .filter(interfaceIpAddress -> !deviceIpAddrs.contains(interfaceIpAddress))
+                .forEach(interfaceIpAddress ->
+                                 routingRulePopulator.revokeSingleIpPunts(
+                                         cp.deviceId(), interfaceIpAddress));
+
+        // 3. Host unicast routing rule
+        // Remove unicast routing rule
+        hostHandler.processIntfIpUpdatedEvent(cp, ipPrefixSet, false);
+    }
+
+    private void addSubnetConfig(ConnectPoint cp, Set<InterfaceIpAddress> ipAddressSet) {
+        Set<IpPrefix> ipPrefixSet = ipAddressSet.stream().
+                map(InterfaceIpAddress::subnetAddress).collect(Collectors.toSet());
+
+        Set<InterfaceIpAddress> deviceIntfIpAddrs = interfaceService.getInterfaces().stream()
+                .filter(intf -> intf.connectPoint().deviceId().equals(cp.deviceId()))
+                .filter(intf -> !intf.connectPoint().equals(cp))
+                .flatMap(intf -> intf.ipAddressesList().stream())
+                .collect(Collectors.toSet());
+        // 1. Partial subnet population
+        // Add routing rules for newly added subnet, which does not also exist in
+        // other interfaces in the same device
+        Set<IpPrefix> deviceIpPrefixSet = deviceIntfIpAddrs.stream()
+                .map(InterfaceIpAddress::subnetAddress)
+                .collect(Collectors.toSet());
+
+        defaultRoutingHandler.populateSubnet(
+                Collections.singleton(cp),
+                ipPrefixSet.stream()
+                        .filter(ipPrefix -> !deviceIpPrefixSet.contains(ipPrefix))
+                        .collect(Collectors.toSet()));
+
+        // 2. Interface IP punts
+        // Add IP punts for new Intf address
+        Set<IpAddress> deviceIpAddrs = deviceIntfIpAddrs.stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .collect(Collectors.toSet());
+        ipAddressSet.stream()
+                .map(InterfaceIpAddress::ipAddress)
+                .filter(interfaceIpAddress -> !deviceIpAddrs.contains(interfaceIpAddress))
+                .forEach(interfaceIpAddress ->
+                                 routingRulePopulator.populateSingleIpPunts(
+                                         cp.deviceId(), interfaceIpAddress));
+
+        // 3. Host unicast routing rule
+        // Add unicast routing rule
+        hostHandler.processIntfIpUpdatedEvent(cp, ipPrefixSet, true);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java
new file mode 100644
index 0000000..f170fae
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourDispatcher.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.neighbour.NeighbourMessageHandler;
+import org.onosproject.net.host.HostService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This handler dispatches to the appropriate handlers the
+ * neighbour discovery protocols.
+ */
+public class SegmentRoutingNeighbourDispatcher implements NeighbourMessageHandler {
+
+    private static Logger log = LoggerFactory.getLogger(SegmentRoutingNeighbourDispatcher.class);
+    private SegmentRoutingManager manager;
+
+    /**
+     * Create a segment routing neighbour dispatcher.
+     *
+     * @param segmentRoutingManager the segment routing manager
+     */
+    public SegmentRoutingNeighbourDispatcher(SegmentRoutingManager segmentRoutingManager) {
+        this.manager = segmentRoutingManager;
+    }
+
+    @Override
+    public void handleMessage(NeighbourMessageContext context, HostService hostService) {
+        log.trace("Received {} packet on {}: {}", context.protocol(),
+                  context.inPort(), context.packet());
+        switch (context.protocol()) {
+            case ARP:
+                if (this.manager.arpHandler != null) {
+                    this.manager.arpHandler.processPacketIn(context, hostService);
+                }
+                break;
+            case NDP:
+                if (this.manager.icmpHandler != null) {
+                    this.manager.icmpHandler.processPacketIn(context, hostService);
+                }
+                break;
+            default:
+                log.warn("Unknown protocol", context.protocol());
+        }
+    }
+
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
new file mode 100644
index 0000000..f03e0be
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingNeighbourHandler.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.neighbour.NeighbourMessageContext;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * This handler provides provides useful functions to the
+ * neighbour handlers (ARP, NDP).
+ */
+public class SegmentRoutingNeighbourHandler {
+
+    private static Logger log = LoggerFactory.getLogger(SegmentRoutingNeighbourHandler.class);
+
+    protected SegmentRoutingManager srManager;
+    protected DeviceConfiguration config;
+
+    /**
+     * Creates an SegmentRoutingNeighbourHandler object.
+     *
+     * @param srManager SegmentRoutingManager object
+     */
+    public SegmentRoutingNeighbourHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.config = checkNotNull(srManager.deviceConfiguration);
+    }
+
+    /**
+     * Creates an SegmentRoutingNeighbourHandler object.
+     */
+    public SegmentRoutingNeighbourHandler() {
+        this.srManager = null;
+        this.config = null;
+    }
+
+    /**
+     * Retrieve router (device) info.
+     *
+     * @param mac where to copy the mac
+     * @param ip where to copy the ip
+     * @param deviceId the device id
+     * @param targetAddress the target address
+     * @return true if it was possible to get the necessary info.
+     * False for errors
+     */
+    protected boolean getSenderInfo(byte[] mac,
+                                 byte[] ip,
+                                 DeviceId deviceId,
+                                 IpAddress targetAddress) {
+        byte[] senderMacAddress;
+        byte[] senderIpAddress;
+        IpAddress sender;
+        try {
+            senderMacAddress = config.getDeviceMac(deviceId).toBytes();
+            if (targetAddress.isIp4()) {
+                sender = config.getRouterIpAddressForASubnetHost(targetAddress.getIp4Address());
+            } else {
+                sender = config.getRouterIpAddressForASubnetHost(targetAddress.getIp6Address());
+            }
+            // If sender is null we abort.
+            if (sender == null) {
+                log.warn("Sender ip is null. Aborting getSenderInfo");
+                return false;
+            }
+            senderIpAddress = sender.toOctets();
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting getSenderInfo");
+            return false;
+        }
+        System.arraycopy(senderMacAddress, 0, mac, 0, senderMacAddress.length);
+        System.arraycopy(senderIpAddress, 0, ip, 0, senderIpAddress.length);
+        return true;
+    }
+
+    /**
+     * Utility to send a ND reply using the supplied information.
+     *
+     * @param pkt the request
+     * @param targetMac the target mac
+     * @param hostService the host service
+     */
+    protected void sendResponse(NeighbourMessageContext pkt, MacAddress targetMac, HostService hostService) {
+        HostId dstId = HostId.hostId(pkt.srcMac(), pkt.vlan());
+        Host dst = hostService.getHost(dstId);
+        if (dst == null) {
+            log.warn("Cannot send {} response to host {} - does not exist in the store",
+                     pkt.protocol(), dstId);
+            return;
+        }
+        pkt.reply(targetMac);
+    }
+
+    /**
+     * Flood to all ports in the same subnet.
+     *
+     * @param packet packet to be flooded
+     * @param inPort where the packet comes from
+     * @param targetAddress the target address
+     */
+    protected void flood(Ethernet packet, ConnectPoint inPort, IpAddress targetAddress) {
+        try {
+            srManager.deviceConfiguration
+                    .getSubnetPortsMap(inPort.deviceId()).forEach((subnet, ports) -> {
+                if (subnet.contains(targetAddress)) {
+                    ports.stream()
+                            .filter(port -> port != inPort.port())
+                            .forEach(port -> {
+                                forward(packet, new ConnectPoint(inPort.deviceId(), port));
+                            });
+                }
+            });
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage()
+                             + " Cannot flood in subnet as device config not available"
+                             + " for device: " + inPort.deviceId());
+        }
+    }
+
+    /*
+     * Floods only on the port which have been configured with the subnet
+     * of the target address. The in port is excluded.
+     *
+     * @param pkt the ndp/arp packet and context information
+     */
+    protected void flood(NeighbourMessageContext pkt) {
+        try {
+            srManager.deviceConfiguration
+                    .getSubnetPortsMap(pkt.inPort().deviceId()).forEach((subnet, ports) -> {
+                if (subnet.contains(pkt.target())) {
+                    ports.stream()
+                            .filter(port -> port != pkt.inPort().port())
+                            .forEach(port -> {
+                                ConnectPoint outPoint = new ConnectPoint(
+                                        pkt.inPort().deviceId(),
+                                        port
+                                );
+                                pkt.forward(outPoint);
+                            });
+                }
+            });
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage()
+                             + " Cannot flood in subnet as device config not available"
+                             + " for device: " + pkt.inPort().deviceId());
+        }
+    }
+
+    /**
+     * Packet out to given port.
+     *
+     * Note: In current implementation, we expect all communication with
+     * end hosts within a subnet to be untagged.
+     * <p>
+     * For those pipelines that internally assigns a VLAN, the VLAN tag will be
+     * removed before egress.
+     * <p>
+     * For those pipelines that do not assign internal VLAN, the packet remains
+     * untagged.
+     *
+     * @param packet packet to be forwarded
+     * @param outPort where the packet should be forwarded
+     */
+    private void forward(Ethernet packet, ConnectPoint outPort) {
+        ByteBuffer buf = ByteBuffer.wrap(packet.serialize());
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.setOutput(outPort.port());
+        srManager.packetService.emit(new DefaultOutboundPacket(outPort.deviceId(),
+                                                               tbuilder.build(), buf));
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
new file mode 100644
index 0000000..6f115e4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/SegmentRoutingService.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+
+import com.google.common.collect.ImmutableMap;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Segment Routing Service for REST API.
+ */
+public interface SegmentRoutingService {
+    /**
+     * VLAN cross-connect priority.
+     */
+    int XCONNECT_PRIORITY = 1000;
+
+    /**
+     * Default flow priority.
+     */
+    int DEFAULT_PRIORITY = 100;
+
+    /**
+     * Minimum IP priority.
+     *
+     * Should &lt; 0 such that priority of /0 will not conflict with lowest
+     * priority default entries.
+     */
+    int MIN_IP_PRIORITY = 10;
+
+    /**
+     * Subnet flooding flow priority.
+     */
+    int FLOOD_PRIORITY = 5;
+
+    /**
+     * Returns all tunnels.
+     *
+     * @return list of tunnels
+     */
+    List<Tunnel> getTunnels();
+
+    /**
+     * Creates a tunnel.
+     *
+     * @param tunnel tunnel reference to create
+     * @return WRONG_PATH if the tunnel path is wrong, ID_EXISTS if the tunnel ID
+     * exists already, TUNNEL_EXISTS if the same tunnel exists, INTERNAL_ERROR
+     * if the tunnel creation failed internally, SUCCESS if the tunnel is created
+     * successfully
+     */
+    TunnelHandler.Result createTunnel(Tunnel tunnel);
+
+    /**
+     * Returns all policies.
+     *
+     * @return list of policy
+     */
+    List<Policy> getPolicies();
+
+    /**
+     * Returns all l2 tunnels of pseudowires.
+     *
+     * @return list of l2 tunnels
+     */
+    List<L2Tunnel> getL2Tunnels();
+
+    /**
+     * Returns all l2 policie of pseudowires.
+     *
+     * @return list of l2 policies.
+     */
+    List<L2TunnelPolicy> getL2Policies();
+
+    /**
+     * Removes pw. Essentially updates configuration for PwaasConfig
+     * and sends event for removal. The rest are handled by DefaultL2TunnelHandler
+     *
+     * @param pwId The pseudowire id
+     * @return SUCCESS if operation successful or a descriptive error otherwise.
+     */
+    L2TunnelHandler.Result removePseudowire(String pwId);
+
+    /**
+     * Adds a Pseudowire to the configuration.
+     *
+     * @param tunnelId The pseudowire id
+     * @param pwLabel Pw label
+     * @param cP1 Connection Point 1 of pw
+     * @param cP1InnerVlan Outer vlan of cp2
+     * @param cP1OuterVlan Outer vlan of cp1
+     * @param cP2 Connection Point 2 of pw
+     * @param cP2InnerVlan Inner vlan of cp2
+     * @param cP2OuterVlan Outer vlan of cp1
+     * @param mode Mode of pw
+     * @param sdTag Service Delimiting tag of pw
+     * @return SUCCESS if operation is successful or a descriptive error otherwise.
+     */
+    L2TunnelHandler.Result addPseudowire(String tunnelId, String pwLabel, String cP1,
+                                          String cP1InnerVlan, String cP1OuterVlan, String cP2,
+                                          String cP2InnerVlan, String cP2OuterVlan,
+                                          String mode, String sdTag);
+
+    /**
+     * Creates a policy.
+     *
+     * @param policy policy reference to create
+     * @return ID_EXISTS if the same policy ID exists,
+     *  POLICY_EXISTS if the same policy exists, TUNNEL_NOT_FOUND if the tunnel
+     *  does not exists, UNSUPPORTED_TYPE if the policy type is not supported,
+     *  SUCCESS if the policy is created successfully.
+     */
+    PolicyHandler.Result createPolicy(Policy policy);
+
+    /**
+     * Removes a tunnel.
+     *
+     * @param tunnel tunnel reference to remove
+     * @return TUNNEL_NOT_FOUND if the tunnel to remove does not exists,
+     * INTERNAL_ERROR if the tunnel creation failed internally, SUCCESS
+     * if the tunnel is created successfully.
+     */
+    TunnelHandler.Result removeTunnel(Tunnel tunnel);
+
+    /**
+     * Removes a policy.
+     *
+     * @param policy policy reference to remove
+     * @return POLICY_NOT_FOUND if the policy to remove does not exists,
+     * SUCCESS if it is removed successfully
+     */
+    PolicyHandler.Result removePolicy(Policy policy);
+
+    /**
+     * Use current state of the network to repopulate forwarding rules.
+     *
+     */
+    void rerouteNetwork();
+
+    /**
+     * Returns device-subnet mapping.
+     *
+     * @return device-subnet mapping
+     */
+    Map<DeviceId, Set<IpPrefix>> getDeviceSubnetMap();
+
+    /**
+     * Returns the current ECMP shortest path graph in this controller instance.
+     *
+     * @return ECMP shortest path graph
+     */
+    ImmutableMap<DeviceId, EcmpShortestPathGraph> getCurrentEcmpSpg();
+
+    /**
+     * Returns the destinatiomSet-NextObjective store contents.
+     *
+     * @return current contents of the destinationSetNextObjectiveStore
+     */
+    ImmutableMap<DestinationSetNextObjectiveStoreKey, NextNeighbors> getDestinationSet();
+
+    /**
+     * Triggers the verification of all ECMP groups in the specified device.
+     * Adjusts the group buckets if verification finds that there are more or less
+     * buckets than what should be there.
+     *
+     * @param id the device identifier
+     */
+    void verifyGroups(DeviceId id);
+
+    /**
+     * Returns the internal link state as seen by this instance of the
+     * controller.
+     *
+     * @return the internal link state
+     */
+    ImmutableMap<Link, Boolean> getSeenLinks();
+
+    /**
+     * Returns the ports administratively disabled by the controller.
+     *
+     * @return a map of devices and port numbers for administratively disabled
+     *         ports. Does not include ports manually disabled by the operator.
+     */
+    ImmutableMap<DeviceId, Set<PortNumber>> getDownedPortState();
+
+   /**
+     * Returns the associated next ids to the mcast groups or to the single
+     * group if mcastIp is present.
+     *
+     * @param mcastIp the group ip
+     * @return the mapping mcastIp-device to next id
+     */
+    Map<McastStoreKey, Integer> getMcastNextIds(IpAddress mcastIp);
+
+    /**
+     * Returns the associated roles to the mcast groups or to the single
+     * group if mcastIp is present.
+     *
+     * @param mcastIp the group ip
+     * @return the mapping mcastIp-device to mcast role
+     */
+    Map<McastStoreKey, McastHandler.McastRole> getMcastRoles(IpAddress mcastIp);
+
+    /**
+     * Returns the associated paths to the mcast group.
+     *
+     * @param mcastIp the group ip
+     * @return the mapping egress point to mcast path
+     */
+    Map<ConnectPoint, List<ConnectPoint>> getMcastPaths(IpAddress mcastIp);
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java
new file mode 100644
index 0000000..470662f
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/Tunnel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import java.util.List;
+
+/**
+ * Tunnel interface.
+ */
+public interface Tunnel {
+
+    /**
+     * Returns the tunnel ID.
+     *
+     * @return tunnel ID
+     */
+    String id();
+
+    /**
+     * Returns Segment IDs for the tunnel including source and destination.
+     *
+     * @return List of Node ID
+     */
+    List<Integer> labelIds();
+
+    /**
+     * Returns the group ID for the tunnel.
+     *
+     * @return group ID
+     */
+    int groupId();
+
+    /**
+     * Sets group ID for the tunnel.
+     *
+     * @param groupId group identifier
+     */
+    void setGroupId(int groupId);
+
+    /**
+     * Sets the flag to allow to remove the group or not.
+     *
+     * @param ok the flag; true - allow to remove
+     */
+    void allowToRemoveGroup(boolean ok);
+
+    /**
+     * Checks if it is allowed to remove the group for the tunnel.
+     *
+     * @return true if allowed, false otherwise
+     */
+    boolean isAllowedToRemoveGroup();
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java
new file mode 100644
index 0000000..e18b920
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelHandler.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Tunnel Handler.
+ */
+public class TunnelHandler {
+    protected final Logger log = getLogger(getClass());
+
+    private final DeviceConfiguration config;
+    private final EventuallyConsistentMap<String, Tunnel> tunnelStore;
+    private Map<DeviceId, DefaultGroupHandler> groupHandlerMap;
+    private LinkService linkService;
+
+    /**
+     * Result of tunnel creation or removal.
+     */
+    public enum Result {
+        /**
+         * Success.
+         */
+        SUCCESS,
+
+        /**
+         * More than one router needs to specified to created a tunnel.
+         */
+        WRONG_PATH,
+
+        /**
+         * The same tunnel exists already.
+         */
+        TUNNEL_EXISTS,
+
+        /**
+         * The same tunnel ID exists already.
+         */
+        ID_EXISTS,
+
+        /**
+         * Tunnel not found.
+         */
+        TUNNEL_NOT_FOUND,
+
+        /**
+         * Cannot remove the tunnel used by a policy.
+         */
+        TUNNEL_IN_USE,
+
+        /**
+         * Failed to create/remove groups for the tunnel.
+         */
+        INTERNAL_ERROR
+    }
+
+    /**
+     * Constructs tunnel handler.
+     *
+     * @param linkService link service
+     * @param deviceConfiguration device configuration
+     * @param groupHandlerMap group handler map
+     * @param tunnelStore tunnel store
+     */
+    public TunnelHandler(LinkService linkService,
+                         DeviceConfiguration deviceConfiguration,
+                         Map<DeviceId, DefaultGroupHandler> groupHandlerMap,
+                         EventuallyConsistentMap<String, Tunnel> tunnelStore) {
+        this.linkService = linkService;
+        this.config = deviceConfiguration;
+        this.groupHandlerMap = groupHandlerMap;
+        this.tunnelStore = tunnelStore;
+    }
+
+    /**
+     * Creates a tunnel.
+     *
+     * @param tunnel tunnel reference to create a tunnel
+     * @return WRONG_PATH if the tunnel path is wrong, ID_EXISTS if the tunnel ID
+     * exists already, TUNNEL_EXISTS if the same tunnel exists, INTERNAL_ERROR
+     * if the tunnel creation failed internally, SUCCESS if the tunnel is created
+     * successfully
+     */
+    public Result createTunnel(Tunnel tunnel) {
+
+        if (tunnel.labelIds().isEmpty() || tunnel.labelIds().size() < 3) {
+            log.error("More than one router needs to specified to created a tunnel");
+            return Result.WRONG_PATH;
+        }
+
+        if (tunnelStore.containsKey(tunnel.id())) {
+            log.warn("The same tunnel ID exists already");
+            return Result.ID_EXISTS;
+        }
+
+        if (tunnelStore.containsValue(tunnel)) {
+            log.warn("The same tunnel exists already");
+            return Result.TUNNEL_EXISTS;
+        }
+
+        int groupId = createGroupsForTunnel(tunnel);
+        if (groupId < 0) {
+            log.error("Failed to create groups for the tunnel");
+            return Result.INTERNAL_ERROR;
+        }
+
+        tunnel.setGroupId(groupId);
+        tunnelStore.put(tunnel.id(), tunnel);
+
+        return Result.SUCCESS;
+    }
+
+    /**
+     * Removes the tunnel with the tunnel ID given.
+     *
+     * @param tunnelInfo tunnel information to delete tunnels
+     * @return TUNNEL_NOT_FOUND if the tunnel to remove does not exists,
+     * INTERNAL_ERROR if the tunnel creation failed internally, SUCCESS
+     * if the tunnel is created successfully.
+     */
+    public Result removeTunnel(Tunnel tunnelInfo) {
+
+        Tunnel tunnel = tunnelStore.get(tunnelInfo.id());
+        if (tunnel != null) {
+            DeviceId deviceId = config.getDeviceId(tunnel.labelIds().get(0));
+            if (tunnel.isAllowedToRemoveGroup()) {
+                if (groupHandlerMap.get(deviceId).removeGroup(tunnel.groupId())) {
+                    tunnelStore.remove(tunnel.id());
+                } else {
+                    log.error("Failed to remove the tunnel {}", tunnelInfo.id());
+                    return Result.INTERNAL_ERROR;
+                }
+            } else {
+                log.debug("The group is not removed because it is being used.");
+                tunnelStore.remove(tunnel.id());
+            }
+        } else {
+            log.error("No tunnel found for tunnel ID {}", tunnelInfo.id());
+            return Result.TUNNEL_NOT_FOUND;
+        }
+
+        return Result.SUCCESS;
+    }
+
+    /**
+     * Returns the tunnel with the tunnel ID given.
+     *
+     * @param tid Tunnel ID
+     * @return Tunnel reference
+     */
+    public Tunnel getTunnel(String tid) {
+        return tunnelStore.get(tid);
+    }
+
+    /**
+     * Returns all tunnels.
+     *
+     * @return list of Tunnels
+     */
+    public List<Tunnel> getTunnels() {
+        List<Tunnel> tunnels = new ArrayList<>();
+        tunnelStore.values().forEach(tunnel -> tunnels.add(
+                new DefaultTunnel((DefaultTunnel) tunnel)));
+
+        return tunnels;
+    }
+
+    private int createGroupsForTunnel(Tunnel tunnel) {
+
+        Set<Integer> portNumbers;
+        final int groupError = -1;
+
+        DeviceId deviceId = config.getDeviceId(tunnel.labelIds().get(0));
+        if (deviceId == null) {
+            log.warn("No device found for SID {}", tunnel.labelIds().get(0));
+            return groupError;
+        } else if (groupHandlerMap.get(deviceId) == null) {
+            log.warn("group handler not found for {}", deviceId);
+            return groupError;
+        }
+        Set<DeviceId> deviceIds = new HashSet<>();
+        int sid = tunnel.labelIds().get(1);
+        if (config.isAdjacencySid(deviceId, sid)) {
+            portNumbers = config.getPortsForAdjacencySid(deviceId, sid);
+            for (Link link: linkService.getDeviceEgressLinks(deviceId)) {
+                for (Integer port: portNumbers) {
+                    if (link.src().port().toLong() == port) {
+                        deviceIds.add(link.dst().deviceId());
+                    }
+                }
+            }
+        } else {
+            deviceIds.add(config.getDeviceId(sid));
+        }
+        // For these NeighborSet isMpls is meaningless.
+        DestinationSet ns = new DestinationSet(false,
+                                         tunnel.labelIds().get(2),
+                                         DeviceId.NONE);
+
+        // If the tunnel reuses any existing groups, then tunnel handler
+        // should not remove the group.
+        if (groupHandlerMap.get(deviceId).hasNextObjectiveId(ns)) {
+            tunnel.allowToRemoveGroup(false);
+        } else {
+            tunnel.allowToRemoveGroup(true);
+        }
+
+        return groupHandlerMap.get(deviceId).getNextObjectiveId(ns, null, null, true);
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java
new file mode 100644
index 0000000..5df82af
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/TunnelPolicy.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2015-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.segmentrouting;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Tunnel Policy.
+ */
+public final class TunnelPolicy implements Policy {
+
+    private final Type type;
+    private final String id;
+    private final int priority;
+    private final String tunnelId;
+    private String dstIp;
+    private String srcIp;
+    private String ipProto;
+    private short srcPort;
+    private short dstPort;
+
+    private TunnelPolicy(String policyId, Type type, int priority, String tunnelId, String srcIp,
+                         String dstIp, String ipProto, short srcPort, short dstPort) {
+        this.id = checkNotNull(policyId);
+        this.type = type;
+        this.tunnelId = tunnelId;
+        this.priority = priority;
+        this.dstIp = dstIp;
+        this.srcIp = srcIp;
+        this.ipProto = ipProto;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+
+    }
+
+    /**
+     * Creates a TunnelPolicy reference.
+     *
+     * @param p TunnelPolicy reference
+     */
+    public TunnelPolicy(TunnelPolicy p) {
+        this.id = p.id;
+        this.type = p.type;
+        this.tunnelId = p.tunnelId;
+        this.priority = p.priority;
+        this.srcIp = p.srcIp;
+        this.dstIp = p.dstIp;
+        this.ipProto = p.ipProto;
+        this.srcPort = p.srcPort;
+        this.dstPort = p.dstPort;
+    }
+
+    /**
+     * Returns the TunnelPolicy builder reference.
+     *
+     * @return TunnelPolicy builder
+     */
+    public static TunnelPolicy.Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public String id() {
+        return this.id;
+    }
+
+    @Override
+    public int priority() {
+        return priority;
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public String srcIp() {
+        return srcIp;
+    }
+
+    @Override
+    public String dstIp() {
+        return dstIp;
+    }
+
+    @Override
+    public String ipProto() {
+        return ipProto;
+    }
+
+    @Override
+    public short srcPort() {
+        return srcPort;
+    }
+
+    @Override
+    public short dstPort() {
+        return dstPort;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof TunnelPolicy) {
+            TunnelPolicy that = (TunnelPolicy) o;
+            // We do not compare the policy ID
+            if (this.type.equals(that.type) &&
+                    this.tunnelId.equals(that.tunnelId) &&
+                    this.priority == that.priority &&
+                    this.srcIp.equals(that.srcIp) &&
+                    this.dstIp.equals(that.dstIp) &&
+                    this.srcPort == that.srcPort &&
+                    this.dstPort == that.dstPort &&
+                    this.ipProto.equals(that.ipProto)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, tunnelId, srcIp, dstIp, ipProto,
+                srcPort, dstPort, priority);
+    }
+
+    /**
+     * Returns the tunnel ID of the policy.
+     *
+     * @return Tunnel ID
+     */
+    public String tunnelId() {
+        return this.tunnelId;
+    }
+
+
+    /**
+     * Tunnel Policy Builder.
+     */
+    public static final class Builder {
+
+        private String id;
+        private Type type;
+        private int priority;
+        private String tunnelId;
+        private String dstIp;
+        private String srcIp;
+        private String ipProto;
+        private short srcPort;
+        private short dstPort;
+
+        /**
+         * Sets the policy Id.
+         *
+         * @param id policy Id
+         * @return Builder object
+         */
+        public Builder setPolicyId(String id) {
+            this.id = id;
+
+            return this;
+        }
+
+        /**
+         * Sets the policy type.
+         *
+         * @param type policy type
+         * @return Builder object
+         */
+        public Builder setType(Type type) {
+            this.type = type;
+
+            return this;
+        }
+
+        /**
+         * Sets the source IP address.
+         *
+         * @param srcIp source IP address
+         * @return Builder object
+         */
+        public Builder setSrcIp(String srcIp) {
+            this.srcIp = srcIp;
+
+            return this;
+        }
+
+        /**
+         * Sets the destination IP address.
+         *
+         * @param dstIp destination IP address
+         * @return Builder object
+         */
+        public Builder setDstIp(String dstIp) {
+            this.dstIp = dstIp;
+
+            return this;
+        }
+
+        /**
+         * Sets the IP protocol.
+         *
+         * @param proto IP protocol
+         * @return Builder object
+         */
+        public Builder setIpProto(String proto) {
+            this.ipProto = proto;
+
+            return this;
+        }
+
+        /**
+         * Sets the source port.
+         *
+         * @param srcPort source port
+         * @return Builder object
+         */
+        public Builder setSrcPort(short srcPort) {
+            this.srcPort = srcPort;
+
+            return this;
+        }
+
+        /**
+         * Sets the destination port.
+         *
+         * @param dstPort destination port
+         * @return Builder object
+         */
+        public Builder setDstPort(short dstPort) {
+            this.dstPort = dstPort;
+
+            return this;
+        }
+
+        /**
+         * Sets the priority of the policy.
+         *
+         * @param p priority
+         * @return Builder object
+         */
+        public Builder setPriority(int p) {
+            this.priority = p;
+
+            return this;
+        }
+
+        /**
+         * Sets the tunnel Id.
+         *
+         * @param tunnelId tunnel Id
+         * @return Builder object
+         */
+        public Builder setTunnelId(String tunnelId) {
+            this.tunnelId = tunnelId;
+
+            return this;
+        }
+
+        /**
+         * Builds the policy.
+         *
+         * @return Tunnel Policy reference
+         */
+        public Policy build() {
+            return new TunnelPolicy(id, type, priority, tunnelId, srcIp, dstIp,
+                    ipProto, srcPort, dstPort);
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
new file mode 100644
index 0000000..aa60684
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/XConnectHandler.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.segmentrouting.config.XConnectConfig;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+/**
+ * Handles cross connect related events.
+ */
+public class XConnectHandler {
+    private static final Logger log = LoggerFactory.getLogger(XConnectHandler.class);
+    private static final String CONFIG_NOT_FOUND = "XConnect config not found";
+    private static final String NOT_MASTER = "Not master controller";
+    private final SegmentRoutingManager srManager;
+    private final StorageService storageService;
+    private final ConsistentMap<XConnectStoreKey, NextObjective> xConnectNextObjStore;
+    private final KryoNamespace.Builder xConnectKryo;
+
+    protected XConnectHandler(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        this.storageService = srManager.storageService;
+        xConnectKryo = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(XConnectStoreKey.class)
+                .register(NextObjContext.class);
+        xConnectNextObjStore = storageService
+                .<XConnectStoreKey, NextObjective>consistentMapBuilder()
+                .withName("onos-xconnect-nextobj-store")
+                .withSerializer(Serializer.using(xConnectKryo.build()))
+                .build();
+    }
+
+    /**
+     * Read initial XConnect for given device.
+     *
+     * @param deviceId ID of the device to be initialized
+     */
+    public void init(DeviceId deviceId) {
+        // Try to read XConnect config
+        XConnectConfig config =
+                srManager.cfgService.getConfig(srManager.appId, XConnectConfig.class);
+        if (config == null) {
+            log.info("Skip XConnect initialization: {}", CONFIG_NOT_FOUND);
+            return;
+        }
+
+        config.getXconnects(deviceId).forEach(key -> {
+            populateXConnect(key, config.getPorts(key));
+        });
+    }
+
+    /**
+     * Processes Segment Routing App Config added event.
+     *
+     * @param event network config added event
+     */
+    protected void processXConnectConfigAdded(NetworkConfigEvent event) {
+        log.info("Processing XConnect CONFIG_ADDED");
+        XConnectConfig config = (XConnectConfig) event.config().get();
+        config.getXconnects().forEach(key -> {
+            populateXConnect(key, config.getPorts(key));
+        });
+    }
+
+    /**
+     * Processes Segment Routing App Config updated event.
+     *
+     * @param event network config updated event
+     */
+    protected void processXConnectConfigUpdated(NetworkConfigEvent event) {
+        log.info("Processing XConnect CONFIG_UPDATED");
+        XConnectConfig prevConfig = (XConnectConfig) event.prevConfig().get();
+        XConnectConfig config = (XConnectConfig) event.config().get();
+        Set<XConnectStoreKey> prevKeys = prevConfig.getXconnects();
+        Set<XConnectStoreKey> keys = config.getXconnects();
+
+        Set<XConnectStoreKey> pendingRemove = prevKeys.stream()
+                .filter(key -> !keys.contains(key)).collect(Collectors.toSet());
+        Set<XConnectStoreKey> pendingAdd = keys.stream()
+                .filter(key -> !prevKeys.contains(key)).collect(Collectors.toSet());
+        Set<XConnectStoreKey> pendingUpdate = keys.stream()
+                .filter(key -> prevKeys.contains(key) &&
+                        !config.getPorts(key).equals(prevConfig.getPorts(key)))
+                .collect(Collectors.toSet());
+
+        pendingRemove.forEach(key -> {
+            revokeXConnect(key, prevConfig.getPorts(key));
+        });
+        pendingAdd.forEach(key -> {
+            populateXConnect(key, config.getPorts(key));
+        });
+        pendingUpdate.forEach(key -> {
+            updateXConnect(key, prevConfig.getPorts(key), config.getPorts(key));
+        });
+    }
+
+    /**
+     * Processes Segment Routing App Config removed event.
+     *
+     * @param event network config removed event
+     */
+    protected void processXConnectConfigRemoved(NetworkConfigEvent event) {
+        log.info("Processing XConnect CONFIG_REMOVED");
+        XConnectConfig prevConfig = (XConnectConfig) event.prevConfig().get();
+        prevConfig.getXconnects().forEach(key -> {
+            revokeXConnect(key, prevConfig.getPorts(key));
+        });
+    }
+
+    /**
+     * Checks if there is any XConnect configured on given connect point.
+     *
+     * @param cp connect point
+     * @return true if there is XConnect configured on given connect point.
+     */
+    public boolean hasXConnect(ConnectPoint cp) {
+        // Try to read XConnect config
+        XConnectConfig config =
+                srManager.cfgService.getConfig(srManager.appId, XConnectConfig.class);
+        if (config == null) {
+            log.warn("Failed to read XConnect config: {}", CONFIG_NOT_FOUND);
+            return false;
+        }
+        return config.getXconnects(cp.deviceId()).stream()
+                .anyMatch(key -> config.getPorts(key).contains(cp.port()));
+    }
+
+    /**
+     * Populates XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param ports a set of ports to be cross-connected
+     */
+    private void populateXConnect(XConnectStoreKey key, Set<PortNumber> ports) {
+        if (!srManager.mastershipService.isLocalMaster(key.deviceId())) {
+            log.info("Abort populating XConnect {}: {}", key, NOT_MASTER);
+            return;
+        }
+        populateFilter(key, ports);
+        populateFwd(key, populateNext(key, ports));
+    }
+
+    /**
+     * Populates filtering objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private void populateFilter(XConnectStoreKey key, Set<PortNumber> ports) {
+        ports.forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
+                            key, port),
+                    (objective, error) ->
+                            log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
+                                    key, port, error));
+            srManager.flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
+        });
+    }
+
+    /**
+     * Populates next objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private NextObjective populateNext(XConnectStoreKey key, Set<PortNumber> ports) {
+        NextObjective nextObj = null;
+        if (xConnectNextObjStore.containsKey(key)) {
+            nextObj = xConnectNextObjStore.get(key).value();
+            log.debug("NextObj for {} found, id={}", key, nextObj.id());
+        } else {
+            NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports);
+            ObjectiveContext nextContext = new NextObjContext(Objective.Operation.ADD, key);
+            nextObj = nextObjBuilder.add(nextContext);
+            srManager.flowObjectiveService.next(key.deviceId(), nextObj);
+            xConnectNextObjStore.put(key, nextObj);
+            log.debug("NextObj for {} not found. Creating new NextObj with id={}", key, nextObj.id());
+        }
+        return nextObj;
+    }
+
+    /**
+     * Populates forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     */
+    private void populateFwd(XConnectStoreKey key, NextObjective nextObj) {
+        ForwardingObjective.Builder fwdObjBuilder = fwdObjBuilder(key, nextObj.id());
+        ObjectiveContext fwdContext = new DefaultObjectiveContext(
+                (objective) -> log.debug("XConnect FwdObj for {} populated", key),
+                (objective, error) ->
+                        log.warn("Failed to populate XConnect FwdObj for {}: {}", key, error));
+        srManager.flowObjectiveService.forward(key.deviceId(), fwdObjBuilder.add(fwdContext));
+    }
+
+    /**
+     * Revokes XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param ports XConnect ports
+     */
+    private void revokeXConnect(XConnectStoreKey key, Set<PortNumber> ports) {
+        if (!srManager.mastershipService.isLocalMaster(key.deviceId())) {
+            log.info("Abort populating XConnect {}: {}", key, NOT_MASTER);
+            return;
+        }
+
+        revokeFilter(key, ports);
+        if (xConnectNextObjStore.containsKey(key)) {
+            NextObjective nextObj = xConnectNextObjStore.get(key).value();
+            revokeFwd(key, nextObj, null);
+            revokeNext(key, nextObj, null);
+        } else {
+            log.warn("NextObj for {} does not exist in the store.", key);
+        }
+    }
+
+    /**
+     * Revokes filtering objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param ports XConnect ports
+     */
+    private void revokeFilter(XConnectStoreKey key, Set<PortNumber> ports) {
+        ports.forEach(port -> {
+            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
+                            key, port),
+                    (objective, error) ->
+                            log.warn("Failed to revoke XConnect FilterObj for {} on port {}: {}",
+                                    key, port, error));
+            srManager.flowObjectiveService.filter(key.deviceId(), filtObjBuilder.remove(context));
+        });
+    }
+
+    /**
+     * Revokes next objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     * @param nextFuture completable future for this next objective operation
+     */
+    private void revokeNext(XConnectStoreKey key, NextObjective nextObj,
+            CompletableFuture<ObjectiveError> nextFuture) {
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous NextObj for {} removed", key);
+                if (nextFuture != null) {
+                    nextFuture.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous NextObj for {}: {}", key, error);
+                if (nextFuture != null) {
+                    nextFuture.complete(error);
+                }
+            }
+        };
+        srManager.flowObjectiveService.next(key.deviceId(),
+                (NextObjective) nextObj.copy().remove(context));
+        xConnectNextObjStore.remove(key);
+    }
+
+    /**
+     * Revokes forwarding objectives for given XConnect.
+     *
+     * @param key XConnect store key
+     * @param nextObj next objective
+     * @param fwdFuture completable future for this forwarding objective operation
+     */
+    private void revokeFwd(XConnectStoreKey key, NextObjective nextObj,
+            CompletableFuture<ObjectiveError> fwdFuture) {
+        ForwardingObjective.Builder fwdObjBuilder = fwdObjBuilder(key, nextObj.id());
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous FwdObj for {} removed", key);
+                if (fwdFuture != null) {
+                    fwdFuture.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous FwdObj for {}: {}", key, error);
+                if (fwdFuture != null) {
+                    fwdFuture.complete(error);
+                }
+            }
+        };
+        srManager.flowObjectiveService
+                .forward(key.deviceId(), fwdObjBuilder.remove(context));
+    }
+
+    /**
+     * Updates XConnect groups and flows for given key.
+     *
+     * @param key XConnect key
+     * @param prevPorts previous XConnect ports
+     * @param ports new XConnect ports
+     */
+    private void updateXConnect(XConnectStoreKey key, Set<PortNumber> prevPorts,
+            Set<PortNumber> ports) {
+        // remove old filter
+        prevPorts.stream().filter(port -> !ports.contains(port)).forEach(port -> {
+            revokeFilter(key, ImmutableSet.of(port));
+        });
+        // install new filter
+        ports.stream().filter(port -> !prevPorts.contains(port)).forEach(port -> {
+            populateFilter(key, ImmutableSet.of(port));
+        });
+
+        CompletableFuture<ObjectiveError> fwdFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> nextFuture = new CompletableFuture<>();
+
+        if (xConnectNextObjStore.containsKey(key)) {
+            NextObjective nextObj = xConnectNextObjStore.get(key).value();
+            revokeFwd(key, nextObj, fwdFuture);
+
+            fwdFuture.thenAcceptAsync(fwdStatus -> {
+                if (fwdStatus == null) {
+                    log.debug("Fwd removed. Now remove group {}", key);
+                    revokeNext(key, nextObj, nextFuture);
+                }
+            });
+
+            nextFuture.thenAcceptAsync(nextStatus -> {
+                if (nextStatus == null) {
+                    log.debug("Installing new group and flow for {}", key);
+                    populateFwd(key, populateNext(key, ports));
+                }
+            });
+        } else {
+            log.warn("NextObj for {} does not exist in the store.", key);
+        }
+    }
+
+    /**
+     * Remove all groups on given device.
+     *
+     * @param deviceId device ID
+     */
+    protected void removeDevice(DeviceId deviceId) {
+        xConnectNextObjStore.entrySet().stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                .forEach(entry -> {
+                    xConnectNextObjStore.remove(entry.getKey());
+                });
+        log.debug("{} is removed from xConnectNextObjStore", deviceId);
+    }
+
+    /**
+     * Creates a next objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param ports set of XConnect ports
+     * @return next objective builder
+     */
+    private NextObjective.Builder nextObjBuilder(XConnectStoreKey key, Set<PortNumber> ports) {
+        int nextId = srManager.flowObjectiveService.allocateNextId();
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(key.vlanId()).build();
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(srManager.appId)
+                .withMeta(metadata);
+        ports.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+        return nextObjBuilder;
+    }
+
+    /**
+     * Creates a forwarding objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param nextId next ID of the broadcast group for this XConnect key
+     * @return next objective builder
+     */
+    private ForwardingObjective.Builder fwdObjBuilder(XConnectStoreKey key, int nextId) {
+        /*
+         * Driver should treat objectives with MacAddress.NONE and !VlanId.NONE
+         * as the VLAN cross-connect broadcast rules
+         */
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        sbuilder.matchVlanId(key.vlanId());
+        sbuilder.matchEthDst(MacAddress.NONE);
+
+        ForwardingObjective.Builder fob = DefaultForwardingObjective.builder();
+        fob.withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .withSelector(sbuilder.build())
+                .nextStep(nextId)
+                .withPriority(SegmentRoutingService.XCONNECT_PRIORITY)
+                .fromApp(srManager.appId)
+                .makePermanent();
+        return fob;
+    }
+
+    /**
+     * Creates a filtering objective builder for XConnect.
+     *
+     * @param key XConnect key
+     * @param port XConnect ports
+     * @return next objective builder
+     */
+    private FilteringObjective.Builder filterObjBuilder(XConnectStoreKey key, PortNumber port) {
+        FilteringObjective.Builder fob = DefaultFilteringObjective.builder();
+        fob.withKey(Criteria.matchInPort(port))
+                .addCondition(Criteria.matchVlanId(key.vlanId()))
+                .addCondition(Criteria.matchEthDst(MacAddress.NONE))
+                .withPriority(SegmentRoutingService.XCONNECT_PRIORITY);
+        return fob.permit().fromApp(srManager.appId);
+    }
+
+    // TODO: Lambda closure in DefaultObjectiveContext cannot be serialized properly
+    //       with Kryo 3.0.3. It will be fixed in 3.0.4. By then we can use
+    //       DefaultObjectiveContext again.
+    private final class NextObjContext implements ObjectiveContext {
+        Objective.Operation op;
+        XConnectStoreKey key;
+
+        private NextObjContext(Objective.Operation op, XConnectStoreKey key) {
+            this.op = op;
+            this.key = key;
+        }
+
+        @Override
+        public void onSuccess(Objective objective) {
+            log.debug("XConnect NextObj for {} {}ED", key, op);
+        }
+
+        @Override
+        public void onError(Objective objective, ObjectiveError error) {
+            log.warn("Failed to {} XConnect NextObj for {}: {}", op, key, error);
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java
new file mode 100644
index 0000000..a364b6e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/DeviceSubnetListCommand.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Command to list device-subnet mapping in Segment Routing.
+ */
+@Command(scope = "onos", name = "sr-device-subnets",
+        description = "List device-subnet mapping in Segment Routing")
+public class DeviceSubnetListCommand extends AbstractShellCommand {
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+        printDeviceSubnetMap(srService.getDeviceSubnetMap());
+    }
+
+    private void printDeviceSubnetMap(Map<DeviceId, Set<IpPrefix>> deviceSubnetMap) {
+        deviceSubnetMap.forEach(((deviceId, ipPrefices) -> {
+            print("%s", deviceId);
+            ipPrefices.forEach(ipPrefix -> print("    %s", ipPrefix));
+        }));
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java
new file mode 100644
index 0000000..678114a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/EcmpGraphCommand.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-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.segmentrouting.cli;
+
+
+import java.util.Map;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.EcmpShortestPathGraph;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Command to read the current state of the ECMP shortest-path graph.
+ *
+ */
+@Command(scope = "onos", name = "sr-ecmp-spg",
+        description = "Displays the current ecmp shortest-path-graph in this "
+                + "controller instance")
+public class EcmpGraphCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_MAPPING = "  %s";
+
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+        printEcmpGraph(srService.getCurrentEcmpSpg());
+    }
+
+    private void printEcmpGraph(Map<DeviceId, EcmpShortestPathGraph> currentEcmpSpg) {
+        if (currentEcmpSpg == null) {
+            print(FORMAT_MAPPING, "No ECMP graph found");
+            return;
+        }
+        StringBuilder ecmp = new StringBuilder();
+        currentEcmpSpg.forEach((key, value) -> {
+            ecmp.append("\n\nRoot Device: " + key + " ECMP Paths: " + value);
+        });
+        print(FORMAT_MAPPING, ecmp.toString());
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java
new file mode 100644
index 0000000..ded4ae6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/LinkStateCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+
+/**
+ * Command to read the current state of the DestinationSetNextObjectiveStore.
+ *
+ */
+@Command(scope = "onos", name = "sr-link-state", description = "Displays the current internal link state "
+        + "noted by this instance of the controller")
+public class LinkStateCommand extends AbstractShellCommand {
+    private static final String FORMAT_MAPPING = "  %s";
+
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService = AbstractShellCommand
+                .get(SegmentRoutingService.class);
+        printLinkState(srService.getSeenLinks(),
+                       srService.getDownedPortState());
+    }
+
+    private void printLinkState(ImmutableMap<Link, Boolean> seenLinks,
+                                ImmutableMap<DeviceId, Set<PortNumber>> downedPortState) {
+        List<Link> a = Lists.newArrayList();
+        a.addAll(seenLinks.keySet());
+        a.sort(new CompLinks());
+
+        StringBuilder slbldr = new StringBuilder();
+        slbldr.append("\n Seen Links: ");
+        for (int i = 0; i < a.size(); i++) {
+            slbldr.append("\n "
+                    + (seenLinks.get(a.get(i)) == Boolean.TRUE ? "  up : "
+                                                               : "down : "));
+            slbldr.append(a.get(i).src() + " --> " + a.get(i).dst());
+        }
+        print(FORMAT_MAPPING, slbldr.toString());
+
+        StringBuilder dpbldr = new StringBuilder();
+        dpbldr.append("\n\n Administratively Disabled Ports: ");
+        downedPortState.entrySet().forEach(entry -> dpbldr
+                .append("\n " + entry.getKey() + entry.getValue()));
+        print(FORMAT_MAPPING, dpbldr.toString());
+    }
+
+    static class CompLinks implements Comparator<Link> {
+
+        @Override
+        public int compare(Link o1, Link o2) {
+            int res = o1.src().deviceId().toString()
+                    .compareTo(o2.src().deviceId().toString());
+            if (res < 0) {
+                return -1;
+            } else if (res > 0) {
+                return +1;
+            }
+            if (o1.src().port().toLong() < o2.src().port().toLong()) {
+                return -1;
+            }
+            return +1;
+        }
+
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
new file mode 100644
index 0000000..341c87d
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastNextListCommand.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import com.google.common.collect.Maps;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Command to show the list of mcast nextids.
+ */
+@Command(scope = "onos", name = "sr-mcast-next",
+        description = "Lists all mcast nextids")
+public class McastNextListCommand extends AbstractShellCommand {
+
+    // Format for group line
+    private static final String FORMAT_MAPPING = "group=%s, deviceIds-nextIds=%s";
+
+    @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+            required = false, multiValued = false)
+    String mcastIp;
+
+    @Override
+    protected void execute() {
+        // Verify mcast group
+        IpAddress mcastGroup = null;
+        if (!isNullOrEmpty(mcastIp)) {
+            mcastGroup = IpAddress.valueOf(mcastIp);
+
+        }
+        // Get SR service
+        SegmentRoutingService srService = get(SegmentRoutingService.class);
+        // Get the mapping
+        Map<McastStoreKey, Integer> keyToNextId = srService.getMcastNextIds(mcastGroup);
+        // Reduce to the set of mcast groups
+        Set<IpAddress> mcastGroups = keyToNextId.keySet().stream()
+                .map(McastStoreKey::mcastIp)
+                .collect(Collectors.toSet());
+        // Print the nextids for each group
+        mcastGroups.forEach(group -> {
+            // Create a new map for the group
+            Map<DeviceId, Integer> deviceIdNextMap = Maps.newHashMap();
+            keyToNextId.entrySet()
+                    .stream()
+                    // Filter only the elements related to this group
+                    .filter(entry -> entry.getKey().mcastIp().equals(group))
+                    // For each create a new entry in the group related map
+                    .forEach(entry -> deviceIdNextMap.put(entry.getKey().deviceId(), entry.getValue()));
+            // Print the map
+            printMcastNext(group, deviceIdNextMap);
+        });
+    }
+
+    private void printMcastNext(IpAddress mcastGroup, Map<DeviceId, Integer> deviceIdNextMap) {
+        print(FORMAT_MAPPING, mcastGroup, deviceIdNextMap);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
new file mode 100644
index 0000000..73cbb1a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/McastTreeListCommand.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018-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.segmentrouting.cli;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.McastHandler.McastRole;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.storekey.McastStoreKey;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.EGRESS;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.INGRESS;
+import static org.onosproject.segmentrouting.McastHandler.McastRole.TRANSIT;
+
+/**
+ * Command to show the list of mcast trees.
+ */
+@Command(scope = "onos", name = "sr-mcast-tree",
+        description = "Lists all mcast trees")
+public class McastTreeListCommand extends AbstractShellCommand {
+
+    // Format for group line
+    private static final String G_FORMAT_MAPPING = "group=%s, ingress=%s, transit=%s, egress=%s";
+    // Format for sink line
+    private static final String S_FORMAT_MAPPING = "\tsink=%s\tpath=%s";
+
+    @Argument(index = 0, name = "mcastIp", description = "mcast Ip",
+            required = false, multiValued = false)
+    String mcastIp;
+
+    @Override
+    protected void execute() {
+        // Verify mcast group
+        IpAddress mcastGroup = null;
+        if (!isNullOrEmpty(mcastIp)) {
+            mcastGroup = IpAddress.valueOf(mcastIp);
+
+        }
+        // Get SR service
+        SegmentRoutingService srService = get(SegmentRoutingService.class);
+        // Get the mapping
+        Map<McastStoreKey, McastRole> keyToRole = srService.getMcastRoles(mcastGroup);
+        // Reduce to the set of mcast groups
+        Set<IpAddress> mcastGroups = keyToRole.keySet().stream()
+                .map(McastStoreKey::mcastIp)
+                .collect(Collectors.toSet());
+        // Print the trees for each group
+        mcastGroups.forEach(group -> {
+            // Create a new map for the group
+            Multimap<McastRole, DeviceId> roleDeviceIdMap = ArrayListMultimap.create();
+            keyToRole.entrySet()
+                    .stream()
+                    // Filter only the elements related to this group
+                    .filter(entry -> entry.getKey().mcastIp().equals(group))
+                    // For each create a new entry in the group related map
+                    .forEach(entry -> roleDeviceIdMap.put(entry.getValue(), entry.getKey().deviceId()));
+            // Print the map
+            printMcastRole(group,
+                           roleDeviceIdMap.get(INGRESS),
+                           roleDeviceIdMap.get(TRANSIT),
+                           roleDeviceIdMap.get(EGRESS)
+            );
+            // Get sinks paths
+            Map<ConnectPoint, List<ConnectPoint>> mcastPaths = srService.getMcastPaths(group);
+            // Print the paths
+            mcastPaths.forEach(this::printMcastSink);
+        });
+    }
+
+    private void printMcastRole(IpAddress mcastGroup,
+                                Collection<DeviceId> ingress,
+                                Collection<DeviceId> transit,
+                                Collection<DeviceId> egress) {
+        print(G_FORMAT_MAPPING, mcastGroup, ingress, transit, egress);
+    }
+
+    private void printMcastSink(ConnectPoint sink, List<ConnectPoint> path) {
+        print(S_FORMAT_MAPPING, sink, path);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java
new file mode 100644
index 0000000..2b43de5
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/NextHopCommand.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-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.segmentrouting.cli;
+
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Map;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.grouphandler.NextNeighbors;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+
+/**
+ * Command to read the current state of the DestinationSetNextObjectiveStore.
+ *
+ */
+@Command(scope = "onos", name = "sr-next-hops",
+        description = "Displays the current next-hops seen by each switch "
+                + "towards a set of destinations and the next-id it maps to")
+public class NextHopCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_MAPPING = "  %s";
+
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+        printDestinationSet(srService.getDestinationSet());
+    }
+
+    private void printDestinationSet(Map<DestinationSetNextObjectiveStoreKey,
+                                      NextNeighbors> ds) {
+        ArrayList<DestinationSetNextObjectiveStoreKey> a = new ArrayList<>();
+        ds.keySet().forEach(key -> a.add(key));
+        a.sort(new Comp());
+
+        StringBuilder dsbldr = new StringBuilder();
+        for (int i = 0; i < a.size(); i++) {
+            dsbldr.append("\n " + a.get(i));
+            dsbldr.append(" --> via: " + ds.get(a.get(i)));
+        }
+        print(FORMAT_MAPPING, dsbldr.toString());
+    }
+
+    static class Comp implements Comparator<DestinationSetNextObjectiveStoreKey> {
+
+        @Override
+        public int compare(DestinationSetNextObjectiveStoreKey o1,
+                           DestinationSetNextObjectiveStoreKey o2) {
+            int res = o1.deviceId().toString().compareTo(o2.deviceId().toString());
+            if (res < 0) {
+                return -1;
+            } else if (res > 0) {
+                return +1;
+            }
+            return 0;
+        }
+
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java
new file mode 100644
index 0000000..605ba83
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyAddCommand.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.Policy;
+import org.onosproject.segmentrouting.PolicyHandler;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to add a new policy.
+ */
+@Command(scope = "onos", name = "sr-policy-add",
+        description = "Create a new policy")
+public class PolicyAddCommand extends AbstractShellCommand {
+
+    // TODO: Need to support skipping some parameters
+
+    @Argument(index = 0, name = "ID",
+            description = "policy ID",
+            required = true, multiValued = false)
+    String policyId;
+
+    @Argument(index = 1, name = "priority",
+            description = "priority",
+            required = true, multiValued = false)
+    int priority;
+
+    @Argument(index = 2, name = "src_IP",
+            description = "src IP",
+            required = false, multiValued = false)
+    String srcIp;
+
+    @Argument(index = 3, name = "src_port",
+            description = "src port",
+            required = false, multiValued = false)
+    short srcPort;
+
+    @Argument(index = 4, name = "dst_IP",
+            description = "dst IP",
+            required = false, multiValued = false)
+    String dstIp;
+
+    @Argument(index = 5, name = "dst_port",
+            description = "dst port",
+            required = false, multiValued = false)
+    short dstPort;
+
+    @Argument(index = 6, name = "proto",
+            description = "IP protocol",
+            required = false, multiValued = false)
+    String proto;
+
+    @Argument(index = 7, name = "policy_type",
+            description = "policy type",
+            required = true, multiValued = false)
+    String policyType;
+
+    @Argument(index = 8, name = "tunnel_ID",
+            description = "tunnel ID",
+            required = false, multiValued = false)
+    String tunnelId;
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        TunnelPolicy.Builder tpb = TunnelPolicy.builder().setPolicyId(policyId);
+        tpb.setPriority(priority);
+        tpb.setType(Policy.Type.valueOf(policyType));
+
+        if (srcIp != null) {
+            tpb.setSrcIp(srcIp);
+        }
+        if (dstIp != null) {
+            tpb.setDstIp(dstIp);
+        }
+        if (srcPort != 0) {
+            tpb.setSrcPort(srcPort);
+        }
+        if (dstPort != 0) {
+            tpb.setDstPort(dstPort);
+        }
+        if (!"ip".equals(proto)) {
+            tpb.setIpProto(proto);
+        }
+        if (Policy.Type.valueOf(policyType) == Policy.Type.TUNNEL_FLOW) {
+            if (tunnelId == null) {
+                error("tunnel ID must be specified for TUNNEL_FLOW policy");
+                return;
+            }
+            tpb.setTunnelId(tunnelId);
+        }
+        PolicyHandler.Result result = srService.createPolicy(tpb.build());
+
+        switch (result) {
+            case POLICY_EXISTS:
+                error("the same policy exists");
+                break;
+            case ID_EXISTS:
+                error("the same policy ID exists");
+                break;
+            case TUNNEL_NOT_FOUND:
+                error("the tunnel is not found");
+                break;
+            case UNSUPPORTED_TYPE:
+                error("the policy type specified is not supported");
+                break;
+            default:
+                break;
+        }
+
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java
new file mode 100644
index 0000000..6c91b8b
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyListCommand.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.Policy;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to show the list of policies.
+ */
+@Command(scope = "onos", name = "sr-policy-list",
+        description = "Lists all policies")
+public class PolicyListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_MAPPING_TUNNEL =
+            "  id=%s, type=%s,  prio=%d, src=%s, port=%d, dst=%s, port=%d, proto=%s, tunnel=%s";
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        srService.getPolicies().forEach(policy -> printPolicy(policy));
+    }
+
+    private void printPolicy(Policy policy) {
+        if (policy.type() == Policy.Type.TUNNEL_FLOW) {
+            print(FORMAT_MAPPING_TUNNEL, policy.id(), policy.type(), policy.priority(),
+                    policy.srcIp(), policy.srcPort(), policy.dstIp(), policy.dstPort(),
+                    (policy.ipProto() == null) ? "" : policy.ipProto(),
+                    ((TunnelPolicy) policy).tunnelId());
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java
new file mode 100644
index 0000000..cdc3034
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PolicyRemoveCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.PolicyHandler;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.TunnelPolicy;
+
+/**
+ * Command to remove a policy.
+ */
+@Command(scope = "onos", name = "sr-policy-remove",
+        description = "Remove a policy")
+public class PolicyRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "policy ID",
+            description = "policy ID",
+            required = true, multiValued = false)
+    String policyId;
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        TunnelPolicy.Builder tpb = TunnelPolicy.builder().setPolicyId(policyId);
+        PolicyHandler.Result result = srService.removePolicy(tpb.build());
+        if (result == PolicyHandler.Result.POLICY_NOT_FOUND) {
+            print("ERROR: the policy is not found");
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java
new file mode 100644
index 0000000..d916823
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireAddCommand.java
@@ -0,0 +1,105 @@
+/*
+ * 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.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+
+
+/**
+ * Command to add a pseuwodire.
+ */
+@Command(scope = "onos", name = "pseudowire-add",
+        description = "Add a pseudowire to the network configuration, if it already exists update it.")
+public class PseudowireAddCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "pwId",
+            description = "Pseudowire ID",
+            required = true, multiValued = false)
+    String pwId;
+
+    @Argument(index = 1, name = "pwLabel",
+            description = "Pseudowire Label",
+            required = true, multiValued = false)
+    String pwLabel;
+
+    @Argument(index = 2, name = "mode",
+            description = "Mode used for pseudowire",
+            required = true, multiValued = false)
+    String mode;
+
+    @Argument(index = 3, name = "sDTag",
+            description = "Service delimiting tag",
+            required = true, multiValued = false)
+    String sDTag;
+
+    @Argument(index = 4, name = "cP1",
+            description = "Connection Point 1",
+            required = true, multiValued = false)
+    String cP1;
+
+    @Argument(index = 5, name = "cP1InnerVlan",
+            description = "Inner Vlan of Connection Point 1",
+            required = true, multiValued = false)
+    String cP1InnerVlan;
+
+    @Argument(index = 6, name = "cP1OuterVlan",
+            description = "Outer Vlan of Connection Point 1",
+            required = true, multiValued = false)
+    String cP1OuterVlan;
+
+    @Argument(index = 7, name = "cP2",
+            description = "Connection Point 2",
+            required = true, multiValued = false)
+    String cP2;
+
+    @Argument(index = 8, name = "cP2InnerVlan",
+            description = "Inner Vlan of Connection Point 2",
+            required = true, multiValued = false)
+    String cP2InnerVlan;
+
+    @Argument(index = 9, name = "cP2OuterVlan",
+            description = "Outer Vlan of Connection Point 2",
+            required = true, multiValued = false)
+    String cP2OuterVlan;
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        L2TunnelHandler.Result res = srService.addPseudowire(pwId, pwLabel,
+                                                             cP1, cP1InnerVlan, cP1OuterVlan,
+                                                             cP2, cP2InnerVlan, cP2OuterVlan,
+                                                             mode, sDTag);
+        switch (res) {
+            case ADDITION_ERROR:
+                print("Pseudowire could not be added, error in configuration, please check logs for more details!");
+                break;
+            case CONFIG_NOT_FOUND:
+                print("Configuration for pwaas was not found! Initialize the configuration first through netcfg.");
+                break;
+            default:
+                break;
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java
new file mode 100644
index 0000000..b44ca76
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireIdCompleter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.console.Completer;
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.stream.Collectors;
+
+/**
+ * Device ID completer.
+ */
+public class PseudowireIdCompleter implements Completer {
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+
+        List<L2Tunnel> tunnels = srService.getL2Tunnels();
+
+        // combine polices and tunnels to pseudowires
+        Iterator<String> pseudowires = tunnels.stream()
+                .map(l2Tunnel -> Long.toString(l2Tunnel.tunnelId()))
+                .collect(Collectors.toList()).iterator();
+
+        SortedSet<String> strings = delegate.getStrings();
+        while (pseudowires.hasNext()) {
+            strings.add(pseudowires.next());
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(buffer, cursor, candidates);
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java
new file mode 100644
index 0000000..c48f633
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireListCommand.java
@@ -0,0 +1,84 @@
+/*
+ * 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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Command to show the pseudowires.
+ */
+@Command(scope = "onos", name = "pseudowires",
+        description = "Lists all pseudowires")
+public class PseudowireListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_PSEUDOWIRE =
+            "Pseudowire id = %s \n" +
+                    "   mode : %s, sdTag : %s, pwLabel : %s \n" +
+                    "   cP1 : %s , cP1OuterTag : %s, cP1InnerTag : %s \n" +
+                    "   cP2 : %s , cP2OuterTag : %s, cP2InnerTag : %s \n" +
+                    "   transportVlan : %s";
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        List<L2Tunnel> tunnels = srService.getL2Tunnels();
+        List<L2TunnelPolicy> policies = srService.getL2Policies();
+
+        // combine polices and tunnels to pseudowires
+        List<L2TunnelDescription> pseudowires = tunnels.stream()
+                                    .map(l2Tunnel -> {
+                                            L2TunnelPolicy policy = null;
+                                            for (L2TunnelPolicy l2Policy : policies) {
+                                                if (l2Policy.tunnelId() == l2Tunnel.tunnelId()) {
+                                                    policy = l2Policy;
+                                                    break;
+                                                }
+                                            }
+
+                                            return new DefaultL2TunnelDescription(l2Tunnel, policy);
+                                    })
+                                    .collect(Collectors.toList());
+
+        pseudowires.forEach(pw -> printPseudowire(pw));
+    }
+
+    private void printPseudowire(L2TunnelDescription pseudowire) {
+
+
+        VlanId vlan = pseudowire.l2Tunnel().transportVlan().equals(VlanId.vlanId((short) 4094)) ?
+                VlanId.NONE : pseudowire.l2Tunnel().transportVlan();
+
+        print(FORMAT_PSEUDOWIRE, pseudowire.l2Tunnel().tunnelId(), pseudowire.l2Tunnel().pwMode(),
+              pseudowire.l2Tunnel().sdTag(), pseudowire.l2Tunnel().pwLabel(),
+              pseudowire.l2TunnelPolicy().cP1(), pseudowire.l2TunnelPolicy().cP1OuterTag(),
+              pseudowire.l2TunnelPolicy().cP1InnerTag(), pseudowire.l2TunnelPolicy().cP2(),
+              pseudowire.l2TunnelPolicy().cP2OuterTag(), pseudowire.l2TunnelPolicy().cP2InnerTag(),
+              vlan);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.java
new file mode 100644
index 0000000..098bbf9
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/PseudowireRemoveCommand.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.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.pwaas.L2TunnelHandler;
+
+
+/**
+ * Command to remove a pseudowire.
+ */
+@Command(scope = "onos", name = "pseudowire-remove",
+        description = "Remove a pseudowire")
+public class PseudowireRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "pwId",
+            description = "pseudowire ID",
+            required = true, multiValued = false)
+    String pwId;
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        // remove the pseudowire
+        SegmentRoutingManager mngr = (SegmentRoutingManager) srService;
+        L2TunnelHandler.Result res = mngr.removePseudowire(pwId);
+
+        switch (res) {
+            case REMOVAL_ERROR:
+                error("Error in deletion, pseudowire not found!");
+                break;
+            case CONFIG_NOT_FOUND:
+                error("Could not fetch pseudowire class configuration!");
+                break;
+            default:
+                break;
+            }
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java
new file mode 100644
index 0000000..40525f2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/RerouteNetworkCommand.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016-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.segmentrouting.cli;
+
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Command to manually trigger routing and rule-population in the network.
+ *
+ */
+@Command(scope = "onos", name = "sr-reroute-network",
+        description = "Repopulate routing rules given current network state")
+public class RerouteNetworkCommand extends AbstractShellCommand {
+
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+        srService.rerouteNetwork();
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java
new file mode 100644
index 0000000..6b3e1fc
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelAddCommand.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.DefaultTunnel;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+import org.onosproject.segmentrouting.TunnelHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Command to add a new tunnel.
+ */
+@Command(scope = "onos", name = "sr-tunnel-add",
+        description = "Create a new tunnel")
+public class TunnelAddCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "tunnel ID",
+            description = "tunnel ID",
+            required = true, multiValued = false)
+    String tunnelId;
+
+    @Argument(index = 1, name = "label path",
+            description = "label path",
+            required = true, multiValued = false)
+    String labels;
+
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        List<Integer> labelIds = new ArrayList<>();
+        StringTokenizer strToken = new StringTokenizer(labels, ",");
+        while (strToken.hasMoreTokens()) {
+            labelIds.add(Integer.valueOf(strToken.nextToken()));
+        }
+        Tunnel tunnel = new DefaultTunnel(tunnelId, labelIds);
+
+        TunnelHandler.Result result = srService.createTunnel(tunnel);
+        switch (result) {
+            case ID_EXISTS:
+                print("ERROR: the same tunnel ID exists");
+                break;
+            case TUNNEL_EXISTS:
+                print("ERROR: the same tunnel exists");
+                break;
+            case INTERNAL_ERROR:
+                print("ERROR: internal tunnel creation error");
+                break;
+            case WRONG_PATH:
+                print("ERROR: the tunnel path is wrong");
+                break;
+            default:
+                break;
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java
new file mode 100644
index 0000000..7984eb4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelListCommand.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+
+/**
+ * Command to show the list of tunnels.
+ */
+@Command(scope = "onos", name = "sr-tunnel-list",
+        description = "Lists all tunnels")
+public class TunnelListCommand extends AbstractShellCommand {
+
+    private static final String FORMAT_MAPPING =
+            "  id=%s, path=%s";
+
+    @Override
+    protected void execute() {
+
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        srService.getTunnels().forEach(tunnel -> printTunnel(tunnel));
+    }
+
+    private void printTunnel(Tunnel tunnel) {
+        print(FORMAT_MAPPING, tunnel.id(), tunnel.labelIds());
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java
new file mode 100644
index 0000000..b9a7edb
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/TunnelRemoveCommand.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-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.segmentrouting.cli;
+
+
+import com.google.common.collect.Lists;
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.segmentrouting.DefaultTunnel;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.Tunnel;
+import org.onosproject.segmentrouting.TunnelHandler;
+
+/**
+ * Command to remove a tunnel.
+ */
+@Command(scope = "onos", name = "sr-tunnel-remove",
+        description = "Remove a tunnel")
+public class TunnelRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "tunnel ID",
+            description = "tunnel ID",
+            required = true, multiValued = false)
+    String tunnelId;
+
+    @Override
+    protected void execute() {
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        Tunnel tunnel = new DefaultTunnel(tunnelId, Lists.newArrayList());
+        TunnelHandler.Result result = srService.removeTunnel(tunnel);
+        switch (result) {
+            case TUNNEL_IN_USE:
+                print("ERROR: the tunnel is still in use");
+                break;
+            case TUNNEL_NOT_FOUND:
+                print("ERROR: the tunnel is not found");
+                break;
+            default:
+                break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java
new file mode 100644
index 0000000..3e89ca4
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/VerifyGroupsCommand.java
@@ -0,0 +1,55 @@
+/*
+ * 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.segmentrouting.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+
+/**
+ * Triggers the verification of hashed group buckets in the specified device,
+ * and corrects the buckets if necessary. Outcome can be viewed in the 'groups'
+ * command.
+ */
+@Command(scope = "onos", name = "sr-verify-groups",
+        description = "Triggers the verification of hashed groups in the specified "
+                + "device. Does not return any output; users can query the results "
+                + "in the 'groups' command")
+public class VerifyGroupsCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "uri", description = "Device ID",
+            required = true, multiValued = false)
+    String uri = null;
+
+    @Override
+    protected void execute() {
+        DeviceService deviceService = get(DeviceService.class);
+        SegmentRoutingService srService =
+                AbstractShellCommand.get(SegmentRoutingService.class);
+
+        if (uri != null) {
+            Device dev = deviceService.getDevice(DeviceId.deviceId(uri));
+            if (dev != null) {
+                srService.verifyGroups(dev.id());
+            }
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java
new file mode 100644
index 0000000..0f2cea6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing application CLI handlers.
+ */
+package org.onosproject.segmentrouting.cli;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java
new file mode 100644
index 0000000..a5c5557
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/BlockedPortsConfig.java
@@ -0,0 +1,213 @@
+/*
+ * 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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.config.Config;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Network config to describe ports that should be blocked until authenticated.
+ */
+public class BlockedPortsConfig extends Config<ApplicationId> {
+
+    /**
+     * Returns the top level keys to the config,
+     * which should be device ID strings.
+     *
+     * @return this list of top level keys
+     */
+    public List<String> deviceIds() {
+        List<String> devIds = new ArrayList<>();
+        if (object != null) {
+            Iterator<String> it = object.fieldNames();
+            if (it != null) {
+                it.forEachRemaining(devIds::add);
+            }
+        }
+        return devIds;
+    }
+
+    /**
+     * Returns the port range strings associated with the given device id key.
+     *
+     * @param deviceId the device id key
+     * @return the associated port range strings
+     */
+    public List<String> portRanges(String deviceId) {
+        List<String> portRanges = new ArrayList<>();
+        if (object != null) {
+            JsonNode jnode = object.get(deviceId);
+            if (ArrayNode.class.isInstance(jnode)) {
+                ArrayNode array = (ArrayNode) jnode;
+                array.forEach(pr -> portRanges.add(pr.asText()));
+            }
+        }
+        return portRanges;
+    }
+
+    /**
+     * Returns an iterator over the port numbers defined by the port ranges
+     * defined in the configuration, for the given device.
+     *
+     * @param deviceId the specific device
+     * @return an iterator over the configured ports
+     */
+    public Iterator<Long> portIterator(String deviceId) {
+        List<String> ranges = portRanges(deviceId);
+        return new PortIterator(ranges);
+    }
+
+    /**
+     * Private implementation of an iterator that aggregates several range
+     * iterators into a single iterator.
+     */
+    class PortIterator implements Iterator<Long> {
+        private final List<Range> ranges;
+        private final int nRanges;
+        private int currentRange = 0;
+        private Iterator<Long> iterator;
+
+        PortIterator(List<String> rangeSpecs) {
+            nRanges = rangeSpecs.size();
+            ranges = new ArrayList<>(nRanges);
+            if (nRanges > 0) {
+                for (String rs : rangeSpecs) {
+                    ranges.add(new Range(rs));
+                }
+                iterator = ranges.get(0).iterator();
+            }
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nRanges > 0 &&
+                    (currentRange < nRanges - 1 ||
+                            (currentRange < nRanges && iterator.hasNext()));
+        }
+
+        @Override
+        public Long next() {
+            if (nRanges == 0) {
+                throw new NoSuchElementException();
+            }
+
+            Long value;
+            if (iterator.hasNext()) {
+                value = iterator.next();
+            } else {
+                currentRange++;
+                if (currentRange < nRanges) {
+                    iterator = ranges.get(currentRange).iterator();
+                    value = iterator.next();
+                } else {
+                    throw new NoSuchElementException();
+                }
+            }
+            return value;
+        }
+    }
+
+    /**
+     * Private implementation of a "range" of long numbers, defined by a
+     * string of the form {@code "<lo>-<hi>"}, for example, "17-32".
+     */
+    static final class Range {
+        private static final Pattern RE_SINGLE = Pattern.compile("(\\d+)");
+        private static final Pattern RE_RANGE = Pattern.compile("(\\d+)-(\\d+)");
+        private static final String E_BAD_FORMAT = "Bad Range Format ";
+
+        private final long lo;
+        private final long hi;
+
+        /**
+         * Constructs a range from the given string definition.
+         * For example:
+         * <pre>
+         *     Range r = new Range("17-32");
+         * </pre>
+         *
+         * @param s the string representation of the range
+         * @throws IllegalArgumentException if the range string is malformed
+         */
+        Range(String s) {
+            String lohi = s;
+            Matcher m = RE_SINGLE.matcher(s);
+            if (m.matches()) {
+                lohi = s + "-" + s;
+            }
+            m = RE_RANGE.matcher(lohi);
+            if (!m.matches()) {
+                throw new IllegalArgumentException(E_BAD_FORMAT + s);
+            }
+            try {
+                lo = Long.parseLong(m.group(1));
+                hi = Long.parseLong(m.group(2));
+
+                if (hi < lo) {
+                    throw new IllegalArgumentException(E_BAD_FORMAT + s);
+                }
+            } catch (NumberFormatException nfe) {
+                // unlikely to be thrown, since the matcher will have failed first
+                throw new IllegalArgumentException(E_BAD_FORMAT + s, nfe);
+            }
+        }
+
+
+        /**
+         * Returns an iterator over this range, starting from the lowest value
+         * and iterating up to the highest value (inclusive).
+         *
+         * @return an iterator over this range
+         */
+        Iterator<Long> iterator() {
+            return new RangeIterator();
+        }
+
+        /**
+         * Private implementation of an iterator over the range.
+         */
+        class RangeIterator implements Iterator<Long> {
+            long current;
+
+            RangeIterator() {
+                current = lo - 1;
+            }
+
+            @Override
+            public boolean hasNext() {
+                return current < hi;
+            }
+
+            @Override
+            public Long next() {
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+                return ++current;
+            }
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java
new file mode 100644
index 0000000..773c7f6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfigNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015-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.segmentrouting.config;
+
+/**
+ * Signals that an error occurred during reading device configuration.
+ */
+public class DeviceConfigNotFoundException extends Exception {
+
+    /**
+     * Creates a new ConfigNotFoundException with the given message.
+     *
+     * @param message exception message
+     */
+    public DeviceConfigNotFoundException(String message) {
+        super(message);
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
new file mode 100644
index 0000000..7ea430d
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceConfiguration.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright 2015-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.SetMultimap;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.ConfigException;
+import org.onosproject.net.config.basics.InterfaceConfig;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Segment Routing configuration component that reads the
+ * segment routing related configuration from Network Configuration Manager
+ * component and organizes in more accessible formats.
+ */
+public class DeviceConfiguration implements DeviceProperties {
+
+    private static final String NO_SUBNET = "No subnet configured on {}";
+
+    private static final Logger log = LoggerFactory.getLogger(DeviceConfiguration.class);
+    private final List<Integer> allSegmentIds = new ArrayList<>();
+    private final Map<DeviceId, SegmentRouterInfo> deviceConfigMap = new ConcurrentHashMap<>();
+    private SegmentRoutingManager srManager;
+
+    private class SegmentRouterInfo {
+        int ipv4NodeSid;
+        int ipv6NodeSid;
+        DeviceId deviceId;
+        Ip4Address ipv4Loopback;
+        Ip6Address ipv6Loopback;
+        MacAddress mac;
+        boolean isEdge;
+        SetMultimap<PortNumber, IpAddress> gatewayIps;
+        SetMultimap<PortNumber, IpPrefix> subnets;
+        Map<Integer, Set<Integer>> adjacencySids;
+        DeviceId pairDeviceId;
+        PortNumber pairLocalPort;
+        int pwRoutingLabel;
+
+        public SegmentRouterInfo() {
+            gatewayIps = HashMultimap.create();
+            subnets = HashMultimap.create();
+        }
+    }
+
+    /**
+     * Constructs device configuration for all Segment Router devices,
+     * organizing the data into various maps for easier access.
+     *
+     * @param srManager Segment Routing Manager
+     */
+    public DeviceConfiguration(SegmentRoutingManager srManager) {
+        this.srManager = srManager;
+        updateConfig();
+    }
+
+    public void updateConfig() {
+        // Read config from device subject, excluding gatewayIps and subnets.
+        Set<DeviceId> deviceSubjects =
+                srManager.cfgService.getSubjects(DeviceId.class, SegmentRoutingDeviceConfig.class);
+        deviceSubjects.forEach(subject -> {
+            SegmentRoutingDeviceConfig config =
+                    srManager.cfgService.getConfig(subject, SegmentRoutingDeviceConfig.class);
+            SegmentRouterInfo info = new SegmentRouterInfo();
+            info.deviceId = subject;
+            info.ipv4NodeSid = config.nodeSidIPv4();
+            info.ipv6NodeSid = config.nodeSidIPv6();
+            info.ipv4Loopback = config.routerIpv4();
+            info.ipv6Loopback = config.routerIpv6();
+            info.mac = config.routerMac();
+            info.isEdge = config.isEdgeRouter();
+            info.adjacencySids = config.adjacencySids();
+            info.pairDeviceId = config.pairDeviceId();
+            info.pairLocalPort = config.pairLocalPort();
+            info.pwRoutingLabel = info.ipv4NodeSid + 1000;
+            deviceConfigMap.put(info.deviceId, info);
+            log.debug("Read device config for device: {}", info.deviceId);
+            /*
+             * IPv6 sid is not inserted. this part of the code is not used for now.
+             */
+            allSegmentIds.add(info.ipv4NodeSid);
+        });
+
+        // Read gatewayIps and subnets from port subject. Ignore suppressed ports.
+        Set<ConnectPoint> portSubjects = srManager.cfgService
+                .getSubjects(ConnectPoint.class, InterfaceConfig.class);
+        portSubjects.stream()
+                .filter(subject -> deviceConfigMap.containsKey(subject.deviceId()))
+                .filter(subject -> !isSuppressedPort(subject)).forEach(subject -> {
+            InterfaceConfig config =
+                    srManager.cfgService.getConfig(subject, InterfaceConfig.class);
+            Set<Interface> networkInterfaces;
+            try {
+                networkInterfaces = config.getInterfaces();
+            } catch (ConfigException e) {
+                log.error("Error loading port configuration");
+                return;
+            }
+            networkInterfaces.forEach(networkInterface -> {
+                VlanId vlanId = networkInterface.vlan();
+                ConnectPoint connectPoint = networkInterface.connectPoint();
+                DeviceId dpid = connectPoint.deviceId();
+                PortNumber port = connectPoint.port();
+                MacAddress mac = networkInterface.mac();
+                SegmentRouterInfo info = deviceConfigMap.get(dpid);
+
+                // skip if there is no corresponding device for this ConenctPoint
+                if (info != null) {
+                    // Extract subnet information
+                    List<InterfaceIpAddress> interfaceAddresses = networkInterface.ipAddressesList();
+                    interfaceAddresses.forEach(interfaceAddress -> {
+                        // Do not add /0, /32 and /128 to gateway IP list
+                        int prefixLength = interfaceAddress.subnetAddress().prefixLength();
+                        IpPrefix ipPrefix = interfaceAddress.subnetAddress();
+                        if (ipPrefix.isIp4()) {
+                            if (prefixLength != 0 && prefixLength != IpPrefix.MAX_INET_MASK_LENGTH) {
+                                info.gatewayIps.put(port, interfaceAddress.ipAddress());
+                            }
+                            info.subnets.put(port, interfaceAddress.subnetAddress());
+                        } else {
+                            if (prefixLength != 0 && prefixLength != IpPrefix.MAX_INET6_MASK_LENGTH) {
+                                info.gatewayIps.put(port, interfaceAddress.ipAddress());
+                            }
+                            info.subnets.put(port, interfaceAddress.subnetAddress());
+                        }
+                    });
+
+                    // Override interface mac with router mac
+                    if (!mac.equals(info.mac)) {
+                        ArrayNode array = (ArrayNode) config.node();
+                        for (JsonNode intfNode : array) {
+                            ObjectNode objNode = (ObjectNode) intfNode;
+                            objNode.put(InterfaceConfig.MAC, info.mac.toString());
+                        }
+                        srManager.cfgService.applyConfig(connectPoint, InterfaceConfig.class, array);
+                    }
+                }
+            });
+            // We register the connect point with the NRS.
+            srManager.registerConnectPoint(subject);
+        });
+    }
+
+    public Collection<DeviceId> getRouters() {
+        return deviceConfigMap.keySet();
+    }
+
+    @Override
+    public boolean isConfigured(DeviceId deviceId) {
+        return deviceConfigMap.get(deviceId) != null;
+    }
+
+    @Override
+    public int getIPv4SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        log.info("DEVICE MAP IS {}", deviceConfigMap);
+        if (srinfo != null) {
+            log.trace("getIPv4SegmentId for device{} is {}", deviceId, srinfo.ipv4NodeSid);
+            return srinfo.ipv4NodeSid;
+        } else {
+            String message = "getIPv4SegmentId fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public int getIPv6SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("getIPv6SegmentId for device{} is {}", deviceId, srinfo.ipv6NodeSid);
+            return srinfo.ipv6NodeSid;
+        } else {
+            String message = "getIPv6SegmentId fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public int getPWRoutingLabel(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("pwRoutingLabel for device{} is {}", deviceId, srinfo.pwRoutingLabel);
+            return srinfo.pwRoutingLabel;
+        } else {
+            String message = "getPWRoutingLabel fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    /**
+     * Returns the IPv4 Node segment id of a segment router given its Router mac address.
+     *
+     * @param routerMac router mac address
+     * @return node segment id, or -1 if not found in config
+     */
+    public int getIPv4SegmentId(MacAddress routerMac) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+                    deviceConfigMap.entrySet()) {
+            if (entry.getValue().mac.equals(routerMac)) {
+                return entry.getValue().ipv4NodeSid;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns the IPv6 Node segment id of a segment router given its Router mac address.
+     *
+     * @param routerMac router mac address
+     * @return node segment id, or -1 if not found in config
+     */
+    public int getIPv6SegmentId(MacAddress routerMac) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+                deviceConfigMap.entrySet()) {
+            if (entry.getValue().mac.equals(routerMac)) {
+                return entry.getValue().ipv6NodeSid;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns the IPv4 Node segment id of a segment router given its Router ip address.
+     *
+     * @param routerAddress router ip address
+     * @return node segment id, or -1 if not found in config
+     */
+    public int getIPv4SegmentId(Ip4Address routerAddress) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+            deviceConfigMap.entrySet()) {
+            if (entry.getValue().ipv4Loopback.equals(routerAddress)) {
+                return entry.getValue().ipv4NodeSid;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns the IPv6 Node segment id of a segment router given its Router ip address.
+     *
+     * @param routerAddress router ip address
+     * @return node segment id, or -1 if not found in config
+     */
+    public int getIPv6SegmentId(Ip6Address routerAddress) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+                deviceConfigMap.entrySet()) {
+            if (entry.getValue().ipv6Loopback.equals(routerAddress)) {
+                return entry.getValue().ipv6NodeSid;
+            }
+        }
+
+        return -1;
+    }
+
+    @Override
+    public MacAddress getDeviceMac(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            return srinfo.mac;
+        } else {
+            String message = "getDeviceMac fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public Ip4Address getRouterIpv4(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("getRouterIpv4 for device{} is {}", deviceId, srinfo.ipv4Loopback);
+            return srinfo.ipv4Loopback;
+        } else {
+            String message = "getRouterIpv4 fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public Ip6Address getRouterIpv6(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("getRouterIpv6 for device{} is {}", deviceId, srinfo.ipv6Loopback);
+            return srinfo.ipv6Loopback;
+        } else {
+            String message = "getRouterIpv6 fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public boolean isEdgeDevice(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("isEdgeDevice for device{} is {}", deviceId, srinfo.isEdge);
+            return srinfo.isEdge;
+        } else {
+            String message = "isEdgeDevice fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    @Override
+    public List<Integer> getAllDeviceSegmentIds() {
+        return allSegmentIds;
+    }
+
+    @Override
+    public Map<IpPrefix, List<PortNumber>> getSubnetPortsMap(DeviceId deviceId)
+            throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo == null) {
+            String message = "getSubnetPortsMap fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+        // Construct subnet-port mapping from port-subnet mapping
+        SetMultimap<PortNumber, IpPrefix> portSubnetMap = srinfo.subnets;
+        Map<IpPrefix, List<PortNumber>> subnetPortMap = new HashMap<>();
+
+        portSubnetMap.entries().forEach(entry -> {
+            PortNumber port = entry.getKey();
+            IpPrefix subnet = entry.getValue();
+
+            if (subnet.prefixLength() == IpPrefix.MAX_INET_MASK_LENGTH ||
+                    subnet.prefixLength() == IpPrefix.MAX_INET6_MASK_LENGTH) {
+                return;
+            }
+
+            if (subnetPortMap.containsKey(subnet)) {
+                subnetPortMap.get(subnet).add(port);
+            } else {
+                ArrayList<PortNumber> ports = new ArrayList<>();
+                ports.add(port);
+                subnetPortMap.put(subnet, ports);
+            }
+        });
+        return subnetPortMap;
+    }
+
+    /**
+     * Returns the device identifier or data plane identifier (dpid)
+     * of a segment router given its segment id.
+     *
+     * @param sid segment id
+     * @return deviceId device identifier
+     */
+    public DeviceId getDeviceId(int sid) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+            deviceConfigMap.entrySet()) {
+            if (entry.getValue().ipv4NodeSid == sid ||
+                    entry.getValue().ipv6NodeSid == sid) {
+                return entry.getValue().deviceId;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the device identifier or data plane identifier (dpid)
+     * of a segment router given its router ip address.
+     *
+     * @param ipAddress router ip address
+     * @return deviceId device identifier
+     */
+    public DeviceId getDeviceId(Ip4Address ipAddress) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+            deviceConfigMap.entrySet()) {
+            if (entry.getValue().ipv4Loopback.equals(ipAddress)) {
+                return entry.getValue().deviceId;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the device identifier or data plane identifier (dpid)
+     * of a segment router given its router ipv6 address.
+     *
+     * @param ipAddress router ipv6 address
+     * @return deviceId device identifier
+     */
+    public DeviceId getDeviceId(Ip6Address ipAddress) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+                deviceConfigMap.entrySet()) {
+            if (entry.getValue().ipv6Loopback.equals(ipAddress)) {
+                return entry.getValue().deviceId;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the configured port ip addresses for a segment router.
+     * These addresses serve as gateway IP addresses for the subnets configured
+     * on those ports.
+     *
+     * @param deviceId device identifier
+     * @return immutable set of ip addresses configured on the ports or null if not found
+     */
+    public Set<IpAddress> getPortIPs(DeviceId deviceId) {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            log.trace("getSubnetGatewayIps for device{} is {}", deviceId,
+                      srinfo.gatewayIps.values());
+            return ImmutableSet.copyOf(srinfo.gatewayIps.values());
+        }
+        return null;
+    }
+
+    /**
+     * Returns the configured subnet prefixes for a segment router.
+     *
+     * @param deviceId device identifier
+     * @return list of ip prefixes or null if not found
+     */
+    public Set<IpPrefix> getSubnets(DeviceId deviceId) {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            ImmutableSet.Builder<IpPrefix> builder = ImmutableSet.builder();
+            return builder.addAll(srinfo.subnets.values()).build();
+        }
+        return null;
+    }
+
+
+    /**
+     * Returns the subnet configuration of given device and port.
+     *
+     * @param deviceId Device ID
+     * @param port Port number
+     * @return The subnets configured on given port or empty set if
+     *         the port is unconfigured or suppressed.
+     */
+    public Set<IpPrefix> getPortSubnets(DeviceId deviceId, PortNumber port) {
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+
+        if (isSuppressedPort(connectPoint)) {
+            return Collections.emptySet();
+        }
+
+        Set<IpPrefix> subnets =
+                srManager.interfaceService.getInterfacesByPort(connectPoint).stream()
+                        .flatMap(intf -> intf.ipAddressesList().stream())
+                        .map(InterfaceIpAddress::subnetAddress)
+                        .collect(Collectors.toSet());
+
+        if (subnets.isEmpty()) {
+            log.debug(NO_SUBNET, connectPoint);
+            return Collections.emptySet();
+        }
+
+        return subnets;
+    }
+
+    /**
+     * Returns all ports that has a subnet that contains any of the given IP addresses.
+     *
+     * @param ips a set of IP addresses
+     * @return a set of connect point that has a subnet that contains any of the given IP addresses
+     */
+    public Set<ConnectPoint> getPortByIps(Set<IpAddress> ips) {
+        return srManager.interfaceService.getInterfaces().stream()
+                .filter(intf -> intf.ipAddressesList().stream().anyMatch(intfAddress ->
+                            ips.stream().anyMatch(ip -> intfAddress.subnetAddress().contains(ip))))
+                .map(Interface::connectPoint)
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Returns the router ip address of segment router that has the
+     * specified ip address in its subnets.
+     *
+     * @param destIpAddress target ip address
+     * @return router ip address
+     */
+    public Ip4Address getRouterIpAddressForASubnetHost(Ip4Address destIpAddress) {
+        Interface matchIntf = srManager.interfaceService.getMatchingInterface(destIpAddress);
+
+        if (matchIntf == null) {
+            log.debug("No router was found for {}", destIpAddress);
+            return null;
+        }
+
+        DeviceId routerDeviceId = matchIntf.connectPoint().deviceId();
+        SegmentRouterInfo srInfo = deviceConfigMap.get(routerDeviceId);
+        if (srInfo == null) {
+            log.debug("No device config was found for {}", routerDeviceId);
+            return null;
+        }
+
+        return srInfo.ipv4Loopback;
+    }
+
+    /**
+     * Returns the router ipv6 address of segment router that has the
+     * specified ip address in its subnets.
+     *
+     * @param destIpAddress target ip address
+     * @return router ip address
+     */
+    public Ip6Address getRouterIpAddressForASubnetHost(Ip6Address destIpAddress) {
+        Interface matchIntf = srManager.interfaceService.getMatchingInterface(destIpAddress);
+
+        if (matchIntf == null) {
+            log.debug("No router was found for {}", destIpAddress);
+            return null;
+        }
+
+        DeviceId routerDeviceId = matchIntf.connectPoint().deviceId();
+        SegmentRouterInfo srInfo = deviceConfigMap.get(routerDeviceId);
+        if (srInfo == null) {
+            log.debug("No device config was found for {}", routerDeviceId);
+            return null;
+        }
+
+        return srInfo.ipv6Loopback;
+    }
+
+    /**
+     * Returns the router mac address of segment router that has the
+     * specified ip address as one of its subnet gateway ip address.
+     *
+     * @param gatewayIpAddress router gateway ip address
+     * @return router mac address or null if not found
+     */
+    public MacAddress getRouterMacForAGatewayIp(IpAddress gatewayIpAddress) {
+        for (Map.Entry<DeviceId, SegmentRouterInfo> entry:
+                deviceConfigMap.entrySet()) {
+            if (entry.getValue().gatewayIps.
+                    values().contains(gatewayIpAddress)) {
+                return entry.getValue().mac;
+            }
+        }
+
+        log.debug("Cannot find a router for {}", gatewayIpAddress);
+        return null;
+    }
+
+    /**
+     * Checks if the host IP is in any of the subnet defined in the router with the
+     * device ID given.
+     *
+     * @param deviceId device identification of the router
+     * @param hostIp   host IP address to check
+     * @return true if the given IP is within any of the subnet defined in the router,
+     * false if no subnet is defined in the router or if the host is not
+     * within any subnet defined in the router
+     */
+    public boolean inSameSubnet(DeviceId deviceId, IpAddress hostIp) {
+
+        Set<IpPrefix> subnets = getSubnets(deviceId);
+        if (subnets == null) {
+            return false;
+        }
+
+        for (IpPrefix subnet: subnets) {
+            // Exclude /0 since it is a special case used for default route
+            if (subnet.prefixLength() != 0 && subnet.contains(hostIp)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if the IP is in the subnet defined on given connect point.
+     *
+     * @param connectPoint Connect point
+     * @param ip The IP address to check
+     * @return True if the IP belongs to the subnet.
+     *         False if the IP does not belong to the subnet, or
+     *         there is no subnet configuration on given connect point.
+     */
+    public boolean inSameSubnet(ConnectPoint connectPoint, IpAddress ip) {
+        return getPortSubnets(connectPoint.deviceId(), connectPoint.port()).stream()
+                .anyMatch(ipPrefix -> ipPrefix.contains(ip));
+    }
+
+    /**
+     * Returns the ports corresponding to the adjacency Sid given.
+     *
+     * @param deviceId device identification of the router
+     * @param sid adjacency Sid
+     * @return set of port numbers
+     */
+    public Set<Integer> getPortsForAdjacencySid(DeviceId deviceId, int sid) {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        return srinfo != null ?
+                ImmutableSet.copyOf(srinfo.adjacencySids.get(sid)) :
+                ImmutableSet.copyOf(new HashSet<>());
+    }
+
+    /**
+     * Check if the Sid given is whether adjacency Sid of the router device or not.
+     *
+     * @param deviceId device identification of the router
+     * @param sid Sid to check
+     * @return true if the Sid given is the adjacency Sid of the device,
+     * otherwise false
+     */
+    public boolean isAdjacencySid(DeviceId deviceId, int sid) {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        return srinfo != null && srinfo.adjacencySids.containsKey(sid);
+    }
+
+    /**
+     * Add subnet to specific connect point.
+     *
+     * @param cp connect point
+     * @param ipPrefix subnet being added to the device
+     */
+    public void addSubnet(ConnectPoint cp, IpPrefix ipPrefix) {
+        checkNotNull(cp);
+        checkNotNull(ipPrefix);
+        SegmentRouterInfo srinfo = deviceConfigMap.get(cp.deviceId());
+        if (srinfo == null) {
+            log.warn("Device {} is not configured. Abort.", cp.deviceId());
+            return;
+        }
+        srinfo.subnets.put(cp.port(), ipPrefix);
+    }
+
+    /**
+     * Remove subnet from specific connect point.
+     *
+     * @param cp connect point
+     * @param ipPrefix subnet being removed to the device
+     */
+    public void removeSubnet(ConnectPoint cp, IpPrefix ipPrefix) {
+        checkNotNull(cp);
+        checkNotNull(ipPrefix);
+        SegmentRouterInfo srinfo = deviceConfigMap.get(cp.deviceId());
+        if (srinfo == null) {
+            log.warn("Device {} is not configured. Abort.", cp.deviceId());
+            return;
+        }
+        srinfo.subnets.remove(cp.port(), ipPrefix);
+    }
+
+    private boolean isSuppressedPort(ConnectPoint connectPoint) {
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId(), SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+            log.info("Interface configuration on port {} is ignored", connectPoint);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isPairedEdge(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        if (!isEdgeDevice(deviceId)) {
+            return false;
+        }
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        return (srinfo.pairDeviceId == null) ? false : true;
+    }
+
+    public DeviceId getPairDeviceId(DeviceId deviceId) throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            return srinfo.pairDeviceId;
+        } else {
+            String message = "getPairDeviceId fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+    public PortNumber getPairLocalPort(DeviceId deviceId)
+            throws DeviceConfigNotFoundException {
+        SegmentRouterInfo srinfo = deviceConfigMap.get(deviceId);
+        if (srinfo != null) {
+            return srinfo.pairLocalPort;
+        } else {
+            String message = "getPairLocalPort fails for device: " + deviceId + ".";
+            throw new DeviceConfigNotFoundException(message);
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java
new file mode 100644
index 0000000..2d0bbd2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/DeviceProperties.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2015-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.segmentrouting.config;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Mechanism through which group handler module retrieves
+ * the device specific attributes such as segment ID,
+ * Mac address...etc from group handler applications.
+ */
+public interface DeviceProperties {
+    /**
+     * Checks if the device is configured.
+     *
+     * @param deviceId device identifier
+     * @return true if the device is configured
+     */
+    boolean isConfigured(DeviceId deviceId);
+
+    /**
+     * Returns the IPv4 segment id of a device to be used in group creation.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return segment id of a device
+     */
+    int getIPv4SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Returns the IPv6 segment id of a device to be used in group creation.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return segment id of a device
+     */
+    int getIPv6SegmentId(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Returns the Mac address of a device to be used in group creation.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return mac address of a device
+     */
+    MacAddress getDeviceMac(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return the pseudowire routing label for a leaf node
+     */
+    int getPWRoutingLabel(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Returns the router ipv4 address of a segment router.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return router ip address
+     */
+    IpAddress getRouterIpv4(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Returns the router ipv6 address of a segment router.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return router ip address
+     */
+    IpAddress getRouterIpv6(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Indicates whether a device is edge device or transit/core device.
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return boolean
+     */
+    boolean isEdgeDevice(DeviceId deviceId) throws DeviceConfigNotFoundException;
+
+    /**
+     * Returns all segment IDs to be considered in building auto
+     *
+     * created groups.
+     * @return list of segment IDs
+     */
+    List<Integer> getAllDeviceSegmentIds();
+
+    /**
+     * Returns subnet-to-ports mapping of given device.
+     *
+     * For each entry of the map
+     * Key: a subnet
+     * Value: a list of ports, which are bound to the subnet
+     *
+     * @param deviceId device identifier
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return a map that contains all subnet-to-ports mapping of given device
+     */
+    Map<IpPrefix, List<PortNumber>> getSubnetPortsMap(DeviceId deviceId)
+            throws DeviceConfigNotFoundException;
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java
new file mode 100644
index 0000000..31cad7e
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/PwaasConfig.java
@@ -0,0 +1,671 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Mode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * App configuration object for Pwaas.
+ */
+public class PwaasConfig extends Config<ApplicationId> {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public InterfaceService intfService;
+
+    private static Logger log = LoggerFactory
+            .getLogger(PwaasConfig.class);
+
+    private static final String SRC_CP = "cP1";
+    private static final String DST_CP = "cP2";
+    private static final String SRC_OUTER_TAG = "cP1OuterTag";
+    private static final String DST_OUTER_TAG = "cP2OuterTag";
+    private static final String SRC_INNER_TAG = "cP1InnerTag";
+    private static final String DST_INNER_TAG = "cP2InnerTag";
+    private static final String MODE = "mode";
+    private static final String SD_TAG = "sdTag";
+    private static final String PW_LABEL = "pwLabel";
+
+    public PwaasConfig(DeviceService devS, InterfaceService intfS) {
+
+        super();
+
+        deviceService = devS;
+        intfService = intfS;
+    }
+
+    public PwaasConfig() {
+
+        super();
+
+        deviceService = AbstractShellCommand.get(DeviceService.class);
+        intfService = AbstractShellCommand.get(InterfaceService.class);
+    }
+    /**
+     * Error message for missing parameters.
+     */
+    private static final String MISSING_PARAMS = "Missing parameters in pseudo wire description";
+
+    /**
+     * Error message for invalid l2 mode.
+     */
+    private static final String INVALID_L2_MODE = "Invalid pseudo wire mode";
+
+    /**
+     * Error message for invalid VLAN.
+     */
+    private static final String INVALID_VLAN = "Vlan should be either int or */-";
+
+    /**
+     * Error message for invalid PW label.
+     */
+    private static final String INVALID_PW_LABEL = "Pseudowire label should be an integer";
+
+    /**
+     * Verify if the pwaas configuration block is valid.
+     *
+     * Here we try to ensure that the provided pseudowires will get instantiated
+     * correctly in the network. We also check for any collisions with already used
+     * interfaces and also between different pseudowires. Most of the restrictions stem
+     * from the fact that all vlan matching is done in table 10 of ofdpa.
+     *
+     * @return true, if the configuration block is valid.
+     *         False otherwise.
+     */
+    @Override
+    public boolean isValid() {
+
+        Set<L2TunnelDescription> pseudowires;
+        try {
+            pseudowires = getPwIds().stream()
+                    .map(this::getPwDescription)
+                    .collect(Collectors.toSet());
+
+            // check semantics now and return
+            return configurationValidity(pseudowires);
+
+        } catch (IllegalArgumentException e) {
+            log.warn("{}", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Helper method to verify if the tunnel is whether or not
+     * supported.
+     *
+     * @param l2Tunnel the tunnel to verify
+     * @return the result of the verification
+     */
+    private void verifyTunnel(L2Tunnel l2Tunnel) {
+
+        // Service delimiting tag not supported yet.
+        if (!l2Tunnel.sdTag().equals(VlanId.NONE)) {
+            throw new IllegalArgumentException(String.format("Service delimiting tag not supported yet for " +
+                                                             "pseudowire %d.", l2Tunnel.tunnelId()));
+        }
+
+        // Tag mode not supported yet.
+        if (l2Tunnel.pwMode() == L2Mode.TAGGED) {
+            throw new IllegalArgumentException(String.format("Tagged mode not supported yet for pseudowire %d.",
+                                                                            l2Tunnel.tunnelId()));
+        }
+
+        // Raw mode without service delimiting tag
+        // is the only mode supported for now.
+    }
+
+    /**
+     * Helper method to verify if the policy is whether or not
+     * supported and if policy will be successfully instantiated in the
+     * network.
+     *
+     * @param ingressInner the ingress inner tag
+     * @param ingressOuter the ingress outer tag
+     * @param egressInner the egress inner tag
+     * @param egressOuter the egress outer tag
+     * @return the result of verification
+     */
+    private void verifyPolicy(ConnectPoint cP1,
+                              ConnectPoint cP2,
+                              VlanId ingressInner,
+                              VlanId ingressOuter,
+                              VlanId egressInner,
+                              VlanId egressOuter,
+                              Long tunnelId) {
+
+        if (cP1.deviceId().equals(cP2.deviceId())) {
+            throw new IllegalArgumentException(String.format("Pseudowire connection points can not reside in the " +
+                                                             "same node, in pseudowire %d.", tunnelId));
+        }
+
+        // We can have multiple tags, all of them can be NONE,
+        // indicating untagged traffic, however, the outer tag can
+        // not have value if the inner tag is None
+        if (ingressInner.equals(VlanId.NONE) && !ingressOuter.equals(VlanId.NONE)) {
+            throw new IllegalArgumentException(String.format("Inner tag should not be empty when " +
+                                                             "outer tag is set for pseudowire %d for cP1.",
+                                                              tunnelId));
+        }
+
+        if (egressInner.equals(VlanId.NONE) && !egressOuter.equals(VlanId.NONE)) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Inner tag should not be empty when" +
+                                                                            " outer tag is set for pseudowire %d " +
+                                                                            "for cP2.", tunnelId)));
+        }
+
+        if (ingressInner.equals(VlanId.ANY) ||
+                ingressOuter.equals(VlanId.ANY) ||
+                egressInner.equals(VlanId.ANY) ||
+                egressOuter.equals(VlanId.ANY)) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Wildcard VLAN matching not yet " +
+                                                                            "supported for pseudowire %d.",
+                                                                            tunnelId)));
+        }
+
+        if (((!ingressOuter.equals(VlanId.NONE) && !ingressInner.equals(VlanId.NONE)) &&
+                (egressOuter.equals(VlanId.NONE) && egressInner.equals(VlanId.NONE)))
+                || ((ingressOuter.equals(VlanId.NONE) && ingressInner.equals(VlanId.NONE)) &&
+                (!egressOuter.equals(VlanId.NONE) && !egressInner.equals(VlanId.NONE)))) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Support for double tag <-> untag is not" +
+                                                                                    "supported for pseudowire %d.",
+                                                                            tunnelId)));
+        }
+        if ((!ingressInner.equals(VlanId.NONE) &&
+                ingressOuter.equals(VlanId.NONE) &&
+                !egressOuter.equals(VlanId.NONE))
+           || (egressOuter.equals(VlanId.NONE) &&
+                !egressInner.equals(VlanId.NONE) &&
+                !ingressOuter.equals(VlanId.NONE))) {
+                throw new IllegalArgumentException(String.valueOf(String.format("Support for double-tag<->" +
+                                                                                "single-tag is not supported" +
+                                                                                " for pseudowire %d.", tunnelId)));
+        }
+
+        if ((ingressInner.equals(VlanId.NONE) && !egressInner.equals(VlanId.NONE))
+                || (!ingressInner.equals(VlanId.NONE) && egressInner.equals(VlanId.NONE))) {
+            throw new IllegalArgumentException(String.valueOf(String.format("single-tag <-> untag is not supported" +
+                                                                            " for pseudowire %d.", tunnelId)));
+        }
+
+
+        if (!ingressInner.equals(egressInner) && !ingressOuter.equals(egressOuter)) {
+            throw new IllegalArgumentException(String.valueOf(String.format("We do not support changing both tags " +
+                                                                             "in double tagged pws, only the outer," +
+                                                                             " for pseudowire %d.", tunnelId)));
+        }
+
+        // check if cp1 and port of cp1 exist
+        if (deviceService.getDevice(cP1.deviceId()) == null) {
+            throw new IllegalArgumentException(String.valueOf(String.format("cP1 device %s does not exist for" +
+                                                                            " pseudowire %d.", cP1.deviceId(),
+                                                                            tunnelId)));
+        }
+
+        if (deviceService.getPort(cP1) == null) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Port %s for cP1 device %s does not" +
+                                                                            " exist for pseudowire %d.", cP1.port(),
+                                                                            cP1.deviceId(), tunnelId)));
+        }
+
+        // check if cp2 and port of cp2 exist
+        if (deviceService.getDevice(cP2.deviceId()) == null) {
+            throw new IllegalArgumentException(String.valueOf(String.format("cP2 device %s does not exist for" +
+                                                                            " pseudowire %d.", cP2.deviceId(),
+                                                                            tunnelId)));
+        }
+
+        if (deviceService.getPort(cP2) == null) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Port %s for cP2 device %s does " +
+                                                                            "not exist for pseudowire %d.",
+                                                                            cP2.port(), cP2.deviceId(), tunnelId)));
+        }
+    }
+
+    /**
+     * Verifies that the pseudowires will not conflict with each other.
+     *
+     * Further, check if vlans for connect points are already used.
+     *
+     * @param tunnel Tunnel for pw
+     * @param policy Policy for pw
+     * @param labelSet Label set used so far with this configuration
+     * @param vlanSet Vlan set used with this configuration
+     * @param tunnelSet Tunnel set used with this configuration
+     */
+    private void verifyGlobalValidity(L2Tunnel tunnel,
+                                      L2TunnelPolicy policy,
+                                      Set<MplsLabel> labelSet,
+                                      Map<ConnectPoint, Set<VlanId>> vlanSet,
+                                      Set<Long> tunnelSet) {
+
+        if (tunnelSet.contains(tunnel.tunnelId())) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Tunnel Id %d already used by" +
+                                                                           " another pseudowire, in " +
+                                                                           "pseudowire %d!", tunnel.tunnelId(),
+                                                                            tunnel.tunnelId())));
+        }
+        tunnelSet.add(tunnel.tunnelId());
+
+        // check if tunnel id is used again
+        ConnectPoint cP1 = policy.cP1();
+        ConnectPoint cP2 = policy.cP2();
+
+        // insert cps to hashmap if this is the first time seen
+        if (!vlanSet.containsKey(cP1)) {
+            vlanSet.put(cP1, new HashSet<VlanId>());
+        }
+        if (!vlanSet.containsKey(cP2)) {
+            vlanSet.put(cP2, new HashSet<VlanId>());
+        }
+
+        // if single tagged or untagged vlan is the inner
+        // if double tagged vlan is the outer
+        VlanId vlanToCheckCP1;
+        if (policy.cP1OuterTag().equals(VlanId.NONE)) {
+            vlanToCheckCP1 = policy.cP1InnerTag();
+        } else {
+            vlanToCheckCP1 = policy.cP1OuterTag();
+        }
+
+        VlanId vlanToCheckCP2;
+        if (policy.cP2OuterTag().equals(VlanId.NONE)) {
+            vlanToCheckCP2 = policy.cP2InnerTag();
+        } else {
+            vlanToCheckCP2 = policy.cP2OuterTag();
+        }
+
+        if (labelSet.contains(tunnel.pwLabel())) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Label %s already used by another" +
+                                                                            " pseudowire, in pseudowire %d!",
+                                                                            tunnel.pwLabel(), tunnel.tunnelId())));
+        }
+        labelSet.add(tunnel.pwLabel());
+
+        if (vlanSet.get(cP1).contains(vlanToCheckCP1)) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP1 %s already used " +
+                                                                            "by another pseudowire, in pseudowire" +
+                                                                            " %d!", vlanToCheckCP1,  cP1,
+                                                                            tunnel.tunnelId())));
+        }
+        vlanSet.get(cP1).add(vlanToCheckCP1);
+
+        if (vlanSet.get(cP2).contains(vlanToCheckCP2)) {
+            throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP2 %s already used" +
+                                                                            " by another pseudowire, in" +
+                                                                            " pseudowire %d!", vlanToCheckCP2, cP2,
+                                                                            tunnel.tunnelId())));
+        }
+        vlanSet.get(cP2).add(vlanToCheckCP2);
+
+        // check that vlans for the connect points are not used
+        intfService.getInterfacesByPort(cP1).stream()
+                .forEach(intf -> {
+
+                    // check if tagged pw affects tagged interface
+                    if (intf.vlanTagged().contains(vlanToCheckCP1)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP1 %s already" +
+                                                                                        " used for this interface, in" +
+                                                                                        " pseudowire %d!",
+                                                                                        vlanToCheckCP1, cP1,
+                                                                                        tunnel.tunnelId())));
+                    }
+
+                    // if vlanNative != null this interface is configured with untagged traffic also
+                    // check if it collides with untagged interface
+                    if ((intf.vlanNative() != null) && vlanToCheckCP1.equals(VlanId.NONE)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP1 " +
+                                                                                         "%s already used for this " +
+                                                                                         "interface, in pseudowire " +
+                                                                                         "%d!", cP1,
+                                                                                         tunnel.tunnelId())));
+                    }
+
+                    // if vlanUntagged != null this interface is configured only with untagged traffic
+                    // check if it collides with untagged interface
+                    if ((intf.vlanUntagged() != null) && vlanToCheckCP1.equals(VlanId.NONE)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for " +
+                                                                                         "cP1 %s already" +
+                                                                                         " used for this interface," +
+                                                                                         " in pseudowire %d!",
+                                                                                         cP1, tunnel.tunnelId())));
+                    }
+                });
+
+        intfService.getInterfacesByPort(cP2).stream()
+                .forEach(intf -> {
+                    if (intf.vlanTagged().contains(vlanToCheckCP2)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Vlan '%s' for cP2 %s already" +
+                                                                                        " used for  this interface, " +
+                                                                                        "in pseudowire %d!",
+                                                                                        vlanToCheckCP2, cP2,
+                                                                                        tunnel.tunnelId())));
+                    }
+
+                    // if vlanNative != null this interface is configured with untagged traffic also
+                    // check if it collides with untagged interface
+                    if ((intf.vlanNative() != null) && vlanToCheckCP2.equals(VlanId.NONE)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP2 %s " +
+                                                                                        "already used for this" +
+                                                                                        " interface, " +
+                                                                                        "in pseudowire %d!",
+                                                                                        cP2, tunnel.tunnelId())));
+                    }
+
+                    // if vlanUntagged != null this interface is configured only with untagged traffic
+                    // check if it collides with untagged interface
+                    if ((intf.vlanUntagged() != null) && vlanToCheckCP2.equals(VlanId.NONE)) {
+                        throw new IllegalArgumentException(String.valueOf(String.format("Untagged traffic for cP2 %s" +
+                                                                                        " already" +
+                                                                                        " used for this interface, " +
+                                                                                        "in pseudowire %d!",
+                                                                                        cP2, tunnel.tunnelId())));
+                    }
+                });
+
+    }
+
+    /**
+     * Helper method to verify the integrity of the pseudo wire.
+     *
+     * @param l2TunnelDescription the pseudo wire description
+     * @return the result of the check
+     */
+    private void verifyPseudoWire(L2TunnelDescription l2TunnelDescription,
+                                  Set<MplsLabel> labelSet,
+                                  Map<ConnectPoint, Set<VlanId>> vlanset,
+                                  Set<Long> tunnelSet) {
+
+        L2Tunnel l2Tunnel = l2TunnelDescription.l2Tunnel();
+        L2TunnelPolicy l2TunnelPolicy = l2TunnelDescription.l2TunnelPolicy();
+
+        verifyTunnel(l2Tunnel);
+
+        verifyPolicy(
+                l2TunnelPolicy.cP1(),
+                l2TunnelPolicy.cP2(),
+                l2TunnelPolicy.cP1InnerTag(),
+                l2TunnelPolicy.cP1OuterTag(),
+                l2TunnelPolicy.cP2InnerTag(),
+                l2TunnelPolicy.cP2OuterTag(),
+                l2Tunnel.tunnelId()
+        );
+
+        verifyGlobalValidity(l2Tunnel,
+                             l2TunnelPolicy,
+                             labelSet,
+                             vlanset,
+                             tunnelSet);
+
+    }
+
+    /**
+     * Checks if the configured pseudowires will create problems in the network.
+     * If yes, then no pseudowires is deployed from this configuration.
+     *
+     * @param pseudowires Set of pseudowries to validate
+     * @return returns true if everything goes well.
+     */
+    public boolean configurationValidity(Set<L2TunnelDescription> pseudowires) {
+
+        // structures to keep pw information
+        // in order to see if instantiating them will create
+        // problems
+        Set<Long> tunIds = new HashSet<>();
+        Set<MplsLabel> labelsUsed = new HashSet<>();
+        Map<ConnectPoint, Set<VlanId>> vlanIds = new HashMap<>();
+
+        // check that pseudowires can be instantiated in the network
+        // we try to guarantee that all the pws will work before
+        // instantiating any of them
+        for (L2TunnelDescription pw : pseudowires) {
+            verifyPseudoWire(pw, labelsUsed, vlanIds, tunIds);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns all pseudo wire keys.
+     *
+     * @return all keys (tunnels id)
+     * @throws IllegalArgumentException if wrong format
+     */
+    public Set<Long> getPwIds() {
+        ImmutableSet.Builder<Long> builder = ImmutableSet.builder();
+        object.fields().forEachRemaining(entry -> {
+            Long tunnelId = Long.parseLong(entry.getKey());
+            builder.add(tunnelId);
+        });
+        return builder.build();
+    }
+
+    /**
+     * Parses a vlan as a string. Returns the VlanId if
+     * provided String can be parsed as an integer or is '' / '*'
+     *
+     * @param vlan string as read from configuration
+     * @return VlanId
+     * @throws IllegalArgumentException if wrong format of vlan
+     */
+    public VlanId parseVlan(String vlan) {
+
+        if (vlan.equals("*") || vlan.equals("Any")) {
+            return VlanId.vlanId("Any");
+        } else if (vlan.equals("") || vlan.equals("None")) {
+            return VlanId.vlanId("None");
+        } else {
+            try {
+                VlanId newVlan = VlanId.vlanId(vlan);
+                return newVlan;
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException(INVALID_VLAN);
+            }
+        }
+    }
+
+    /**
+     *
+     * @param mode RAW or TAGGED
+     * @return the L2Mode if input is correct
+     * @throws  IllegalArgumentException if not supported mode
+     */
+    public L2Mode parseMode(String mode) {
+
+        if (!mode.equals("RAW") && !mode.equals("TAGGED")) {
+            throw  new IllegalArgumentException(INVALID_L2_MODE);
+        }
+
+        return L2Mode.valueOf(mode);
+    }
+
+    /**
+     *
+     * @param label the mpls label of the pseudowire
+     * @return the MplsLabel
+     * @throws IllegalArgumentException if label is invalid
+     */
+    public MplsLabel parsePWLabel(String label) {
+
+        try {
+            MplsLabel pwLabel = MplsLabel.mplsLabel(label);
+            return pwLabel;
+        } catch (Exception e) {
+            throw new IllegalArgumentException(INVALID_PW_LABEL);
+        }
+    }
+
+    /**
+     * Returns pw description of given pseudo wire id.
+     *
+     * @param tunnelId pseudo wire key
+     * @return set of l2 tunnel descriptions
+     * @throws IllegalArgumentException if wrong format
+     */
+    public L2TunnelDescription getPwDescription(Long tunnelId) {
+        JsonNode pwDescription = object.get(tunnelId.toString());
+        if (!hasFields((ObjectNode) pwDescription,
+                      SRC_CP, SRC_INNER_TAG, SRC_OUTER_TAG,
+                      DST_CP, DST_INNER_TAG, DST_OUTER_TAG,
+                      MODE, SD_TAG, PW_LABEL)) {
+            throw new IllegalArgumentException(MISSING_PARAMS);
+        }
+        String tempString;
+
+        tempString = pwDescription.get(SRC_CP).asText();
+        ConnectPoint srcCp = ConnectPoint.deviceConnectPoint(tempString);
+
+        tempString = pwDescription.get(DST_CP).asText();
+        ConnectPoint dstCp = ConnectPoint.deviceConnectPoint(tempString);
+
+        tempString = pwDescription.get(SRC_INNER_TAG).asText();
+        VlanId srcInnerTag = parseVlan(tempString);
+
+        tempString = pwDescription.get(SRC_OUTER_TAG).asText();
+        VlanId srcOuterTag = parseVlan(tempString);
+
+        tempString = pwDescription.get(DST_INNER_TAG).asText();
+        VlanId dstInnerTag = parseVlan(tempString);
+
+        tempString = pwDescription.get(DST_OUTER_TAG).asText();
+        VlanId dstOuterTag = parseVlan(tempString);
+
+        tempString = pwDescription.get(MODE).asText();
+        L2Mode l2Mode = parseMode(tempString);
+
+        tempString = pwDescription.get(SD_TAG).asText();
+        VlanId sdTag = parseVlan(tempString);
+
+        tempString = pwDescription.get(PW_LABEL).asText();
+        MplsLabel pwLabel = parsePWLabel(tempString);
+
+        L2Tunnel l2Tunnel = new DefaultL2Tunnel(
+                l2Mode,
+                sdTag,
+                tunnelId,
+                pwLabel
+        );
+
+        L2TunnelPolicy l2TunnelPolicy = new DefaultL2TunnelPolicy(
+                tunnelId,
+                srcCp,
+                srcInnerTag,
+                srcOuterTag,
+                dstCp,
+                dstInnerTag,
+                dstOuterTag
+        );
+
+        return new DefaultL2TunnelDescription(l2Tunnel, l2TunnelPolicy);
+    }
+
+    /**
+     * Removes a pseudowire from the configuration tree.
+     * @param pwId Pseudowire id
+     * @return null if pwId did not exist, or the object representing the
+     * udpated configuration tree
+     */
+    public ObjectNode removePseudowire(String pwId) {
+
+        JsonNode value = object.remove(pwId);
+        if (value == null) {
+            return (ObjectNode) value;
+        } else {
+            return object;
+        }
+    }
+
+    /**
+     * Adds a pseudowire to the configuration tree of pwwas. It also checks
+     * if the configuration is valid, if not return null and does not add the node,
+     * if yes return the new configuration. Caller will propagate update events.
+     *
+     * If the pseudowire already exists in the configuration it gets updated.
+     *
+     * @param tunnelId Id of tunnel
+     * @param pwLabel PW label of tunnel
+     * @param cP1 Connection point 1
+     * @param cP1InnerVlan Inner vlan of cp1
+     * @param cP1OuterVlan Outer vlan of cp2
+     * @param cP2 Connection point 2
+     * @param cP2InnerVlan Inner vlan of cp2
+     * @param cP2OuterVlan Outer vlan of cp2
+     * @param mode Mode for the pw
+     * @param sdTag Service delimiting tag for the pw
+     * @return The ObjectNode config if configuration is valid with the new pseudowire
+     * or null.
+     */
+    public ObjectNode addPseudowire(String tunnelId, String pwLabel, String cP1,
+                                    String cP1InnerVlan, String cP1OuterVlan, String cP2,
+                                    String cP2InnerVlan, String cP2OuterVlan,
+                                    String mode, String sdTag) {
+
+
+        ObjectNode newPw = new ObjectNode(JsonNodeFactory.instance);
+
+        // add fields for pseudowire
+        newPw.put(SRC_CP, cP1);
+        newPw.put(DST_CP, cP2);
+        newPw.put(PW_LABEL, pwLabel);
+        newPw.put(SRC_INNER_TAG, cP1InnerVlan);
+        newPw.put(SRC_OUTER_TAG, cP1OuterVlan);
+        newPw.put(DST_INNER_TAG, cP2InnerVlan);
+        newPw.put(DST_OUTER_TAG, cP2OuterVlan);
+        newPw.put(SD_TAG, sdTag);
+        newPw.put(MODE, mode);
+
+        object.set(tunnelId, newPw);
+
+        if (!isValid()) {
+            log.info("Pseudowire could not be created : {}");
+            object.remove(tunnelId);
+            return null;
+        }
+
+        return object;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java
new file mode 100644
index 0000000..3e4d17c
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfig.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.Config;
+
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * App configuration object for Segment Routing.
+ */
+public class SegmentRoutingAppConfig extends Config<ApplicationId> {
+    private static final String VROUTER_MACS = "vRouterMacs";
+    private static final String SUPPRESS_SUBNET = "suppressSubnet";
+    private static final String SUPPRESS_HOST_BY_PORT = "suppressHostByPort";
+    // TODO We might want to move SUPPRESS_HOST_BY_PROVIDER to Component Config
+    private static final String SUPPRESS_HOST_BY_PROVIDER = "suppressHostByProvider";
+    private static final String MPLS_ECMP = "MPLS-ECMP";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(VROUTER_MACS, SUPPRESS_SUBNET,
+                SUPPRESS_HOST_BY_PORT, SUPPRESS_HOST_BY_PROVIDER, MPLS_ECMP) &&
+                vRouterMacs() != null &&
+                suppressSubnet() != null && suppressHostByPort() != null &&
+                suppressHostByProvider() != null;
+    }
+
+    /**
+     * Gets MPLS-ECMP configuration from the config.
+     *
+     * @return the configuration of MPLS-ECMP. If it is not
+     *         specified, the default behavior is false.
+     */
+    public boolean mplsEcmp() {
+        return get(MPLS_ECMP, false);
+    }
+
+    /**
+     * Sets MPLS-ECMP to the config.
+     *
+     * @param mplsEcmp the MPLS-ECMP configuration
+     * @return this {@link SegmentRoutingAppConfig}
+     */
+    public SegmentRoutingAppConfig setMplsEcmp(boolean mplsEcmp) {
+        object.put(MPLS_ECMP, mplsEcmp);
+        return this;
+    }
+
+    /**
+     * Gets vRouters from the config.
+     *
+     * @return Set of vRouter MAC addresses, empty is not specified,
+     *         or null if not valid
+     */
+    public Set<MacAddress> vRouterMacs() {
+        if (!object.has(VROUTER_MACS)) {
+            return ImmutableSet.of();
+        }
+
+        ImmutableSet.Builder<MacAddress> builder = ImmutableSet.builder();
+        ArrayNode arrayNode = (ArrayNode) object.path(VROUTER_MACS);
+        for (JsonNode jsonNode : arrayNode) {
+            MacAddress mac;
+
+            String macStr = jsonNode.asText(null);
+            if (macStr == null) {
+                return null;
+            }
+            try {
+                mac = MacAddress.valueOf(macStr);
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+
+            builder.add(mac);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Sets vRouters to the config.
+     *
+     * @param vRouterMacs a set of vRouter MAC addresses
+     * @return this {@link SegmentRoutingAppConfig}
+     */
+    public SegmentRoutingAppConfig setVRouterMacs(Set<MacAddress> vRouterMacs) {
+        if (vRouterMacs == null) {
+            object.remove(VROUTER_MACS);
+        } else {
+            ArrayNode arrayNode = mapper.createArrayNode();
+
+            vRouterMacs.forEach(mac -> {
+                arrayNode.add(mac.toString());
+            });
+
+            object.set(VROUTER_MACS, arrayNode);
+        }
+        return this;
+    }
+
+    /**
+     * Gets names of ports to which SegmentRouting does not push subnet rules.
+     *
+     * @return Set of port names, empty if not specified, or null
+     *         if not valid
+     */
+    public Set<ConnectPoint> suppressSubnet() {
+        if (!object.has(SUPPRESS_SUBNET)) {
+            return ImmutableSet.of();
+        }
+
+        ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+        ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_SUBNET);
+        for (JsonNode jsonNode : arrayNode) {
+            String portName = jsonNode.asText(null);
+            if (portName == null) {
+                return null;
+            }
+            try {
+                builder.add(ConnectPoint.deviceConnectPoint(portName));
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Sets names of ports to which SegmentRouting does not push subnet rules.
+     *
+     * @param suppressSubnet names of ports to which SegmentRouting does not push
+     *                     subnet rules
+     * @return this {@link SegmentRoutingAppConfig}
+     */
+    public SegmentRoutingAppConfig setSuppressSubnet(Set<ConnectPoint> suppressSubnet) {
+        if (suppressSubnet == null) {
+            object.remove(SUPPRESS_SUBNET);
+        } else {
+            ArrayNode arrayNode = mapper.createArrayNode();
+            suppressSubnet.forEach(connectPoint -> {
+                arrayNode.add(connectPoint.deviceId() + "/" + connectPoint.port());
+            });
+            object.set(SUPPRESS_SUBNET, arrayNode);
+        }
+        return this;
+    }
+
+    /**
+     * Gets connect points to which SegmentRouting does not push host rules.
+     *
+     * @return Set of connect points, empty if not specified, or null
+     *         if not valid
+     */
+    public Set<ConnectPoint> suppressHostByPort() {
+        if (!object.has(SUPPRESS_HOST_BY_PORT)) {
+            return ImmutableSet.of();
+        }
+
+        ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+        ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_HOST_BY_PORT);
+        for (JsonNode jsonNode : arrayNode) {
+            String portName = jsonNode.asText(null);
+            if (portName == null) {
+                return null;
+            }
+            try {
+                builder.add(ConnectPoint.deviceConnectPoint(portName));
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Sets connect points to which SegmentRouting does not push host rules.
+     *
+     * @param connectPoints connect points to which SegmentRouting does not push
+     *                     host rules
+     * @return this {@link SegmentRoutingAppConfig}
+     */
+    public SegmentRoutingAppConfig setSuppressHostByPort(Set<ConnectPoint> connectPoints) {
+        if (connectPoints == null) {
+            object.remove(SUPPRESS_HOST_BY_PORT);
+        } else {
+            ArrayNode arrayNode = mapper.createArrayNode();
+            connectPoints.forEach(connectPoint -> {
+                arrayNode.add(connectPoint.deviceId() + "/" + connectPoint.port());
+            });
+            object.set(SUPPRESS_HOST_BY_PORT, arrayNode);
+        }
+        return this;
+    }
+
+    /**
+     * Gets provider names from which SegmentRouting does not learn host info.
+     *
+     * @return array of provider names that need to be ignored
+     */
+    public Set<String> suppressHostByProvider() {
+        if (!object.has(SUPPRESS_HOST_BY_PROVIDER)) {
+            return ImmutableSet.of();
+        }
+
+        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+        ArrayNode arrayNode = (ArrayNode) object.path(SUPPRESS_HOST_BY_PROVIDER);
+        for (JsonNode jsonNode : arrayNode) {
+            String providerName = jsonNode.asText(null);
+            if (providerName == null) {
+                return null;
+            }
+            builder.add(providerName);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Sets provider names from which SegmentRouting does not learn host info.
+     *
+     * @param providers set of provider names
+     * @return this {@link SegmentRoutingAppConfig}
+     */
+    public SegmentRoutingAppConfig setSuppressHostByProvider(Set<String> providers) {
+        if (providers == null) {
+            object.remove(SUPPRESS_HOST_BY_PROVIDER);
+        } else {
+            ArrayNode arrayNode = mapper.createArrayNode();
+            providers.forEach(arrayNode::add);
+            object.set(SUPPRESS_HOST_BY_PROVIDER, arrayNode);
+        }
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("vRouterMacs", vRouterMacs())
+                .add("suppressSubnet", suppressSubnet())
+                .add("suppressHostByPort", suppressHostByPort())
+                .add("suppressHostByProvider", suppressHostByProvider())
+                .add("mplsEcmp", mplsEcmp())
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java
new file mode 100644
index 0000000..7959df6
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfig.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableMap;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Configuration object for Segment Routing Application.
+ */
+public class SegmentRoutingDeviceConfig extends Config<DeviceId> {
+    private static final String NAME = "name";
+    private static final String IP4 = "ipv4Loopback";
+    private static final String IP6 = "ipv6Loopback";
+    private static final String MAC = "routerMac";
+    private static final String IP4_SID = "ipv4NodeSid";
+    private static final String IP6_SID = "ipv6NodeSid";
+    private static final String EDGE = "isEdgeRouter";
+    /**
+     * Adjancency SIDs config.
+     *
+     * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+     */
+    @Deprecated
+    private static final String ADJSIDS = "adjacencySids";
+
+    /**
+     * Adjancency SID config.
+     *
+     * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+     */
+    @Deprecated
+    private static final String ADJSID = "adjSid";
+
+    /**
+     * Adjancency port config.
+     *
+     * @deprecated in Loon (1.11). We are not using and do not plan to use it.
+     */
+    @Deprecated
+    private static final String PORTS = "ports";
+
+    private static final String PAIR_DEVICE_ID = "pairDeviceId";
+    private static final String PAIR_LOCAL_PORT = "pairLocalPort";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(NAME, IP4, IP6, MAC, IP4_SID, IP6_SID, EDGE, ADJSIDS, ADJSID, PORTS,
+                             PAIR_DEVICE_ID, PAIR_LOCAL_PORT) &&
+                name() != null &&
+                routerIpv4() != null && (!hasField(IP6) || routerIpv6() != null) &&
+                routerMac() != null &&
+                nodeSidIPv4() != -1 && (!hasField(IP6_SID) || nodeSidIPv6() != -1) &&
+                isEdgeRouter() != null &&
+                adjacencySids() != null &&
+                // pairDeviceId and pairLocalPort must be both configured or both omitted
+                (hasField(PAIR_DEVICE_ID) == hasField(PAIR_LOCAL_PORT)) &&
+                (!hasField(PAIR_DEVICE_ID) || pairDeviceId() != null) &&
+                (!hasField(PAIR_LOCAL_PORT) || pairLocalPort() != null);
+    }
+
+    /**
+     * Gets the name of the router.
+     *
+     * @return Optional name of the router. May be empty if not configured.
+     */
+    public Optional<String> name() {
+        String name = get(NAME, null);
+        return name != null ? Optional.of(name) : Optional.empty();
+    }
+
+    /**
+     * Sets the name of the router.
+     *
+     * @param name name of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setName(String name) {
+        return (SegmentRoutingDeviceConfig) setOrClear(NAME, name);
+    }
+
+    /**
+     * Gets the IPv4 address of the router.
+     *
+     * @return IP address of the router. Or null if not configured.
+     */
+    public Ip4Address routerIpv4() {
+        String ip = get(IP4, null);
+        return ip != null ? Ip4Address.valueOf(ip) : null;
+    }
+
+    /**
+     * Gets the IPv6 address of the router.
+     *
+     * @return IP address of the router. Or null if not configured.
+     */
+    public Ip6Address routerIpv6() {
+        String ip = get(IP6, null);
+        return ip != null ? Ip6Address.valueOf(ip) : null;
+    }
+
+    /**
+     * Sets the IPv4 address of the router.
+     *
+     * @param ip IPv4 address of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setRouterIpv4(String ip) {
+        return (SegmentRoutingDeviceConfig) setOrClear(IP4, ip);
+    }
+
+    /**
+     * Sets the IPv6 address of the router.
+     *
+     * @param ip IPv6 address of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setRouterIpv6(String ip) {
+        return (SegmentRoutingDeviceConfig) setOrClear(IP6, ip);
+    }
+
+    /**
+     * Gets the MAC address of the router.
+     *
+     * @return MAC address of the router. Or null if not configured.
+     */
+    public MacAddress routerMac() {
+        String mac = get(MAC, null);
+        return mac != null ? MacAddress.valueOf(mac) : null;
+    }
+
+    /**
+     * Sets the MAC address of the router.
+     *
+     * @param mac MAC address of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setRouterMac(String mac) {
+        return (SegmentRoutingDeviceConfig) setOrClear(MAC, mac);
+    }
+
+    /**
+     * Gets the IPv4 node SID of the router.
+     *
+     * @return node SID of the router. Or -1 if not configured.
+     */
+    public int nodeSidIPv4() {
+        return get(IP4_SID, -1);
+    }
+
+    /**
+     * Gets the IPv6 node SID of the router.
+     *
+     * @return node SID of the router. Or -1 if not configured.
+     */
+    public int nodeSidIPv6() {
+        return get(IP6_SID, -1);
+    }
+
+    /**
+     * Sets the node IPv4 node SID of the router.
+     *
+     * @param sid node SID of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setNodeSidIPv4(int sid) {
+        return (SegmentRoutingDeviceConfig) setOrClear(IP4_SID, sid);
+    }
+
+    /**
+     * Sets the node IPv6 node SID of the router.
+     *
+     * @param sid node SID of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setNodeSidIPv6(int sid) {
+        return (SegmentRoutingDeviceConfig) setOrClear(IP6_SID, sid);
+    }
+
+    /**
+     * Checks if the router is an edge router.
+     *
+     * @return true if the router is an edge router.
+     *         false if the router is not an edge router.
+     *         null if the value is not configured.
+     */
+    public Boolean isEdgeRouter() {
+        String isEdgeRouter = get(EDGE, null);
+        return isEdgeRouter != null ?
+                Boolean.valueOf(isEdgeRouter) :
+                null;
+    }
+
+    /**
+     * Specifies if the router is an edge router.
+     *
+     * @param isEdgeRouter true if the router is an edge router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setIsEdgeRouter(boolean isEdgeRouter) {
+        return (SegmentRoutingDeviceConfig) setOrClear(EDGE, isEdgeRouter);
+    }
+
+    /**
+     * Gets the adjacency SIDs of the router.
+     *
+     * @return adjacency SIDs of the router. Or null if not configured.
+     */
+    public Map<Integer, Set<Integer>> adjacencySids() {
+        if (!object.has(ADJSIDS)) {
+            return null;
+        }
+
+        Map<Integer, Set<Integer>> adjacencySids = new HashMap<>();
+        ArrayNode adjacencySidsNode = (ArrayNode) object.path(ADJSIDS);
+        for (JsonNode adjacencySidNode : adjacencySidsNode) {
+            int asid = adjacencySidNode.path(ADJSID).asInt(-1);
+            if (asid == -1) {
+                return null;
+            }
+
+            HashSet<Integer> ports = new HashSet<>();
+            ArrayNode portsNode = (ArrayNode) adjacencySidNode.path(PORTS);
+            for (JsonNode portNode : portsNode) {
+                int port = portNode.asInt(-1);
+                if (port == -1) {
+                    return null;
+                }
+                ports.add(port);
+            }
+            adjacencySids.put(asid, ports);
+        }
+
+        return ImmutableMap.copyOf(adjacencySids);
+    }
+
+    /**
+     * Sets the adjacency SIDs of the router.
+     *
+     * @param adjacencySids adjacency SIDs of the router.
+     * @return the config of the router.
+     */
+    public SegmentRoutingDeviceConfig setAdjacencySids(Map<Integer, Set<Integer>> adjacencySids) {
+        if (adjacencySids == null) {
+            object.remove(ADJSIDS);
+        } else {
+            ArrayNode adjacencySidsNode = mapper.createArrayNode();
+
+            adjacencySids.forEach((sid, ports) -> {
+                ObjectNode adjacencySidNode = mapper.createObjectNode();
+
+                adjacencySidNode.put(ADJSID, sid);
+
+                ArrayNode portsNode = mapper.createArrayNode();
+                ports.forEach(port -> {
+                    portsNode.add(port.toString());
+                });
+                adjacencySidNode.set(PORTS, portsNode);
+
+                adjacencySidsNode.add(adjacencySidNode);
+            });
+
+            object.set(ADJSIDS, adjacencySidsNode);
+        }
+
+        return this;
+    }
+
+    /**
+     * Gets the pair device id.
+     *
+     * @return pair device id; Or null if not configured.
+     */
+    public DeviceId pairDeviceId() {
+        String pairDeviceId = get(PAIR_DEVICE_ID, null);
+        return pairDeviceId != null ? DeviceId.deviceId(pairDeviceId) : null;
+    }
+
+    /**
+     * Sets the pair device id.
+     *
+     * @param deviceId pair device id
+     * @return this configuration
+     */
+    public SegmentRoutingDeviceConfig setPairDeviceId(DeviceId deviceId) {
+        return (SegmentRoutingDeviceConfig) setOrClear(PAIR_DEVICE_ID, deviceId.toString());
+    }
+
+    /**
+     * Gets the pair local port.
+     *
+     * @return pair local port; Or null if not configured.
+     */
+    public PortNumber pairLocalPort() {
+        long pairLocalPort = get(PAIR_LOCAL_PORT, -1L);
+        return pairLocalPort != -1L ? PortNumber.portNumber(pairLocalPort) : null;
+    }
+
+    /**
+     * Sets the pair local port.
+     *
+     * @param portNumber pair local port
+     * @return this configuration
+     */
+    public SegmentRoutingDeviceConfig setPairLocalPort(PortNumber portNumber) {
+        return (SegmentRoutingDeviceConfig) setOrClear(PAIR_LOCAL_PORT, portNumber.toLong());
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
new file mode 100644
index 0000000..76a3917
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/XConnectConfig.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Configuration object for cross-connect.
+ */
+public class XConnectConfig extends Config<ApplicationId> {
+    private static final String VLAN = "vlan";
+    private static final String PORTS = "ports";
+    private static final String NAME = "name"; // dummy field for naming
+
+    private static final String UNEXPECTED_FIELD_NAME = "Unexpected field name";
+
+    @Override
+    public boolean isValid() {
+        try {
+            getXconnects().forEach(this::getPorts);
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns all xconnect keys.
+     *
+     * @return all keys (device/vlan pairs)
+     * @throws IllegalArgumentException if wrong format
+     */
+    public Set<XConnectStoreKey> getXconnects() {
+        ImmutableSet.Builder<XConnectStoreKey> builder = ImmutableSet.builder();
+        object.fields().forEachRemaining(entry -> {
+            DeviceId deviceId = DeviceId.deviceId(entry.getKey());
+            builder.addAll(getXconnects(deviceId));
+        });
+        return builder.build();
+    }
+
+    /**
+     * Returns xconnect keys of given device.
+     *
+     * @param deviceId ID of the device from which we want to get XConnect info
+     * @return xconnect keys (device/vlan pairs) of given device
+     * @throws IllegalArgumentException if wrong format
+     */
+    public Set<XConnectStoreKey> getXconnects(DeviceId deviceId) {
+        ImmutableSet.Builder<XConnectStoreKey> builder = ImmutableSet.builder();
+        JsonNode vlanPortPair = object.get(deviceId.toString());
+        if (vlanPortPair != null) {
+            vlanPortPair.forEach(jsonNode -> {
+                if (!hasOnlyFields((ObjectNode) jsonNode, VLAN, PORTS, NAME)) {
+                    throw new IllegalArgumentException(UNEXPECTED_FIELD_NAME);
+                }
+                VlanId vlanId = VlanId.vlanId((short) jsonNode.get(VLAN).asInt());
+                builder.add(new XConnectStoreKey(deviceId, vlanId));
+            });
+        }
+        return builder.build();
+    }
+
+    /**
+     * Returns ports of given xconnect key.
+     *
+     * @param xconnect xconnect key
+     * @return set of two ports associated with given xconnect key
+     * @throws IllegalArgumentException if wrong format
+     */
+    public Set<PortNumber> getPorts(XConnectStoreKey xconnect) {
+        ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+        object.get(xconnect.deviceId().toString()).forEach(vlanPortsPair -> {
+            if (xconnect.vlanId().toShort() == vlanPortsPair.get(VLAN).asInt()) {
+                int portCount = vlanPortsPair.get(PORTS).size();
+                checkArgument(portCount == 2,
+                        "Expect 2 ports but found " + portCount + " on " + xconnect);
+                vlanPortsPair.get(PORTS).forEach(portNode -> {
+                    builder.add(PortNumber.portNumber(portNode.asInt()));
+                });
+            }
+        });
+        return builder.build();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java
new file mode 100644
index 0000000..a664a8f
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/config/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing network configuration mechanism.
+ */
+package org.onosproject.segmentrouting.config;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
new file mode 100644
index 0000000..75cab32
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DefaultGroupHandler.java
@@ -0,0 +1,1424 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import org.apache.commons.lang3.RandomUtils;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.segmentrouting.DefaultRoutingHandler;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceProperties;
+import org.onosproject.segmentrouting.storekey.DestinationSetNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.PortNextObjectiveStoreKey;
+import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.slf4j.Logger;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Default ECMP group handler creation module. This component creates a set of
+ * ECMP groups for every neighbor that this device is connected to based on
+ * whether the current device is an edge device or a transit device.
+ */
+public class DefaultGroupHandler {
+    private static final Logger log = getLogger(DefaultGroupHandler.class);
+
+    private static final long VERIFY_INTERVAL = 30; // secs
+
+    protected final DeviceId deviceId;
+    protected final ApplicationId appId;
+    protected final DeviceProperties deviceConfig;
+    protected final List<Integer> allSegmentIds;
+    protected int ipv4NodeSegmentId = -1;
+    protected int ipv6NodeSegmentId = -1;
+    protected boolean isEdgeRouter = false;
+    protected MacAddress nodeMacAddr = null;
+    protected LinkService linkService;
+    protected FlowObjectiveService flowObjectiveService;
+    /**
+     * local store for neighbor-device-ids and the set of ports on this device
+     * that connect to the same neighbor.
+     */
+    protected ConcurrentHashMap<DeviceId, Set<PortNumber>> devicePortMap =
+            new ConcurrentHashMap<>();
+    /**
+     *  local store for ports on this device connected to neighbor-device-id.
+     */
+    protected ConcurrentHashMap<PortNumber, DeviceId> portDeviceMap =
+            new ConcurrentHashMap<>();
+
+    // distributed store for (device+destination-set) mapped to next-id and neighbors
+    protected EventuallyConsistentMap<DestinationSetNextObjectiveStoreKey, NextNeighbors>
+            dsNextObjStore = null;
+    // distributed store for (device+subnet-ip-prefix) mapped to next-id
+    protected EventuallyConsistentMap<VlanNextObjectiveStoreKey, Integer>
+            vlanNextObjStore = null;
+    // distributed store for (device+port+treatment) mapped to next-id
+    protected EventuallyConsistentMap<PortNextObjectiveStoreKey, Integer>
+            portNextObjStore = null;
+    private SegmentRoutingManager srManager;
+
+    private ScheduledExecutorService executorService
+    = newScheduledThreadPool(1, groupedThreads("bktCorrector", "bktC-%d", log));
+
+    protected KryoNamespace.Builder kryo = new KryoNamespace.Builder()
+            .register(URI.class).register(HashSet.class)
+            .register(DeviceId.class).register(PortNumber.class)
+            .register(DestinationSet.class).register(PolicyGroupIdentifier.class)
+            .register(PolicyGroupParams.class)
+            .register(GroupBucketIdentifier.class)
+            .register(GroupBucketIdentifier.BucketOutputType.class);
+
+    protected DefaultGroupHandler(DeviceId deviceId, ApplicationId appId,
+                                  DeviceProperties config,
+                                  LinkService linkService,
+                                  FlowObjectiveService flowObjService,
+                                  SegmentRoutingManager srManager) {
+        this.deviceId = checkNotNull(deviceId);
+        this.appId = checkNotNull(appId);
+        this.deviceConfig = checkNotNull(config);
+        this.linkService = checkNotNull(linkService);
+        this.allSegmentIds = checkNotNull(config.getAllDeviceSegmentIds());
+        try {
+            this.ipv4NodeSegmentId = config.getIPv4SegmentId(deviceId);
+            this.ipv6NodeSegmentId = config.getIPv6SegmentId(deviceId);
+            this.isEdgeRouter = config.isEdgeDevice(deviceId);
+            this.nodeMacAddr = checkNotNull(config.getDeviceMac(deviceId));
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage()
+                    + " Skipping value assignment in DefaultGroupHandler");
+        }
+        this.flowObjectiveService = flowObjService;
+        this.dsNextObjStore = srManager.dsNextObjStore();
+        this.vlanNextObjStore = srManager.vlanNextObjStore();
+        this.portNextObjStore = srManager.portNextObjStore();
+        this.srManager = srManager;
+        executorService.scheduleWithFixedDelay(new BucketCorrector(), 10,
+                                               VERIFY_INTERVAL,
+                                               TimeUnit.SECONDS);
+        populateNeighborMaps();
+    }
+
+    /**
+     * Gracefully shuts down a groupHandler. Typically called when the handler is
+     * no longer needed.
+     */
+    public void shutdown() {
+        executorService.shutdown();
+    }
+
+    /**
+     * Creates a group handler object.
+     *
+     * @param deviceId device identifier
+     * @param appId application identifier
+     * @param config interface to retrieve the device properties
+     * @param linkService link service object
+     * @param flowObjService flow objective service object
+     * @param srManager segment routing manager
+     * @throws DeviceConfigNotFoundException if the device configuration is not found
+     * @return default group handler type
+     */
+    public static DefaultGroupHandler createGroupHandler(
+                                                         DeviceId deviceId,
+                                                         ApplicationId appId,
+                                                         DeviceProperties config,
+                                                         LinkService linkService,
+                                                         FlowObjectiveService flowObjService,
+                                                         SegmentRoutingManager srManager)
+                                                                 throws DeviceConfigNotFoundException {
+        return new DefaultGroupHandler(deviceId, appId, config,
+                                       linkService,
+                                       flowObjService,
+                                       srManager);
+    }
+
+    /**
+     * Updates local stores for link-src device/port to neighbor (link-dst).
+     *
+     * @param link the infrastructure link
+     */
+    public void portUpForLink(Link link) {
+        if (!link.src().deviceId().equals(deviceId)) {
+            log.warn("linkUp: deviceId{} doesn't match with link src {}",
+                     deviceId, link.src().deviceId());
+            return;
+        }
+
+        log.info("* portUpForLink: Device {} linkUp at local port {} to "
+                + "neighbor {}", deviceId, link.src().port(), link.dst().deviceId());
+        // ensure local state is updated even if linkup is aborted later on
+        addNeighborAtPort(link.dst().deviceId(),
+                          link.src().port());
+    }
+
+    /**
+     * Updates local stores for port that has gone down.
+     *
+     * @param port port number that has gone down
+     */
+    public void portDown(PortNumber port) {
+        if (portDeviceMap.get(port) == null) {
+            log.warn("portDown: unknown port");
+            return;
+        }
+
+        log.debug("Device {} portDown {} to neighbor {}", deviceId, port,
+                  portDeviceMap.get(port));
+        devicePortMap.get(portDeviceMap.get(port)).remove(port);
+        portDeviceMap.remove(port);
+    }
+
+    /**
+     * Checks all groups in the src-device of link for neighbor sets that include
+     * the dst-device of link, and edits the hash groups according to link up
+     * or down. Should only be called by the master instance of the src-switch
+     * of link. Typically used when there are no route-path changes due to the
+     * link up or down, as the ECMPspg does not change.
+     *
+     * @param link the infrastructure link that has gone down or come up
+     * @param linkDown true if link has gone down
+     * @param firstTime true if link has come up for the first time i.e a link
+     *                  not seen-before
+     */
+    public void retryHash(Link link, boolean linkDown, boolean firstTime) {
+        MacAddress neighborMac;
+        try {
+            neighborMac = deviceConfig.getDeviceMac(link.dst().deviceId());
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting retryHash.");
+            return;
+        }
+        // find all the destinationSets related to link
+        Set<DestinationSetNextObjectiveStoreKey> dsKeySet = dsNextObjStore.entrySet()
+                .stream()
+                .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                // Filter out PW transit groups or include them if MPLS ECMP is supported
+                .filter(entry -> !entry.getKey().destinationSet().mplsSet() ||
+                        (entry.getKey().destinationSet().mplsSet() && srManager.getMplsEcmp()))
+                .filter(entry -> entry.getValue().containsNextHop(link.dst().deviceId()))
+                .map(entry -> entry.getKey())
+                .collect(Collectors.toSet());
+
+        log.debug("retryHash: dsNextObjStore contents for linkSrc {} -> linkDst {}: {}",
+                  deviceId, link.dst().deviceId(), dsKeySet);
+
+        for (DestinationSetNextObjectiveStoreKey dsKey : dsKeySet) {
+            NextNeighbors nextHops = dsNextObjStore.get(dsKey);
+            if (nextHops == null) {
+                log.warn("retryHash in device {}, but global store has no record "
+                         + "for dsKey:{}", deviceId, dsKey);
+                continue;
+            }
+            int nextId = nextHops.nextId();
+            Set<DeviceId> dstSet = nextHops.getDstForNextHop(link.dst().deviceId());
+            if (!linkDown) {
+                List<PortLabel> pl = Lists.newArrayList();
+                if (firstTime) {
+                    // some links may have come up before the next-objective was created
+                    // we take this opportunity to ensure other ports to same next-hop-dst
+                    // are part of the hash group (see CORD-1180). Duplicate additions
+                    // to the same hash group are avoided by the driver.
+                    for (PortNumber p : devicePortMap.get(link.dst().deviceId())) {
+                        dstSet.forEach(dst -> {
+                            int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+                            pl.add(new PortLabel(p, edgeLabel));
+                        });
+                    }
+                    addToHashedNextObjective(pl, neighborMac, nextId);
+                } else {
+                    // handle only the port that came up
+                    dstSet.forEach(dst -> {
+                        int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+                        pl.add(new PortLabel(link.src().port(), edgeLabel));
+                    });
+                    addToHashedNextObjective(pl, neighborMac, nextId);
+                }
+            } else {
+                // linkdown
+                List<PortLabel> pl = Lists.newArrayList();
+                dstSet.forEach(dst -> {
+                    int edgeLabel = dsKey.destinationSet().getEdgeLabel(dst);
+                    pl.add(new PortLabel(link.src().port(), edgeLabel));
+                });
+                removeFromHashedNextObjective(pl, neighborMac, nextId);
+            }
+        }
+    }
+
+    /**
+     * Utility class for associating output ports and the corresponding MPLS
+     * labels to push. In dual-homing, there are different labels to push
+     * corresponding to the destination switches in an edge-pair. If both
+     * destinations are reachable via the same spine, then the output-port to
+     * the spine will be associated with two labels i.e. there will be two
+     * PortLabel objects for the same port but with different labels.
+     */
+    private class PortLabel {
+        PortNumber port;
+        int edgeLabel;
+
+        PortLabel(PortNumber port, int edgeLabel) {
+            this.port = port;
+            this.edgeLabel = edgeLabel;
+        }
+
+        @Override
+        public String toString() {
+            return port.toString() + "/" + String.valueOf(edgeLabel);
+        }
+    }
+
+    /**
+     * Makes a call to the FlowObjective service to add buckets to
+     * a hashed group. User must ensure that all the ports & labels are meant
+     * same neighbor (ie. dstMac).
+     *
+     * @param portLabels a collection of port & label combinations to add
+     *                   to the hash group identified by the nextId
+     * @param dstMac destination mac address of next-hop
+     * @param nextId id for next-objective to which buckets will be added
+     *
+     */
+    private void addToHashedNextObjective(Collection<PortLabel> portLabels,
+                                          MacAddress dstMac, Integer nextId) {
+        // setup metadata to pass to nextObjective - indicate the vlan on egress
+        // if needed by the switch pipeline. Since hashed next-hops are always to
+        // other neighboring routers, there is no subnet assigned on those ports.
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+        metabuilder.matchVlanId(INTERNAL_VLAN);
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective.builder()
+                .withId(nextId)
+                .withType(NextObjective.Type.HASHED)
+                .withMeta(metabuilder.build())
+                .fromApp(appId);
+        // Create the new buckets to be updated
+        portLabels.forEach(pl -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            tBuilder.setOutput(pl.port)
+                .setEthDst(dstMac)
+                .setEthSrc(nodeMacAddr);
+            if (pl.edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+                tBuilder.pushMpls()
+                    .copyTtlOut()
+                    .setMpls(MplsLabel.mplsLabel(pl.edgeLabel));
+            }
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+
+        log.debug("addToHash in device {}: Adding Bucket with port/label {} "
+                + "to nextId {}", deviceId, portLabels, nextId);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("addToHash port/label {} addedTo "
+                        + "NextObj {} on {}", portLabels, nextId, deviceId),
+                (objective, error) ->
+                        log.warn("addToHash failed to add port/label {} to"
+                                + " NextObj {} on {}: {}", portLabels,
+                                 nextId, deviceId, error));
+        NextObjective nextObjective = nextObjBuilder.addToExisting(context);
+        flowObjectiveService.next(deviceId, nextObjective);
+    }
+
+    /**
+     * Makes a call to the FlowObjective service to remove buckets from
+     * a hash group. User must ensure that all the ports & labels are meant
+     * same neighbor (ie. dstMac).
+     *
+     * @param portLabels a collection of port & label combinations to remove
+     *                   from the hash group identified by the nextId
+     * @param dstMac destination mac address of next-hop
+     * @param nextId id for next-objective from which buckets will be removed
+     */
+    private void removeFromHashedNextObjective(Collection<PortLabel> portLabels,
+                                               MacAddress dstMac, Integer nextId) {
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder()
+                .withType(NextObjective.Type.HASHED) //same as original
+                .withId(nextId)
+                .fromApp(appId);
+        // Create the buckets to be removed
+        portLabels.forEach(pl -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            tBuilder.setOutput(pl.port)
+                .setEthDst(dstMac)
+                .setEthSrc(nodeMacAddr);
+            if (pl.edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+                tBuilder.pushMpls()
+                    .copyTtlOut()
+                    .setMpls(MplsLabel.mplsLabel(pl.edgeLabel));
+            }
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+        log.debug("removeFromHash in device {}: Removing Bucket with port/label"
+                + " {} from nextId {}", deviceId, portLabels, nextId);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("port/label {} removedFrom NextObj"
+                        + " {} on {}", portLabels, nextId, deviceId),
+                (objective, error) ->
+                log.warn("port/label {} failed to removeFrom NextObj {} on "
+                        + "{}: {}", portLabels, nextId, deviceId, error));
+        NextObjective nextObjective = nextObjBuilder.removeFromExisting(context);
+        flowObjectiveService.next(deviceId, nextObjective);
+    }
+
+    /**
+     * Checks all the hash-groups in the target-switch meant for the destination
+     * switch, and either adds or removes buckets to make the neighbor-set
+     * match the given next-hops. Typically called by the master instance of the
+     * destination switch, which may be different from the master instance of the
+     * target switch where hash-group changes are made.
+     *
+     * @param targetSw the switch in which the hash groups will be edited
+     * @param nextHops the current next hops for the target switch to reach
+     *                  the dest sw
+     * @param destSw  the destination switch
+     * @param revoke true if hash groups need to remove buckets from the
+     *                          the groups to match the current next hops
+     * @return true if calls are made to edit buckets, or if no edits are required
+     */
+    public boolean fixHashGroups(DeviceId targetSw, Set<DeviceId> nextHops,
+                                 DeviceId destSw, boolean revoke) {
+        // temporary storage of keys to be updated
+        Map<DestinationSetNextObjectiveStoreKey, Set<DeviceId>> tempStore =
+                new HashMap<>();
+        boolean foundNextObjective = false, success = true;
+
+        // retrieve hash-groups meant for destSw, which have destinationSets
+        // with different neighbors than the given next-hops
+        for (DestinationSetNextObjectiveStoreKey dskey : dsNextObjStore.keySet()) {
+            if (!dskey.deviceId().equals(targetSw) ||
+                    !dskey.destinationSet().getDestinationSwitches().contains(destSw)) {
+                continue;
+            }
+            foundNextObjective = true;
+            NextNeighbors nhops = dsNextObjStore.get(dskey);
+            Set<DeviceId> currNeighbors = nhops.nextHops(destSw);
+            int edgeLabel = dskey.destinationSet().getEdgeLabel(destSw);
+            Integer nextId = nhops.nextId();
+
+            if (currNeighbors == null || nextHops == null) {
+                log.warn("fixing hash groups but found currNeighbors:{} or nextHops:{}"
+                        + " in targetSw:{} for dstSw:{}", currNeighbors, nextHops,
+                        targetSw, destSw);
+                success &= false;
+                continue;
+            }
+
+            Set<DeviceId> diff;
+            if (revoke) {
+                diff = Sets.difference(currNeighbors, nextHops);
+                log.debug("targetSw:{} -> dstSw:{} in nextId:{} has current next "
+                        + "hops:{} ..removing {}", targetSw, destSw, nextId,
+                        currNeighbors, diff);
+            } else {
+                diff = Sets.difference(nextHops, currNeighbors);
+                log.debug("targetSw:{} -> dstSw:{} in nextId:{} has current next "
+                        + "hops:{} ..adding {}", targetSw, destSw, nextId,
+                        currNeighbors, diff);
+            }
+            boolean suc = updateAllPortsToNextHop(diff, edgeLabel, nextId,
+                                                  revoke);
+            if (suc) {
+                // to update neighbor set with changes made
+                if (revoke) {
+                    tempStore.put(dskey, Sets.difference(currNeighbors, diff));
+                } else {
+                    tempStore.put(dskey, Sets.union(currNeighbors, diff));
+                }
+            }
+            success &= suc;
+        }
+
+        if (!foundNextObjective) {
+            log.debug("Cannot find any nextObjectives for route targetSw:{} "
+                    + "-> dstSw:{}", targetSw, destSw);
+            return true; // nothing to do, return true so ECMPspg is updated
+        }
+
+        // update the dsNextObjectiveStore with new destinationSet to nextId mappings
+        for (DestinationSetNextObjectiveStoreKey key : tempStore.keySet()) {
+            NextNeighbors currentNextHops = dsNextObjStore.get(key);
+            if (currentNextHops == null) {
+                log.warn("fixHashGroups could not update global store in "
+                        + "device {} .. missing nextNeighbors for key {}",
+                        deviceId, key);
+                continue;
+            }
+            Set<DeviceId> newNeighbors = new HashSet<>();
+            newNeighbors.addAll(tempStore.get(key));
+            Map<DeviceId, Set<DeviceId>> oldDstNextHops =
+                    ImmutableMap.copyOf(currentNextHops.dstNextHops());
+            currentNextHops.dstNextHops().put(destSw, newNeighbors); //local change
+            log.debug("Updating nsNextObjStore target:{} -> dst:{} in key:{} nextId:{}",
+                      targetSw, destSw, key, currentNextHops.nextId());
+            log.debug("Old dstNextHops: {}", oldDstNextHops);
+            log.debug("New dstNextHops: {}", currentNextHops.dstNextHops());
+            // update global store
+            dsNextObjStore.put(key,
+                               new NextNeighbors(currentNextHops.dstNextHops(),
+                                                 currentNextHops.nextId()));
+        }
+        // even if one fails and others succeed, return false so ECMPspg not updated
+        return success;
+    }
+
+    /**
+     * Updates the DestinationSetNextObjectiveStore with any per-destination nexthops
+     * that are not already in the store for the given DestinationSet. Note that
+     * this method does not remove existing next hops for the destinations in the
+     * DestinationSet.
+     *
+     * @param ds the DestinationSet for which the next hops need to be updated
+     * @param newDstNextHops a map of per-destination next hops to update the
+     *                          destinationSet with
+     * @return true if successful in updating all next hops
+     */
+    private boolean updateNextHops(DestinationSet ds,
+                                  Map<DeviceId, Set<DeviceId>> newDstNextHops) {
+        DestinationSetNextObjectiveStoreKey key =
+                new DestinationSetNextObjectiveStoreKey(deviceId, ds);
+        NextNeighbors currNext = dsNextObjStore.get(key);
+        Map<DeviceId, Set<DeviceId>> currDstNextHops = currNext.dstNextHops();
+
+        // add newDstNextHops to currDstNextHops for each dst
+        boolean success = true;
+        for (DeviceId dstSw : ds.getDestinationSwitches()) {
+            Set<DeviceId> currNhops = currDstNextHops.get(dstSw);
+            Set<DeviceId> newNhops = newDstNextHops.get(dstSw);
+            currNhops = (currNhops == null) ? Sets.newHashSet() : currNhops;
+            newNhops = (newNhops == null) ? Sets.newHashSet() : newNhops;
+            int edgeLabel = ds.getEdgeLabel(dstSw);
+            int nextId = currNext.nextId();
+
+            // new next hops should be added
+            boolean suc = updateAllPortsToNextHop(Sets.difference(newNhops, currNhops),
+                                                  edgeLabel, nextId, false);
+            if (suc) {
+                currNhops.addAll(newNhops);
+                currDstNextHops.put(dstSw, currNhops); // this is only a local change
+            }
+            success &= suc;
+        }
+
+        if (success) {
+            // update global store
+            dsNextObjStore.put(key, new NextNeighbors(currDstNextHops,
+                                                      currNext.nextId()));
+            log.debug("Updated device:{} ds:{} new next-hops: {}", deviceId, ds,
+                      dsNextObjStore.get(key));
+        }
+        return success;
+    }
+
+    /**
+     * Adds or removes buckets for all ports to a set of neighbor devices. Caller
+     * needs to ensure that the  given neighbors are all next hops towards the
+     * same destination (represented by the given edgeLabel).
+     *
+     * @param neighbors set of neighbor device ids
+     * @param edgeLabel MPLS label to use in buckets
+     * @param nextId the nextObjective to change
+     * @param revoke true if buckets need to be removed, false if they need to
+     *          be added
+     * @return true if successful in adding or removing buckets for all ports
+     *                  to the neighbors
+     */
+    private boolean updateAllPortsToNextHop(Set<DeviceId> neighbors, int edgeLabel,
+                                         int nextId, boolean revoke) {
+        for (DeviceId neighbor : neighbors) {
+            MacAddress neighborMac;
+            try {
+                neighborMac = deviceConfig.getDeviceMac(neighbor);
+            } catch (DeviceConfigNotFoundException e) {
+                log.warn(e.getMessage() + " Aborting updateAllPortsToNextHop"
+                        + " for nextId:" + nextId);
+                return false;
+            }
+            Collection<PortNumber> portsToNeighbor = devicePortMap.get(neighbor);
+            if (portsToNeighbor == null || portsToNeighbor.isEmpty()) {
+                log.warn("No ports found in dev:{} for neighbor:{} .. cannot "
+                        + "updateAllPortsToNextHop for nextId: {}",
+                         deviceId, neighbor, nextId);
+                return false;
+            }
+            List<PortLabel> pl = Lists.newArrayList();
+            portsToNeighbor.forEach(p -> pl.add(new PortLabel(p, edgeLabel)));
+            if (revoke) {
+                log.debug("updateAllPortsToNextHops in device {}: Removing Bucket(s) "
+                        + "with Port/Label:{} to next object id {}",
+                        deviceId, pl, nextId);
+                removeFromHashedNextObjective(pl, neighborMac, nextId);
+            } else {
+                log.debug("fixHashGroup in device {}: Adding Bucket(s) "
+                        + "with Port/Label: {} to next object id {}",
+                        deviceId, pl, nextId);
+                addToHashedNextObjective(pl, neighborMac, nextId);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Adds or removes a port that has been configured with a vlan to a broadcast group
+     * for bridging. Should only be called by the master instance for this device.
+     *
+     * @param port the port on this device that needs to be added/removed to a bcast group
+     * @param vlanId the vlan id corresponding to the broadcast domain/group
+     * @param popVlan indicates if packets should be sent out untagged or not out
+     *                of the port. If true, indicates an access (untagged) or native vlan
+     *                configuration. If false, indicates a trunk (tagged) vlan config.
+     * @param portUp true if port is enabled, false if disabled
+     */
+    public void processEdgePort(PortNumber port, VlanId vlanId,
+                                boolean popVlan, boolean portUp) {
+        //get the next id for the subnet and edit it.
+        Integer nextId = getVlanNextObjectiveId(vlanId);
+        if (nextId == -1) {
+            if (portUp) {
+                log.debug("**Creating flooding group for first port enabled in"
+                        + " vlan {} on dev {} port {}", vlanId, deviceId, port);
+                createBcastGroupFromVlan(vlanId, Collections.singleton(port));
+            } else {
+                log.warn("Could not find flooding group for subnet {} on dev:{} when"
+                        + " removing port:{}", vlanId, deviceId, port);
+            }
+            return;
+        }
+
+        log.info("**port{} in device {}: {} Bucket with Port {} to"
+                + " next-id {}", (portUp) ? "UP" : "DOWN", deviceId,
+                                          (portUp) ? "Adding" : "Removing",
+                                          port, nextId);
+        // Create the bucket to be added or removed
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        if (popVlan) {
+            tBuilder.popVlan();
+        }
+        tBuilder.setOutput(port);
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+                .addTreatment(tBuilder.build())
+                .withMeta(metadata);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+            (objective) -> log.debug("port {} successfully {} NextObj {} on {}",
+                                     port, (portUp) ? "addedTo" : "removedFrom",
+                                     nextId, deviceId),
+            (objective, error) ->
+            log.warn("port {} failed to {} NextObj {} on {}: {}",
+                     port, (portUp) ? "addTo" : "removeFrom",
+                     nextId, deviceId, error));
+
+        NextObjective nextObj = (portUp) ? nextObjBuilder.addToExisting(context)
+                                         : nextObjBuilder.removeFromExisting(context);
+        log.debug("edgePort processed: Submited next objective {} in device {}",
+                  nextId, deviceId);
+        flowObjectiveService.next(deviceId, nextObj);
+    }
+
+    /**
+     * Returns the next objective of type hashed associated with the destination set.
+     * In addition, updates the existing next-objective if new route-route paths found
+     * have resulted in the addition of new next-hops to a particular destination.
+     * If there is no existing next objective for this destination set, this method
+     * would create a next objective and return the nextId. Optionally metadata can be
+     * passed in for the creation of the next objective.
+     *
+     * @param ds destination set
+     * @param nextHops a map of per destination next hops
+     * @param meta metadata passed into the creation of a Next Objective
+     * @param isBos if Bos is set
+     * @return int if found or -1 if there are errors in the creation of the
+     *          neighbor set.
+     */
+    public int getNextObjectiveId(DestinationSet ds,
+                                  Map<DeviceId, Set<DeviceId>> nextHops,
+                                  TrafficSelector meta, boolean isBos) {
+        NextNeighbors next = dsNextObjStore.
+                get(new DestinationSetNextObjectiveStoreKey(deviceId, ds));
+        if (next == null) {
+            log.debug("getNextObjectiveId in device{}: Next objective id "
+                    + "not found for {} ... creating", deviceId, ds);
+            log.trace("getNextObjectiveId: nsNextObjStore contents for device {}: {}",
+                      deviceId,
+                      dsNextObjStore.entrySet()
+                      .stream()
+                      .filter((nsStoreEntry) ->
+                      (nsStoreEntry.getKey().deviceId().equals(deviceId)))
+                      .collect(Collectors.toList()));
+
+            createGroupFromDestinationSet(ds, nextHops, meta, isBos);
+            next = dsNextObjStore.
+                    get(new DestinationSetNextObjectiveStoreKey(deviceId, ds));
+            if (next == null) {
+                log.warn("getNextObjectiveId: unable to create next objective");
+                // failure in creating group
+                return -1;
+            } else {
+                log.debug("getNextObjectiveId in device{}: Next objective id {} "
+                    + "created for {}", deviceId, next.nextId(), ds);
+            }
+        } else {
+            log.trace("getNextObjectiveId in device{}: Next objective id {} "
+                    + "found for {}", deviceId, next.nextId(), ds);
+            // should fix hash groups too if next-hops have changed
+            if (!next.dstNextHops().equals(nextHops)) {
+                log.debug("Nexthops have changed for dev:{} nextId:{} ..updating",
+                          deviceId, next.nextId());
+                if (!updateNextHops(ds, nextHops)) {
+                    // failure in updating group
+                    return -1;
+                }
+            }
+        }
+        return next.nextId();
+    }
+
+    /**
+     * Returns the next objective of type broadcast associated with the vlan,
+     * or -1 if no such objective exists. Note that this method does NOT create
+     * the next objective as a side-effect. It is expected that is objective is
+     * created at startup from network configuration. Typically this is used
+     * for L2 flooding within the subnet configured on the switch.
+     *
+     * @param vlanId vlan id
+     * @return int if found or -1
+     */
+    public int getVlanNextObjectiveId(VlanId vlanId) {
+        Integer nextId = vlanNextObjStore.
+                get(new VlanNextObjectiveStoreKey(deviceId, vlanId));
+
+        return (nextId != null) ? nextId : -1;
+    }
+
+    /**
+     * Returns the next objective of type simple associated with the port on the
+     * device, given the treatment. Different treatments to the same port result
+     * in different next objectives. If no such objective exists, this method
+     * creates one (if requested) and returns the id. Optionally metadata can be passed in for
+     * the creation of the objective. Typically this is used for L2 and L3 forwarding
+     * to compute nodes and containers/VMs on the compute nodes directly attached
+     * to the switch.
+     *
+     * @param portNum the port number for the simple next objective
+     * @param treatment the actions to apply on the packets (should include outport)
+     * @param meta optional metadata passed into the creation of the next objective
+     * @param createIfMissing true if a next object should be created if not found
+     * @return int if found or created, -1 if there are errors during the
+     *          creation of the next objective.
+     */
+    public int getPortNextObjectiveId(PortNumber portNum, TrafficTreatment treatment,
+                                      TrafficSelector meta, boolean createIfMissing) {
+        Integer nextId = portNextObjStore
+                .get(new PortNextObjectiveStoreKey(deviceId, portNum, treatment, meta));
+        if (nextId != null) {
+            return nextId;
+        }
+        log.debug("getPortNextObjectiveId in device {}: Next objective id "
+                + "not found for port: {} .. {}", deviceId, portNum,
+                (createIfMissing) ? "creating" : "aborting");
+        if (!createIfMissing) {
+            return -1;
+        }
+        // create missing next objective
+        createGroupFromPort(portNum, treatment, meta);
+        nextId = portNextObjStore.get(new PortNextObjectiveStoreKey(deviceId, portNum,
+                                                                    treatment, meta));
+        if (nextId == null) {
+            log.warn("getPortNextObjectiveId: unable to create next obj"
+                    + "for dev:{} port:{}", deviceId, portNum);
+            return -1;
+        }
+        return nextId;
+    }
+
+    /**
+     * Checks if the next objective ID (group) for the neighbor set exists or not.
+     *
+     * @param ns neighbor set to check
+     * @return true if it exists, false otherwise
+     */
+    public boolean hasNextObjectiveId(DestinationSet ns) {
+        NextNeighbors nextHops = dsNextObjStore.
+                get(new DestinationSetNextObjectiveStoreKey(deviceId, ns));
+        if (nextHops == null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void populateNeighborMaps() {
+        Set<Link> outgoingLinks = linkService.getDeviceEgressLinks(deviceId);
+        for (Link link : outgoingLinks) {
+            if (link.type() != Link.Type.DIRECT) {
+                continue;
+            }
+            addNeighborAtPort(link.dst().deviceId(), link.src().port());
+        }
+    }
+
+    protected void addNeighborAtPort(DeviceId neighborId,
+                                     PortNumber portToNeighbor) {
+        // Update DeviceToPort database
+        log.debug("Device {} addNeighborAtPort: neighbor {} at port {}",
+                  deviceId, neighborId, portToNeighbor);
+        Set<PortNumber> ports = Collections
+                .newSetFromMap(new ConcurrentHashMap<PortNumber, Boolean>());
+        ports.add(portToNeighbor);
+        Set<PortNumber> portnums = devicePortMap.putIfAbsent(neighborId, ports);
+        if (portnums != null) {
+            portnums.add(portToNeighbor);
+        }
+
+        // Update portToDevice database
+        DeviceId prev = portDeviceMap.putIfAbsent(portToNeighbor, neighborId);
+        if (prev != null) {
+            log.debug("Device: {} port: {} already has neighbor: {} ",
+                      deviceId, portToNeighbor, prev, neighborId);
+        }
+    }
+
+    /**
+     * Creates a NextObjective for a hash group in this device from a given
+     * DestinationSet.
+     *
+     * @param ds the DestinationSet
+     * @param neighbors a map for each destination and its next-hops
+     * @param meta metadata passed into the creation of a Next Objective
+     * @param isBos if BoS is set
+     */
+    public void createGroupFromDestinationSet(DestinationSet ds,
+                                              Map<DeviceId, Set<DeviceId>> neighbors,
+                                              TrafficSelector meta,
+                                              boolean isBos) {
+        int nextId = flowObjectiveService.allocateNextId();
+        NextObjective.Type type = NextObjective.Type.HASHED;
+        if (neighbors == null || neighbors.isEmpty()) {
+            log.warn("createGroupsFromDestinationSet: needs at least one neighbor"
+                    + "to create group in dev:{} for ds: {} with next-hops {}",
+                    deviceId, ds, neighbors);
+            return;
+        }
+        // If Bos == False and MPLS-ECMP == false, we have
+        // to use simple group and we will pick a single neighbor for a single dest.
+        if (!isBos && !srManager.getMplsEcmp()) {
+            type = NextObjective.Type.SIMPLE;
+        }
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder()
+                .withId(nextId)
+                .withType(type)
+                .fromApp(appId);
+        if (meta != null) {
+            nextObjBuilder.withMeta(meta);
+        }
+
+        // create treatment buckets for each neighbor for each dst Device
+        // except in the special case where we only want to pick a single
+        // neighbor for a simple group
+        boolean foundSingleNeighbor = false;
+        boolean treatmentAdded = false;
+        Map<DeviceId, Set<DeviceId>> dstNextHops = new ConcurrentHashMap<>();
+        for (DeviceId dst : ds.getDestinationSwitches()) {
+            Set<DeviceId> nextHops = neighbors.get(dst);
+            if (nextHops == null || nextHops.isEmpty()) {
+                continue;
+            }
+
+            if (foundSingleNeighbor) {
+                break;
+            }
+
+            for (DeviceId neighborId : nextHops) {
+                if (devicePortMap.get(neighborId) == null) {
+                    log.warn("Neighbor {} is not in the port map yet for dev:{}",
+                             neighborId, deviceId);
+                    return;
+                } else if (devicePortMap.get(neighborId).isEmpty()) {
+                    log.warn("There are no ports for "
+                            + "the Device {} in the port map yet", neighborId);
+                    return;
+                }
+
+                MacAddress neighborMac;
+                try {
+                    neighborMac = deviceConfig.getDeviceMac(neighborId);
+                } catch (DeviceConfigNotFoundException e) {
+                    log.warn(e.getMessage() + " Aborting createGroupsFromDestinationset.");
+                    return;
+                }
+                // For each port to the neighbor, we create a new treatment
+                Set<PortNumber> neighborPorts = devicePortMap.get(neighborId);
+                // In this case we are using a SIMPLE group. We randomly pick a port
+                if (!isBos && !srManager.getMplsEcmp()) {
+                    int size = devicePortMap.get(neighborId).size();
+                    int index = RandomUtils.nextInt(0, size);
+                    neighborPorts = Collections.singleton(
+                                        Iterables.get(devicePortMap.get(neighborId),
+                                                      index));
+                    foundSingleNeighbor = true;
+                }
+
+                for (PortNumber sp : neighborPorts) {
+                    TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment
+                            .builder();
+                    tBuilder.setEthDst(neighborMac).setEthSrc(nodeMacAddr);
+                    int edgeLabel = ds.getEdgeLabel(dst);
+                    if (edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+                        tBuilder.pushMpls()
+                        .copyTtlOut()
+                        .setMpls(MplsLabel.mplsLabel(edgeLabel));
+                    }
+                    tBuilder.setOutput(sp);
+                    nextObjBuilder.addTreatment(tBuilder.build());
+                    treatmentAdded = true;
+                    //update store
+                    Set<DeviceId> existingNeighbors = dstNextHops.get(dst);
+                    if (existingNeighbors == null) {
+                        existingNeighbors = new HashSet<>();
+                    }
+                    existingNeighbors.add(neighborId);
+                    dstNextHops.put(dst, existingNeighbors);
+                    log.debug("creating treatment for port/label {}/{} in next:{}",
+                              sp, edgeLabel, nextId);
+                }
+
+                if (foundSingleNeighbor) {
+                    break;
+                }
+            }
+        }
+
+        if (!treatmentAdded) {
+            log.warn("Could not createGroup from DestinationSet {} without any"
+                    + "next hops {}", ds, neighbors);
+            return;
+        }
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                log.debug("createGroupsFromDestinationSet installed "
+                        + "NextObj {} on {}", nextId, deviceId),
+                (objective, error) ->
+                log.warn("createGroupsFromDestinationSet failed to install"
+                        + " NextObj {} on {}: {}", nextId, deviceId, error)
+                );
+        NextObjective nextObj = nextObjBuilder.add(context);
+        log.debug(".. createGroupsFromDestinationSet: Submitted "
+                + "next objective {} in device {}", nextId, deviceId);
+        flowObjectiveService.next(deviceId, nextObj);
+        //update store
+        dsNextObjStore.put(new DestinationSetNextObjectiveStoreKey(deviceId, ds),
+                           new NextNeighbors(dstNextHops, nextId));
+    }
+
+    /**
+     * Creates broadcast groups for all ports in the same subnet for
+     * all configured subnets.
+     */
+    public void createGroupsFromVlanConfig() {
+        srManager.getVlanPortMap(deviceId).asMap().forEach((vlanId, ports) -> {
+            createBcastGroupFromVlan(vlanId, ports);
+        });
+    }
+
+    /**
+     * Creates a single broadcast group from a given vlan id and list of ports.
+     *
+     * @param vlanId vlan id
+     * @param ports list of ports in the subnet
+     */
+    public void createBcastGroupFromVlan(VlanId vlanId, Collection<PortNumber> ports) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+
+        if (vlanNextObjStore.containsKey(key)) {
+            log.debug("Broadcast group for device {} and subnet {} exists",
+                      deviceId, vlanId);
+            return;
+        }
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+        int nextId = flowObjectiveService.allocateNextId();
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+                .withMeta(metadata);
+
+        ports.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            if (toPopVlan(port, vlanId)) {
+                tBuilder.popVlan();
+            }
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+            (objective) ->
+                log.debug("createBroadcastGroupFromVlan installed "
+                        + "NextObj {} on {}", nextId, deviceId),
+            (objective, error) ->
+                log.warn("createBroadcastGroupFromVlan failed to install"
+                        + " NextObj {} on {}: {}", nextId, deviceId, error)
+            );
+        NextObjective nextObj = nextObjBuilder.add(context);
+        flowObjectiveService.next(deviceId, nextObj);
+        log.debug("createBcastGroupFromVlan: Submitted next objective {} "
+                + "for vlan: {} in device {}", nextId, vlanId, deviceId);
+
+        vlanNextObjStore.put(key, nextId);
+    }
+
+    /**
+     * Removes a single broadcast group from a given vlan id.
+     * The group should be empty.
+     * @param deviceId device Id to remove the group
+     * @param portNum port number related to the group
+     * @param vlanId vlan id of the broadcast group to remove
+     * @param popVlan true if the TrafficTreatment involves pop vlan tag action
+     */
+    public void removeBcastGroupFromVlan(DeviceId deviceId, PortNumber portNum,
+                                         VlanId vlanId, boolean popVlan) {
+        VlanNextObjectiveStoreKey key = new VlanNextObjectiveStoreKey(deviceId, vlanId);
+
+        if (!vlanNextObjStore.containsKey(key)) {
+            log.debug("Broadcast group for device {} and subnet {} does not exist",
+                      deviceId, vlanId);
+            return;
+        }
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+        int nextId = vlanNextObjStore.get(key);
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+                .withMeta(metadata);
+
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        if (popVlan) {
+            tBuilder.popVlan();
+        }
+        tBuilder.setOutput(portNum);
+        nextObjBuilder.addTreatment(tBuilder.build());
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.debug("removeBroadcastGroupFromVlan removed "
+                                          + "NextObj {} on {}", nextId, deviceId),
+                (objective, error) ->
+                        log.warn("removeBroadcastGroupFromVlan failed to remove "
+                                         + " NextObj {} on {}: {}", nextId, deviceId, error)
+        );
+        NextObjective nextObj = nextObjBuilder.remove(context);
+        flowObjectiveService.next(deviceId, nextObj);
+        log.debug("removeBcastGroupFromVlan: Submited next objective {} in device {}",
+                  nextId, deviceId);
+
+        vlanNextObjStore.remove(key, nextId);
+    }
+
+    /**
+     * Determine if we should pop given vlan before sending packets to the given port.
+     *
+     * @param portNumber port number
+     * @param vlanId vlan id
+     * @return true if the vlan id is not contained in any vlanTagged config
+     */
+    private boolean toPopVlan(PortNumber portNumber, VlanId vlanId) {
+        return srManager.interfaceService
+                .getInterfacesByPort(new ConnectPoint(deviceId, portNumber))
+                .stream().noneMatch(intf -> intf.vlanTagged().contains(vlanId));
+    }
+
+    /**
+     * Create simple next objective for a single port. The treatments can include
+     * all outgoing actions that need to happen on the packet.
+     *
+     * @param portNum  the outgoing port on the device
+     * @param treatment the actions to apply on the packets (should include outport)
+     * @param meta optional data to pass to the driver
+     */
+    public void createGroupFromPort(PortNumber portNum, TrafficTreatment treatment,
+                                    TrafficSelector meta) {
+        int nextId = flowObjectiveService.allocateNextId();
+        PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+                                                deviceId, portNum, treatment, meta);
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.SIMPLE)
+                .addTreatment(treatment)
+                .fromApp(appId)
+                .withMeta(meta);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+            (objective) ->
+                log.debug("createGroupFromPort installed "
+                        + "NextObj {} on {}", nextId, deviceId),
+            (objective, error) ->
+                log.warn("createGroupFromPort failed to install"
+                        + " NextObj {} on {}: {}", nextId, deviceId, error)
+            );
+        NextObjective nextObj = nextObjBuilder.add(context);
+        flowObjectiveService.next(deviceId, nextObj);
+        log.debug("createGroupFromPort: Submited next objective {} in device {} "
+                + "for port {}", nextId, deviceId, portNum);
+
+        portNextObjStore.put(key, nextId);
+    }
+
+    /**
+     * Removes simple next objective for a single port.
+     *
+     * @param deviceId device id that has the port to deal with
+     * @param portNum the outgoing port on the device
+     * @param vlanId vlan id associated with the port
+     * @param popVlan true if POP_VLAN action is applied on the packets, false otherwise
+     */
+    public void removePortNextObjective(DeviceId deviceId, PortNumber portNum, VlanId vlanId, boolean popVlan) {
+        TrafficSelector.Builder mbuilder = DefaultTrafficSelector.builder();
+        mbuilder.matchVlanId(vlanId);
+
+        TrafficTreatment.Builder tbuilder = DefaultTrafficTreatment.builder();
+        tbuilder.immediate().setOutput(portNum);
+        if (popVlan) {
+            tbuilder.immediate().popVlan();
+        }
+
+        int portNextObjId = srManager.getPortNextObjectiveId(deviceId, portNum,
+                                                             tbuilder.build(), mbuilder.build(), false);
+
+        PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+                deviceId, portNum, tbuilder.build(), mbuilder.build());
+        if (portNextObjId != -1 && portNextObjStore.containsKey(key)) {
+            NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                    .builder().withId(portNextObjId)
+                    .withType(NextObjective.Type.SIMPLE).fromApp(appId);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("removePortNextObjective removes NextObj {} on {}",
+                                             portNextObjId, deviceId),
+                    (objective, error) ->
+                            log.warn("removePortNextObjective failed to remove NextObj {} on {}: {}",
+                                     portNextObjId, deviceId, error));
+            NextObjective nextObjective = nextObjBuilder.remove(context);
+            log.info("**removePortNextObjective: Submitted "
+                             + "next objective {} in device {}",
+                     portNextObjId, deviceId);
+            flowObjectiveService.next(deviceId, nextObjective);
+
+            portNextObjStore.remove(key);
+        }
+    }
+    /**
+     * Removes groups for the next objective ID given.
+     *
+     * @param objectiveId next objective ID to remove
+     * @return true if succeeds, false otherwise
+     */
+    public boolean removeGroup(int objectiveId) {
+        for (Map.Entry<DestinationSetNextObjectiveStoreKey, NextNeighbors> e :
+                dsNextObjStore.entrySet()) {
+            if (e.getValue().nextId() != objectiveId) {
+                continue;
+            }
+            // Right now it is just used in TunnelHandler
+            // remember in future that PW transit groups could
+            // be Indirect groups
+            NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                    .builder().withId(objectiveId)
+                    .withType(NextObjective.Type.HASHED).fromApp(appId);
+            ObjectiveContext context = new DefaultObjectiveContext(
+                    (objective) -> log.debug("RemoveGroup removes NextObj {} on {}",
+                            objectiveId, deviceId),
+                    (objective, error) ->
+                            log.warn("RemoveGroup failed to remove NextObj {} on {}: {}",
+                                    objectiveId, deviceId, error));
+            NextObjective nextObjective = nextObjBuilder.remove(context);
+            log.info("**removeGroup: Submited "
+                    + "next objective {} in device {}",
+                    objectiveId, deviceId);
+            flowObjectiveService.next(deviceId, nextObjective);
+
+            dsNextObjStore.remove(e.getKey());
+            return true;
+        }
+
+        return false;
+    }
+    /**
+     * Remove simple next objective for a single port. The treatments can include
+     * all outgoing actions that need to happen on the packet.
+     *
+     * @param portNum  the outgoing port on the device
+     * @param treatment the actions applied on the packets (should include outport)
+     * @param meta optional data to pass to the driver
+     */
+    public void removeGroupFromPort(PortNumber portNum, TrafficTreatment treatment,
+                                    TrafficSelector meta) {
+        PortNextObjectiveStoreKey key = new PortNextObjectiveStoreKey(
+                deviceId, portNum, treatment, meta);
+        Integer nextId = portNextObjStore.get(key);
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.SIMPLE)
+                .addTreatment(treatment)
+                .fromApp(appId)
+                .withMeta(meta);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) ->
+                        log.info("removeGroupFromPort installed "
+                                          + "NextObj {} on {}", nextId, deviceId),
+                (objective, error) ->
+                        log.warn("removeGroupFromPort failed to install"
+                                         + " NextObj {} on {}: {}", nextId, deviceId, error)
+        );
+        NextObjective nextObj = nextObjBuilder.remove(context);
+        flowObjectiveService.next(deviceId, nextObj);
+        log.info("removeGroupFromPort: Submitted next objective {} in device {} "
+                          + "for port {}", nextId, deviceId, portNum);
+
+        portNextObjStore.remove(key);
+    }
+
+    /**
+     * Removes all groups from all next objective stores.
+     */
+    /*public void removeAllGroups() {
+        for (Map.Entry<NeighborSetNextObjectiveStoreKey, NextNeighbors> entry:
+                nsNextObjStore.entrySet()) {
+            removeGroup(entry.getValue().nextId());
+        }
+        for (Map.Entry<PortNextObjectiveStoreKey, Integer> entry:
+                portNextObjStore.entrySet()) {
+            removeGroup(entry.getValue());
+        }
+        for (Map.Entry<VlanNextObjectiveStoreKey, Integer> entry:
+                vlanNextObjStore.entrySet()) {
+            removeGroup(entry.getValue());
+        }
+    }*/ //XXX revisit
+
+    /**
+     * Triggers a one time bucket verification operation on all hash groups
+     * on this device.
+     */
+    public void triggerBucketCorrector() {
+        BucketCorrector bc = new BucketCorrector();
+        bc.run();
+    }
+
+    public void updateGroupFromVlanConfiguration(PortNumber portNumber, Collection<VlanId> vlanIds,
+                                                 int nextId, boolean install) {
+        vlanIds.forEach(vlanId -> updateGroupFromVlanInternal(vlanId, portNumber, nextId, install));
+    }
+
+    private void updateGroupFromVlanInternal(VlanId vlanId, PortNumber portNum, int nextId, boolean install) {
+        TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+        if (toPopVlan(portNum, vlanId)) {
+            tBuilder.popVlan();
+        }
+        tBuilder.setOutput(portNum);
+
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder().matchVlanId(vlanId).build();
+
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST).fromApp(appId)
+                .addTreatment(tBuilder.build())
+                .withMeta(metadata);
+
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("port {} successfully removedFrom NextObj {} on {}",
+                                         portNum, nextId, deviceId),
+                (objective, error) ->
+                        log.warn("port {} failed to removedFrom NextObj {} on {}: {}",
+                                 portNum, nextId, deviceId, error));
+
+        if (install) {
+            flowObjectiveService.next(deviceId, nextObjBuilder.addToExisting(context));
+        } else {
+            flowObjectiveService.next(deviceId, nextObjBuilder.removeFromExisting(context));
+        }
+    }
+
+    /**
+     * Performs bucket verification operation for all hash groups in this device.
+     * Checks RouteHandler to ensure that routing is stable before attempting
+     * verification. Verification involves creating a nextObjective with
+     * operation VERIFY for existing next objectives in the store, and passing
+     * it to the driver. It is the driver that actually performs the verification
+     * by adding or removing buckets to match the verification next objective
+     * created here.
+     */
+    protected final class BucketCorrector implements Runnable {
+        Integer nextId;
+
+        BucketCorrector() {
+            this.nextId = null;
+        }
+
+        BucketCorrector(Integer nextId) {
+            this.nextId = nextId;
+        }
+
+        @Override
+        public void run() {
+            if (!srManager.mastershipService.isLocalMaster(deviceId)) {
+                return;
+            }
+            DefaultRoutingHandler rh = srManager.getRoutingHandler();
+            if (rh == null) {
+                return;
+            }
+            if (!rh.isRoutingStable()) {
+                return;
+            }
+            rh.acquireRoutingLock();
+            try {
+                log.trace("running bucket corrector for dev: {}", deviceId);
+                Set<DestinationSetNextObjectiveStoreKey> dsKeySet = dsNextObjStore.entrySet()
+                        .stream()
+                        .filter(entry -> entry.getKey().deviceId().equals(deviceId))
+                        // Filter out PW transit groups or include them if MPLS ECMP is supported
+                        .filter(entry -> !entry.getKey().destinationSet().mplsSet() ||
+                                (entry.getKey().destinationSet().mplsSet() && srManager.getMplsEcmp()))
+                        .map(entry -> entry.getKey())
+                        .collect(Collectors.toSet());
+                for (DestinationSetNextObjectiveStoreKey dsKey : dsKeySet) {
+                    NextNeighbors next = dsNextObjStore.get(dsKey);
+                    if (next == null) {
+                        continue;
+                    }
+                    int nid = next.nextId();
+                    if (nextId != null && nextId != nid) {
+                        continue;
+                    }
+                    log.trace("bkt-corr: dsNextObjStore for device {}: {}",
+                              deviceId, dsKey, next);
+                    TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+                    metabuilder.matchVlanId(INTERNAL_VLAN);
+                    NextObjective.Builder nextObjBuilder = DefaultNextObjective.builder()
+                            .withId(nid)
+                            .withType(NextObjective.Type.HASHED)
+                            .withMeta(metabuilder.build())
+                            .fromApp(appId);
+
+                    next.dstNextHops().forEach((dstDev, nextHops) -> {
+                        int edgeLabel = dsKey.destinationSet().getEdgeLabel(dstDev);
+                        nextHops.forEach(neighbor -> {
+                            MacAddress neighborMac;
+                            try {
+                                neighborMac = deviceConfig.getDeviceMac(neighbor);
+                            } catch (DeviceConfigNotFoundException e) {
+                                log.warn(e.getMessage() + " Aborting neighbor"
+                                        + neighbor);
+                                return;
+                            }
+                            devicePortMap.get(neighbor).forEach(port -> {
+                                log.trace("verify in device {} nextId {}: bucket with"
+                                        + " port/label {}/{} to dst {} via {}",
+                                        deviceId, nid, port, edgeLabel,
+                                        dstDev, neighbor);
+                                nextObjBuilder.addTreatment(treatmentBuilder(port,
+                                                                neighborMac, edgeLabel));
+                            });
+                        });
+                    });
+
+                    NextObjective nextObjective = nextObjBuilder.verify();
+                    flowObjectiveService.next(deviceId, nextObjective);
+                }
+            } finally {
+                rh.releaseRoutingLock();
+            }
+
+        }
+
+        TrafficTreatment treatmentBuilder(PortNumber outport, MacAddress dstMac,
+                                          int edgeLabel) {
+            TrafficTreatment.Builder tBuilder =
+                    DefaultTrafficTreatment.builder();
+            tBuilder.setOutput(outport)
+                .setEthDst(dstMac)
+                .setEthSrc(nodeMacAddr);
+            if (edgeLabel != DestinationSet.NO_EDGE_LABEL) {
+                tBuilder.pushMpls()
+                    .copyTtlOut()
+                    .setMpls(MplsLabel.mplsLabel(edgeLabel));
+            }
+            return tBuilder.build();
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java
new file mode 100644
index 0000000..7f839cf
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/DestinationSet.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import org.onosproject.net.DeviceId;
+import org.slf4j.Logger;
+
+import com.google.common.base.MoreObjects.ToStringHelper;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Representation of a set of destination switch dpids along with their edge-node
+ * labels. Meant to be used as a lookup-key in a hash-map to retrieve an ECMP-group
+ * that hashes packets towards a specific destination switch,
+ * or paired-destination switches.
+ */
+public class DestinationSet {
+    public static final int NO_EDGE_LABEL = -1;
+    private static final int NOT_ASSIGNED = 0;
+    private boolean mplsSet;
+    private final DeviceId dstSw1;
+    private final int edgeLabel1;
+    private final DeviceId dstSw2;
+    private final int edgeLabel2;
+
+
+    private static final Logger log = getLogger(DestinationSet.class);
+
+    /**
+     * Constructor for a single destination with no Edge label.
+     *
+     * @param isMplsSet indicates if it is a mpls destination set
+     * @param dstSw the destination switch
+     */
+    public DestinationSet(boolean isMplsSet, DeviceId dstSw) {
+        this.edgeLabel1 = NO_EDGE_LABEL;
+        this.mplsSet = isMplsSet;
+        this.dstSw1 = dstSw;
+        this.edgeLabel2 = NOT_ASSIGNED;
+        this.dstSw2 = null;
+    }
+
+    /**
+     * Constructor for a single destination with Edge label.
+     *
+     * @param isMplsSet indicates if it is a mpls destination set
+     * @param edgeLabel label to be pushed as part of group operation
+     * @param dstSw the destination switch
+     */
+    public DestinationSet(boolean isMplsSet,
+                       int edgeLabel, DeviceId dstSw) {
+        this.mplsSet = isMplsSet;
+        this.edgeLabel1 = edgeLabel;
+        this.dstSw1 = dstSw;
+        this.edgeLabel2 = NOT_ASSIGNED;
+        this.dstSw2 = null;
+    }
+
+    /**
+     * Constructor for paired destination switches and their associated
+     * edge labels.
+     *
+     * @param isMplsSet indicates if it is a mpls destination set
+     * @param edgeLabel1 label to be pushed as part of group operation for dstSw1
+     * @param dstSw1 one of the paired destination switches
+     * @param edgeLabel2 label to be pushed as part of group operation for dstSw2
+     * @param dstSw2 the other paired destination switch
+     */
+    public DestinationSet(boolean isMplsSet,
+                          int edgeLabel1, DeviceId dstSw1,
+                          int edgeLabel2, DeviceId dstSw2) {
+        this.mplsSet = isMplsSet;
+        if (dstSw1.toString().compareTo(dstSw2.toString()) <= 0) {
+            this.edgeLabel1 = edgeLabel1;
+            this.dstSw1 = dstSw1;
+            this.edgeLabel2 = edgeLabel2;
+            this.dstSw2 = dstSw2;
+        } else {
+            this.edgeLabel1 = edgeLabel2;
+            this.dstSw1 = dstSw2;
+            this.edgeLabel2 = edgeLabel1;
+            this.dstSw2 = dstSw1;
+        }
+    }
+
+    /**
+     * Default constructor for kryo serialization.
+     */
+    public DestinationSet() {
+        this.edgeLabel1 = NOT_ASSIGNED;
+        this.edgeLabel2 = NOT_ASSIGNED;
+        this.mplsSet = true;
+        this.dstSw1 = DeviceId.NONE;
+        this.dstSw2 = DeviceId.NONE;
+    }
+
+    /**
+     * Factory method for DestinationSet hierarchy.
+     *
+     * @param random the expected behavior.
+     * @param isMplsSet indicates if it is a mpls destination set
+     * @param dstSw the destination switch
+     * @return the destination set object.
+     */
+    public static DestinationSet destinationSet(boolean random,
+                                          boolean isMplsSet, DeviceId dstSw) {
+        return random ? new RandomDestinationSet(dstSw)
+                      : new DestinationSet(isMplsSet, dstSw);
+    }
+
+    /**
+     * Factory method for DestinationSet hierarchy.
+     *
+     * @param random the expected behavior.
+     * @param isMplsSet indicates if it is a mpls destination set
+     * @param edgeLabel label to be pushed as part of group operation
+     * @param dstSw the destination switch
+     * @return the destination set object
+     */
+    public static DestinationSet destinationSet(boolean random,
+                                          boolean isMplsSet, int edgeLabel,
+                                          DeviceId dstSw) {
+        return random ? new RandomDestinationSet(edgeLabel, dstSw)
+                      : new DestinationSet(isMplsSet, edgeLabel, dstSw);
+    }
+
+    /**
+     * Factory method for DestinationSet hierarchy.
+     *
+     * @param random the expected behavior.
+     * @return the destination set object
+     */
+    public static DestinationSet destinationSet(boolean random) {
+        return random ? new RandomDestinationSet() : new DestinationSet();
+    }
+
+    /**
+     * Gets the label associated with given destination switch.
+     *
+     * @param dstSw the destination switch
+     * @return integer the label associated with the destination switch
+     */
+    public int getEdgeLabel(DeviceId dstSw) {
+        if (dstSw.equals(dstSw1)) {
+            return edgeLabel1;
+        } else if (dstSw.equals(dstSw2)) {
+            return edgeLabel2;
+        }
+        return NOT_ASSIGNED;
+    }
+
+    /**
+     * Gets all the destination switches in this destination set.
+     *
+     * @return a set of destination switch ids
+     */
+    public Set<DeviceId> getDestinationSwitches() {
+        Set<DeviceId> dests = new HashSet<>();
+        dests.add(dstSw1);
+        if (dstSw2 != null) {
+            dests.add(dstSw2);
+        }
+        return dests;
+    }
+
+    /**
+     * Gets the value of mplsSet.
+     *
+     * @return the value of mplsSet
+     */
+    public boolean mplsSet() {
+        return mplsSet;
+    }
+
+    // The list of destination ids and label are used for comparison.
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DestinationSet)) {
+            return false;
+        }
+        DestinationSet that = (DestinationSet) o;
+        boolean equal = (this.edgeLabel1 == that.edgeLabel1 &&
+                            this.mplsSet == that.mplsSet &&
+                            this.dstSw1.equals(that.dstSw1));
+        if (this.dstSw2 != null && that.dstSw2 == null ||
+                this.dstSw2 == null && that.dstSw2 != null) {
+            return false;
+        }
+        if (this.dstSw2 != null && that.dstSw2 != null) {
+            equal = equal && (this.edgeLabel2 == that.edgeLabel2 &&
+                                this.dstSw2.equals(that.dstSw2));
+        }
+        return equal;
+    }
+
+    // The list of destination ids and label are used for comparison.
+    @Override
+    public int hashCode() {
+        if (dstSw2 == null) {
+            return Objects.hash(mplsSet, edgeLabel1, dstSw1);
+        }
+        return Objects.hash(mplsSet, edgeLabel1, dstSw1, edgeLabel2, dstSw2);
+    }
+
+    @Override
+    public String toString() {
+        ToStringHelper h = toStringHelper(this)
+                                .add("MplsSet", mplsSet)
+                                .add("DstSw1", dstSw1)
+                                .add("Label1", edgeLabel1);
+        if (dstSw2 != null) {
+            h.add("DstSw2", dstSw2)
+             .add("Label2", edgeLabel2);
+        }
+        return h.toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java
new file mode 100644
index 0000000..1c2e1ac
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/GroupBucketIdentifier.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onosproject.net.PortNumber;
+
+/**
+ * Representation of policy group bucket identifier. Not exposed to
+ * the application and only to be used internally.
+ */
+public class GroupBucketIdentifier {
+    private int label;
+    private BucketOutputType type;
+    private PortNumber outPort;
+    private PolicyGroupIdentifier outGroup;
+
+    protected enum BucketOutputType {
+        PORT,
+        GROUP
+    }
+
+    protected GroupBucketIdentifier(int label,
+                                    PortNumber outPort) {
+        this.label = label;
+        this.type = BucketOutputType.PORT;
+        this.outPort = checkNotNull(outPort);
+        this.outGroup = null;
+    }
+
+    protected GroupBucketIdentifier(int label,
+                                    PolicyGroupIdentifier outGroup) {
+        this.label = label;
+        this.type = BucketOutputType.GROUP;
+        this.outPort = null;
+        this.outGroup = checkNotNull(outGroup);
+    }
+
+    protected int label() {
+        return this.label;
+    }
+
+    protected BucketOutputType type() {
+        return this.type;
+    }
+
+    protected PortNumber outPort() {
+        return this.outPort;
+    }
+
+    protected PolicyGroupIdentifier outGroup() {
+        return this.outGroup;
+    }
+}
+
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java
new file mode 100644
index 0000000..eaca1aa
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/NextNeighbors.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.onosproject.net.DeviceId;
+
+/**
+ * Represents the nexthop information associated with a route-path towards a
+ * set of destinations.
+ */
+public class NextNeighbors {
+    private final Map<DeviceId, Set<DeviceId>> dstNextHops;
+    private final int nextId;
+
+    /**
+     * Constructor.
+     *
+     * @param dstNextHops map of destinations and the next-hops towards each dest
+     * @param nextId id of nextObjective that manifests the next-hop info
+     */
+    public NextNeighbors(Map<DeviceId, Set<DeviceId>> dstNextHops, int nextId) {
+        this.dstNextHops = dstNextHops;
+        this.nextId = nextId;
+    }
+
+    /**
+     * Returns a map of destinations and the next-hops towards them.
+     *
+     * @return map of destinations and the next-hops towards them
+     */
+    public Map<DeviceId, Set<DeviceId>> dstNextHops() {
+        return dstNextHops;
+    }
+
+    /**
+     * Set of next-hops towards the given destination.
+     *
+     * @param deviceId the destination
+     * @return set of nexthops towards the destination
+     */
+    public Set<DeviceId> nextHops(DeviceId deviceId) {
+        return dstNextHops.get(deviceId);
+    }
+
+    /**
+     * Return the nextId representing the nextObjective towards the next-hops.
+     *
+     * @return nextId representing the nextObjective towards the next-hops
+     */
+    public int nextId() {
+        return nextId;
+    }
+
+    /**
+     * Checks if the given nextHopId is a valid next hop to any one of the
+     * destinations.
+     *
+     * @param nextHopId the deviceId for the next hop
+     * @return true if given next
+     */
+    public boolean containsNextHop(DeviceId nextHopId) {
+        for (Set<DeviceId> nextHops : dstNextHops.values()) {
+            if (nextHops != null && nextHops.contains(nextHopId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a set of destinations which have the given nextHopId as one
+     * of the next-hops to that destination.
+     *
+     * @param nextHopId the deviceId for the next hop
+     * @return set of deviceIds that have the given nextHopId as a next-hop
+     *          which could be empty if no destinations were found
+     */
+    public Set<DeviceId> getDstForNextHop(DeviceId nextHopId) {
+        Set<DeviceId> dstSet = new HashSet<>();
+        for (DeviceId dstKey : dstNextHops.keySet()) {
+            if (dstNextHops.get(dstKey).contains(nextHopId)) {
+                dstSet.add(dstKey);
+            }
+        }
+        return dstSet;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof NextNeighbors)) {
+            return false;
+        }
+        NextNeighbors that = (NextNeighbors) o;
+        return (this.nextId == that.nextId) &&
+                this.dstNextHops.equals(that.dstNextHops);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(nextId, dstNextHops);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("nextId", nextId)
+                .add("dstNextHops", dstNextHops)
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java
new file mode 100644
index 0000000..c0935ce
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupHandler.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.DeviceProperties;
+import org.onosproject.segmentrouting.grouphandler.GroupBucketIdentifier.BucketOutputType;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.link.LinkService;
+import org.slf4j.Logger;
+
+/**
+ * A module to create group chains based on the specified device
+ * ports and label stack to be applied on each port.
+ */
+public class PolicyGroupHandler extends DefaultGroupHandler {
+
+    private final Logger log = getLogger(getClass());
+    private HashMap<PolicyGroupIdentifier, PolicyGroupIdentifier> dependentGroups = new HashMap<>();
+
+    /**
+     * Constructs policy group handler.
+     *
+     * @param deviceId device identifier
+     * @param appId application identifier
+     * @param config interface to retrieve the device properties
+     * @param linkService link service object
+     * @param flowObjService flow objective service object
+     * @param srManager segment routing manager
+     */
+    public PolicyGroupHandler(DeviceId deviceId,
+                              ApplicationId appId,
+                              DeviceProperties config,
+                              LinkService linkService,
+                              FlowObjectiveService flowObjService,
+                              SegmentRoutingManager srManager) {
+        super(deviceId, appId, config, linkService, flowObjService, srManager);
+    }
+
+    /**
+     * Creates policy group chain.
+     *
+     * @param id unique identifier associated with the policy group
+     * @param params a list of policy group params
+     * @return policy group identifier
+     */
+    public PolicyGroupIdentifier createPolicyGroupChain(String id,
+                                                        List<PolicyGroupParams> params) {
+        List<GroupBucketIdentifier> bucketIds = new ArrayList<>();
+        for (PolicyGroupParams param: params) {
+            List<PortNumber> ports = param.getPorts();
+            if (ports == null) {
+                log.warn("createPolicyGroupChain in sw {} with wrong "
+                        + "input parameters", deviceId);
+                return null;
+            }
+
+            int labelStackSize = (param.getLabelStack() != null) ?
+                                      param.getLabelStack().size() : 0;
+
+            if (labelStackSize > 1) {
+                for (PortNumber sp : ports) {
+                    PolicyGroupIdentifier previousGroupkey = null;
+                    DeviceId neighbor = portDeviceMap.get(sp);
+                    for (int idx = 0; idx < param.getLabelStack().size(); idx++) {
+                        int label = param.getLabelStack().get(idx);
+                        if (idx == (labelStackSize - 1)) {
+                            // Innermost Group
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label,
+                                                              previousGroupkey);
+                            bucketIds.add(bucketId);
+                        } else if (idx == 0) {
+                            // Outermost Group
+                            List<GroupBucket> outBuckets = new ArrayList<>();
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label, sp);
+                            PolicyGroupIdentifier key = new
+                                    PolicyGroupIdentifier(id,
+                                                          Collections.singletonList(param),
+                                                          Collections.singletonList(bucketId));
+                            MacAddress neighborEthDst;
+                            try {
+                                neighborEthDst = deviceConfig.getDeviceMac(neighbor);
+                            } catch (DeviceConfigNotFoundException e) {
+                                log.warn(e.getMessage()
+                                        + " Skipping createPolicyGroupChain for this label.");
+                                continue;
+                            }
+
+                            TrafficTreatment.Builder tBuilder =
+                                    DefaultTrafficTreatment.builder();
+                            tBuilder.setOutput(sp)
+                                    .setEthDst(neighborEthDst)
+                                    .setEthSrc(nodeMacAddr)
+                                    .pushMpls()
+                                    .setMpls(MplsLabel.mplsLabel(label));
+                            /*outBuckets.add(DefaultGroupBucket.
+                                           createSelectGroupBucket(tBuilder.build()));
+                            GroupDescription desc = new
+                                    DefaultGroupDescription(deviceId,
+                                                            GroupDescription.Type.INDIRECT,
+                                                            new GroupBuckets(outBuckets));
+                            //TODO: BoS*/
+                            previousGroupkey = key;
+                            //groupService.addGroup(desc);
+                            //TODO: Use nextObjective APIs here
+                        } else {
+                            // Intermediate Groups
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label,
+                                                              previousGroupkey);
+                            PolicyGroupIdentifier key = new
+                                    PolicyGroupIdentifier(id,
+                                                          Collections.singletonList(param),
+                                                          Collections.singletonList(bucketId));
+                            // Add to group dependency list
+                            dependentGroups.put(previousGroupkey, key);
+                            previousGroupkey = key;
+                        }
+                    }
+                }
+            } else {
+                int label = -1;
+                if (labelStackSize == 1) {
+                    label = param.getLabelStack().get(0);
+                }
+                for (PortNumber sp : ports) {
+                    GroupBucketIdentifier bucketId =
+                            new GroupBucketIdentifier(label, sp);
+                    bucketIds.add(bucketId);
+                }
+            }
+        }
+        PolicyGroupIdentifier innermostGroupkey = null;
+        if (!bucketIds.isEmpty()) {
+            innermostGroupkey = new
+                    PolicyGroupIdentifier(id,
+                                          params,
+                                          bucketIds);
+            // Add to group dependency list
+            boolean fullyResolved = true;
+            for (GroupBucketIdentifier bucketId:bucketIds) {
+                if (bucketId.type() == BucketOutputType.GROUP) {
+                    dependentGroups.put(bucketId.outGroup(),
+                                        innermostGroupkey);
+                    fullyResolved = false;
+                }
+            }
+
+            if (fullyResolved) {
+                List<GroupBucket> outBuckets = new ArrayList<>();
+                for (GroupBucketIdentifier bucketId : bucketIds) {
+                    DeviceId neighbor = portDeviceMap.
+                            get(bucketId.outPort());
+
+                    MacAddress neighborEthDst;
+                    try {
+                        neighborEthDst = deviceConfig.getDeviceMac(neighbor);
+                    } catch (DeviceConfigNotFoundException e) {
+                        log.warn(e.getMessage()
+                                + " Skipping createPolicyGroupChain for this bucketId.");
+                        continue;
+                    }
+
+                    TrafficTreatment.Builder tBuilder =
+                            DefaultTrafficTreatment.builder();
+                    tBuilder.setOutput(bucketId.outPort())
+                            .setEthDst(neighborEthDst)
+                            .setEthSrc(nodeMacAddr);
+                    if (bucketId.label() != DestinationSet.NO_EDGE_LABEL) {
+                        tBuilder.pushMpls()
+                            .setMpls(MplsLabel.mplsLabel(bucketId.label()));
+                    }
+                    //TODO: BoS
+                    /*outBuckets.add(DefaultGroupBucket.
+                                   createSelectGroupBucket(tBuilder.build()));*/
+                }
+                /*GroupDescription desc = new
+                        DefaultGroupDescription(deviceId,
+                                                GroupDescription.Type.SELECT,
+                                                new GroupBuckets(outBuckets));
+                groupService.addGroup(desc);*/
+                //TODO: Use nextObjective APIs here
+            }
+        }
+        return innermostGroupkey;
+    }
+
+    //TODO: Use nextObjective APIs to handle the group chains
+    /*
+    @Override
+    protected void handleGroupEvent(GroupEvent event) {}
+    */
+
+    /**
+     * Generates policy group key.
+     *
+     * @param id unique identifier associated with the policy group
+     * @param params a list of policy group params
+     * @return policy group identifier
+     */
+    public PolicyGroupIdentifier generatePolicyGroupKey(String id,
+                                   List<PolicyGroupParams> params) {
+        List<GroupBucketIdentifier> bucketIds = new ArrayList<>();
+        for (PolicyGroupParams param: params) {
+            List<PortNumber> ports = param.getPorts();
+            if (ports == null) {
+                log.warn("generateGroupKey in sw {} with wrong "
+                        + "input parameters", deviceId);
+                return null;
+            }
+
+            int labelStackSize = (param.getLabelStack() != null)
+                    ? param.getLabelStack().size() : 0;
+
+            if (labelStackSize > 1) {
+                for (PortNumber sp : ports) {
+                    PolicyGroupIdentifier previousGroupkey = null;
+                    for (int idx = 0; idx < param.getLabelStack().size(); idx++) {
+                        int label = param.getLabelStack().get(idx);
+                        if (idx == (labelStackSize - 1)) {
+                            // Innermost Group
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label,
+                                                              previousGroupkey);
+                            bucketIds.add(bucketId);
+                        } else if (idx == 0) {
+                            // Outermost Group
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label, sp);
+                            PolicyGroupIdentifier key = new
+                                    PolicyGroupIdentifier(id,
+                                                          Collections.singletonList(param),
+                                                          Collections.singletonList(bucketId));
+                            previousGroupkey = key;
+                        } else {
+                            // Intermediate Groups
+                            GroupBucketIdentifier bucketId =
+                                    new GroupBucketIdentifier(label,
+                                                              previousGroupkey);
+                            PolicyGroupIdentifier key = new
+                                    PolicyGroupIdentifier(id,
+                                                          Collections.singletonList(param),
+                                                          Collections.singletonList(bucketId));
+                            previousGroupkey = key;
+                        }
+                    }
+                }
+            } else {
+                int label = -1;
+                if (labelStackSize == 1) {
+                    label = param.getLabelStack().get(0);
+                }
+                for (PortNumber sp : ports) {
+                    GroupBucketIdentifier bucketId =
+                            new GroupBucketIdentifier(label, sp);
+                    bucketIds.add(bucketId);
+                }
+            }
+        }
+        PolicyGroupIdentifier innermostGroupkey = null;
+        if (!bucketIds.isEmpty()) {
+            innermostGroupkey = new
+                    PolicyGroupIdentifier(id,
+                                          params,
+                                          bucketIds);
+        }
+        return innermostGroupkey;
+    }
+
+    /**
+     * Removes policy group chain.
+     *
+     * @param key policy group identifier
+     */
+    public void removeGroupChain(PolicyGroupIdentifier key) {
+        checkArgument(key != null);
+        List<PolicyGroupIdentifier> groupsToBeDeleted = new ArrayList<>();
+        groupsToBeDeleted.add(key);
+
+        Iterator<PolicyGroupIdentifier> it =
+                groupsToBeDeleted.iterator();
+
+        while (it.hasNext()) {
+            PolicyGroupIdentifier innerMostGroupKey = it.next();
+            for (GroupBucketIdentifier bucketId:
+                        innerMostGroupKey.bucketIds()) {
+                if (bucketId.type() != BucketOutputType.GROUP) {
+                    groupsToBeDeleted.add(bucketId.outGroup());
+                }
+            }
+            /*groupService.removeGroup(deviceId,
+                                     getGroupKey(innerMostGroupKey),
+                                     appId);*/
+            //TODO: Use nextObjective APIs here
+            it.remove();
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java
new file mode 100644
index 0000000..fdc6dea
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupIdentifier.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import java.util.List;
+
+/**
+ * Representation of policy based group identifiers.
+ * Opaque to group handler applications and only the outermost
+ * policy group identifier in a chain is visible to the applications.
+ */
+public class PolicyGroupIdentifier {
+    private String id;
+    private List<PolicyGroupParams> inputParams;
+    private List<GroupBucketIdentifier> bucketIds;
+
+    /**
+     * Constructs policy group identifier.
+     *
+     * @param id unique identifier associated with the policy group
+     * @param input policy group params associated with this group
+     * @param bucketIds buckets associated with this group
+     */
+    protected PolicyGroupIdentifier(String id,
+                                 List<PolicyGroupParams> input,
+                                 List<GroupBucketIdentifier> bucketIds) {
+        this.id = id;
+        this.inputParams = input;
+        this.bucketIds = bucketIds;
+    }
+
+    /**
+     * Returns the bucket identifier list associated with the policy
+     * group identifier.
+     *
+     * @return list of bucket identifier
+     */
+    protected List<GroupBucketIdentifier> bucketIds() {
+        return this.bucketIds;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        int combinedHash = 0;
+        for (PolicyGroupParams input:inputParams) {
+            combinedHash = combinedHash + input.hashCode();
+        }
+        for (GroupBucketIdentifier bucketId:bucketIds) {
+            combinedHash = combinedHash + bucketId.hashCode();
+        }
+        result = 31 * result + combinedHash;
+
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof PolicyGroupIdentifier) {
+            PolicyGroupIdentifier that = (PolicyGroupIdentifier) obj;
+            boolean result = this.id.equals(that.id);
+            result = result &&
+                    this.inputParams.containsAll(that.inputParams) &&
+                    that.inputParams.containsAll(this.inputParams);
+            result = result &&
+                    this.bucketIds.containsAll(that.bucketIds) &&
+                    that.bucketIds.containsAll(this.bucketIds);
+            return result;
+        }
+
+        return false;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java
new file mode 100644
index 0000000..5baf849
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/PolicyGroupParams.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015-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.segmentrouting.grouphandler;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.onosproject.net.PortNumber;
+
+/**
+ * Representation of parameters used to create policy based groups.
+ */
+public class PolicyGroupParams {
+    private final List<PortNumber> ports;
+    private final List<Integer> labelStack;
+
+    /**
+     * Constructor.
+     *
+     * @param labelStack mpls label stack to be applied on the ports
+     * @param ports ports to be part of the policy group
+     */
+    public PolicyGroupParams(List<Integer> labelStack,
+                             List<PortNumber> ports) {
+        this.ports = checkNotNull(ports);
+        this.labelStack = checkNotNull(labelStack);
+    }
+
+    /**
+     * Returns the ports associated with the policy group params.
+     *
+     * @return list of port numbers
+     */
+    public List<PortNumber> getPorts() {
+        return ports;
+    }
+
+    /**
+     * Returns the label stack associated with the policy group params.
+     *
+     * @return list of integers
+     */
+    public List<Integer> getLabelStack() {
+        return labelStack;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        int combinedHash = 0;
+        for (PortNumber port:ports) {
+            combinedHash = combinedHash + port.hashCode();
+        }
+        combinedHash = combinedHash + Objects.hash(labelStack);
+        result = 31 * result + combinedHash;
+
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof PolicyGroupParams) {
+            PolicyGroupParams that = (PolicyGroupParams) obj;
+            boolean result = this.labelStack.equals(that.labelStack);
+            result = result &&
+                    this.ports.containsAll(that.ports) &&
+                    that.ports.containsAll(this.ports);
+            return result;
+        }
+
+        return false;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java
new file mode 100644
index 0000000..3671b03
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/RandomDestinationSet.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-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.segmentrouting.grouphandler;
+
+import org.onosproject.net.DeviceId;
+
+/**
+ * Extends its super class modifying its internal behavior.
+ * Pick a neighbor will pick a random neighbor.
+ */
+public class RandomDestinationSet extends DestinationSet {
+
+    public RandomDestinationSet(DeviceId dstSw) {
+        super(true, dstSw);
+    }
+
+    public RandomDestinationSet(int edgeLabel,
+                             DeviceId dstSw) {
+        super(true, edgeLabel, dstSw);
+    }
+
+    public RandomDestinationSet() {
+        super();
+    }
+
+    // XXX revisit the need for this class since neighbors no longer stored here
+    // will be handled when we fix pseudowires for dual-Tor scenarios
+
+
+    /*@Override
+    public DeviceId getFirstNeighbor() {
+        if (getDeviceIds().isEmpty()) {
+            return DeviceId.NONE;
+        }
+        int size = getDeviceIds().size();
+        int index = RandomUtils.nextInt(0, size);
+        return Iterables.get(getDeviceIds(), index);
+    }*/
+
+    @Override
+    public String toString() {
+        return " RandomNeighborset Sw: " //+ getDeviceIds()
+                + " and Label: " //+ getEdgeLabel()
+                + " for destination: "; // + getDestinationSw();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java
new file mode 100644
index 0000000..d99f1d1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/grouphandler/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing group handling.
+ */
+package org.onosproject.segmentrouting.grouphandler;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java
new file mode 100644
index 0000000..137ba3c
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-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.
+ */
+
+/**
+ * Segment routing application components.
+ */
+package org.onosproject.segmentrouting;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java
new file mode 100644
index 0000000..417a795
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2Tunnel.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.Link;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the default l2 tunnel.
+ */
+public class DefaultL2Tunnel implements L2Tunnel {
+
+    /**
+     * Mode of the pseudo wire.
+     */
+    private L2Mode pwMode;
+    /**
+     * Service delimiting tag.
+     */
+    private VlanId sdTag;
+    /**
+     * Tunnel id.
+     */
+    private long tunnelId;
+    /**
+     * Pseudo wire label.
+     */
+    private MplsLabel pwLabel;
+    /**
+     * Inter-CO label.
+     */
+    private MplsLabel interCoLabel;
+
+    private List<Link> pathUsed;
+
+    /**
+     * Vlan which will be used for the encapsualted
+     * vlan traffic.
+     */
+    private VlanId transportVlan;
+
+    /**
+     * Creates a inter-co l2 tunnel using the
+     * supplied parameters.
+     *
+     * @param mode         the tunnel mode
+     * @param sdtag        the service delimiting tag
+     * @param tunnelId     the tunnel id
+     * @param pwLabel      the pseudo wire label
+     * @param interCoLabel the inter central office label
+     */
+    public DefaultL2Tunnel(L2Mode mode, VlanId sdtag, long tunnelId, MplsLabel pwLabel, MplsLabel interCoLabel) {
+        checkNotNull(mode);
+        checkArgument(tunnelId > 0);
+        checkNotNull(pwLabel);
+        checkNotNull(interCoLabel);
+
+        this.pwMode = mode;
+        this.sdTag = sdtag;
+        this.tunnelId = tunnelId;
+        this.pwLabel = pwLabel;
+        this.interCoLabel = interCoLabel;
+    }
+
+    /**
+     * Creates a l2Tunnel from a given tunnel.
+     *
+     * @param l2Tunnel to replicate
+     */
+    public DefaultL2Tunnel(DefaultL2Tunnel l2Tunnel) {
+
+        this.pwMode = l2Tunnel.pwMode();
+        this.sdTag = l2Tunnel.sdTag();
+        this.tunnelId = l2Tunnel.tunnelId();
+        this.pwLabel = l2Tunnel.pwLabel();
+        this.interCoLabel = l2Tunnel.interCoLabel();
+        this.pathUsed = l2Tunnel.pathUsed();
+        this.transportVlan = l2Tunnel.transportVlan;
+    }
+
+    /**
+     * Creates a intra-co l2 tunnel using the
+     * supplied parameters.
+     *
+     * @param mode     the tunnel mode
+     * @param sdtag    the service delimiting tag
+     * @param tunnelId the tunnel id
+     * @param pwLabel  the pseudo wire label
+     */
+    public DefaultL2Tunnel(L2Mode mode, VlanId sdtag, long tunnelId, MplsLabel pwLabel) {
+        this(mode, sdtag, tunnelId, pwLabel, MplsLabel.mplsLabel(MplsLabel.MAX_MPLS));
+    }
+
+
+    /**
+     * Creates an empty l2 tunnel.
+     **/
+    public DefaultL2Tunnel() {
+        this.pwMode = null;
+        this.sdTag = null;
+        this.tunnelId = 0;
+        this.pwLabel = null;
+        this.interCoLabel = null;
+    }
+
+    /**
+     * Returns the mode of the pseudo wire.
+     *
+     * @return the pseudo wire mode
+     */
+    @Override
+    public L2Mode pwMode() {
+        return pwMode;
+    }
+
+    /**
+     * Returns the service delimitation
+     * tag.
+     *
+     * @return the service delimitation vlan id
+     */
+    @Override
+    public VlanId sdTag() {
+        return sdTag;
+    }
+
+    /**
+     * Returns the tunnel id of the pseudo wire.
+     *
+     * @return the pseudo wire tunnel id
+     */
+    @Override
+    public long tunnelId() {
+        return tunnelId;
+    }
+
+    /**
+     * Returns the pw label.
+     *
+     * @return the mpls pw label
+     */
+    @Override
+    public MplsLabel pwLabel() {
+        return pwLabel;
+    }
+
+    /**
+     * Set the path for the pseudowire.
+     *
+     * @param path The path to set
+     */
+    @Override
+    public void setPath(List<Link> path) {
+        pathUsed = new ArrayList<>(path);
+    }
+
+    /**
+     * Set the transport vlan for the pseudowire.
+     *
+     * @param vlan the vlan to use.
+     */
+    @Override
+    public void setTransportVlan(VlanId vlan) {
+        transportVlan = vlan;
+    }
+
+    /**
+     * Returns the used path of the pseudowire.
+     *
+     * @return pathUsed
+     */
+    @Override
+    public List<Link> pathUsed() {
+        return pathUsed;
+    }
+
+    @Override
+    public VlanId transportVlan() {
+        return transportVlan;
+    }
+
+
+    /**
+     * Returns the inter-co label.
+     *
+     * @return the mpls inter-co label
+     */
+    @Override
+    public MplsLabel interCoLabel() {
+        return interCoLabel;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.tunnelId, this.pwMode, this.sdTag, this.pwLabel, this.interCoLabel);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof DefaultL2Tunnel) {
+            DefaultL2Tunnel that = (DefaultL2Tunnel) o;
+            return this.tunnelId == that.tunnelId &&
+                    this.pwMode.equals(that.pwMode) &&
+                    this.sdTag.equals(that.sdTag) &&
+                    this.pwLabel.equals(that.pwLabel) &&
+                    this.interCoLabel.equals(that.interCoLabel);
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("pwMode", pwMode())
+                .add("sdTag", sdTag())
+                .add("tunnelId", tunnelId())
+                .add("pwLabel", pwLabel())
+                .add("interCoLabel", interCoLabel())
+                .add("transportVlan", transportVlan())
+                .toString();
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java
new file mode 100644
index 0000000..7a47972
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelDescription.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import com.google.common.base.MoreObjects;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Helper class to carry the l2 tunnel
+ * and its policy.
+ */
+public class DefaultL2TunnelDescription implements L2TunnelDescription {
+
+    /**
+     * The l2 tunnel.
+     */
+    private L2Tunnel l2Tunnel;
+
+    /**
+     * The l2 tunnel policy.
+     */
+    private L2TunnelPolicy l2TunnelPolicy;
+
+    /**
+     * Creates a l2 tunnel description using the given info.
+     *
+     * @param l2Tunnel the l2 tunnel
+     * @param l2TunnelPolicy the l2 tunnel description
+     */
+    public DefaultL2TunnelDescription(L2Tunnel l2Tunnel,
+                                      L2TunnelPolicy l2TunnelPolicy) {
+        checkNotNull(l2Tunnel);
+        checkNotNull(l2TunnelPolicy);
+
+        this.l2Tunnel = l2Tunnel;
+        this.l2TunnelPolicy = l2TunnelPolicy;
+    }
+
+    /**
+     * Creates an empty l2 tunnel description.
+     */
+    public DefaultL2TunnelDescription() {
+        this.l2Tunnel = null;
+        this.l2TunnelPolicy = null;
+    }
+
+    @Override
+    public L2Tunnel l2Tunnel() {
+        return l2Tunnel;
+    }
+
+    @Override
+    public L2TunnelPolicy l2TunnelPolicy() {
+        return l2TunnelPolicy;
+    }
+
+    @Override
+    public void setL2Tunnel(L2Tunnel tunnel) {
+        l2Tunnel = tunnel;
+    }
+
+    @Override
+    public void setL2TunnelPolicy(L2TunnelPolicy policy) {
+        l2TunnelPolicy = policy;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.l2Tunnel, this.l2TunnelPolicy);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof DefaultL2TunnelDescription) {
+            DefaultL2TunnelDescription that = (DefaultL2TunnelDescription) o;
+            // Equality is based on tunnel id and pw label
+            // which is always the last label.
+            return this.l2Tunnel.equals(that.l2Tunnel) &&
+                    this.l2TunnelPolicy.equals(that.l2TunnelPolicy);
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("l2Tunnel", l2Tunnel())
+                .add("l2TunnelPolicy", l2TunnelPolicy())
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java
new file mode 100644
index 0000000..2dc9de2
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelHandler.java
@@ -0,0 +1,1733 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.RandomUtils;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.DistributedSet;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.Versioned;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.onosproject.net.flowobjective.ForwardingObjective.Flag.VERSATILE;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Pipeline.INITIATION;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Pipeline.TERMINATION;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Result.*;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Direction.FWD;
+import static org.onosproject.segmentrouting.pwaas.L2TunnelHandler.Direction.REV;
+
+/**
+ * Handles pwaas related events.
+ */
+public class DefaultL2TunnelHandler implements L2TunnelHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultL2TunnelHandler.class);
+
+    private final SegmentRoutingManager srManager;
+    /**
+     * To store the next objectives related to the initiation.
+     */
+    private final ConsistentMap<String, NextObjective> l2InitiationNextObjStore;
+    /**
+     * To store the next objectives related to the termination.
+     */
+    private final ConsistentMap<String, NextObjective> l2TerminationNextObjStore;
+
+    /**
+     * To store policies.
+     */
+    private final ConsistentMap<String, L2TunnelPolicy> l2PolicyStore;
+
+    /**
+     * To store tunnels.
+     */
+    private final ConsistentMap<String, L2Tunnel> l2TunnelStore;
+
+    private final KryoNamespace.Builder l2TunnelKryo;
+
+    /**
+     * Contains transport vlans used for spine-leaf pseudowires.
+     */
+    private final DistributedSet<VlanId> vlanStore;
+
+    /**
+     * Used for determining transport vlans for leaf-spine.
+     */
+    private short transportVlanUpper = 4093, transportVlanLower = 3500;
+
+    private static final VlanId UNTAGGED_TRANSPORT_VLAN = VlanId.vlanId((short) 4094);
+
+    /**
+     * Create a l2 tunnel handler for the deploy and
+     * for the tear down of pseudo wires.
+     *
+     * @param segmentRoutingManager the segment routing manager
+     */
+    public DefaultL2TunnelHandler(SegmentRoutingManager segmentRoutingManager) {
+        srManager = segmentRoutingManager;
+        l2TunnelKryo = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(L2Tunnel.class,
+                          L2TunnelPolicy.class,
+                          DefaultL2Tunnel.class,
+                          DefaultL2TunnelPolicy.class,
+                          L2Mode.class,
+                          MplsLabel.class,
+                          VlanId.class,
+                          ConnectPoint.class);
+
+        l2InitiationNextObjStore = srManager.
+                storageService.
+                <String, NextObjective>consistentMapBuilder().
+                withName("onos-l2initiation-nextobj-store").
+                withSerializer(Serializer.using(l2TunnelKryo.build())).
+                build();
+
+        l2TerminationNextObjStore = srManager.storageService.
+                <String, NextObjective>consistentMapBuilder()
+                .withName("onos-l2termination-nextobj-store")
+                .withSerializer(Serializer.using(l2TunnelKryo.build()))
+                .build();
+
+        l2PolicyStore = srManager.storageService
+                .<String, L2TunnelPolicy>consistentMapBuilder()
+                .withName("onos-l2-policy-store")
+                .withSerializer(Serializer.using(l2TunnelKryo.build()))
+                .build();
+
+        l2TunnelStore = srManager.storageService
+                .<String, L2Tunnel>consistentMapBuilder()
+                .withName("onos-l2-tunnel-store")
+                .withSerializer(Serializer.using(l2TunnelKryo.build()))
+                .build();
+
+        vlanStore = srManager.storageService.<VlanId>setBuilder()
+                .withName("onos-transport-vlan-store")
+                .withSerializer(Serializer.using(
+                        new KryoNamespace.Builder()
+                                .register(KryoNamespaces.API)
+                                .build()))
+                .build()
+                .asDistributedSet();
+    }
+
+    /**
+     * Deploys any pre-existing pseudowires in the configuration.
+     * Used by manager only in initialization.
+     */
+    @Override
+    public void init() {
+
+        PwaasConfig config = srManager.cfgService.getConfig(srManager.appId(), PwaasConfig.class);
+        if (config == null) {
+            return;
+        }
+
+        log.info("Deploying existing pseudowires");
+
+        // gather pseudowires
+        Set<L2TunnelDescription> pwToAdd = config
+                .getPwIds()
+                .stream()
+                .map(config::getPwDescription)
+                .collect(Collectors.toSet());
+
+        // deploy pseudowires
+        deploy(pwToAdd);
+    }
+
+    /**
+     * Returns all L2 Policies.
+     *
+     * @return List of policies
+     */
+    @Override
+    public List<L2TunnelPolicy> getL2Policies() {
+
+        return new ArrayList<>(l2PolicyStore
+                .values()
+                .stream()
+                .map(Versioned::value)
+                .collect(Collectors.toList()));
+
+    }
+
+    /**
+     * Returns all L2 Tunnels.
+     *
+     * @return List of tunnels.
+     */
+    @Override
+    public List<L2Tunnel> getL2Tunnels() {
+
+        return new ArrayList<>(l2TunnelStore
+                .values()
+                .stream()
+                .map(Versioned::value)
+                .collect(Collectors.toList()));
+
+    }
+
+    @Override
+    public void processLinkDown(Link link) {
+
+        List<L2Tunnel> tunnels = getL2Tunnels();
+        List<L2TunnelPolicy> policies = getL2Policies();
+
+        // determine affected pseudowires and update them at once
+        Set<L2TunnelDescription> pwToUpdate = tunnels
+                .stream()
+                .filter(tun -> tun.pathUsed().contains(link))
+                .map(l2Tunnel -> {
+                        L2TunnelPolicy policy = null;
+                        for (L2TunnelPolicy l2Policy : policies) {
+                            if (l2Policy.tunnelId() == l2Tunnel.tunnelId()) {
+                                policy = l2Policy;
+                                break;
+                            }
+                        }
+
+                        return new DefaultL2TunnelDescription(l2Tunnel, policy);
+                })
+                .collect(Collectors.toSet());
+
+
+        log.info("Pseudowires affected by link failure : {}, rerouting them...", pwToUpdate);
+
+        // update all pseudowires
+        pwToUpdate.forEach(tun -> updatePw(tun, tun));
+    }
+
+    @Override
+    public void processPwaasConfigAdded(NetworkConfigEvent event) {
+        checkArgument(event.config().isPresent(),
+                "Config is not presented in PwaasConfigAdded event {}", event);
+
+        log.info("Network event : Pseudowire configuration added!");
+        PwaasConfig config = (PwaasConfig) event.config().get();
+
+        // gather pseudowires
+        Set<L2TunnelDescription> pwToAdd = config
+                .getPwIds()
+                .stream()
+                .map(config::getPwDescription)
+                .collect(Collectors.toSet());
+
+        // deploy pseudowires
+        deploy(pwToAdd);
+    }
+
+    /**
+     * Returns the new vlan id for an ingress point of a
+     * pseudowire. For double tagged, it is the outer,
+     * For single tagged it is the single tag, and for
+     * inner it is None.
+     *
+     * @param ingressOuter vlanid of ingress outer
+     * @param ingressInner vlanid of ingress inner
+     * @param egressOuter  vlanid of egress outer
+     * @param egressInner  vlanid of egress inner
+     * @return returns the vlan id which will be installed at vlan table 1.
+     */
+    private VlanId determineEgressVlan(VlanId ingressOuter, VlanId ingressInner,
+                                      VlanId egressOuter, VlanId egressInner) {
+
+        // validity of vlan combinations was checked at verifyPseudowire
+        if (!(ingressOuter.equals(VlanId.NONE))) {
+            return egressOuter;
+        } else if (!(ingressInner.equals(VlanId.NONE))) {
+            return egressInner;
+        } else {
+            return VlanId.vlanId("None");
+        }
+    }
+
+    /**
+     * Determines vlan used for transporting the pw traffic.
+     *
+     * Leaf-Leaf traffic is transferred untagged, thus we choose the UNTAGGED_TRANSPORT_VLAN
+     * and also make sure to add the popVlan instruction.
+     * For spine-leaf pws we choose the highest vlan value available from a certain range.
+     *
+     * @param spinePw if the pw is leaf-spine.
+     * @return The vlan id chossen to transport this pseudowire. If vlan is UNTAGGED_TRANSPORT_VLAN
+     *         then the pw is transported untagged.
+     */
+    private VlanId determineTransportVlan(boolean spinePw) {
+
+        if (!spinePw) {
+
+            log.info("Untagged transport with internal vlan {} for pseudowire!", UNTAGGED_TRANSPORT_VLAN);
+            return UNTAGGED_TRANSPORT_VLAN;
+        } else {
+            for (short i = transportVlanUpper; i > transportVlanLower; i--) {
+
+                VlanId vlanToUse = VlanId.vlanId((short) i);
+                if (!vlanStore.contains(vlanToUse)) {
+
+                    vlanStore.add(vlanToUse);
+                    log.info("Transport vlan {} for pseudowire!", vlanToUse);
+                    return vlanToUse;
+                }
+            }
+
+            log.info("No available transport vlan found, pseudowire traffic will be carried untagged " +
+                             "with internal vlan {}!", UNTAGGED_TRANSPORT_VLAN);
+            return UNTAGGED_TRANSPORT_VLAN;
+        }
+    }
+
+    /**
+     * Adds a single pseudowire from leaf to a leaf.
+     * This method can be called from cli commands
+     * without configuration updates, thus it does not check for mastership
+     * of the ingress pseudowire device.
+     *
+     * @param pw The pseudowire
+     * @param spinePw True if pseudowire is from leaf to spine
+     * @return result of pseudowire deployment
+     */
+    private Result deployPseudowire(L2TunnelDescription pw, boolean spinePw) {
+
+        Result result;
+        long l2TunnelId;
+
+        l2TunnelId = pw.l2Tunnel().tunnelId();
+
+        // The tunnel id cannot be 0.
+        if (l2TunnelId == 0) {
+            log.warn("Tunnel id id must be > 0");
+            return Result.ADDITION_ERROR;
+        }
+
+        // get path here, need to use the same for fwd and rev direction
+        List<Link> path = getPath(pw.l2TunnelPolicy().cP1(),
+                                  pw.l2TunnelPolicy().cP2());
+        if (path == null) {
+            log.info("Deploying process : No path between the connection points for pseudowire {}", l2TunnelId);
+            return WRONG_PARAMETERS;
+        }
+
+        Link fwdNextHop;
+        Link revNextHop;
+        if (!spinePw) {
+            if (path.size() != 2) {
+                log.info("Deploying process : Path between two leafs should have size of 2, for pseudowire {}",
+                         l2TunnelId);
+                return INTERNAL_ERROR;
+            }
+
+            fwdNextHop = path.get(0);
+            revNextHop = reverseLink(path.get(1));
+        } else {
+            if (path.size() != 1) {
+                log.info("Deploying process : Path between leaf spine should equal to 1, for pseudowire {}",
+                         l2TunnelId);
+                return INTERNAL_ERROR;
+            }
+
+            fwdNextHop = path.get(0);
+            revNextHop = reverseLink(path.get(0));
+        }
+
+        pw.l2Tunnel().setPath(path);
+        pw.l2Tunnel().setTransportVlan(determineTransportVlan(spinePw));
+
+        // next hops for next objectives
+
+        log.info("Deploying process : Establishing forward direction for pseudowire {}", l2TunnelId);
+
+        VlanId egressVlan = determineEgressVlan(pw.l2TunnelPolicy().cP1OuterTag(),
+                                                pw.l2TunnelPolicy().cP1InnerTag(),
+                                                pw.l2TunnelPolicy().cP2OuterTag(),
+                                                pw.l2TunnelPolicy().cP2InnerTag());
+        // We establish the tunnel.
+        // result.nextId will be used in fwd
+        result = deployPseudoWireInit(pw.l2Tunnel(),
+                                      pw.l2TunnelPolicy().cP1(),
+                                      pw.l2TunnelPolicy().cP2(),
+                                      FWD,
+                                      fwdNextHop,
+                                      spinePw,
+                                      egressVlan);
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying pseudowire initiation for CP1");
+            return Result.ADDITION_ERROR;
+        }
+
+        // We create the policy.
+        result = deployPolicy(l2TunnelId,
+                              pw.l2TunnelPolicy().cP1(),
+                              pw.l2TunnelPolicy().cP1InnerTag(),
+                              pw.l2TunnelPolicy().cP1OuterTag(),
+                              egressVlan,
+                              result.nextId);
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying pseudowire policy for CP1");
+            return Result.ADDITION_ERROR;
+        }
+
+        // We terminate the tunnel
+        result = deployPseudoWireTerm(pw.l2Tunnel(),
+                                       pw.l2TunnelPolicy().cP2(),
+                                       egressVlan,
+                                       FWD,
+                                      spinePw);
+
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying pseudowire termination for CP1");
+            return Result.ADDITION_ERROR;
+
+        }
+
+        log.info("Deploying process : Establishing reverse direction for pseudowire {}", l2TunnelId);
+
+        egressVlan = determineEgressVlan(pw.l2TunnelPolicy().cP2OuterTag(),
+                                         pw.l2TunnelPolicy().cP2InnerTag(),
+                                         pw.l2TunnelPolicy().cP1OuterTag(),
+                                         pw.l2TunnelPolicy().cP1InnerTag());
+
+        // We establish the reverse tunnel.
+        result = deployPseudoWireInit(pw.l2Tunnel(),
+                                       pw.l2TunnelPolicy().cP2(),
+                                       pw.l2TunnelPolicy().cP1(),
+                                       REV,
+                                       revNextHop,
+                                       spinePw,
+                                       egressVlan);
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying pseudowire initiation for CP2");
+            return Result.ADDITION_ERROR;
+        }
+
+
+        result = deployPolicy(l2TunnelId,
+                               pw.l2TunnelPolicy().cP2(),
+                               pw.l2TunnelPolicy().cP2InnerTag(),
+                               pw.l2TunnelPolicy().cP2OuterTag(),
+                               egressVlan,
+                               result.nextId);
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying policy for CP2");
+            return Result.ADDITION_ERROR;
+        }
+
+        result = deployPseudoWireTerm(pw.l2Tunnel(),
+                                       pw.l2TunnelPolicy().cP1(),
+                                       egressVlan,
+                                       REV,
+                                      spinePw);
+
+        if (result != SUCCESS) {
+            log.info("Deploying process : Error in deploying pseudowire termination for CP2");
+            return Result.ADDITION_ERROR;
+        }
+
+        log.info("Deploying process : Updating relevant information for pseudowire {}", l2TunnelId);
+
+        // Populate stores
+        l2TunnelStore.put(Long.toString(l2TunnelId), pw.l2Tunnel());
+        l2PolicyStore.put(Long.toString(l2TunnelId), pw.l2TunnelPolicy());
+
+        return Result.SUCCESS;
+    }
+
+    /**
+     * To deploy a number of pseudo wires.
+     * <p>
+     * Called ONLY when configuration changes, thus the check
+     * for the mastership of the device.
+     * <p>
+     * Only the master of CP1 will deploy this pseudowire.
+     *
+     * @param pwToAdd the set of pseudo wires to add
+     */
+    private void deploy(Set<L2TunnelDescription> pwToAdd) {
+
+        Result result;
+
+        for (L2TunnelDescription currentL2Tunnel : pwToAdd) {
+            ConnectPoint cp1 = currentL2Tunnel.l2TunnelPolicy().cP1();
+            ConnectPoint cp2 = currentL2Tunnel.l2TunnelPolicy().cP2();
+            long tunnelId = currentL2Tunnel.l2TunnelPolicy().tunnelId();
+
+            // only the master of CP1 will program this pseudowire
+            if (!srManager.isMasterOf(cp1)) {
+                log.debug("Not the master of {}. Ignore pseudo wire deployment id={}", cp1, tunnelId);
+                continue;
+            }
+
+            try {
+                // differentiate between leaf-leaf pseudowires and leaf-spine
+                // and pass the appropriate flag in them.
+                if (!srManager.deviceConfiguration().isEdgeDevice(cp1.deviceId()) &&
+                    !srManager.deviceConfiguration().isEdgeDevice(cp2.deviceId())) {
+                    log.warn("Can not deploy pseudowire from spine to spine!");
+                    result = Result.INTERNAL_ERROR;
+                } else if (srManager.deviceConfiguration().isEdgeDevice(cp1.deviceId()) &&
+                     srManager.deviceConfiguration().isEdgeDevice(cp2.deviceId())) {
+                    log.info("Deploying a leaf-leaf pseudowire {}", tunnelId);
+                    result = deployPseudowire(currentL2Tunnel, false);
+                } else {
+                    log.info("Deploying a leaf-spine pseudowire {}", tunnelId);
+                    result = deployPseudowire(currentL2Tunnel, true);
+                }
+            } catch (DeviceConfigNotFoundException e) {
+                log.error("Exception caught when deploying pseudowire", e.toString());
+                result = Result.INTERNAL_ERROR;
+            }
+
+            switch (result) {
+                case INTERNAL_ERROR:
+                    log.warn("Could not deploy pseudowire {}, internal error!", tunnelId);
+                    break;
+                case WRONG_PARAMETERS:
+                    log.warn("Could not deploy pseudowire {}, wrong parameters!", tunnelId);
+                    break;
+                case ADDITION_ERROR:
+                    log.warn("Could not deploy pseudowire {}, error in populating rules!", tunnelId);
+                    break;
+                default:
+                    log.info("Pseudowire with {} succesfully deployed!", tunnelId);
+                    break;
+            }
+        }
+    }
+
+
+    @Override
+    public void processPwaasConfigUpdated(NetworkConfigEvent event) {
+        checkArgument(event.config().isPresent(),
+                "Config is not presented in PwaasConfigUpdated event {}", event);
+        checkArgument(event.prevConfig().isPresent(),
+                "PrevConfig is not presented in PwaasConfigUpdated event {}", event);
+
+        log.info("Pseudowire configuration updated.");
+
+        // We retrieve the old pseudo wires.
+        PwaasConfig prevConfig = (PwaasConfig) event.prevConfig().get();
+        Set<Long> prevPws = prevConfig.getPwIds();
+
+        // We retrieve the new pseudo wires.
+        PwaasConfig config = (PwaasConfig) event.config().get();
+        Set<Long> newPws = config.getPwIds();
+
+        // We compute the pseudo wires to update.
+        Set<Long> updPws = newPws.stream()
+                .filter(tunnelId -> prevPws.contains(tunnelId)
+                        && !config.getPwDescription(tunnelId).equals(prevConfig.getPwDescription(tunnelId)))
+                .collect(Collectors.toSet());
+
+        // The pseudo wires to remove.
+        Set<Long> rmvPWs = prevPws.stream()
+                .filter(tunnelId -> !newPws.contains(tunnelId)).collect(Collectors.toSet());
+
+        Set<L2TunnelDescription> pwToRemove = rmvPWs.stream()
+                .map(prevConfig::getPwDescription)
+                .collect(Collectors.toSet());
+        tearDown(pwToRemove);
+
+        // The pseudo wires to add.
+        Set<Long> addedPWs = newPws.stream()
+                .filter(tunnelId -> !prevPws.contains(tunnelId))
+                .collect(Collectors.toSet());
+        Set<L2TunnelDescription> pwToAdd = addedPWs.stream()
+                .map(config::getPwDescription)
+                .collect(Collectors.toSet());
+        deploy(pwToAdd);
+
+
+        // The pseudo wires to update.
+        updPws.forEach(tunnelId -> updatePw(prevConfig.getPwDescription(tunnelId),
+                                            config.getPwDescription(tunnelId)));
+
+        log.info("Pseudowires removed : {}, Pseudowires updated : {}, Pseudowires added : {}", rmvPWs,
+                 updPws, addedPWs);
+    }
+
+    /**
+     * Helper function to update a pw.
+     * <p>
+     * Called upon configuration changes that update existing pseudowires and
+     * when links fail. Checking of mastership for CP1 is mandatory because it is
+     * called in multiple instances for both cases.
+     * <p>
+     * Meant to call asynchronously for various events, thus this call can not block and need
+     * to perform asynchronous operations.
+     * <p>
+     * For this reason error checking is omitted.
+     *
+     * @param oldPw the pseudo wire to remove
+     * @param newPw the pseudo wire to add
+     */
+    private void updatePw(L2TunnelDescription oldPw,
+                         L2TunnelDescription newPw) {
+        ConnectPoint oldCp1 = oldPw.l2TunnelPolicy().cP1();
+        long tunnelId = oldPw.l2Tunnel().tunnelId();
+
+        // only the master of CP1 will update this pseudowire
+        if (!srManager.isMasterOf(oldPw.l2TunnelPolicy().cP1())) {
+            log.debug("Not the master of {}. Ignore pseudo wire update id={}", oldCp1, tunnelId);
+            return;
+        }
+        // only determine if the new pseudowire is leaf-spine, because
+        // removal process is the same for both leaf-leaf and leaf-spine pws
+        boolean newPwSpine;
+        try {
+            newPwSpine = !srManager.deviceConfiguration().isEdgeDevice(newPw.l2TunnelPolicy().cP1().deviceId()) ||
+                    !srManager.deviceConfiguration().isEdgeDevice(newPw.l2TunnelPolicy().cP2().deviceId());
+        } catch (DeviceConfigNotFoundException e) {
+            // if exception is caught treat the new pw as leaf-leaf
+            newPwSpine = false;
+        }
+
+        // copy the variable here because we need to use it in lambda thus it needs to be final
+        boolean finalNewPwSpine = newPwSpine;
+
+        log.info("Updating pseudowire {}", oldPw.l2Tunnel().tunnelId());
+
+        // The async tasks to orchestrate the next and forwarding update
+        CompletableFuture<ObjectiveError> fwdInitNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> revInitNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> fwdTermNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> revTermNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> fwdPwFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> revPwFuture = new CompletableFuture<>();
+
+        // first delete all information from our stores, we can not do it asynchronously
+        l2PolicyStore.remove(Long.toString(tunnelId));
+
+        // grab the old l2 tunnel from the store, since it carries information which is not exposed
+        // to the user configuration and set it to oldPw.
+        oldPw.setL2Tunnel(l2TunnelStore.get(Long.toString(tunnelId)).value());
+        VlanId transportVlan = l2TunnelStore.get(Long.toString(tunnelId)).value().transportVlan();
+        l2TunnelStore.remove(Long.toString(tunnelId));
+
+        // remove the reserved transport vlan, if one is used
+        if (!transportVlan.equals(UNTAGGED_TRANSPORT_VLAN)) {
+            vlanStore.remove(transportVlan);
+        }
+
+        // First we remove both policy.
+        log.debug("Start deleting fwd policy for {}", tunnelId);
+        VlanId egressVlan = determineEgressVlan(oldPw.l2TunnelPolicy().cP1OuterTag(),
+                                                 oldPw.l2TunnelPolicy().cP1InnerTag(),
+                                                 oldPw.l2TunnelPolicy().cP2OuterTag(),
+                                                 oldPw.l2TunnelPolicy().cP2InnerTag());
+        deletePolicy(tunnelId, oldPw.l2TunnelPolicy().cP1(),
+                      oldPw.l2TunnelPolicy().cP1InnerTag(),
+                      oldPw.l2TunnelPolicy().cP1OuterTag(),
+                      egressVlan,
+                      fwdInitNextFuture,
+                      FWD);
+
+        deletePolicy(tunnelId, oldPw.l2TunnelPolicy().cP2(),
+                      oldPw.l2TunnelPolicy().cP2InnerTag(),
+                      oldPw.l2TunnelPolicy().cP2OuterTag(),
+                      egressVlan, revInitNextFuture,
+                      REV);
+
+        // Finally we remove both the tunnels.
+        fwdInitNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                log.debug("Update process : Fwd policy removed. " +
+                                  "Now remove fwd {} for {}", INITIATION, tunnelId);
+                tearDownPseudoWireInit(tunnelId, oldPw.l2TunnelPolicy().cP1(), fwdTermNextFuture, FWD);
+            }
+        });
+        revInitNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                log.debug("Update process : Rev policy removed. " +
+                                  "Now remove rev {} for {}", INITIATION, tunnelId);
+                tearDownPseudoWireInit(tunnelId, oldPw.l2TunnelPolicy().cP2(), revTermNextFuture, REV);
+            }
+        });
+        fwdTermNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                log.debug("Update process : Fwd {} removed. " +
+                                  "Now remove fwd {} for {}", INITIATION, TERMINATION, tunnelId);
+                tearDownPseudoWireTerm(oldPw.l2Tunnel(), oldPw.l2TunnelPolicy().cP2(),  fwdPwFuture, FWD);
+            }
+        });
+        revTermNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                log.debug("Update process : Rev {} removed. " +
+                                  "Now remove rev {} for {}", INITIATION, TERMINATION, tunnelId);
+                tearDownPseudoWireTerm(oldPw.l2Tunnel(), oldPw.l2TunnelPolicy().cP1(), revPwFuture, REV);
+            }
+        });
+
+        // get path here, need to use the same for fwd and rev direction
+        List<Link> path = getPath(newPw.l2TunnelPolicy().cP1(),
+                                   newPw.l2TunnelPolicy().cP2());
+        if (path == null) {
+            log.error("Update process : " +
+                             "No path between the connection points for pseudowire {}", newPw.l2Tunnel().tunnelId());
+            return;
+        }
+
+        Link fwdNextHop, revNextHop;
+        if (!finalNewPwSpine) {
+            if (path.size() != 2) {
+                log.error("Update process : Error, path between two leafs should have size of 2, for pseudowire {}",
+                         newPw.l2Tunnel().tunnelId());
+                return;
+            }
+            fwdNextHop = path.get(0);
+            revNextHop = reverseLink(path.get(1));
+        } else {
+            if (path.size() != 1) {
+                log.error("Update process : Error, path between leaf spine should equal to 1, for pseudowire {}",
+                         newPw.l2Tunnel().tunnelId());
+                return;
+            }
+            fwdNextHop = path.get(0);
+            revNextHop = reverseLink(path.get(0));
+        }
+
+        // set new path and transport vlan.
+        newPw.l2Tunnel().setPath(path);
+        newPw.l2Tunnel().setTransportVlan(determineTransportVlan(newPwSpine));
+
+        // At the end we install the updated PW.
+        fwdPwFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+
+                // Upgrade stores and book keeping information, need to move this here
+                // cause this call is asynchronous.
+                l2PolicyStore.put(Long.toString(tunnelId), newPw.l2TunnelPolicy());
+                l2TunnelStore.put(Long.toString(tunnelId), newPw.l2Tunnel());
+
+                VlanId egressVlanId = determineEgressVlan(newPw.l2TunnelPolicy().cP1OuterTag(),
+                                                          newPw.l2TunnelPolicy().cP1InnerTag(),
+                                                          newPw.l2TunnelPolicy().cP2OuterTag(),
+                                                          newPw.l2TunnelPolicy().cP2InnerTag());
+
+                log.debug("Update process : Deploying new fwd pw for {}", tunnelId);
+                Result lamdaResult = deployPseudoWireInit(newPw.l2Tunnel(), newPw.l2TunnelPolicy().cP1(),
+                                                           newPw.l2TunnelPolicy().cP2(), FWD,
+                                                           fwdNextHop, finalNewPwSpine, egressVlanId);
+                if (lamdaResult != SUCCESS) {
+                    return;
+                }
+
+                lamdaResult = deployPolicy(tunnelId, newPw.l2TunnelPolicy().cP1(),
+                                            newPw.l2TunnelPolicy().cP1InnerTag(),
+                                           newPw.l2TunnelPolicy().cP1OuterTag(),
+                                            egressVlanId, lamdaResult.nextId);
+                if (lamdaResult != SUCCESS) {
+                    return;
+                }
+                deployPseudoWireTerm(newPw.l2Tunnel(), newPw.l2TunnelPolicy().cP2(),
+                                      egressVlanId, FWD, finalNewPwSpine);
+
+            }
+        });
+        revPwFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+
+                log.debug("Update process : Deploying new rev pw for {}", tunnelId);
+
+                VlanId egressVlanId = determineEgressVlan(newPw.l2TunnelPolicy().cP2OuterTag(),
+                                                          newPw.l2TunnelPolicy().cP2InnerTag(),
+                                                          newPw.l2TunnelPolicy().cP1OuterTag(),
+                                                          newPw.l2TunnelPolicy().cP1InnerTag());
+
+                Result lamdaResult = deployPseudoWireInit(newPw.l2Tunnel(),
+                                                           newPw.l2TunnelPolicy().cP2(),
+                                                           newPw.l2TunnelPolicy().cP1(),
+                                                           REV,
+                                                           revNextHop, finalNewPwSpine, egressVlanId);
+                if (lamdaResult != SUCCESS) {
+                    return;
+                }
+
+                lamdaResult = deployPolicy(tunnelId,
+                                            newPw.l2TunnelPolicy().cP2(),
+                                            newPw.l2TunnelPolicy().cP2InnerTag(),
+                                            newPw.l2TunnelPolicy().cP2OuterTag(),
+                                            egressVlanId,
+                                            lamdaResult.nextId);
+                if (lamdaResult != SUCCESS) {
+                    return;
+                }
+                deployPseudoWireTerm(newPw.l2Tunnel(),
+                                      newPw.l2TunnelPolicy().cP1(),
+                                      egressVlanId,
+                                      REV, finalNewPwSpine);
+            }
+        });
+    }
+
+    @Override
+    public void processPwaasConfigRemoved(NetworkConfigEvent event) {
+        checkArgument(event.prevConfig().isPresent(),
+                "PrevConfig is not presented in PwaasConfigRemoved event {}", event);
+
+        log.info("Network event : Pseudowire configuration removed!");
+        PwaasConfig config = (PwaasConfig) event.prevConfig().get();
+
+        Set<L2TunnelDescription> pwToRemove = config
+                .getPwIds()
+                .stream()
+                .map(config::getPwDescription)
+                .collect(Collectors.toSet());
+
+        // We teardown all the pseudo wire deployed
+        tearDown(pwToRemove);
+    }
+
+    /**
+     * Helper function for removing a single pseudowire.
+     * <p>
+     * No mastership of CP1 is checked, because it can be called from
+     * the CLI for removal of pseudowires.
+     *
+     * @param l2TunnelId the id of the pseudowire to tear down
+     * @return Returns SUCCESS if no error is obeserved or an appropriate
+     * error on a failure
+     */
+    private Result tearDownPseudowire(long l2TunnelId) {
+
+        CompletableFuture<ObjectiveError> fwdInitNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> fwdTermNextFuture = new CompletableFuture<>();
+
+        CompletableFuture<ObjectiveError> revInitNextFuture = new CompletableFuture<>();
+        CompletableFuture<ObjectiveError> revTermNextFuture = new CompletableFuture<>();
+
+        if (l2TunnelId == 0) {
+            log.warn("Removal process : Tunnel id cannot be 0");
+            return Result.WRONG_PARAMETERS;
+        }
+
+        // check existence of tunnels/policy in the store, if one is missing abort!
+        Versioned<L2Tunnel> l2TunnelVersioned = l2TunnelStore.get(Long.toString(l2TunnelId));
+        Versioned<L2TunnelPolicy> l2TunnelPolicyVersioned = l2PolicyStore.get(Long.toString(l2TunnelId));
+        if ((l2TunnelVersioned == null) || (l2TunnelPolicyVersioned == null)) {
+            log.warn("Removal process : Policy and/or tunnel missing for tunnel id {}", l2TunnelId);
+            return Result.REMOVAL_ERROR;
+        }
+
+        L2TunnelDescription pwToRemove = new DefaultL2TunnelDescription(l2TunnelVersioned.value(),
+                                                                               l2TunnelPolicyVersioned.value());
+
+        // remove the tunnels and the policies from the store
+        l2PolicyStore.remove(Long.toString(l2TunnelId));
+        l2TunnelStore.remove(Long.toString(l2TunnelId));
+
+        // remove the reserved transport vlan
+        if (!pwToRemove.l2Tunnel().transportVlan().equals(UNTAGGED_TRANSPORT_VLAN)) {
+            vlanStore.remove(pwToRemove.l2Tunnel().transportVlan());
+        }
+
+        log.info("Removal process : Tearing down forward direction of pseudowire {}", l2TunnelId);
+
+        VlanId egressVlan = determineEgressVlan(pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+                                                 pwToRemove.l2TunnelPolicy().cP1InnerTag(),
+                                                 pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+                                                 pwToRemove.l2TunnelPolicy().cP2InnerTag());
+        deletePolicy(l2TunnelId,
+                      pwToRemove.l2TunnelPolicy().cP1(),
+                      pwToRemove.l2TunnelPolicy().cP1InnerTag(),
+                      pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+                      egressVlan,
+                      fwdInitNextFuture,
+                      FWD);
+
+        fwdInitNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                // Finally we will tear down the pseudo wire.
+                tearDownPseudoWireInit(l2TunnelId,
+                                        pwToRemove.l2TunnelPolicy().cP1(),
+                                        fwdTermNextFuture,
+                                        FWD);
+            }
+        });
+
+        fwdTermNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                tearDownPseudoWireTerm(pwToRemove.l2Tunnel(),
+                                        pwToRemove.l2TunnelPolicy().cP2(),
+                                        null,
+                                        FWD);
+            }
+        });
+
+        log.info("Removal process : Tearing down reverse direction of pseudowire {}", l2TunnelId);
+
+        egressVlan = determineEgressVlan(pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+                                          pwToRemove.l2TunnelPolicy().cP2InnerTag(),
+                                          pwToRemove.l2TunnelPolicy().cP1OuterTag(),
+                                          pwToRemove.l2TunnelPolicy().cP1InnerTag());
+
+        // We do the same operations on the reverse side.
+        deletePolicy(l2TunnelId,
+                      pwToRemove.l2TunnelPolicy().cP2(),
+                      pwToRemove.l2TunnelPolicy().cP2InnerTag(),
+                      pwToRemove.l2TunnelPolicy().cP2OuterTag(),
+                      egressVlan,
+                      revInitNextFuture,
+                      REV);
+
+        revInitNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                tearDownPseudoWireInit(l2TunnelId,
+                                        pwToRemove.l2TunnelPolicy().cP2(),
+                                        revTermNextFuture,
+                                        REV);
+            }
+        });
+
+        revTermNextFuture.thenAcceptAsync(status -> {
+            if (status == null) {
+                tearDownPseudoWireTerm(pwToRemove.l2Tunnel(),
+                                        pwToRemove.l2TunnelPolicy().cP1(),
+                                        null,
+                                        REV);
+            }
+        });
+
+        return Result.SUCCESS;
+    }
+
+    @Override
+    public void tearDown(Set<L2TunnelDescription> pwToRemove) {
+
+        Result result;
+
+        // We remove all the pw in the configuration file.
+        for (L2TunnelDescription currentL2Tunnel : pwToRemove) {
+            ConnectPoint cp1 = currentL2Tunnel.l2TunnelPolicy().cP1();
+            ConnectPoint cp2 = currentL2Tunnel.l2TunnelPolicy().cP2();
+            long tunnelId = currentL2Tunnel.l2TunnelPolicy().tunnelId();
+
+            // only the master of CP1 will program this pseudowire
+            if (!srManager.isMasterOf(cp1)) {
+                log.debug("Not the master of {}. Ignore pseudo wire removal id={}", cp1, tunnelId);
+                continue;
+            }
+
+            // no need to differentiate here between leaf-leaf and leaf-spine, because
+            // the only change is in the groups, which we do not remove either way
+            log.info("Removing pseudowire {}", tunnelId);
+
+            result = tearDownPseudowire(tunnelId);
+            switch (result) {
+                case WRONG_PARAMETERS:
+                    log.warn("Error in supplied parameters for the pseudowire removal with tunnel id {}!",
+                            tunnelId);
+                    break;
+                case REMOVAL_ERROR:
+                    log.warn("Error in pseudowire removal with tunnel id {}!", tunnelId);
+                    break;
+                default:
+                    log.warn("Pseudowire with tunnel id {} was removed successfully", tunnelId);
+            }
+        }
+    }
+
+    /**
+     * Handles the policy establishment which consists in
+     * create the filtering and forwarding objectives related
+     * to the initiation and termination.
+     *
+     * @param tunnelId     the tunnel id
+     * @param ingress      the ingress point
+     * @param ingressInner the ingress inner tag
+     * @param ingressOuter the ingress outer tag
+     * @param nextId       the next objective id
+     * @param egressVlan   Vlan-id to set, depends on ingress vlan
+     *                     combinations. For example, if pw is double tagged
+     *                     then this is the value of the outer vlan, if single
+     *                     tagged then it is the new value of the single tag.
+     *                     Should be None for untagged traffic.
+     * @return the result of the operation
+     */
+    private Result deployPolicy(long tunnelId, ConnectPoint ingress, VlanId ingressInner,
+                                VlanId ingressOuter, VlanId egressVlan, int nextId) {
+
+        List<Objective> objectives = Lists.newArrayList();
+        // We create the forwarding objective for supporting
+        // the l2 tunnel.
+        ForwardingObjective.Builder fwdBuilder = createInitFwdObjective(tunnelId, ingress.port(), nextId);
+        // We create and add objective context.
+        ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+                                                                log.debug("FwdObj for tunnel {} populated", tunnelId),
+                                                               (objective, error) ->
+                                                                log.warn("Failed to populate fwdrObj " +
+                                                                                 "for tunnel {}", tunnelId, error));
+        objectives.add(fwdBuilder.add(context));
+
+        // We create the filtering objective to define the
+        // permit traffic in the switch
+        FilteringObjective.Builder filtBuilder = createFiltObjective(ingress.port(), ingressInner, ingressOuter);
+
+        // We add the metadata.
+        TrafficTreatment.Builder treatment = DefaultTrafficTreatment
+                .builder()
+                .setTunnelId(tunnelId)
+                .setVlanId(egressVlan);
+        filtBuilder.withMeta(treatment.build());
+
+        // We create and add objective context.
+        context = new DefaultObjectiveContext((objective) -> log.debug("FilterObj for tunnel {} populated", tunnelId),
+                                              (objective, error) -> log.warn("Failed to populate filterObj for " +
+                                                                                     "tunnel {}", tunnelId, error));
+        objectives.add(filtBuilder.add(context));
+
+        for (Objective objective : objectives) {
+            if (objective instanceof ForwardingObjective) {
+                srManager.flowObjectiveService.forward(ingress.deviceId(), (ForwardingObjective) objective);
+                log.debug("Creating new FwdObj for initiation NextObj with id={} for tunnel {}", nextId, tunnelId);
+            } else {
+                srManager.flowObjectiveService.filter(ingress.deviceId(), (FilteringObjective) objective);
+                log.debug("Creating new FiltObj for tunnel {}", tunnelId);
+            }
+        }
+        return SUCCESS;
+    }
+
+    /**
+     * Handles the tunnel establishment which consists in
+     * create the next objectives related to the initiation.
+     *
+     * @param l2Tunnel  the tunnel to deploy
+     * @param ingress   the ingress connect point
+     * @param egress    the egress connect point
+     * @param direction the direction of the pw
+     * @param spinePw if the pseudowire involves a spine switch
+     * @return the result of the operation
+     */
+    private Result deployPseudoWireInit(L2Tunnel l2Tunnel, ConnectPoint ingress,
+                                        ConnectPoint egress, Direction direction,
+                                        Link nextHop, boolean spinePw, VlanId termVlanId) {
+
+        if (nextHop == null) {
+            log.warn("No path between ingress and egress cps for tunnel {}", l2Tunnel.tunnelId());
+            return WRONG_PARAMETERS;
+        }
+
+        // We create the next objective without the metadata
+        // context and id. We check if it already exists in the
+        // store. If not we store as it is in the store.
+        NextObjective.Builder nextObjectiveBuilder = createNextObjective(INITIATION,
+                                                                         nextHop.src(),
+                                                                         nextHop.dst(),
+                                                                         l2Tunnel,
+                                                                         egress.deviceId(),
+                                                                         spinePw,
+                                                                         termVlanId);
+
+        if (nextObjectiveBuilder == null) {
+            return INTERNAL_ERROR;
+        }
+        // We set the metadata. We will use this metadata
+        // to inform the driver we are doing a l2 tunnel.
+        TrafficSelector metadata = DefaultTrafficSelector
+                .builder()
+                .matchTunnelId(l2Tunnel.tunnelId())
+                .build();
+        nextObjectiveBuilder.withMeta(metadata);
+        int nextId = srManager.flowObjectiveService.allocateNextId();
+        if (nextId < 0) {
+            log.warn("Not able to allocate a next id for initiation");
+            return INTERNAL_ERROR;
+        }
+        nextObjectiveBuilder.withId(nextId);
+        String key = generateKey(l2Tunnel.tunnelId(), direction);
+        l2InitiationNextObjStore.put(key, nextObjectiveBuilder.add());
+        ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+                                                                 log.debug("Initiation l2 tunnel rule " +
+                                                                                   "for {} populated",
+                                                                           l2Tunnel.tunnelId()),
+                                                               (objective, error) ->
+                                                                       log.warn("Failed to populate Initiation " +
+                                                                                        "l2 tunnel rule for {}: {}",
+                                                                                l2Tunnel.tunnelId(), error));
+        NextObjective nextObjective = nextObjectiveBuilder.add(context);
+        srManager.flowObjectiveService.next(ingress.deviceId(), nextObjective);
+        log.debug("Initiation next objective for {} not found. Creating new NextObj with id={}",
+                  l2Tunnel.tunnelId(), nextObjective.id());
+        Result result = SUCCESS;
+        result.nextId = nextObjective.id();
+        return result;
+    }
+
+    /**
+     * Handles the tunnel termination, which consists in the creation
+     * of a forwarding objective and a next objective.
+     *
+     * @param l2Tunnel   the tunnel to terminate
+     * @param egress     the egress point
+     * @param egressVlan the expected vlan at egress
+     * @param direction  the direction
+     * @param spinePw if the pseudowire involves a spine switch
+     * @return the result of the operation
+     */
+    private Result deployPseudoWireTerm(L2Tunnel l2Tunnel, ConnectPoint egress,
+                                        VlanId egressVlan, Direction direction, boolean spinePw) {
+
+        // We create the group relative to the termination.
+        NextObjective.Builder nextObjectiveBuilder = createNextObjective(TERMINATION, egress, null,
+                                                                         l2Tunnel, egress.deviceId(),
+                                                                         spinePw,
+                                                                         egressVlan);
+        if (nextObjectiveBuilder == null) {
+            return INTERNAL_ERROR;
+        }
+        TrafficSelector metadata = DefaultTrafficSelector
+                .builder()
+                .matchVlanId(egressVlan)
+                .build();
+        nextObjectiveBuilder.withMeta(metadata);
+        int nextId = srManager.flowObjectiveService.allocateNextId();
+        if (nextId < 0) {
+            log.warn("Not able to allocate a next id for initiation");
+            return INTERNAL_ERROR;
+        }
+        nextObjectiveBuilder.withId(nextId);
+        String key = generateKey(l2Tunnel.tunnelId(), direction);
+        l2TerminationNextObjStore.put(key, nextObjectiveBuilder.add());
+        ObjectiveContext context = new DefaultObjectiveContext((objective) -> log.debug("Termination l2 tunnel rule " +
+                                                                                        "for {} populated",
+                                                                                        l2Tunnel.tunnelId()),
+                                                               (objective, error) -> log.warn("Failed to populate " +
+                                                                                              "termination l2 tunnel " +
+                                                                                              "rule for {}: {}",
+                                                                                              l2Tunnel.tunnelId(),
+                                                                                              error));
+        NextObjective nextObjective = nextObjectiveBuilder.add(context);
+        srManager.flowObjectiveService.next(egress.deviceId(), nextObjective);
+        log.debug("Termination next objective for {} not found. Creating new NextObj with id={}",
+                  l2Tunnel.tunnelId(), nextObjective.id());
+
+        // We create the flow relative to the termination.
+        ForwardingObjective.Builder fwdBuilder = createTermFwdObjective(l2Tunnel.pwLabel(), l2Tunnel.tunnelId(),
+                                                                        egress.port(), nextObjective.id());
+        context = new DefaultObjectiveContext((objective) -> log.debug("FwdObj for tunnel termination {} populated",
+                                                                       l2Tunnel.tunnelId()),
+                                              (objective, error) -> log.warn("Failed to populate fwdrObj" +
+                                                                             " for tunnel termination {}",
+                                                                             l2Tunnel.tunnelId(), error));
+        srManager.flowObjectiveService.forward(egress.deviceId(), fwdBuilder.add(context));
+        log.debug("Creating new FwdObj for termination NextObj with id={} for tunnel {}",
+                  nextId, l2Tunnel.tunnelId());
+
+        if (spinePw) {
+
+            // determine the input port at the
+            PortNumber inPort;
+
+            if (egress.deviceId().
+                    equals(l2Tunnel.pathUsed().get(0).dst().deviceId())) {
+                    inPort = l2Tunnel.pathUsed().get(0).dst().port();
+            } else {
+                    inPort = l2Tunnel.pathUsed().get(0).src().port();
+            }
+
+            MacAddress dstMac;
+            try {
+                dstMac = srManager.deviceConfiguration().getDeviceMac(egress.deviceId());
+            } catch (Exception e) {
+                log.info("Device not found in configuration, no programming of MAC address");
+                dstMac = null;
+            }
+
+            log.info("Populating filtering objective for pseudowire transport" +
+                             " with vlan = {}, port = {}, mac = {}",
+                     l2Tunnel.transportVlan(),
+                     inPort,
+                     dstMac);
+            FilteringObjective.Builder filteringObjectiveBuilder =
+                    createNormalPipelineFiltObjective(inPort, l2Tunnel.transportVlan(), dstMac);
+            context = new DefaultObjectiveContext(( objective ) ->
+                                                          log.debug("Special filtObj for  " + "for {} populated",
+                                                                    l2Tunnel.tunnelId()),
+                                                  ( objective, error ) ->
+                                                          log.warn("Failed to populate " +
+                                                                           "special filtObj " +
+                                                                           "rule for {}: {}",
+                                                                                   l2Tunnel.tunnelId(), error));
+            TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
+            filteringObjectiveBuilder.withMeta(treatment.build());
+            srManager.flowObjectiveService.filter(egress.deviceId(), filteringObjectiveBuilder.add(context));
+            log.debug("Creating new special FiltObj for termination point with tunnel {} for port {}",
+                      l2Tunnel.tunnelId(),
+                      inPort);
+        }
+
+        return SUCCESS;
+    }
+
+
+    /**
+     * Creates the filtering objective according to a given port and vlanid.
+     *
+     * @param inPort   the in port
+     * @param vlanId the inner vlan tag
+     * @return the filtering objective
+     */
+    private FilteringObjective.Builder createNormalPipelineFiltObjective(PortNumber inPort,
+                                                                         VlanId vlanId,
+                                                                         MacAddress dstMac) {
+
+        log.info("Creating filtering objective for pseudowire transport with vlan={}, port={}, mac={}",
+                 vlanId,
+                 inPort,
+                 dstMac);
+        FilteringObjective.Builder fwdBuilder = DefaultFilteringObjective
+                .builder()
+                .withKey(Criteria.matchInPort(inPort))
+                .addCondition(Criteria.matchVlanId(vlanId))
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+                .permit()
+                .fromApp(srManager.appId());
+
+        if (dstMac != null) {
+            fwdBuilder.addCondition(Criteria.matchEthDst(dstMac));
+        }
+
+        return fwdBuilder;
+    }
+
+    /**
+     * Creates the filtering objective according to a given policy.
+     *
+     * @param inPort   the in port
+     * @param innerTag the inner vlan tag
+     * @param outerTag the outer vlan tag
+     * @return the filtering objective
+     */
+    private FilteringObjective.Builder createFiltObjective(PortNumber inPort, VlanId innerTag, VlanId outerTag) {
+
+        log.info("Creating filtering objective for vlans {} / {}", outerTag, innerTag);
+        return DefaultFilteringObjective
+                .builder()
+                .withKey(Criteria.matchInPort(inPort))
+                .addCondition(Criteria.matchInnerVlanId(innerTag))
+                .addCondition(Criteria.matchVlanId(outerTag))
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+                .permit()
+                .fromApp(srManager.appId());
+    }
+
+    /**
+     * Creates the forwarding objective for the termination.
+     *
+     * @param pwLabel    the pseudo wire label
+     * @param tunnelId   the tunnel id
+     * @param egressPort the egress port
+     * @param nextId     the next step
+     * @return the forwarding objective to support the termination
+     */
+    private ForwardingObjective.Builder createTermFwdObjective(MplsLabel pwLabel, long tunnelId,
+                                                               PortNumber egressPort, int nextId) {
+
+        TrafficSelector.Builder trafficSelector = DefaultTrafficSelector.builder();
+        TrafficTreatment.Builder trafficTreatment = DefaultTrafficTreatment.builder();
+        // The flow has to match on the pw label and bos
+        trafficSelector.matchEthType(Ethernet.MPLS_UNICAST);
+        trafficSelector.matchMplsLabel(pwLabel);
+        trafficSelector.matchMplsBos(true);
+        // The flow has to decrement ttl, restore ttl in
+        // pop mpls, set tunnel id and port.
+        trafficTreatment.decMplsTtl();
+        trafficTreatment.copyTtlIn();
+        trafficTreatment.popMpls();
+        trafficTreatment.setTunnelId(tunnelId);
+        trafficTreatment.setOutput(egressPort);
+
+        return DefaultForwardingObjective
+                .builder()
+                .fromApp(srManager.appId())
+                .makePermanent()
+                .nextStep(nextId)
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+                .withSelector(trafficSelector.build())
+                .withTreatment(trafficTreatment.build())
+                .withFlag(VERSATILE);
+    }
+
+    /**
+     * Creates the forwarding objective for the initiation.
+     *
+     * @param tunnelId the tunnel id
+     * @param inPort   the input port
+     * @param nextId   the next step
+     * @return the forwarding objective to support the initiation.
+     */
+    private ForwardingObjective.Builder createInitFwdObjective(long tunnelId, PortNumber inPort, int nextId) {
+
+        TrafficSelector.Builder trafficSelector = DefaultTrafficSelector.builder();
+
+        // The flow has to match on the mpls logical
+        // port and the tunnel id.
+        trafficSelector.matchTunnelId(tunnelId);
+        trafficSelector.matchInPort(inPort);
+
+        return DefaultForwardingObjective
+                .builder()
+                .fromApp(srManager.appId())
+                .makePermanent()
+                .nextStep(nextId)
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY)
+                .withSelector(trafficSelector.build())
+                .withFlag(VERSATILE);
+
+    }
+
+    /**
+     * Creates the next objective according to a given
+     * pipeline. We don't set the next id and we don't
+     * create the final meta to check if we are re-using
+     * the same next objective for different tunnels.
+     *
+     * @param pipeline the pipeline to support
+     * @param srcCp    the source port
+     * @param dstCp    the destination port
+     * @param l2Tunnel the tunnel to support
+     * @param egressId the egress device id
+     * @param spinePw if the pw involves a spine switch
+     * @return the next objective to support the pipeline
+     */
+    private NextObjective.Builder createNextObjective(Pipeline pipeline, ConnectPoint srcCp,
+                                                      ConnectPoint dstCp,  L2Tunnel l2Tunnel,
+                                                      DeviceId egressId, boolean spinePw, VlanId termVlanId) {
+        NextObjective.Builder nextObjBuilder;
+        TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment.builder();
+        if (pipeline == INITIATION) {
+            nextObjBuilder = DefaultNextObjective
+                    .builder()
+                    .withType(NextObjective.Type.SIMPLE)
+                    .fromApp(srManager.appId());
+            // The pw label is the bottom of stack. It has to
+            // be different -1.
+            if (l2Tunnel.pwLabel().toInt() == MplsLabel.MAX_MPLS) {
+                log.warn("Pw label not configured");
+                return null;
+            }
+            treatmentBuilder.pushMpls();
+            treatmentBuilder.setMpls(l2Tunnel.pwLabel());
+            treatmentBuilder.setMplsBos(true);
+            treatmentBuilder.copyTtlOut();
+
+            // If the inter-co label is present we have to set the label.
+            if (l2Tunnel.interCoLabel().toInt() != MplsLabel.MAX_MPLS) {
+                treatmentBuilder.pushMpls();
+                treatmentBuilder.setMpls(l2Tunnel.interCoLabel());
+                treatmentBuilder.setMplsBos(false);
+                treatmentBuilder.copyTtlOut();
+            }
+
+            // if pw is leaf-to-leaf we need to
+            // add the routing label also
+            if (!spinePw) {
+                // We retrieve the sr label from the config
+                // specific for pseudowire traffic
+                // using the egress leaf device id.
+                MplsLabel srLabel;
+                try {
+                    srLabel = MplsLabel.mplsLabel(srManager.deviceConfiguration().getPWRoutingLabel(egressId));
+
+                } catch (DeviceConfigNotFoundException e) {
+                    log.warn("Sr label for pw traffic not configured");
+                    return null;
+                }
+
+                treatmentBuilder.pushMpls();
+                treatmentBuilder.setMpls(srLabel);
+                treatmentBuilder.setMplsBos(false);
+                treatmentBuilder.copyTtlOut();
+            }
+
+            // We have to rewrite the src and dst mac address.
+            MacAddress ingressMac;
+            try {
+                ingressMac = srManager.deviceConfiguration().getDeviceMac(srcCp.deviceId());
+            } catch (DeviceConfigNotFoundException e) {
+                log.warn("Was not able to find the ingress mac");
+                return null;
+            }
+            treatmentBuilder.setEthSrc(ingressMac);
+            MacAddress neighborMac;
+            try {
+                neighborMac = srManager.deviceConfiguration().getDeviceMac(dstCp.deviceId());
+            } catch (DeviceConfigNotFoundException e) {
+                log.warn("Was not able to find the neighbor mac");
+                return null;
+            }
+            treatmentBuilder.setEthDst(neighborMac);
+
+            // if not a leaf-spine pw we need to POP the vlan at the output
+            // since we carry this traffic untagged.
+            if (!spinePw) {
+                treatmentBuilder.popVlan();
+            }
+
+            // set the appropriate transport vlan
+            treatmentBuilder.setVlanId(l2Tunnel.transportVlan());
+        } else {
+            // We create the next objective which
+            // will be a simple l2 group.
+            nextObjBuilder = DefaultNextObjective
+                    .builder()
+                    .withType(NextObjective.Type.SIMPLE)
+                    .fromApp(srManager.appId());
+
+            // for termination point we use the outer vlan of the
+            // encapsulated packet
+            treatmentBuilder.setVlanId(termVlanId);
+        }
+
+        treatmentBuilder.setOutput(srcCp.port());
+        nextObjBuilder.addTreatment(treatmentBuilder.build());
+        return nextObjBuilder;
+    }
+
+    /**
+     * Reverses a link.
+     *
+     * @param link link to be reversed
+     * @return the reversed link
+     */
+    private Link reverseLink(Link link) {
+
+        DefaultLink.Builder linkBuilder = DefaultLink.builder();
+
+        linkBuilder.src(link.dst());
+        linkBuilder.dst(link.src());
+        linkBuilder.type(link.type());
+        linkBuilder.providerId(link.providerId());
+
+        return linkBuilder.build();
+    }
+
+    /**
+     * Returns the path betwwen two connect points.
+     *
+     * @param srcCp source connect point
+     * @param dstCp destination connect point
+     * @return the path
+     */
+    private List<Link> getPath(ConnectPoint srcCp, ConnectPoint dstCp) {
+        /* TODO We retrieve a set of paths in case of a link failure, what happens
+         * if the TopologyService gets the link notification AFTER us and has not updated the paths?
+         *
+         * TODO This has the potential to act on old topology.
+         * Maybe we should make SRManager be a listener on topology events instead raw link events.
+         */
+        Set<Path> paths = srManager.topologyService.getPaths(
+                srManager.topologyService.currentTopology(),
+                srcCp.deviceId(), dstCp.deviceId());
+
+        log.debug("Paths obtained from topology service {}", paths);
+
+        // We randomly pick a path.
+        if (paths.isEmpty()) {
+            return null;
+        }
+        int size = paths.size();
+        int index = RandomUtils.nextInt(0, size);
+
+        List<Link> result = Iterables.get(paths, index).links();
+        log.debug("Randomly picked a path {}", result);
+
+        return result;
+    }
+
+    /**
+     * Deletes a given policy using the parameter supplied.
+     *
+     * @param tunnelId     the tunnel id
+     * @param ingress      the ingress point
+     * @param ingressInner the ingress inner vlan id
+     * @param ingressOuter the ingress outer vlan id
+     * @param future       to perform the async operation
+     * @param direction    the direction: forward or reverse
+     */
+    private void deletePolicy(long tunnelId, ConnectPoint ingress, VlanId ingressInner, VlanId ingressOuter,
+                              VlanId egressVlan, CompletableFuture<ObjectiveError> future, Direction direction) {
+
+        String key = generateKey(tunnelId, direction);
+        if (!l2InitiationNextObjStore.containsKey(key)) {
+            log.warn("Abort delete of policy for tunnel {}: next does not exist in the store", tunnelId);
+            if (future != null) {
+                future.complete(null);
+            }
+            return;
+        }
+        NextObjective nextObjective = l2InitiationNextObjStore.get(key).value();
+        int nextId = nextObjective.id();
+        List<Objective> objectives = Lists.newArrayList();
+        // We create the forwarding objective.
+        ForwardingObjective.Builder fwdBuilder = createInitFwdObjective(tunnelId, ingress.port(), nextId);
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous fwdObj for policy {} removed", tunnelId);
+                if (future != null) {
+                    future.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous fwdObj for policy {}: {}", tunnelId, error);
+                if (future != null) {
+                    future.complete(error);
+                }
+            }
+        };
+        objectives.add(fwdBuilder.remove(context));
+        // We create the filtering objective to define the
+        // permit traffic in the switch
+        FilteringObjective.Builder filtBuilder = createFiltObjective(ingress.port(), ingressInner, ingressOuter);
+        TrafficTreatment.Builder treatment = DefaultTrafficTreatment
+                .builder()
+                .setTunnelId(tunnelId)
+                .setVlanId(egressVlan);
+        filtBuilder.withMeta(treatment.build());
+        context = new DefaultObjectiveContext((objective) -> log.debug("FilterObj for policy {} revoked", tunnelId),
+                                              (objective, error) ->
+                                                      log.warn("Failed to revoke filterObj for policy {}",
+                                                               tunnelId, error));
+        objectives.add(filtBuilder.remove(context));
+
+        for (Objective objective : objectives) {
+            if (objective instanceof ForwardingObjective) {
+                srManager.flowObjectiveService.forward(ingress.deviceId(), (ForwardingObjective) objective);
+            } else {
+                srManager.flowObjectiveService.filter(ingress.deviceId(), (FilteringObjective) objective);
+            }
+        }
+    }
+
+    /**
+     * Deletes the pseudo wire initiation.
+     *
+     * @param l2TunnelId the tunnel id
+     * @param ingress    the ingress connect point
+     * @param future     to perform an async operation
+     * @param direction  the direction: reverse of forward
+     */
+    private void tearDownPseudoWireInit(long l2TunnelId, ConnectPoint ingress,
+                                        CompletableFuture<ObjectiveError> future, Direction direction) {
+
+        String key = generateKey(l2TunnelId, direction);
+        if (!l2InitiationNextObjStore.containsKey(key)) {
+            log.info("Abort delete of {} for {}: next does not exist in the store", INITIATION, key);
+            if (future != null) {
+                future.complete(null);
+            }
+            return;
+        }
+        NextObjective nextObjective = l2InitiationNextObjStore.get(key).value();
+
+        // un-comment in case you want to delete groups used by the pw
+        // however, this will break the update of pseudowires cause the L2 interface group can
+        // not be deleted (it is referenced by other groups)
+        /*
+        ObjectiveContext context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous {} next for {} removed", INITIATION, key);
+                if (future != null) {
+                    future.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous {} next for {}: {}", INITIATION, key, error);
+                if (future != null) {
+                    future.complete(error);
+                }
+            }
+        };
+        srManager.flowObjectiveService.next(ingress.deviceId(), (NextObjective) nextObjective.copy().remove(context));
+        */
+
+        future.complete(null);
+        l2InitiationNextObjStore.remove(key);
+    }
+
+    /**
+     * Deletes the pseudo wire termination.
+     *
+     * @param l2Tunnel  the tunnel
+     * @param egress    the egress connect point
+     * @param future    the async task
+     * @param direction the direction of the tunnel
+     */
+    private void tearDownPseudoWireTerm(L2Tunnel l2Tunnel,
+                                        ConnectPoint egress,
+                                        CompletableFuture<ObjectiveError> future,
+                                        Direction direction) {
+
+        String key = generateKey(l2Tunnel.tunnelId(), direction);
+        if (!l2TerminationNextObjStore.containsKey(key)) {
+            log.info("Abort delete of {} for {}: next does not exist in the store", TERMINATION, key);
+            if (future != null) {
+                future.complete(null);
+            }
+            return;
+        }
+        NextObjective nextObjective = l2TerminationNextObjStore.get(key).value();
+        ForwardingObjective.Builder fwdBuilder = createTermFwdObjective(l2Tunnel.pwLabel(),
+                                                                        l2Tunnel.tunnelId(),
+                                                                        egress.port(),
+                                                                        nextObjective.id());
+        ObjectiveContext context = new DefaultObjectiveContext((objective) ->
+                                                                       log.debug("FwdObj for {} {}, " +
+                                                                                         "direction {} removed",
+                                                                                        TERMINATION,
+                                                                                        l2Tunnel.tunnelId(),
+                                                                                        direction),
+                                                               (objective, error) ->
+                                                                       log.warn("Failed to remove fwdObj " +
+                                                                                        "for {} {}" +
+                                                                                        ", direction {}",
+                                                                                TERMINATION,
+                                                                                l2Tunnel.tunnelId(),
+                                                                                error,
+                                                                                direction));
+        srManager.flowObjectiveService.forward(egress.deviceId(), fwdBuilder.remove(context));
+
+        // un-comment in case you want to delete groups used by the pw
+        // however, this will break the update of pseudowires cause the L2 interface group can
+        // not be deleted (it is referenced by other groups)
+        /*
+        context = new ObjectiveContext() {
+            @Override
+            public void onSuccess(Objective objective) {
+                log.debug("Previous {} next for {} removed", TERMINATION, key);
+                if (future != null) {
+                    future.complete(null);
+                }
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                log.warn("Failed to remove previous {} next for {}: {}", TERMINATION, key, error);
+                if (future != null) {
+                    future.complete(error);
+                }
+            }
+        };
+        srManager.flowObjectiveService.next(egress.deviceId(), (NextObjective) nextObjective.copy().remove(context));
+        */
+
+        // delete the extra filtering objective for terminating
+        // spine-spine pws
+        if (!l2Tunnel.transportVlan().equals(UNTAGGED_TRANSPORT_VLAN)) {
+
+            // determine the input port at the
+            PortNumber inPort;
+
+            if (egress.deviceId().
+                    equals(l2Tunnel.pathUsed().get(0).dst().deviceId())) {
+                inPort = l2Tunnel.pathUsed().get(0).dst().port();
+            } else {
+                inPort = l2Tunnel.pathUsed().get(0).src().port();
+            }
+
+            MacAddress dstMac;
+            try {
+                dstMac = srManager.deviceConfiguration().getDeviceMac(egress.deviceId());
+            } catch (Exception e) {
+                log.info("Device not found in configuration, no programming of MAC address");
+                dstMac = null;
+            }
+
+            log.info("Removing filtering objective for pseudowire transport" +
+                             " with vlan = {}, port = {}, mac = {}",
+                     l2Tunnel.transportVlan(),
+                     inPort,
+                     dstMac);
+            FilteringObjective.Builder filteringObjectiveBuilder =
+                    createNormalPipelineFiltObjective(inPort, l2Tunnel.transportVlan(), dstMac);
+            context = new DefaultObjectiveContext(( objective ) ->
+                                                          log.debug("Special filtObj for  " + "for {} removed",
+                                                                    l2Tunnel.tunnelId()), ( objective, error ) ->
+                    log.warn("Failed to populate " + "special filtObj " +
+                                     "rule for {}: {}", l2Tunnel.tunnelId(), error));
+            TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
+            filteringObjectiveBuilder.withMeta(treatment.build());
+            srManager.flowObjectiveService.filter(egress.deviceId(), filteringObjectiveBuilder.remove(context));
+            log.debug("Removing special FiltObj for termination point with tunnel {} for port {}",
+                      l2Tunnel.tunnelId(),
+                      inPort);
+        }
+
+        l2TerminationNextObjStore.remove(key);
+        future.complete(null);
+    }
+
+    /**
+     * Utilities to generate pw key.
+     *
+     * @param tunnelId  the tunnel id
+     * @param direction the direction of the pw
+     * @return the key of the store
+     */
+    private String generateKey(long tunnelId, Direction direction) {
+        return String.format("%s-%s", tunnelId, direction);
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java
new file mode 100644
index 0000000..4738744
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/DefaultL2TunnelPolicy.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the default l2 tunnel policy.
+ */
+public class DefaultL2TunnelPolicy implements L2TunnelPolicy {
+
+    /**
+     * Id of the tunnel associated to this policy.
+     */
+    private long tunnelId;
+    /**
+     * First connect point.
+     */
+    private ConnectPoint cP1;
+    /**
+     * Second connect point.
+     */
+    private ConnectPoint cP2;
+    /**
+     * cP1 inner vlan tag. Used in QinQ packets.
+     */
+    private VlanId cP1InnerTag;
+    /**
+     * cP1 outer vlan tag.
+     */
+    private VlanId cP1OuterTag;
+    /**
+     * cP2 inner vlan tag. Used in QinQ packets.
+     */
+    private VlanId cP2InnerTag;
+    /**
+     * cP2 outer vlan tag.
+     */
+    private VlanId cP2OuterTag;
+
+    /**
+     * Creates a default l2 tunnel policy using
+     * the given parameters.
+     *
+     * @param tunnelId the tunnel id
+     * @param cP1 the first connect point
+     * @param cP1InnerTag the cP1 inner tag
+     * @param cP1OuterTag the cP1 outer tag
+     * @param cP2 the second connect point
+     * @param cP2InnerTag the cP2 inner tag
+     * @param cP2OuterTag the cP2 outer tag
+     */
+    public DefaultL2TunnelPolicy(long tunnelId,
+                                 ConnectPoint cP1, VlanId cP1InnerTag, VlanId cP1OuterTag,
+                                 ConnectPoint cP2, VlanId cP2InnerTag, VlanId cP2OuterTag) {
+        this.cP1 = checkNotNull(cP1);
+        this.cP2 = checkNotNull(cP2);
+        this.tunnelId = tunnelId;
+        this.cP1InnerTag = cP1InnerTag;
+        this.cP1OuterTag = cP1OuterTag;
+        this.cP2InnerTag = cP2InnerTag;
+        this.cP2OuterTag = cP2OuterTag;
+    }
+
+    /**
+     * Creates a default l2 policy given the provided policy.
+     * @param policy L2policy to replicate
+     */
+    public DefaultL2TunnelPolicy(DefaultL2TunnelPolicy policy) {
+
+        this.cP1 = policy.cP1;
+        this.cP2 = policy.cP2;
+        this.tunnelId = policy.tunnelId;
+        this.cP1InnerTag = policy.cP1InnerTag;
+        this.cP1OuterTag = policy.cP1OuterTag;
+        this.cP2InnerTag = policy.cP2InnerTag;
+        this.cP2OuterTag = policy.cP2OuterTag;
+    }
+
+    @Override
+    public ConnectPoint cP1() {
+        return cP1;
+    }
+
+    @Override
+    public ConnectPoint cP2() {
+        return cP2;
+    }
+
+    @Override
+    public VlanId cP1InnerTag() {
+        return cP1InnerTag;
+    }
+
+    @Override
+    public VlanId cP1OuterTag() {
+        return cP1OuterTag;
+    }
+
+    @Override
+    public VlanId cP2InnerTag() {
+        return cP2InnerTag;
+    }
+
+    @Override
+    public VlanId cP2OuterTag() {
+        return cP2OuterTag;
+    }
+
+    @Override
+    public long tunnelId() {
+        return this.tunnelId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(tunnelId,
+                            cP1,
+                            cP2,
+                            cP1InnerTag,
+                            cP1OuterTag,
+                            cP2InnerTag,
+                            cP2OuterTag
+        );
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof DefaultL2TunnelPolicy) {
+            DefaultL2TunnelPolicy that = (DefaultL2TunnelPolicy) o;
+            if (this.tunnelId == that.tunnelId &&
+                    this.cP1.equals(that.cP1) &&
+                    this.cP2.equals(that.cP2) &&
+                    this.cP1InnerTag.equals(that.cP1InnerTag) &&
+                    this.cP1OuterTag.equals(that.cP1OuterTag) &&
+                    this.cP2InnerTag.equals(that.cP2InnerTag) &&
+                    this.cP2OuterTag.equals(that.cP2OuterTag)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("tunnelId", tunnelId())
+                .add("cP1", cP1())
+                .add("cP2", cP2())
+                .add("cP1InnerTag", cP1InnerTag())
+                .add("cP1OuterTag", cP1OuterTag())
+                .add("cP2InnerTag", cP2InnerTag())
+                .add("cP2OuterTag", cP2OuterTag())
+                .toString();
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java
new file mode 100644
index 0000000..685a606
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Mode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+/**
+ * Enum to identify mode of the pwaas.
+ */
+public enum L2Mode {
+    /**
+     * Raw mode.
+     */
+    RAW,
+    /**
+     * Tagged mode. In this case the packet need
+     * the sd tag.
+     */
+    TAGGED
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java
new file mode 100644
index 0000000..3e98480
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2Tunnel.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.Link;
+
+import java.util.List;
+
+public interface L2Tunnel {
+
+    /**
+     * Return the mode of the l2 tunnel.
+     *
+     * @return The pw mode.
+     */
+    L2Mode pwMode();
+
+    /**
+     * Returns the service delimiting tag.
+     *
+     * @return the sd tag
+     */
+    VlanId sdTag();
+
+    /**
+     * Returns the id of the tunnel.
+     *
+     * @return the tunnel id
+     */
+    long tunnelId();
+
+    /**
+     * Return the label of the pseudowire.
+     *
+     * @return the pw label.
+     */
+    MplsLabel pwLabel();
+
+    /**
+     * Returns the path used by the pseudowire.
+     *
+     * @return The path that is used
+     */
+    List<Link> pathUsed();
+
+    /**
+     * Returns the transport vlan used by the pseudowire.
+     *
+     * @return The transport vlan
+     */
+    VlanId transportVlan();
+
+    /**
+     * Returns the inter-co label used by the pseudowire.
+     *
+     * @return The inter CO label.
+     */
+    MplsLabel interCoLabel();
+
+    /**
+     * Sets the path that this pw uses.
+     *
+     * @param path The apth to use
+     */
+    void setPath(List<Link> path);
+
+    /**
+     * Set the transport vlan that this pw will use.
+     *
+     * @param vlan The vlan to use.
+     */
+    void setTransportVlan(VlanId vlan);
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java
new file mode 100644
index 0000000..7be187a
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelDescription.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+public interface L2TunnelDescription {
+
+    /**
+     * Returns the l2 tunnel.
+     *
+     * @return the l2 tunnel
+     */
+    L2Tunnel l2Tunnel();
+
+    /**
+     * Returns the l2 tunnel policy.
+     *
+     * @return the l2 tunnel policy.
+     */
+    L2TunnelPolicy l2TunnelPolicy();
+
+    /**
+     * Sets the l2 tunnel.
+     *
+     * @param tunnel the l2 tunnel to set.
+     */
+    void setL2Tunnel(L2Tunnel tunnel);
+
+    /**
+     * Sets the l2 policy.
+     *
+     * @param policy the policy to set.
+     */
+    void setL2TunnelPolicy(L2TunnelPolicy policy);
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java
new file mode 100644
index 0000000..3a89a21
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelHandler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import org.onosproject.net.Link;
+import org.onosproject.net.config.NetworkConfigEvent;
+
+import java.util.List;
+import java.util.Set;
+
+public interface L2TunnelHandler {
+    void init();
+
+    /**
+     * Returns a copy of the l2 policies that exist in the store.
+     *
+     * @return The l2 policies
+     */
+    List<L2TunnelPolicy> getL2Policies();
+
+    /**
+     * Returns a copy of the l2 tunnels that exist in the store.
+     *
+     * @return The l2 tunnels.
+     */
+    List<L2Tunnel> getL2Tunnels();
+
+    /**
+     * Processes a link removal. Finds affected pseudowires and rewires them.
+     * TODO: Make it also take into account failures of links that are used for pw
+     * traffic in the spine.
+     * @param link The link that failed
+     */
+    void processLinkDown(Link link);
+
+    /**
+     * Processes Pwaas Config added event.
+     *
+     * @param event network config add event
+     */
+    void processPwaasConfigAdded(NetworkConfigEvent event);
+
+    /**
+     * Processes PWaaS Config updated event.
+     *
+     * @param event network config updated event
+     */
+    void processPwaasConfigUpdated(NetworkConfigEvent event);
+
+    /**
+     * Processes Pwaas Config removed event.
+     *
+     * @param event network config removed event
+     */
+    void processPwaasConfigRemoved(NetworkConfigEvent event);
+
+    /**
+     * Helper function to handle the pw removal.
+     * <p>
+     * This method should for the mastership of the device because it is
+     * used only from network configuration updates, thus we only want
+     * one instance only to program each pseudowire.
+     *
+     * @param pwToRemove the pseudo wires to remove
+     */
+    void tearDown(Set<L2TunnelDescription> pwToRemove);
+
+    /**
+     * Pwaas pipelines.
+     */
+    enum Pipeline {
+        /**
+         * The initiation pipeline.
+         */
+        INITIATION, /**
+         * The termination pipeline.
+         */
+        TERMINATION
+    }
+
+    /**
+     * Enum helper to carry results of various operations.
+     */
+    enum Result {
+        /**
+         * Happy ending scenario.
+         */
+        SUCCESS(0, "No error occurred"),
+
+        /**
+         * We have problems with the supplied parameters.
+         */
+        WRONG_PARAMETERS(1, "Wrong parameters"),
+
+        /**
+         * We have an internal error during the deployment
+         * or removal phase.
+         */
+        INTERNAL_ERROR(3, "Internal error"),
+
+        /**
+         *
+         */
+        REMOVAL_ERROR(5, "Can not remove pseudowire from network configuration"),
+
+        /**
+         *
+         */
+        ADDITION_ERROR(6, "Can not add pseudowire in network configuration"),
+
+        /**
+         *
+         */
+        CONFIG_NOT_FOUND(7, "Can not find configuration class for pseudowires");
+
+        private final int code;
+        private final String description;
+        public int nextId;
+
+        Result(int code, String description) {
+            this.code = code;
+            this.description = description;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        @Override
+        public String toString() {
+            return code + ": " + description;
+        }
+    }
+
+    /**
+     * Enum helper for handling the direction of the pw.
+     */
+    enum Direction {
+        /**
+         * The forward direction of the pseudo wire.
+         */
+        FWD, /**
+         * The reverse direction of the pseudo wire.
+         */
+        REV
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java
new file mode 100644
index 0000000..17be45b
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/L2TunnelPolicy.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016-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.segmentrouting.pwaas;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+public interface L2TunnelPolicy {
+
+    /**
+     * Returns the first connect point of the policy.
+     *
+     * @return first connect point
+     */
+    ConnectPoint cP1();
+
+    /**
+     * Returns the second connect point of the policy.
+     *
+     * @return second connect point
+     */
+    ConnectPoint cP2();
+
+    /**
+     * Returns the cP1 inner vlan tag of the policy.
+     *
+     * @return cP1 inner vlan tag
+     */
+    VlanId cP1InnerTag();
+
+    /**
+     * Returns the cP1 outer vlan tag of the policy.
+     *
+     * @return cP1 outer vlan tag
+     */
+    VlanId cP1OuterTag();
+
+    /**
+     * Returns the cP2 inner vlan tag of the policy.
+     *
+     * @return cP2 inner vlan tag
+     */
+    VlanId cP2InnerTag();
+
+    /**
+     * Returns the cP2 outer vlan tag of the policy.
+     *
+     * @return cP2 outer vlan tag
+     */
+    VlanId cP2OuterTag();
+
+    /**
+     * Returns the tunnel ID of the policy.
+     *
+     * @return Tunnel ID
+     */
+    long tunnelId();
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java
new file mode 100644
index 0000000..463b163
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/pwaas/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/**
+ * Set of resources implementing the Pwaas.
+ */
+package org.onosproject.segmentrouting.pwaas;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java
new file mode 100644
index 0000000..9b6e621
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/DestinationSetNextObjectiveStoreKey.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-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/licedses/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.segmentrouting.storekey;
+
+import java.util.Objects;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.grouphandler.DestinationSet;
+
+/**
+ * Key of Destination set next objective store.
+ */
+public class DestinationSetNextObjectiveStoreKey {
+    private final DeviceId deviceId;
+    private final DestinationSet ds;
+
+    /**
+     * Constructs the key of destination set next objective store.
+     *
+     * @param deviceId device ID
+     * @param ds destination set
+     */
+    public DestinationSetNextObjectiveStoreKey(DeviceId deviceId,
+                                            DestinationSet ds) {
+        this.deviceId = deviceId;
+        this.ds = ds;
+    }
+
+    /**
+     * Returns the device ID in the key.
+     *
+     * @return device ID
+     */
+    public DeviceId deviceId() {
+        return this.deviceId;
+    }
+
+    /**
+     * Returns the destination set in the key.
+     *
+     * @return destination set
+     */
+    public DestinationSet destinationSet() {
+        return this.ds;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof DestinationSetNextObjectiveStoreKey)) {
+            return false;
+        }
+        DestinationSetNextObjectiveStoreKey that =
+                (DestinationSetNextObjectiveStoreKey) o;
+        return (Objects.equals(this.deviceId, that.deviceId) &&
+                Objects.equals(this.ds, that.ds));
+    }
+
+    // The list of destination ids and label are used for comparison.
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + Objects.hashCode(this.deviceId)
+                + Objects.hashCode(this.ds);
+
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Device: " + deviceId + " " + ds;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
new file mode 100644
index 0000000..6891b77
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/McastStoreKey.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-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.segmentrouting.storekey;
+
+import org.onlab.packet.IpAddress;
+import org.onosproject.net.DeviceId;
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.Objects;
+
+/**
+ * Key of multicast next objective store.
+ */
+public class McastStoreKey {
+    private final IpAddress mcastIp;
+    private final DeviceId deviceId;
+
+    /**
+     * Constructs the key of multicast next objective store.
+     *
+     * @param mcastIp multicast group IP address
+     * @param deviceId device ID
+     */
+    public McastStoreKey(IpAddress mcastIp, DeviceId deviceId) {
+        checkNotNull(mcastIp, "mcastIp cannot be null");
+        checkNotNull(deviceId, "deviceId cannot be null");
+        checkArgument(mcastIp.isMulticast(), "mcastIp must be a multicast address");
+        this.mcastIp = mcastIp;
+        this.deviceId = deviceId;
+    }
+
+    /**
+     * Returns the multicast IP address of this key.
+     *
+     * @return multicast IP
+     */
+    public IpAddress mcastIp() {
+        return mcastIp;
+    }
+
+    /**
+     * Returns the device ID of this key.
+     *
+     * @return device ID
+     */
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof McastStoreKey)) {
+            return false;
+        }
+        McastStoreKey that =
+                (McastStoreKey) o;
+        return (Objects.equals(this.mcastIp, that.mcastIp) &&
+                Objects.equals(this.deviceId, that.deviceId));
+    }
+
+    @Override
+    public int hashCode() {
+         return Objects.hash(mcastIp, deviceId);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+                .add("mcastIp", mcastIp)
+                .add("deviceId", deviceId)
+                .toString();
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java
new file mode 100644
index 0000000..0429bd1
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/PortNextObjectiveStoreKey.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016-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.segmentrouting.storekey;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+
+import java.util.Objects;
+
+/**
+ * Key of Device/Port to NextObjective store.
+ *
+ * Since there can be multiple next objectives to the same physical port,
+ * we differentiate between them by including the treatment in the key.
+ */
+public class PortNextObjectiveStoreKey {
+    private final DeviceId deviceId;
+    private final PortNumber portNum;
+    private final TrafficTreatment treatment;
+    private final TrafficSelector meta;
+
+    /**
+     * Constructs the key of port next objective store.
+     *
+     * @param deviceId device ID
+     * @param portNum port number
+     * @param treatment treatment that will be applied to the interface
+     * @param meta optional data to pass to the driver
+     */
+    public PortNextObjectiveStoreKey(DeviceId deviceId, PortNumber portNum,
+                                     TrafficTreatment treatment,
+                                     TrafficSelector meta) {
+        this.deviceId = deviceId;
+        this.portNum = portNum;
+        this.treatment = treatment;
+        this.meta = meta;
+    }
+
+    /**
+     * Gets device id in this PortNextObjectiveStoreKey.
+     *
+     * @return device id
+     */
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    /**
+     * Gets port information in this PortNextObjectiveStoreKey.
+     *
+     * @return port information
+     */
+    public PortNumber portNumber() {
+        return portNum;
+    }
+
+    /**
+     * Gets treatment information in this PortNextObjectiveStoreKey.
+     *
+     * @return treatment information
+     */
+    public TrafficTreatment treatment() {
+        return treatment;
+    }
+
+    /**
+     * Gets metadata information in this PortNextObjectiveStoreKey.
+     *
+     * @return meta information
+     */
+    public TrafficSelector meta() {
+        return meta;
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof PortNextObjectiveStoreKey)) {
+            return false;
+        }
+        PortNextObjectiveStoreKey that =
+                (PortNextObjectiveStoreKey) o;
+        return (Objects.equals(this.deviceId, that.deviceId) &&
+                Objects.equals(this.portNum, that.portNum) &&
+                Objects.equals(this.treatment, that.treatment) &&
+                Objects.equals(this.meta, that.meta));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, portNum, treatment, meta);
+    }
+
+    @Override
+    public String toString() {
+        return "Device: " + deviceId + " Port: " + portNum +
+                " Treatment: " + treatment +
+                " Meta: " + meta;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java
new file mode 100644
index 0000000..5d9f2fd
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/VlanNextObjectiveStoreKey.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-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.segmentrouting.storekey;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Key of VLAN to NextObjective store.
+ */
+public class VlanNextObjectiveStoreKey {
+    private final DeviceId deviceId;
+    private final VlanId vlanId;
+
+    /**
+     * Constructs the key of VLAN next objective store.
+     *
+     * @param deviceId device ID
+     * @param vlanId VLAN information
+     */
+    public VlanNextObjectiveStoreKey(DeviceId deviceId,
+                                     VlanId vlanId) {
+        this.deviceId = deviceId;
+        this.vlanId = vlanId;
+    }
+
+    /**
+     * Gets device id in this VlanNextObjectiveStoreKey.
+     *
+     * @return device id
+     */
+    public DeviceId deviceId() {
+        return this.deviceId;
+    }
+
+    /**
+     * Gets vlan information in this VlanNextObjectiveStoreKey.
+     *
+     * @return vlan id
+     */
+    public VlanId vlanId() {
+        return this.vlanId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof VlanNextObjectiveStoreKey)) {
+            return false;
+        }
+        VlanNextObjectiveStoreKey that =
+                (VlanNextObjectiveStoreKey) o;
+        return (Objects.equals(this.deviceId, that.deviceId) &&
+                Objects.equals(this.vlanId, that.vlanId));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, vlanId);
+    }
+
+    @Override
+    public String toString() {
+        return "deviceId: " + deviceId + " vlanId: " + vlanId;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java
new file mode 100644
index 0000000..0e90a22
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/XConnectStoreKey.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-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.segmentrouting.storekey;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Key of VLAN cross-connect next objective store.
+ */
+public class XConnectStoreKey {
+    private final DeviceId deviceId;
+    private final VlanId vlanId;
+
+    /**
+     * Constructs the key of cross-connect next objective store.
+     *
+     * @param deviceId device ID of the VLAN cross-connection
+     * @param vlanId VLAN ID of the VLAN cross-connection
+     */
+    public XConnectStoreKey(DeviceId deviceId, VlanId vlanId) {
+        this.deviceId = deviceId;
+        this.vlanId = vlanId;
+    }
+
+    /**
+     * Returns the device ID of this key.
+     *
+     * @return device ID
+     */
+    public DeviceId deviceId() {
+        return this.deviceId;
+    }
+
+    /**
+     * Returns the VLAN ID of this key.
+     *
+     * @return VLAN ID
+     */
+    public VlanId vlanId() {
+        return this.vlanId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof XConnectStoreKey)) {
+            return false;
+        }
+        XConnectStoreKey that =
+                (XConnectStoreKey) o;
+        return (Objects.equals(this.deviceId, that.deviceId) &&
+                Objects.equals(this.vlanId, that.vlanId));
+    }
+
+    // The list of neighbor ids and label are used for comparison.
+    @Override
+    public int hashCode() {
+         return Objects.hash(deviceId, vlanId);
+    }
+
+    @Override
+    public String toString() {
+        return "Device: " + deviceId + " VlanId: " + vlanId;
+    }
+}
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java
new file mode 100644
index 0000000..44fc6a7
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/storekey/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/**
+ * Key data structure of various stores used by Segment Routing.
+ */
+package org.onosproject.segmentrouting.storekey;
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..cfd7502
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,95 @@
+<!--
+  ~ Copyright 2015-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.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+        <!-- XXX revisit when we formally add policies
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.TunnelListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PolicyListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PolicyAddCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PolicyRemoveCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.TunnelAddCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.TunnelRemoveCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.RerouteNetworkCommand"/>
+        </command>
+        -->
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.DeviceSubnetListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.EcmpGraphCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.NextHopCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.VerifyGroupsCommand"/>
+            <completers>
+                <ref component-id="deviceIdCompleter"/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PseudowireListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PseudowireRemoveCommand"/>
+            <completers>
+                <ref component-id="pseudowireIdCompleter"/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.PseudowireAddCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.LinkStateCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.McastNextListCommand"/>
+            <completers>
+                <ref component-id="mcastGroupCompleter"/>
+                <ref component-id="nullCompleter"/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.segmentrouting.cli.McastTreeListCommand"/>
+            <completers>
+                <ref component-id="mcastGroupCompleter"/>
+                <ref component-id="nullCompleter"/>
+            </completers>
+        </command>
+    </command-bundle>
+
+    <bean id="nullCompleter" class="org.apache.karaf.shell.console.completer.NullCompleter"/>
+    <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
+    <bean id="pseudowireIdCompleter" class="org.onosproject.segmentrouting.cli.PseudowireIdCompleter"/>
+    <bean id="mcastGroupCompleter" class="org.onosproject.cli.net.McastGroupCompleter"/>
+
+</blueprint>
+
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java
new file mode 100644
index 0000000..cc214e3
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/AugmentedPortAuthTracker.java
@@ -0,0 +1,53 @@
+/*
+ * 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.segmentrouting;
+
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An augmented implementation of {@link PortAuthTracker}, so that we can
+ * instrument its behavior for unit test assertions.
+ */
+class AugmentedPortAuthTracker extends PortAuthTracker {
+
+    // instrument blocking flow activity, so we can see when we get hits
+    final List<ConnectPoint> installed = new ArrayList<>();
+    final List<ConnectPoint> cleared = new ArrayList<>();
+
+
+    void resetMetrics() {
+        installed.clear();
+        cleared.clear();
+    }
+
+    @Override
+    void installBlockingFlow(DeviceId d, PortNumber p) {
+        super.installBlockingFlow(d, p);
+        installed.add(new ConnectPoint(d, p));
+    }
+
+    @Override
+    void clearBlockingFlow(DeviceId d, PortNumber p) {
+        super.clearBlockingFlow(d, p);
+        cleared.add(new ConnectPoint(d, p));
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
new file mode 100644
index 0000000..e33a36d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
@@ -0,0 +1,855 @@
+/*
+ * 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.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.host.HostLocationProbingService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**r
+ * Unit test for {@link HostHandler}.
+ */
+public class HostHandlerTest {
+    private HostHandler hostHandler;
+
+    // Mocked routing and bridging tables
+    private static final Map<MockBridgingTableKey, MockBridgingTableValue> BRIDGING_TABLE =
+            Maps.newConcurrentMap();
+    private static final Map<MockRoutingTableKey, MockRoutingTableValue> ROUTING_TABLE =
+            Maps.newConcurrentMap();
+    private static final Map<ConnectPoint, Set<IpPrefix>> SUBNET_TABLE = Maps.newConcurrentMap();
+    // Mocked Next Id
+    private static final Map<Integer, TrafficTreatment> NEXT_TABLE = Maps.newConcurrentMap();
+
+    // Host Mac, VLAN
+    private static final ProviderId PROVIDER_ID = ProviderId.NONE;
+    private static final MacAddress HOST_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId HOST_VLAN_UNTAGGED = VlanId.NONE;
+    private static final HostId HOST_ID_UNTAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_UNTAGGED);
+    private static final VlanId HOST_VLAN_TAGGED = VlanId.vlanId((short) 20);
+    private static final HostId HOST_ID_TAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_TAGGED);
+    // Host IP
+    private static final IpAddress HOST_IP11 = IpAddress.valueOf("10.0.1.1");
+    private static final IpAddress HOST_IP21 = IpAddress.valueOf("10.0.2.1");
+    private static final IpAddress HOST_IP12 = IpAddress.valueOf("10.0.1.2");
+    private static final IpAddress HOST_IP13 = IpAddress.valueOf("10.0.1.3");
+    private static final IpAddress HOST_IP14 = IpAddress.valueOf("10.0.1.4");
+    private static final IpAddress HOST_IP32 = IpAddress.valueOf("10.0.3.2");
+    // Device
+    private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+    private static final DeviceId DEV3 = DeviceId.deviceId("of:0000000000000003");
+    private static final DeviceId DEV4 = DeviceId.deviceId("of:0000000000000004");
+    private static final DeviceId DEV5 = DeviceId.deviceId("of:0000000000000005");
+    private static final DeviceId DEV6 = DeviceId.deviceId("of:0000000000000006");
+    // Port
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+    private static final PortNumber P9 = PortNumber.portNumber(9);
+    // Connect Point
+    private static final ConnectPoint CP11 = new ConnectPoint(DEV1, P1);
+    private static final HostLocation HOST_LOC11 = new HostLocation(CP11, 0);
+    private static final ConnectPoint CP12 = new ConnectPoint(DEV1, P2);
+    private static final HostLocation HOST_LOC12 = new HostLocation(CP12, 0);
+    private static final ConnectPoint CP13 = new ConnectPoint(DEV1, P3);
+    private static final HostLocation HOST_LOC13 = new HostLocation(CP13, 0);
+    private static final ConnectPoint CP21 = new ConnectPoint(DEV2, P1);
+    private static final HostLocation HOST_LOC21 = new HostLocation(CP21, 0);
+    private static final ConnectPoint CP22 = new ConnectPoint(DEV2, P2);
+    private static final HostLocation HOST_LOC22 = new HostLocation(CP22, 0);
+    // Connect Point for dual-homed host failover
+    private static final ConnectPoint CP31 = new ConnectPoint(DEV3, P1);
+    private static final HostLocation HOST_LOC31 = new HostLocation(CP31, 0);
+    private static final ConnectPoint CP32 = new ConnectPoint(DEV3, P2);
+    private static final HostLocation HOST_LOC32 = new HostLocation(CP32, 0);
+    private static final ConnectPoint CP41 = new ConnectPoint(DEV4, P1);
+    private static final HostLocation HOST_LOC41 = new HostLocation(CP41, 0);
+    private static final ConnectPoint CP39 = new ConnectPoint(DEV3, P9);
+    private static final ConnectPoint CP49 = new ConnectPoint(DEV4, P9);
+    // Conenct Point for mastership test
+    private static final ConnectPoint CP51 = new ConnectPoint(DEV5, P1);
+    private static final HostLocation HOST_LOC51 = new HostLocation(CP51, 0);
+    private static final ConnectPoint CP61 = new ConnectPoint(DEV6, P1);
+    private static final HostLocation HOST_LOC61 = new HostLocation(CP61, 0);
+    // Interface VLAN
+    private static final VlanId INTF_VLAN_UNTAGGED = VlanId.vlanId((short) 10);
+    private static final Set<VlanId> INTF_VLAN_TAGGED = Sets.newHashSet(VlanId.vlanId((short) 20));
+    private static final VlanId INTF_VLAN_NATIVE = VlanId.vlanId((short) 30);
+    private static final Set<VlanId> INTF_VLAN_PAIR = Sets.newHashSet(VlanId.vlanId((short) 10),
+            VlanId.vlanId((short) 20), VlanId.vlanId((short) 30));
+    private static final VlanId INTF_VLAN_OTHER = VlanId.vlanId((short) 40);
+    // Interface subnet
+    private static final IpPrefix INTF_PREFIX1 = IpPrefix.valueOf("10.0.1.254/24");
+    private static final IpPrefix INTF_PREFIX2 = IpPrefix.valueOf("10.0.2.254/24");
+    private static final IpPrefix INTF_PREFIX3 = IpPrefix.valueOf("10.0.3.254/24");
+    private static final InterfaceIpAddress INTF_IP1 =
+            new InterfaceIpAddress(INTF_PREFIX1.address(), INTF_PREFIX1);
+    private static final InterfaceIpAddress INTF_IP2 =
+            new InterfaceIpAddress(INTF_PREFIX2.address(), INTF_PREFIX2);
+    private static final InterfaceIpAddress INTF_IP3 =
+            new InterfaceIpAddress(INTF_PREFIX3.address(), INTF_PREFIX3);
+    // Interfaces
+    private static final Interface INTF11 =
+            new Interface(null, CP11, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF12 =
+            new Interface(null, CP12, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF13 =
+            new Interface(null, CP13, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
+                    null, INTF_VLAN_TAGGED, INTF_VLAN_NATIVE);
+    private static final Interface INTF21 =
+            new Interface(null, CP21, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF22 =
+            new Interface(null, CP22, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF31 =
+            new Interface(null, CP31, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF32 =
+            new Interface(null, CP32, Lists.newArrayList(INTF_IP3), MacAddress.NONE, null,
+                    INTF_VLAN_OTHER, null, null);
+    private static final Interface INTF39 =
+            new Interface(null, CP39, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    null, INTF_VLAN_PAIR, null);
+    private static final Interface INTF41 =
+            new Interface(null, CP41, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF49 =
+            new Interface(null, CP49, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                    null, INTF_VLAN_PAIR, null);
+    // Host
+    private static final Host HOST1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC,
+            HOST_VLAN_UNTAGGED, Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11),
+            false);
+
+    // A set of hosts
+    private static final Set<Host> HOSTS = Sets.newHashSet(HOST1);
+    // A set of devices of which we have mastership
+    private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(DEV1, DEV2, DEV3, DEV4);
+    // A set of interfaces
+    private static final Set<Interface> INTERFACES = Sets.newHashSet(INTF11, INTF12, INTF13, INTF21,
+            INTF22, INTF31, INTF32, INTF39, INTF41, INTF49);
+
+    private MockLocationProbingService mockLocationProbingService;
+
+    @Before
+    public void setUp() throws Exception {
+        // Initialize pairDevice and pairLocalPort config
+        ObjectMapper mapper = new ObjectMapper();
+        ConfigApplyDelegate delegate = config -> { };
+
+        SegmentRoutingDeviceConfig dev3Config = new SegmentRoutingDeviceConfig();
+        JsonNode dev3Tree = mapper.createObjectNode();
+        dev3Config.init(DEV3, "host-handler-test", dev3Tree, mapper, delegate);
+        dev3Config.setPairDeviceId(DEV4).setPairLocalPort(P9);
+
+        SegmentRoutingDeviceConfig dev4Config = new SegmentRoutingDeviceConfig();
+        JsonNode dev4Tree = mapper.createObjectNode();
+        dev4Config.init(DEV4, "host-handler-test", dev4Tree, mapper, delegate);
+        dev4Config.setPairDeviceId(DEV3).setPairLocalPort(P9);
+
+        MockNetworkConfigRegistry mockNetworkConfigRegistry = new MockNetworkConfigRegistry();
+        mockNetworkConfigRegistry.applyConfig(dev3Config);
+        mockNetworkConfigRegistry.applyConfig(dev4Config);
+
+        // Initialize Segment Routing Manager
+        SegmentRoutingManager srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+        srManager.cfgService = new NetworkConfigRegistryAdapter();
+        srManager.deviceConfiguration = new DeviceConfiguration(srManager);
+        srManager.flowObjectiveService = new MockFlowObjectiveService(BRIDGING_TABLE, NEXT_TABLE);
+        srManager.routingRulePopulator = new MockRoutingRulePopulator(srManager, ROUTING_TABLE);
+        srManager.defaultRoutingHandler = new MockDefaultRoutingHandler(srManager, SUBNET_TABLE);
+        srManager.interfaceService = new MockInterfaceService(INTERFACES);
+        srManager.mastershipService = new MockMastershipService(LOCAL_DEVICES);
+        srManager.hostService = new MockHostService(HOSTS);
+        srManager.cfgService = mockNetworkConfigRegistry;
+        mockLocationProbingService = new MockLocationProbingService();
+        srManager.probingService = mockLocationProbingService;
+        srManager.linkHandler = new MockLinkHandler(srManager);
+
+        hostHandler = new HostHandler(srManager);
+
+        ROUTING_TABLE.clear();
+        BRIDGING_TABLE.clear();
+    }
+
+    @Test
+    public void init() throws Exception {
+        hostHandler.init(DEV1);
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        hostHandler.init(DEV2);
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testHostAddedAtWrongLocation() throws Exception {
+        hostHandler.processHostAddedAtLocation(HOST1, HOST_LOC13);
+    }
+
+
+    @Test()
+    public void testHostAddedAtCorrectLocation() throws Exception {
+        hostHandler.processHostAddedAtLocation(HOST1, HOST_LOC11);
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testHostAdded() throws Exception {
+        Host subject;
+
+        // Untagged host discovered on untagged port
+        // Expect: add one routing rule and one bridging rule
+        subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Untagged host discovered on tagged/native port
+        // Expect: add one routing rule and one bridging rule
+        subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_NATIVE)));
+
+        // Tagged host discovered on untagged port
+        // Expect: ignore the host. No rule is added.
+        subject = new DefaultHost(PROVIDER_ID, HOST_ID_TAGGED, HOST_MAC, HOST_VLAN_TAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(2, BRIDGING_TABLE.size());
+
+        // Tagged host discovered on tagged port with the same IP
+        // Expect: update existing route, add one bridging rule
+        subject = new DefaultHost(PROVIDER_ID, HOST_ID_TAGGED, HOST_MAC, HOST_VLAN_TAGGED,
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertEquals(HOST_VLAN_TAGGED, ROUTING_TABLE.get(new MockRoutingTableKey(HOST_LOC13.deviceId(),
+                HOST_IP21.toIpPrefix())).vlanId);
+        assertEquals(3, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, HOST_VLAN_TAGGED)));
+    }
+
+    @Test
+    public void testDualHomedHostAdded() throws Exception {
+        // Add a dual-homed host that has 2 locations
+        // Expect: add two routing rules and two bridging rules
+        Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testSingleHomedHostAddedOnPairLeaf() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC32), Sets.newHashSet(HOST_IP32), false);
+
+        // Add a single-homed host with one location
+        // Expect: the pair link should not be utilized
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP32.toIpPrefix())).portNumber);
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_OTHER)).portNumber);
+    }
+
+    @Test
+    public void testDualHomedHostAddedOneByOne() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a dual-homed host with one location
+        // Expect: the pair link is utilized temporarily before the second location is discovered
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        // Expect probe to be sent out on pair device
+        assertTrue(mockLocationProbingService.verifyProbe(host1, CP41, HostLocationProbingService.ProbeMode.DISCOVER));
+
+        // Add the second location of dual-homed host
+        // Expect: no longer use the pair link
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+        //        The permanent solution would be introducing CompletableFuture and wait for it
+        Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testHostRemoved() throws Exception {
+        Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a host
+        // Expect: add one routing rule and one bridging rule
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Remove the host
+        // Expect: add the routing rule and the bridging rule
+        hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, BRIDGING_TABLE.size());
+    }
+
+    @Test
+    public void testDualHomedHostRemoved() throws Exception {
+        // Add a dual-homed host that has 2 locations
+        // Expect: add two routing rules and two bridging rules
+        Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Remove a dual-homed host that has 2 locations
+        // Expect: all routing and bridging rules are removed
+        hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, BRIDGING_TABLE.size());
+    }
+
+    @Test
+    public void testHostMoved() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a host
+        // Expect: add one new routing rule, one new bridging rule
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Move the host to CP13, which has different subnet setting
+        // Expect: remove routing rule. Change vlan in bridging rule.
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host1));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_NATIVE)));
+        assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Move the host to CP21, which has same subnet setting
+        // Expect: add a new routing rule. Change vlan in bridging rule.
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host3));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testDualHomedHostMoved() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC12, HOST_LOC22), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP13, HOST_IP14), false);
+        Host host4 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC22), Sets.newHashSet(HOST_IP12, HOST_IP13), false);
+
+        // Add a host with IP11, IP12 and LOC11, LOC21
+        // Expect: 4 routing rules and 2 bridging rules
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move the host to LOC12, LOC22 and keep the IP
+        // Expect: 4 routing rules and 2 bridging rules all at the new location
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move the host to LOC11, LOC21 and change the IP to IP13, IP14 at the same time
+        // Expect: 4 routing rules and 2 bridging rules all at the new location
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP14.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP14.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move the host to LOC11, LOC22 and change the IP to IP12, IP13 at the same time
+        // Expect: 4 routing rules and 2 bridging rules all at the new location
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host4, host3));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P2, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P2, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testHostMoveToInvalidLocation() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a host
+        // Expect: add one new routing rule, one new bridging rule
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Move the host to an invalid location
+        // Expect: Old flow is removed. New flow is not created
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, BRIDGING_TABLE.size());
+
+        // Move the host to a valid location
+        // Expect: add one new routing rule, one new bridging rule
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testDualHomedHostMoveToInvalidLocation() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC61, HOST_LOC51), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a host
+        // Expect: add two new routing rules, two new bridging rules
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move first host location to an invalid location
+        // Expect: One routing and one bridging flow
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move second host location to an invalid location
+        // Expect: No routing or bridging rule
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, BRIDGING_TABLE.size());
+
+        // Move second host location back to a valid location
+        // Expect: One routing and one bridging flow
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host3));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Move first host location back to a valid location
+        // Expect: Two routing and two bridging flow
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testDualHomingSingleLocationFail() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+
+        // Add a host
+        // Expect: add four new routing rules, two new bridging rules
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Host becomes single-homed
+        // Expect: redirect flows from host location to pair link
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Host becomes dual-homed again
+        // Expect: Redirect flows from pair link back to host location
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host1, host2));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+        //        The permanent solution would be introducing CompletableFuture and wait for it
+        Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testDualHomingBothLocationFail() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+
+        // Add a host
+        // Expect: add four new routing rules, two new bridging rules
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Host becomes single-homed
+        // Expect: redirect flows from host location to pair link
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Host loses both locations
+        // Expect: Remove last location and all previous redirection flows
+        hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, host2));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, BRIDGING_TABLE.size());
+    }
+
+    @Test
+    public void testHostUpdated() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP21), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP12), false);
+
+        // Add a host
+        // Expect: add one new routing rule. Add one new bridging rule.
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(HOST_LOC11.deviceId(), HOST_MAC,
+                INTF_VLAN_UNTAGGED)));
+
+        // Update the host IP to same subnet
+        // Expect: update routing rule with new IP. No change to bridging rule.
+        hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host1));
+        assertEquals(1, ROUTING_TABLE.size());
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Update the host IP to different subnet
+        // Expect: Remove routing rule. No change to bridging rule.
+        hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host3));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(1, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testDelayedIpAndLocation() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(HOST_IP11), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a dual-home host with only one location and no IP
+        // Expect: only bridging redirection works
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Discover IP
+        // Expect: routing redirection should also work
+        hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host1));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        // Expect probe to be sent out on pair device
+        assertTrue(mockLocationProbingService.verifyProbe(host2, CP41, HostLocationProbingService.ProbeMode.DISCOVER));
+
+        // Discover location
+        // Expect: cancel all redirections
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host2));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P9, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        // FIXME: Delay event handling a little bit to wait for the previous redirection flows to be completed
+        //        The permanent solution would be introducing CompletableFuture and wait for it
+        Thread.sleep(HostHandler.HOST_MOVED_DELAY_MS + 50);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testDelayedIpAndLocation2() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31), Sets.newHashSet(), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC31, HOST_LOC41), Sets.newHashSet(HOST_IP11), false);
+
+        // Add a dual-home host with only one location and no IP
+        // Expect: only bridging redirection works
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P9, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Discover Location
+        // Expect: cancel bridging redirections
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host1));
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+
+        // Discover IP
+        // Expect: add IP rules to both location
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host2));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV3, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, ROUTING_TABLE.get(new MockRoutingTableKey(DEV4, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV3, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, BRIDGING_TABLE.get(new MockBridgingTableKey(DEV4, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+    }
+
+    @Test
+    public void testDualHomedHostUpdated() throws Exception {
+        Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP12), false);
+        Host host2 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP11, HOST_IP21), false);
+        Host host3 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
+                Sets.newHashSet(HOST_LOC11, HOST_LOC21), Sets.newHashSet(HOST_IP13, HOST_IP14), false);
+
+        // Add a dual-homed host with two locations and two IPs
+        // Expect: add four new routing rules. Add two new bridging rules
+        hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Update both host IPs
+        // Expect: update routing rules with new IP. No change to bridging rule.
+        hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host3, host1));
+        assertEquals(4, ROUTING_TABLE.size());
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP14.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP13.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP14.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        // Update one of the host IP to different subnet
+        // Expect: update routing rule with new IP. No change to bridging rule.
+        hostHandler.processHostUpdatedEvent(new HostEvent(HostEvent.Type.HOST_UPDATED, host2, host3));
+        assertEquals(2, ROUTING_TABLE.size());
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP21.toIpPrefix())));
+        assertNull(ROUTING_TABLE.get(new MockRoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertEquals(2, BRIDGING_TABLE.size());
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(BRIDGING_TABLE.get(new MockBridgingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @Test
+    public void testBridgingFwdObjBuilder() throws Exception {
+        assertNotNull(hostHandler.bridgingFwdObjBuilder(DEV2, HOST_MAC, HOST_VLAN_UNTAGGED, P1, false));
+        assertNull(hostHandler.bridgingFwdObjBuilder(DEV2, HOST_MAC, HOST_VLAN_UNTAGGED, P3, false));
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.java
new file mode 100644
index 0000000..a90b1cf
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableKey.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.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Mock Bridging Table Key.
+ */
+class MockBridgingTableKey {
+    DeviceId deviceId;
+    MacAddress macAddress;
+    VlanId vlanId;
+
+    MockBridgingTableKey(DeviceId deviceId, MacAddress macAddress, VlanId vlanId) {
+        this.deviceId = deviceId;
+        this.macAddress = macAddress;
+        this.vlanId = vlanId;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof MockBridgingTableKey)) {
+            return false;
+        }
+        final MockBridgingTableKey other = (MockBridgingTableKey) obj;
+        return Objects.equals(this.macAddress, other.macAddress) &&
+                Objects.equals(this.deviceId, other.deviceId) &&
+                Objects.equals(this.vlanId, other.vlanId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(macAddress, vlanId);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java
new file mode 100644
index 0000000..68b0db8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockBridgingTableValue.java
@@ -0,0 +1,52 @@
+/*
+ * 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.segmentrouting;
+
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+/**
+ * Mock Bridging Table Value.
+ */
+class MockBridgingTableValue {
+    boolean popVlan;
+    PortNumber portNumber;
+
+    MockBridgingTableValue(boolean popVlan, PortNumber portNumber) {
+        this.popVlan = popVlan;
+        this.portNumber = portNumber;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof MockBridgingTableValue)) {
+            return false;
+        }
+        final MockBridgingTableValue other = (MockBridgingTableValue) obj;
+        return Objects.equals(this.popVlan, other.popVlan) &&
+                Objects.equals(this.portNumber, other.portNumber);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(popVlan, portNumber);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java
new file mode 100644
index 0000000..ec07238
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDefaultRoutingHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Mock Default Routing Handler.
+ */
+public class MockDefaultRoutingHandler extends DefaultRoutingHandler {
+    private Map<ConnectPoint, Set<IpPrefix>> subnetTable;
+
+    MockDefaultRoutingHandler(SegmentRoutingManager srManager,
+                              Map<ConnectPoint, Set<IpPrefix>> subnetTable) {
+        super(srManager);
+        this.subnetTable = subnetTable;
+    }
+
+    @Override
+    protected void populateSubnet(Set<ConnectPoint> cpts, Set<IpPrefix> subnets) {
+        subnetTable.forEach((k, v) -> {
+            if (!cpts.contains(k)) {
+                subnetTable.get(k).removeAll(subnets);
+                if (subnetTable.get(k).isEmpty()) {
+                    subnetTable.remove(k);
+                }
+            }
+        });
+
+        cpts.forEach(cpt -> subnetTable.put(cpt, subnets));
+    }
+
+    @Override
+    protected boolean revokeSubnet(Set<IpPrefix> subnets) {
+        for (Map.Entry<ConnectPoint, Set<IpPrefix>> entry : subnetTable.entrySet()) {
+            entry.getValue().removeAll(subnets);
+            if (entry.getValue().isEmpty()) {
+                subnetTable.remove(entry.getKey());
+            }
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java
new file mode 100644
index 0000000..0abbf2a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDevice.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import org.onosproject.net.Annotations;
+import org.onosproject.net.DefaultDevice;
+import org.onosproject.net.DeviceId;
+/**
+ * Test fixture for the device service.
+ */
+public class MockDevice extends DefaultDevice {
+
+        public MockDevice(DeviceId id, Annotations annotations) {
+            super(null, id, null, null, null, null, null, null, annotations);
+        }
+ }
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java
new file mode 100644
index 0000000..1d173f6
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockDeviceService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultPort;
+import org.onosproject.net.Port;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceServiceAdapter;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Test fixture for the device service.
+ */
+public class MockDeviceService extends DeviceServiceAdapter {
+    private List<Device> devices = new LinkedList<>();
+    private DeviceListener listener;
+
+    public void addDevice(Device dev) {
+        devices.add(dev);
+    }
+
+    public void addMultipleDevices(Set<Device> devicesToAdd) {
+        devicesToAdd.forEach(dev -> devices.add(dev));
+    }
+
+    @Override
+    public Device getDevice(DeviceId deviceId) {
+        for (Device dev : devices) {
+            if (dev.id().equals(deviceId)) {
+                return dev;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Port getPort(ConnectPoint cp) {
+        return new DefaultPort(null, null, false);
+    }
+
+    @Override
+    public Iterable<Device> getAvailableDevices() {
+        return devices;
+    }
+
+    @Override
+    public void addListener(DeviceListener listener) {
+        this.listener = listener;
+    }
+
+    /**
+     * Get the listener.
+     */
+    public DeviceListener getListener() {
+        return listener;
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java
new file mode 100644
index 0000000..138b98a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockFlowObjectiveService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.EthCriterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.instructions.L2ModificationInstruction;
+import org.onosproject.net.flowobjective.FlowObjectiveServiceAdapter;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+
+import java.util.Map;
+
+/**
+ * Mock Flow Objective Service.
+ */
+public class MockFlowObjectiveService extends FlowObjectiveServiceAdapter {
+    private Map<MockBridgingTableKey, MockBridgingTableValue> bridgingTable;
+    private Map<Integer, TrafficTreatment> nextTable;
+
+    MockFlowObjectiveService(Map<MockBridgingTableKey, MockBridgingTableValue> bridgingTable,
+                             Map<Integer, TrafficTreatment> nextTable) {
+        this.bridgingTable = bridgingTable;
+        this.nextTable = nextTable;
+    }
+
+    @Override
+    public void forward(DeviceId deviceId, ForwardingObjective forwardingObjective) {
+        TrafficSelector selector = forwardingObjective.selector();
+        TrafficTreatment treatment = nextTable.get(forwardingObjective.nextId());
+        MacAddress macAddress = ((EthCriterion) selector.getCriterion(Criterion.Type.ETH_DST)).mac();
+        VlanId vlanId = ((VlanIdCriterion) selector.getCriterion(Criterion.Type.VLAN_VID)).vlanId();
+
+        boolean popVlan = treatment.allInstructions().stream()
+                .filter(instruction -> instruction.type().equals(Instruction.Type.L2MODIFICATION))
+                .anyMatch(instruction -> ((L2ModificationInstruction) instruction).subtype()
+                        .equals(L2ModificationInstruction.L2SubType.VLAN_POP));
+        PortNumber portNumber = treatment.allInstructions().stream()
+                .filter(instruction -> instruction.type().equals(Instruction.Type.OUTPUT))
+                .map(instruction -> ((Instructions.OutputInstruction) instruction).port()).findFirst().orElse(null);
+        if (portNumber == null) {
+            throw new IllegalArgumentException();
+        }
+
+        Objective.Operation op = forwardingObjective.op();
+
+        MockBridgingTableKey btKey = new MockBridgingTableKey(deviceId, macAddress, vlanId);
+        MockBridgingTableValue btValue = new MockBridgingTableValue(popVlan, portNumber);
+
+        if (op.equals(Objective.Operation.ADD)) {
+            bridgingTable.put(btKey, btValue);
+        } else if (op.equals(Objective.Operation.REMOVE)) {
+            bridgingTable.remove(btKey, btValue);
+        } else {
+            throw new IllegalArgumentException();
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java
new file mode 100644
index 0000000..e8c4701
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockHostService.java
@@ -0,0 +1,45 @@
+/*
+ * 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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.host.HostServiceAdapter;
+
+import java.util.Set;
+
+/**
+ * Mock Host Service.
+ */
+public class MockHostService extends HostServiceAdapter {
+    private Set<Host> hosts;
+
+    MockHostService(Set<Host> hosts) {
+        this.hosts = ImmutableSet.copyOf(hosts);
+    }
+
+    @Override
+    public Set<Host> getHosts() {
+        return hosts;
+    }
+
+    @Override
+    public Host getHost(HostId hostId) {
+        return hosts.stream().filter(host -> hostId.equals(host.id())).findFirst().orElse(null);
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java
new file mode 100644
index 0000000..f8d04ca
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockInterfaceService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceServiceAdapter;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Mock Interface Service.
+ */
+public class MockInterfaceService extends InterfaceServiceAdapter {
+    private Set<Interface> interfaces;
+
+    MockInterfaceService(Set<Interface> interfaces) {
+        this.interfaces = ImmutableSet.copyOf(interfaces);
+    }
+
+    @Override
+    public Set<Interface> getInterfacesByPort(ConnectPoint cp) {
+        return interfaces.stream().filter(intf -> cp.equals(intf.connectPoint()))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Interface> getInterfaces() {
+        return interfaces;
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java
new file mode 100644
index 0000000..2819c9d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLinkHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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.segmentrouting;
+
+import org.onosproject.net.HostLocation;
+
+/**
+ * Mocks the LinkHandler in SR.
+ *
+ */
+public class MockLinkHandler extends LinkHandler {
+
+    MockLinkHandler(SegmentRoutingManager srManager) {
+        super(srManager, null);
+    }
+
+    @Override
+    void checkUplinksForDualHomedHosts(HostLocation loc) {
+        // currently does nothing - can be extended to be a useful mock when
+        // implementing unit tests for link handling
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.java
new file mode 100644
index 0000000..b0cdf43
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockLocationProbingService.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.segmentrouting;
+
+import com.google.common.collect.Lists;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Host;
+import org.onosproject.net.host.HostLocationProbingService;
+
+import java.util.List;
+import java.util.Objects;
+
+public class MockLocationProbingService implements HostLocationProbingService {
+    List<Probe> probes;
+
+    private class Probe {
+        private Host host;
+        private ConnectPoint connectPoint;
+        private ProbeMode probeMode;
+
+        Probe(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+            this.host = host;
+            this.connectPoint = connectPoint;
+            this.probeMode = probeMode;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof Probe)) {
+                return false;
+            }
+            Probe that = (Probe) o;
+            return (Objects.equals(this.host, that.host) &&
+                    Objects.equals(this.connectPoint, that.connectPoint) &&
+                    Objects.equals(this.probeMode, that.probeMode));
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(host, connectPoint, probeMode);
+        }
+    }
+
+    MockLocationProbingService() {
+        probes = Lists.newArrayList();
+    }
+
+    boolean verifyProbe(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+        Probe probe = new Probe(host, connectPoint, probeMode);
+        return probes.contains(probe);
+    }
+
+    @Override
+    public void probeHostLocation(Host host, ConnectPoint connectPoint, ProbeMode probeMode) {
+        probes.add(new Probe(host, connectPoint, probeMode));
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.java
new file mode 100644
index 0000000..5494e27
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockMastershipService.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.segmentrouting;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.mastership.MastershipServiceAdapter;
+import org.onosproject.net.DeviceId;
+
+import java.util.Set;
+
+/**
+ * Mock Mastership Service.
+ */
+public class MockMastershipService extends MastershipServiceAdapter {
+    // A set of devices of which we have mastership.
+    private Set<DeviceId> localDevices;
+
+    MockMastershipService(Set<DeviceId> localDevices) {
+        this.localDevices = ImmutableSet.copyOf(localDevices);
+    }
+
+    @Override
+    public boolean isLocalMaster(DeviceId deviceId) {
+        return localDevices.contains(deviceId);
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java
new file mode 100644
index 0000000..09f8da2
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockNetworkConfigRegistry.java
@@ -0,0 +1,43 @@
+/*
+ * 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.segmentrouting;
+
+import com.google.common.collect.Sets;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+
+import java.util.Set;
+
+/**
+ * Mock Network Config Registry.
+ */
+class MockNetworkConfigRegistry extends NetworkConfigRegistryAdapter {
+    private Set<Config> configs = Sets.newHashSet();
+
+    public void applyConfig(Config config) {
+        configs.add(config);
+    }
+
+    @Override
+    public <S, C extends Config<S>> C getConfig(S subject, Class<C> configClass) {
+        Config c = configs.stream()
+                .filter(config -> subject.equals(config.subject()))
+                .filter(config -> configClass.equals(config.getClass()))
+                .findFirst().orElse(null);
+        return (C) c;
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java
new file mode 100644
index 0000000..7927636
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRouteService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.segmentrouting;
+
+import com.google.common.collect.Sets;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteInfo;
+import org.onosproject.routeservice.RouteServiceAdapter;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Mock Route Service.
+ * We assume there is only one routing table named "default".
+ */
+public class MockRouteService extends RouteServiceAdapter {
+    private Map<MockRoutingTableKey, MockRoutingTableValue> routingTable;
+
+    MockRouteService(Map<MockRoutingTableKey, MockRoutingTableValue> routingTable) {
+        this.routingTable = routingTable;
+    }
+
+    @Override
+    public Collection<RouteInfo> getRoutes(RouteTableId id) {
+        return routingTable.entrySet().stream().map(e -> {
+            IpPrefix prefix = e.getKey().ipPrefix;
+            IpAddress nextHop = IpAddress.valueOf(0); // dummy
+            MacAddress mac = e.getValue().macAddress;
+            VlanId vlan = e.getValue().vlanId;
+            Route route = new Route(Route.Source.STATIC, prefix, nextHop);
+            ResolvedRoute rr = new ResolvedRoute(route, mac, vlan);
+
+            return new RouteInfo(prefix, rr, Sets.newHashSet(rr));
+        }).collect(Collectors.toSet());
+    }
+
+    @Override
+    public Collection<RouteTableId> getRouteTables() {
+        return Sets.newHashSet(new RouteTableId("default"));
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java
new file mode 100644
index 0000000..4eab7c0
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingRulePopulator.java
@@ -0,0 +1,54 @@
+/*
+ * 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.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Map;
+
+/**
+ * Mock Routing Rule Populator.
+ */
+public class MockRoutingRulePopulator extends RoutingRulePopulator {
+    private Map<MockRoutingTableKey, MockRoutingTableValue> routingTable;
+
+    MockRoutingRulePopulator(SegmentRoutingManager srManager,
+                             Map<MockRoutingTableKey, MockRoutingTableValue> routingTable) {
+        super(srManager);
+        this.routingTable = routingTable;
+    }
+
+    @Override
+    public void populateRoute(DeviceId deviceId, IpPrefix prefix,
+                              MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        MockRoutingTableKey rtKey = new MockRoutingTableKey(deviceId, prefix);
+        MockRoutingTableValue rtValue = new MockRoutingTableValue(outPort, hostMac, hostVlanId);
+        routingTable.put(rtKey, rtValue);
+    }
+
+    @Override
+    public void revokeRoute(DeviceId deviceId, IpPrefix prefix,
+                            MacAddress hostMac, VlanId hostVlanId, PortNumber outPort) {
+        MockRoutingTableKey rtKey = new MockRoutingTableKey(deviceId, prefix);
+        MockRoutingTableValue rtValue = new MockRoutingTableValue(outPort, hostMac, hostVlanId);
+        routingTable.remove(rtKey, rtValue);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java
new file mode 100644
index 0000000..4f09c2a
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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.segmentrouting;
+
+import org.onlab.packet.IpPrefix;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * Mock Routing Table Key.
+ */
+class MockRoutingTableKey {
+    DeviceId deviceId;
+    IpPrefix ipPrefix;
+
+    MockRoutingTableKey(DeviceId deviceId, IpPrefix ipPrefix) {
+        this.deviceId = deviceId;
+        this.ipPrefix = ipPrefix;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof MockRoutingTableKey)) {
+            return false;
+        }
+        final MockRoutingTableKey other = (MockRoutingTableKey) obj;
+        return Objects.equals(this.deviceId, other.deviceId) &&
+                Objects.equals(this.ipPrefix, other.ipPrefix);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, ipPrefix);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.java
new file mode 100644
index 0000000..5842d2d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockRoutingTableValue.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.segmentrouting;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+/**
+ * Mock Routing Table Value.
+ */
+class MockRoutingTableValue {
+    PortNumber portNumber;
+    MacAddress macAddress;
+    VlanId vlanId;
+
+    MockRoutingTableValue(PortNumber portNumber, MacAddress macAddress, VlanId vlanId) {
+        this.portNumber = portNumber;
+        this.macAddress = macAddress;
+        this.vlanId = vlanId;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof MockRoutingTableValue)) {
+            return false;
+        }
+        final MockRoutingTableValue other = (MockRoutingTableValue) obj;
+        return Objects.equals(this.portNumber, other.portNumber) &&
+                Objects.equals(this.macAddress, other.macAddress) &&
+                Objects.equals(this.vlanId, other.vlanId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(portNumber, macAddress, vlanId);
+    }
+}
+
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
new file mode 100644
index 0000000..ad098b4
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/MockSegmentRoutingManager.java
@@ -0,0 +1,49 @@
+/*
+ * 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.segmentrouting;
+
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Mock Segment Routing Manager.
+ */
+public class MockSegmentRoutingManager extends SegmentRoutingManager {
+    private Map<Integer, TrafficTreatment> nextTable;
+    private AtomicInteger atomicNextId = new AtomicInteger();
+
+    MockSegmentRoutingManager(Map<Integer, TrafficTreatment> nextTable) {
+        appId = new DefaultApplicationId(1, SegmentRoutingManager.APP_NAME);
+        this.nextTable = nextTable;
+    }
+
+    @Override
+    public int getPortNextObjectiveId(DeviceId deviceId, PortNumber portNum,
+                                      TrafficTreatment treatment,
+                                      TrafficSelector meta,
+                                      boolean createIfMissing) {
+        int nextId = atomicNextId.incrementAndGet();
+        nextTable.put(nextId, treatment);
+        return nextId;
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java
new file mode 100644
index 0000000..6ab4bad
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PortAuthTrackerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.segmentrouting.PortAuthTracker.BlockState;
+import org.onosproject.segmentrouting.config.BlockedPortsConfig;
+import org.onosproject.segmentrouting.config.BlockedPortsConfigTest;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.net.ConnectPoint.deviceConnectPoint;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.AUTHENTICATED;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.BLOCKED;
+import static org.onosproject.segmentrouting.PortAuthTracker.BlockState.UNCHECKED;
+
+/**
+ * Unit Tests for {@link PortAuthTracker}.
+ */
+public class PortAuthTrackerTest {
+    private static final ApplicationId APP_ID = new DefaultApplicationId(1, "foo");
+    private static final String KEY = "blocked";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+    private static final String PATH_CFG = "/blocked-ports.json";
+    private static final String PATH_CFG_ALT = "/blocked-ports-alt.json";
+
+    private static final String DEV1 = "of:0000000000000001";
+    private static final String DEV3 = "of:0000000000000003";
+    private static final String DEV4 = "of:0000000000000004";
+
+    private BlockedPortsConfig cfg;
+    private AugmentedPortAuthTracker tracker;
+
+    private void print(String s) {
+        System.out.println(s);
+    }
+
+    private void print(Object o) {
+        print(o.toString());
+    }
+
+    private void print(String fmt, Object... params) {
+        print(String.format(fmt, params));
+    }
+
+    private void title(String s) {
+        print("=== %s ===", s);
+    }
+
+    private BlockedPortsConfig makeConfig(String path) throws IOException {
+        InputStream blockedPortsJson = BlockedPortsConfigTest.class
+                .getResourceAsStream(path);
+        JsonNode node = MAPPER.readTree(blockedPortsJson);
+        BlockedPortsConfig cfg = new BlockedPortsConfig();
+        cfg.init(APP_ID, KEY, node, MAPPER, null);
+        return cfg;
+    }
+
+    ConnectPoint cp(String devId, int port) {
+        return ConnectPoint.deviceConnectPoint(devId + "/" + port);
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        cfg = makeConfig(PATH_CFG);
+        tracker = new AugmentedPortAuthTracker();
+    }
+
+    private void verifyPortState(String devId, int first, BlockState... states) {
+        DeviceId dev = deviceId(devId);
+        int last = first + states.length;
+        int pn = first;
+        int i = 0;
+        while (pn < last) {
+            PortNumber pnum = portNumber(pn);
+            BlockState actual = tracker.currentState(dev, pnum);
+            print("%s/%s [%s]  --> %s", devId, pn, states[i], actual);
+            assertEquals("oops: " + devId + "/" + pn + "~" + actual,
+                         states[i], actual);
+            pn++;
+            i++;
+        }
+    }
+
+    @Test
+    public void basic() {
+        title("basic");
+        print(tracker);
+        print(cfg);
+
+        assertEquals("wrong entry count", 0, tracker.entryCount());
+
+        // let's assume that the net config just got loaded..
+        tracker.configurePortBlocking(cfg);
+        assertEquals("wrong entry count", 13, tracker.entryCount());
+
+        verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+        verifyPortState(DEV1, 6, UNCHECKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+
+        verifyPortState(DEV3, 1, UNCHECKED, UNCHECKED, UNCHECKED);
+        verifyPortState(DEV3, 6, UNCHECKED, BLOCKED, BLOCKED, BLOCKED, UNCHECKED);
+
+        verifyPortState(DEV4, 1, BLOCKED, UNCHECKED, UNCHECKED, UNCHECKED, BLOCKED);
+    }
+
+    @Test
+    public void logonLogoff() {
+        title("logonLogoff");
+
+        tracker.configurePortBlocking(cfg);
+        assertEquals("wrong entry count", 13, tracker.entryCount());
+        verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED);
+
+        ConnectPoint cp = deviceConnectPoint(DEV1 + "/2");
+        tracker.radiusAuthorize(cp);
+        print("");
+        verifyPortState(DEV1, 1, BLOCKED, AUTHENTICATED, BLOCKED);
+
+        tracker.radiusLogoff(cp);
+        print("");
+        verifyPortState(DEV1, 1, BLOCKED, BLOCKED, BLOCKED);
+    }
+
+    @Test
+    public void installedFlows() {
+        title("installed flows");
+
+        assertEquals(0, tracker.installed.size());
+        tracker.configurePortBlocking(cfg);
+        assertEquals(13, tracker.installed.size());
+
+        assertTrue(tracker.installed.contains(cp(DEV1, 1)));
+        assertTrue(tracker.installed.contains(cp(DEV3, 7)));
+        assertTrue(tracker.installed.contains(cp(DEV4, 5)));
+    }
+
+    @Test
+    public void flowsLogonLogoff() {
+        title("flows logon logoff");
+
+        tracker.configurePortBlocking(cfg);
+
+        // let's pick a connect point from the configuration
+        ConnectPoint cp = cp(DEV4, 5);
+
+        assertTrue(tracker.installed.contains(cp));
+        assertEquals(0, tracker.cleared.size());
+
+        tracker.resetMetrics();
+        tracker.radiusAuthorize(cp);
+        // verify we requested the blocking flow to be cleared
+        assertTrue(tracker.cleared.contains(cp));
+
+        tracker.resetMetrics();
+        assertEquals(0, tracker.installed.size());
+        tracker.radiusLogoff(cp);
+        // verify we requested the blocking flow to be reinstated
+        assertTrue(tracker.installed.contains(cp));
+    }
+
+    @Test
+    public void uncheckedPortIgnored() {
+        title("unchecked port ignored");
+
+        tracker.configurePortBlocking(cfg);
+        tracker.resetMetrics();
+
+        // let's pick a connect point NOT in the configuration
+        ConnectPoint cp = cp(DEV4, 2);
+        assertEquals(BlockState.UNCHECKED, tracker.currentState(cp));
+
+        assertEquals(0, tracker.installed.size());
+        assertEquals(0, tracker.cleared.size());
+        tracker.radiusAuthorize(cp);
+        assertEquals(0, tracker.installed.size());
+        assertEquals(0, tracker.cleared.size());
+        tracker.radiusLogoff(cp);
+        assertEquals(0, tracker.installed.size());
+        assertEquals(0, tracker.cleared.size());
+    }
+
+    @Test
+    public void reconfiguration() throws IOException {
+        title("reconfiguration");
+
+        /* see 'blocked-ports.json' and 'blocked-ports-alt.json'
+
+          cfg:  "1": ["1-4", "7-9"],
+                "3": ["7-9"],
+                "4": ["1", "5", "9"]
+
+          alt:  "1": ["1-9"],
+                "3": ["7"],
+                "4": ["1"]
+         */
+        tracker.configurePortBlocking(cfg);
+        // dev1: ports 5 and 6 are NOT configured in the original CFG
+        assertFalse(tracker.installed.contains(cp(DEV1, 5)));
+        assertFalse(tracker.installed.contains(cp(DEV1, 6)));
+
+        tracker.resetMetrics();
+        assertEquals(0, tracker.installed.size());
+        assertEquals(0, tracker.cleared.size());
+
+        BlockedPortsConfig alt = makeConfig(PATH_CFG_ALT);
+        tracker.configurePortBlocking(alt);
+
+        // dev1: ports 5 and 6 ARE configured in the alternate CFG
+        assertTrue(tracker.installed.contains(cp(DEV1, 5)));
+        assertTrue(tracker.installed.contains(cp(DEV1, 6)));
+
+        // also, check for the ports that were decommissioned
+        assertTrue(tracker.cleared.contains(cp(DEV3, 8)));
+        assertTrue(tracker.cleared.contains(cp(DEV3, 9)));
+        assertTrue(tracker.cleared.contains(cp(DEV4, 5)));
+        assertTrue(tracker.cleared.contains(cp(DEV4, 9)));
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java
new file mode 100644
index 0000000..23d1233
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/PwaasConfigTest.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2016-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.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.MplsLabel;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.InterfaceIpAddress;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.segmentrouting.config.PwaasConfig;
+
+import org.onosproject.segmentrouting.pwaas.L2Tunnel;
+import org.onosproject.segmentrouting.pwaas.L2TunnelDescription;
+import org.onosproject.segmentrouting.pwaas.L2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.DefaultL2Tunnel;
+import org.onosproject.segmentrouting.pwaas.DefaultL2TunnelPolicy;
+import org.onosproject.segmentrouting.pwaas.L2Mode;
+
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for class {@link PwaasConfig}.
+ */
+public class PwaasConfigTest {
+
+    private static final String TUNNEL_ID_1 = "1";
+    private static final String TUNNEL_ID_2 = "20";
+    private static final String NOT_PRESENT_TUNNEL_ID = "2";
+    private static final ConnectPoint INGRESS_1 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+    private static final ConnectPoint INGRESS_2 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+    private static final ConnectPoint EGRESS_1 = ConnectPoint.deviceConnectPoint("of:0000000000000002/1");
+    private static final ConnectPoint EGRESS_2 = ConnectPoint.deviceConnectPoint("of:0000000000000002/1");
+    private static final VlanId INGRESS_INNER_TAG_1 = VlanId.vlanId("10");
+    private static final VlanId INGRESS_INNER_TAG_2 = VlanId.vlanId("100");
+    private static final VlanId INGRESS_OUTER_TAG_1 = VlanId.vlanId("20");
+    private static final VlanId INGRESS_OUTER_TAG_2 = VlanId.vlanId("200");
+    private static final VlanId EGRESS_INNER_TAG_1 = VlanId.vlanId("10");
+    private static final VlanId EGRESS_INNER_TAG_2 = VlanId.vlanId("100");
+    private static final VlanId EGRESS_OUTER_TAG_1 = VlanId.vlanId("21");
+    private static final VlanId EGRESS_OUTER_TAG_2 = VlanId.vlanId("210");
+    private static final String MODE_1 = "RAW";
+    private static final String MODE_2 = "RAW";
+    private static final VlanId SD_TAG_1 = VlanId.NONE;
+    private static final VlanId SD_TAG_2 = VlanId.NONE;
+    private static final MplsLabel PW_LABEL_1 = MplsLabel.mplsLabel("255");
+    private static final MplsLabel PW_LABEL_2 = MplsLabel.mplsLabel("1255");
+
+    /*
+     * Configuration below copied from host handler test.
+     */
+
+    // Host Mac, VLAN
+    private static final ProviderId PROVIDER_ID = ProviderId.NONE;
+    private static final MacAddress HOST_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId HOST_VLAN_UNTAGGED = VlanId.NONE;
+    private static final HostId HOST_ID_UNTAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_UNTAGGED);
+    private static final VlanId HOST_VLAN_TAGGED = VlanId.vlanId((short) 20);
+    private static final HostId HOST_ID_TAGGED = HostId.hostId(HOST_MAC, HOST_VLAN_TAGGED);
+    // Host IP
+    private static final IpAddress HOST_IP11 = IpAddress.valueOf("10.0.1.1");
+    private static final IpAddress HOST_IP21 = IpAddress.valueOf("10.0.2.1");
+    private static final IpAddress HOST_IP12 = IpAddress.valueOf("10.0.1.2");
+    private static final IpAddress HOST_IP13 = IpAddress.valueOf("10.0.1.3");
+    private static final IpAddress HOST_IP14 = IpAddress.valueOf("10.0.1.4");
+    private static final IpAddress HOST_IP32 = IpAddress.valueOf("10.0.3.2");
+    // Device
+    private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+    private static final DeviceId DEV3 = DeviceId.deviceId("of:0000000000000003");
+    private static final DeviceId DEV4 = DeviceId.deviceId("of:0000000000000004");
+    private static final DeviceId DEV5 = DeviceId.deviceId("of:0000000000000005");
+    private static final DeviceId DEV6 = DeviceId.deviceId("of:0000000000000006");
+    // Port
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+    private static final PortNumber P9 = PortNumber.portNumber(9);
+    // Connect Point
+    private static final ConnectPoint CP11 = new ConnectPoint(DEV1, P1);
+    private static final HostLocation HOST_LOC11 = new HostLocation(CP11, 0);
+    private static final ConnectPoint CP12 = new ConnectPoint(DEV1, P2);
+    private static final HostLocation HOST_LOC12 = new HostLocation(CP12, 0);
+    private static final ConnectPoint CP13 = new ConnectPoint(DEV1, P3);
+    private static final HostLocation HOST_LOC13 = new HostLocation(CP13, 0);
+    private static final ConnectPoint CP21 = new ConnectPoint(DEV2, P1);
+    private static final HostLocation HOST_LOC21 = new HostLocation(CP21, 0);
+    private static final ConnectPoint CP22 = new ConnectPoint(DEV2, P2);
+    private static final HostLocation HOST_LOC22 = new HostLocation(CP22, 0);
+    // Connect Point for dual-homed host failover
+    private static final ConnectPoint CP31 = new ConnectPoint(DEV3, P1);
+    private static final HostLocation HOST_LOC31 = new HostLocation(CP31, 0);
+    private static final ConnectPoint CP32 = new ConnectPoint(DEV3, P2);
+    private static final HostLocation HOST_LOC32 = new HostLocation(CP32, 0);
+    private static final ConnectPoint CP41 = new ConnectPoint(DEV4, P1);
+    private static final HostLocation HOST_LOC41 = new HostLocation(CP41, 0);
+    private static final ConnectPoint CP39 = new ConnectPoint(DEV3, P9);
+    private static final ConnectPoint CP49 = new ConnectPoint(DEV4, P9);
+    // Conenct Point for mastership test
+    private static final ConnectPoint CP51 = new ConnectPoint(DEV5, P1);
+    private static final HostLocation HOST_LOC51 = new HostLocation(CP51, 0);
+    private static final ConnectPoint CP61 = new ConnectPoint(DEV6, P1);
+    private static final HostLocation HOST_LOC61 = new HostLocation(CP61, 0);
+    // Interface VLAN
+    private static final VlanId INTF_VLAN_UNTAGGED = VlanId.vlanId((short) 10);
+    private static final Set<VlanId> INTF_VLAN_TAGGED = Sets.newHashSet(VlanId.vlanId((short) 20));
+    private static final VlanId INTF_VLAN_NATIVE = VlanId.vlanId((short) 30);
+    private static final Set<VlanId> INTF_VLAN_PAIR = Sets.newHashSet(VlanId.vlanId((short) 10),
+                                     VlanId.vlanId((short) 20), VlanId.vlanId((short) 30));
+    private static final VlanId INTF_VLAN_OTHER = VlanId.vlanId((short) 40);
+    // Interface subnet
+    private static final IpPrefix INTF_PREFIX1 = IpPrefix.valueOf("10.0.1.254/24");
+    private static final IpPrefix INTF_PREFIX2 = IpPrefix.valueOf("10.0.2.254/24");
+    private static final IpPrefix INTF_PREFIX3 = IpPrefix.valueOf("10.0.3.254/24");
+    private static final InterfaceIpAddress INTF_IP1 =
+            new InterfaceIpAddress(INTF_PREFIX1.address(), INTF_PREFIX1);
+    private static final InterfaceIpAddress INTF_IP2 =
+            new InterfaceIpAddress(INTF_PREFIX2.address(), INTF_PREFIX2);
+    private static final InterfaceIpAddress INTF_IP3 =
+            new InterfaceIpAddress(INTF_PREFIX3.address(), INTF_PREFIX3);
+    // Interfaces
+    private static final Interface INTF11 =
+            new Interface(null, CP11, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF12 =
+            new Interface(null, CP12, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF13 =
+            new Interface(null, CP13, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
+                          null, INTF_VLAN_TAGGED, INTF_VLAN_NATIVE);
+    private static final Interface INTF21 =
+            new Interface(null, CP21, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF22 =
+            new Interface(null, CP22, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF31 =
+            new Interface(null, CP31, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF32 =
+            new Interface(null, CP32, Lists.newArrayList(INTF_IP3), MacAddress.NONE, null,
+                          INTF_VLAN_OTHER, null, null);
+    private static final Interface INTF39 =
+            new Interface(null, CP39, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          null, INTF_VLAN_PAIR, null);
+    private static final Interface INTF41 =
+            new Interface(null, CP41, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          INTF_VLAN_UNTAGGED, null, null);
+    private static final Interface INTF49 =
+            new Interface(null, CP49, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                          null, INTF_VLAN_PAIR, null);
+    // Host
+    private static final Host HOST1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC,
+                                                      HOST_VLAN_UNTAGGED,
+                                                      Sets.newHashSet(HOST_LOC11, HOST_LOC21),
+                                                      Sets.newHashSet(HOST_IP11),
+                                                      false);
+
+    // A set of hosts
+    private static final Set<Host> HOSTS = Sets.newHashSet(HOST1);
+    // A set of devices of which we have mastership
+    private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(DEV1, DEV2, DEV3, DEV4);
+    // A set of interfaces
+    private static final Set<Interface> INTERFACES = Sets.newHashSet(INTF11, INTF12, INTF13, INTF21,
+                                                                     INTF22, INTF31, INTF32, INTF39, INTF41, INTF49);
+
+    private PwaasConfig config;
+    private PwaasConfig invalidConfigVlan;
+    private PwaasConfig invalidConfigMode;
+    private PwaasConfig invalidConfigLabel;
+    private PwaasConfig invalidConfigConflictingVlan;
+
+    @Before
+    public void setUp() throws Exception {
+        InputStream jsonStream = PwaasConfig.class
+                .getResourceAsStream("/pwaas.json");
+        InputStream jsonStreamInvalid1 = PwaasConfig.class
+                .getResourceAsStream("/pwaas-invalid-mode.json");
+        InputStream jsonStreamInvalid2 = PwaasConfig.class
+                .getResourceAsStream("/pwaas-invalid-pwlabel.json");
+        InputStream jsonStreamInvalid3 = PwaasConfig.class
+                .getResourceAsStream("/pwaas-invalid-vlan.json");
+        InputStream jsonStreamInvalid4 = PwaasConfig.class
+                .getResourceAsStream("/pwaas-conflicting-vlan.json");
+
+        String key = SegmentRoutingManager.APP_NAME;
+        ApplicationId subject = new TestApplicationId(key);
+        ObjectMapper mapper = new ObjectMapper();
+
+        JsonNode jsonNode = mapper.readTree(jsonStream);
+        JsonNode jsonNodeInvalid1 = mapper.readTree(jsonStreamInvalid1);
+        JsonNode jsonNodeInvalid2 = mapper.readTree(jsonStreamInvalid2);
+        JsonNode jsonNodeInvalid3 = mapper.readTree(jsonStreamInvalid3);
+        JsonNode jsonNodeInvalid4 = mapper.readTree(jsonStreamInvalid4);
+
+        ConfigApplyDelegate delegate = new MockDelegate();
+
+        DeviceService devService = new MockDeviceService();
+        InterfaceService infService = new MockInterfaceService(INTERFACES);
+
+        // create two devices and add them
+        DefaultAnnotations.Builder builderDev1 = DefaultAnnotations.builder();
+        DefaultAnnotations.Builder builderDev2 = DefaultAnnotations.builder();
+
+        Device dev1 = new MockDevice(DEV1, builderDev1.build());
+        Device dev2 = new MockDevice(DEV2, builderDev2.build());
+        ((MockDeviceService) devService).addDevice(dev1);
+        ((MockDeviceService) devService).addDevice(dev2);
+
+        config = new PwaasConfig(devService, infService);
+        invalidConfigVlan = new PwaasConfig(devService, infService);
+        invalidConfigMode = new PwaasConfig(devService, infService);
+        invalidConfigLabel = new PwaasConfig(devService, infService);
+        invalidConfigConflictingVlan = new PwaasConfig(devService, infService);
+
+        config.init(subject, key, jsonNode, mapper, delegate);
+        invalidConfigVlan.init(subject, key, jsonNodeInvalid1, mapper, delegate);
+        invalidConfigMode.init(subject, key, jsonNodeInvalid2, mapper, delegate);
+        invalidConfigLabel.init(subject, key, jsonNodeInvalid3, mapper, delegate);
+        invalidConfigConflictingVlan.init(subject, key, jsonNodeInvalid4, mapper, delegate);
+
+        config.deviceService = devService;
+        config.intfService = infService;
+
+        invalidConfigVlan.deviceService = devService;
+        invalidConfigVlan.intfService = infService;
+
+        invalidConfigLabel.deviceService = devService;
+        invalidConfigLabel.intfService = infService;
+
+        invalidConfigMode.deviceService = devService;
+        invalidConfigMode.intfService = infService;
+
+        invalidConfigConflictingVlan.deviceService = devService;
+        invalidConfigConflictingVlan.intfService = infService;
+    }
+
+    /**
+     * Tests config validity.
+     */
+    @Test
+    public void testIsValid() {
+        try {
+            assertTrue(config.isValid());
+        } catch (IllegalArgumentException e) {
+            assertTrue(false);
+        }
+    }
+
+    /**
+     * Tests config in-validity.
+     */
+    @Test
+    public void testValid1() {
+        assertFalse(invalidConfigVlan.isValid());
+    }
+
+    @Test
+    public void testValid2() {
+        assertFalse(invalidConfigMode.isValid());
+    }
+
+    @Test
+    public void testValid3() {
+        assertFalse(invalidConfigLabel.isValid());
+    }
+
+    @Test
+    public void testValid4() {
+        assertFalse(invalidConfigConflictingVlan.isValid());
+    }
+
+    /**
+     * Tests getPwIds.
+     */
+    @Test
+    public void testGetPwIds() {
+        Set<Long> pwIds = config.getPwIds();
+
+        assertThat(pwIds.size(), is(2));
+        assertTrue(pwIds.contains(Long.parseLong(TUNNEL_ID_1)));
+        assertTrue(pwIds.contains(Long.parseLong(TUNNEL_ID_2)));
+        assertFalse(pwIds.contains(Long.parseLong(NOT_PRESENT_TUNNEL_ID)));
+    }
+
+    /**
+     * Tests getPwDescription.
+     */
+    @Test
+    public void testGetPwDescription() {
+        L2TunnelDescription l2TunnelDescription = null;
+
+        L2Tunnel l2Tunnel = new DefaultL2Tunnel(
+            L2Mode.valueOf(MODE_1),
+            SD_TAG_1,
+            Long.parseLong(TUNNEL_ID_1),
+            PW_LABEL_1
+        );
+        L2TunnelPolicy l2TunnelPolicy = new DefaultL2TunnelPolicy(
+                Long.parseLong(TUNNEL_ID_1),
+                INGRESS_1,
+                INGRESS_INNER_TAG_1,
+                INGRESS_OUTER_TAG_1,
+                EGRESS_1,
+                EGRESS_INNER_TAG_1,
+                EGRESS_OUTER_TAG_1
+        );
+
+        l2TunnelDescription = config.getPwDescription(Long.parseLong(TUNNEL_ID_1));
+        assertThat(l2TunnelDescription.l2Tunnel().pwMode(), is(l2Tunnel.pwMode()));
+        assertThat(l2TunnelDescription.l2Tunnel().sdTag(), is(l2Tunnel.sdTag()));
+        assertThat(l2TunnelDescription.l2Tunnel().tunnelId(), is(l2Tunnel.tunnelId()));
+        assertThat(l2TunnelDescription.l2Tunnel().pwLabel(), is(l2Tunnel.pwLabel()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().tunnelId(), is(l2TunnelPolicy.tunnelId()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1InnerTag(), is(l2TunnelPolicy.cP1InnerTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1OuterTag(), is(l2TunnelPolicy.cP1OuterTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2InnerTag(), is(l2TunnelPolicy.cP2InnerTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1(), is(l2TunnelPolicy.cP1()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2(), is(l2TunnelPolicy.cP2()));
+
+        l2Tunnel = new DefaultL2Tunnel(
+                L2Mode.valueOf(MODE_2),
+                SD_TAG_2,
+                Long.parseLong(TUNNEL_ID_2),
+                PW_LABEL_2
+        );
+        l2TunnelPolicy = new DefaultL2TunnelPolicy(
+                Long.parseLong(TUNNEL_ID_2),
+                INGRESS_2,
+                INGRESS_INNER_TAG_2,
+                INGRESS_OUTER_TAG_2,
+                EGRESS_2,
+                EGRESS_INNER_TAG_2,
+                EGRESS_OUTER_TAG_2
+        );
+
+        l2TunnelDescription = config.getPwDescription(Long.parseLong(TUNNEL_ID_2));
+        assertThat(l2TunnelDescription.l2Tunnel().pwMode(), is(l2Tunnel.pwMode()));
+        assertThat(l2TunnelDescription.l2Tunnel().sdTag(), is(l2Tunnel.sdTag()));
+        assertThat(l2TunnelDescription.l2Tunnel().tunnelId(), is(l2Tunnel.tunnelId()));
+        assertThat(l2TunnelDescription.l2Tunnel().pwLabel(), is(l2Tunnel.pwLabel()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().tunnelId(), is(l2TunnelPolicy.tunnelId()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1InnerTag(), is(l2TunnelPolicy.cP1InnerTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1OuterTag(), is(l2TunnelPolicy.cP1OuterTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2OuterTag(), is(l2TunnelPolicy.cP2OuterTag()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP1(), is(l2TunnelPolicy.cP1()));
+        assertThat(l2TunnelDescription.l2TunnelPolicy().cP2(), is(l2TunnelPolicy.cP2()));
+    }
+
+    private class MockDelegate implements ConfigApplyDelegate {
+        @Override
+        public void onApply(Config config) {
+        }
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
new file mode 100644
index 0000000..843ba6b
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/RouteHandlerTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.segmentrouting;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.NetworkConfigRegistryAdapter;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingDeviceConfig;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.easymock.EasyMock.reset;
+import static org.junit.Assert.*;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * Unit test for {@link RouteHandler}.
+ */
+public class RouteHandlerTest {
+    private SegmentRoutingManager srManager;
+    private RouteHandler routeHandler;
+    private HostService hostService;
+
+    // Mocked routing and bridging tables
+    private static final Map<MockBridgingTableKey, MockBridgingTableValue> BRIDGING_TABLE =
+            Maps.newConcurrentMap();
+    private static final Map<MockRoutingTableKey, MockRoutingTableValue> ROUTING_TABLE =
+            Maps.newConcurrentMap();
+    private static final Map<ConnectPoint, Set<IpPrefix>> SUBNET_TABLE = Maps.newConcurrentMap();
+    // Mocked Next Id
+    private static final Map<Integer, TrafficTreatment> NEXT_TABLE = Maps.newConcurrentMap();
+
+    private static final IpPrefix P1 = IpPrefix.valueOf("10.0.0.0/24");
+
+    // Single homed router 1
+    private static final IpAddress N1 = IpAddress.valueOf("10.0.1.254");
+    private static final MacAddress M1 = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId V1 = VlanId.vlanId((short) 1);
+    private static final ConnectPoint CP1 = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+    private static final Route R1 = new Route(Route.Source.STATIC, P1, N1);
+    private static final ResolvedRoute RR1 = new ResolvedRoute(R1, M1, V1);
+
+    // Single homed router 2
+    private static final IpAddress N2 = IpAddress.valueOf("10.0.2.254");
+    private static final MacAddress M2 = MacAddress.valueOf("00:00:00:00:00:02");
+    private static final VlanId V2 = VlanId.vlanId((short) 2);
+    private static final ConnectPoint CP2 = ConnectPoint.deviceConnectPoint("of:0000000000000002/2");
+    private static final Route R2 = new Route(Route.Source.STATIC, P1, N2);
+    private static final ResolvedRoute RR2 = new ResolvedRoute(R2, M2, V2);
+
+    // Dual homed router 1
+    private static final IpAddress N3 = IpAddress.valueOf("10.0.3.254");
+    private static final MacAddress M3 = MacAddress.valueOf("00:00:00:00:00:03");
+    private static final VlanId V3 = VlanId.vlanId((short) 3);
+    private static final Route R3 = new Route(Route.Source.STATIC, P1, N3);
+    private static final ResolvedRoute RR3 = new ResolvedRoute(R3, M3, V3);
+
+    // Hosts
+    private static final Host H1 = new DefaultHost(ProviderId.NONE, HostId.hostId(M1, V1), M1, V1,
+            Sets.newHashSet(new HostLocation(CP1, 0)), Sets.newHashSet(N1), false);
+    private static final Host H2 = new DefaultHost(ProviderId.NONE, HostId.hostId(M2, V2), M2, V2,
+            Sets.newHashSet(new HostLocation(CP2, 0)), Sets.newHashSet(N2), false);
+    private static final Host H3D = new DefaultHost(ProviderId.NONE, HostId.hostId(M3, V3), M3, V3,
+            Sets.newHashSet(new HostLocation(CP1, 0), new HostLocation(CP2, 0)), Sets.newHashSet(N3), false);
+    private static final Host H3S = new DefaultHost(ProviderId.NONE, HostId.hostId(M3, V3), M3, V3,
+            Sets.newHashSet(new HostLocation(CP1, 0)), Sets.newHashSet(N3), false);
+
+    // Pair Local Port
+    private static final PortNumber P9 = PortNumber.portNumber(9);
+
+    // A set of hosts
+    private static final Set<Host> HOSTS = Sets.newHashSet(H1, H2, H3D);
+    private static final Set<Host> HOSTS_ONE_FAIL = Sets.newHashSet(H1, H2, H3S);
+    private static final Set<Host> HOSTS_BOTH_FAIL = Sets.newHashSet(H1, H2);
+    // A set of devices of which we have mastership
+    private static final Set<DeviceId> LOCAL_DEVICES = Sets.newHashSet(CP1.deviceId(), CP2.deviceId());
+    // A set of interfaces
+    private static final Set<Interface> INTERFACES = Sets.newHashSet();
+
+    @Before
+    public void setUp() throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        ConfigApplyDelegate delegate = config -> { };
+
+        SegmentRoutingDeviceConfig dev1Config = new SegmentRoutingDeviceConfig();
+        JsonNode dev1Tree = mapper.createObjectNode();
+        dev1Config.init(CP1.deviceId(), "host-handler-test", dev1Tree, mapper, delegate);
+        dev1Config.setPairDeviceId(CP2.deviceId()).setPairLocalPort(P9);
+
+        SegmentRoutingDeviceConfig dev2Config = new SegmentRoutingDeviceConfig();
+        JsonNode dev2Tree = mapper.createObjectNode();
+        dev2Config.init(CP2.deviceId(), "host-handler-test", dev2Tree, mapper, delegate);
+        dev2Config.setPairDeviceId(CP1.deviceId()).setPairLocalPort(P9);
+
+        MockNetworkConfigRegistry mockNetworkConfigRegistry = new MockNetworkConfigRegistry();
+        mockNetworkConfigRegistry.applyConfig(dev1Config);
+        mockNetworkConfigRegistry.applyConfig(dev2Config);
+
+        // Initialize Segment Routing Manager
+        srManager = new MockSegmentRoutingManager(NEXT_TABLE);
+        srManager.cfgService = new NetworkConfigRegistryAdapter();
+        srManager.deviceConfiguration = createMock(DeviceConfiguration.class);
+        srManager.flowObjectiveService = new MockFlowObjectiveService(BRIDGING_TABLE, NEXT_TABLE);
+        srManager.routingRulePopulator = new MockRoutingRulePopulator(srManager, ROUTING_TABLE);
+        srManager.defaultRoutingHandler = new MockDefaultRoutingHandler(srManager, SUBNET_TABLE);
+        srManager.interfaceService = new MockInterfaceService(INTERFACES);
+        srManager.mastershipService = new MockMastershipService(LOCAL_DEVICES);
+        hostService = new MockHostService(HOSTS);
+        srManager.hostService = hostService;
+        srManager.cfgService = mockNetworkConfigRegistry;
+        srManager.routeService = new MockRouteService(ROUTING_TABLE);
+
+        routeHandler = new RouteHandler(srManager) {
+            // routeEventCache is not necessary for unit tests
+            @Override
+            void enqueueRouteEvent(RouteEvent routeEvent) {
+                dequeueRouteEvent(routeEvent);
+            }
+        };
+
+        ROUTING_TABLE.clear();
+        BRIDGING_TABLE.clear();
+        SUBNET_TABLE.clear();
+    }
+
+    @Test
+    public void init() throws Exception {
+        MockRoutingTableKey rtk = new MockRoutingTableKey(CP1.deviceId(), P1);
+        MockRoutingTableValue rtv = new MockRoutingTableValue(CP1.port(), M1, V1);
+        ROUTING_TABLE.put(rtk, rtv);
+
+        routeHandler.init(CP1.deviceId());
+
+        assertEquals(1, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        assertEquals(1, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+    }
+
+    @Test
+    public void processRouteAdded() throws Exception {
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.addSubnet(CP1, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR1, Sets.newHashSet(RR1));
+        routeHandler.processRouteAdded(re);
+
+        assertEquals(1, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        assertEquals(1, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void processRouteUpdated() throws Exception {
+        processRouteAdded();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_UPDATED, RR2, RR1, Sets.newHashSet(RR2),
+                Sets.newHashSet(RR1));
+        routeHandler.processRouteUpdated(re);
+
+        // Note: We shouldn't remove the old nexthop during the occasion of route update
+        //       since the populate subnet will take care of it and point it to an ECMP group
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M2, rtv2.macAddress);
+        assertEquals(V2, rtv2.vlanId);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(1, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void processRouteRemoved() throws Exception {
+        processRouteAdded();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR1, Sets.newHashSet(RR1));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testTwoSingleHomedAdded() throws Exception {
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.addSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR1, Sets.newHashSet(RR1, RR2));
+        routeHandler.processRouteAdded(re);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(M2, rtv2.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(V2, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testOneDualHomedAdded() throws Exception {
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.addSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_ADDED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteAdded(re);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testOneSingleHomedToTwoSingleHomed() throws Exception {
+        processRouteAdded();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.addSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED, RR1, null,
+                Sets.newHashSet(RR1, RR2), Sets.newHashSet(RR1));
+        routeHandler.processAlternativeRoutesChanged(re);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(M2, rtv2.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(V2, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(CP2.port(), rtv2.portNumber);
+
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testTwoSingleHomedToOneSingleHomed() throws Exception {
+        testTwoSingleHomedAdded();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED, RR1, null,
+                Sets.newHashSet(RR1), Sets.newHashSet(RR1, RR2));
+        routeHandler.processAlternativeRoutesChanged(re);
+
+        // Note: We shouldn't remove the old nexthop during the occasion of route update
+        //       since the populate subnet will take care of it and point it to an ECMP group
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        assertEquals(M1, rtv1.macAddress);
+        assertEquals(V1, rtv1.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+
+        assertEquals(1, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    // TODO Add test cases for two single homed next hop at same location
+
+    @Test
+    public void testDualHomedSingleLocationFail() throws Exception {
+        testOneDualHomedAdded();
+
+        HostEvent he = new HostEvent(HostEvent.Type.HOST_MOVED, H3S, H3D);
+        routeHandler.processHostMovedEvent(he);
+
+        assertEquals(2, ROUTING_TABLE.size());
+        MockRoutingTableValue rtv1 = ROUTING_TABLE.get(new MockRoutingTableKey(CP1.deviceId(), P1));
+        MockRoutingTableValue rtv2 = ROUTING_TABLE.get(new MockRoutingTableKey(CP2.deviceId(), P1));
+        assertEquals(M3, rtv1.macAddress);
+        assertEquals(M3, rtv2.macAddress);
+        assertEquals(V3, rtv1.vlanId);
+        assertEquals(V3, rtv2.vlanId);
+        assertEquals(CP1.port(), rtv1.portNumber);
+        assertEquals(P9, rtv2.portNumber);
+
+        // ECMP route table hasn't changed
+        assertEquals(2, SUBNET_TABLE.size());
+        assertTrue(SUBNET_TABLE.get(CP1).contains(P1));
+        assertTrue(SUBNET_TABLE.get(CP2).contains(P1));
+    }
+
+    @Test
+    public void testDualHomedBothLocationFail() throws Exception {
+        testDualHomedSingleLocationFail();
+
+        hostService = new MockHostService(HOSTS_ONE_FAIL);
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testTwoSingleHomedRemoved() throws Exception {
+        testTwoSingleHomedAdded();
+
+        hostService = new MockHostService(HOSTS_BOTH_FAIL);
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR1, Sets.newHashSet(RR1, RR2));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+
+    @Test
+    public void testOneDualHomeRemoved() throws Exception {
+        testOneDualHomedAdded();
+
+        reset(srManager.deviceConfiguration);
+        srManager.deviceConfiguration.removeSubnet(CP1, P1);
+        expectLastCall().once();
+        srManager.deviceConfiguration.removeSubnet(CP2, P1);
+        expectLastCall().once();
+        replay(srManager.deviceConfiguration);
+
+        RouteEvent re = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, RR3, Sets.newHashSet(RR3));
+        routeHandler.processRouteRemoved(re);
+
+        assertEquals(0, ROUTING_TABLE.size());
+        assertEquals(0, SUBNET_TABLE.size());
+
+        verify(srManager.deviceConfiguration);
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java
new file mode 100644
index 0000000..e457896
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/BlockedPortsConfigTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.DefaultApplicationId;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link BlockedPortsConfig}.
+ */
+public class BlockedPortsConfigTest {
+
+    private static final ApplicationId APP_ID = new DefaultApplicationId(1, "foo");
+    private static final String KEY = "blocked";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final String DEV1 = "of:0000000000000001";
+    private static final String DEV2 = "of:0000000000000002";
+    private static final String DEV3 = "of:0000000000000003";
+    private static final String DEV4 = "of:0000000000000004";
+    private static final String RANGE_14 = "1-4";
+    private static final String RANGE_79 = "7-9";
+    private static final String P1 = "1";
+    private static final String P5 = "5";
+    private static final String P9 = "9";
+
+    private BlockedPortsConfig cfg;
+    private BlockedPortsConfig.Range range;
+
+    private void print(String s) {
+        System.out.println(s);
+    }
+
+    private void print(Object o) {
+        print(o.toString());
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        InputStream blockedPortsJson = BlockedPortsConfigTest.class
+                .getResourceAsStream("/blocked-ports.json");
+        JsonNode node = MAPPER.readTree(blockedPortsJson);
+        cfg = new BlockedPortsConfig();
+        cfg.init(APP_ID, KEY, node, MAPPER, null);
+    }
+
+    @Test
+    public void basic() {
+        cfg = new BlockedPortsConfig();
+        print(cfg);
+
+        assertEquals("non-empty devices list", 0, cfg.deviceIds().size());
+        assertEquals("non-empty port-ranges list", 0, cfg.portRanges("non-exist").size());
+    }
+
+
+    @Test
+    public void overIteratePort() {
+        Iterator<Long> iterator = cfg.portIterator(DEV3);
+        while (iterator.hasNext()) {
+            print(iterator.next());
+        }
+
+        try {
+            print(iterator.next());
+            fail("NoSuchElement exception NOT thrown");
+        } catch (NoSuchElementException e) {
+            print("<good> " + e);
+        }
+    }
+
+    @Test
+    public void overIterateRange() {
+        range = new BlockedPortsConfig.Range("4-6");
+
+        Iterator<Long> iterator = range.iterator();
+        while (iterator.hasNext()) {
+            print(iterator.next());
+        }
+
+        try {
+            print(iterator.next());
+            fail("NoSuchElement exception NOT thrown");
+        } catch (NoSuchElementException e) {
+            print("<good> " + e);
+        }
+    }
+
+
+    @Test
+    public void simple() {
+        List<String> devIds = cfg.deviceIds();
+        print(devIds);
+        assertEquals("wrong dev id count", 3, devIds.size());
+        assertEquals("missing dev 1", true, devIds.contains(DEV1));
+        assertEquals("dev 2??", false, devIds.contains(DEV2));
+        assertEquals("missing dev 3", true, devIds.contains(DEV3));
+
+        List<String> d1ranges = cfg.portRanges(DEV1);
+        print(d1ranges);
+        assertEquals("wrong d1 range count", 2, d1ranges.size());
+        assertEquals("missing 1-4", true, d1ranges.contains(RANGE_14));
+        assertEquals("missing 7-9", true, d1ranges.contains(RANGE_79));
+
+        List<String> d2ranges = cfg.portRanges(DEV2);
+        print(d2ranges);
+        assertEquals("wrong d2 range count", 0, d2ranges.size());
+
+        List<String> d3ranges = cfg.portRanges(DEV3);
+        print(d3ranges);
+        assertEquals("wrong d3 range count", 1, d3ranges.size());
+        assertEquals("range 1-4?", false, d3ranges.contains(RANGE_14));
+        assertEquals("missing 7-9", true, d3ranges.contains(RANGE_79));
+    }
+
+
+    private void verifyPorts(List<Long> ports, long... exp) {
+        assertEquals("Wrong port count", exp.length, ports.size());
+        for (long e : exp) {
+            assertEquals("missing port", true, ports.contains(e));
+        }
+    }
+
+    private void verifyPortIterator(String devid, long... exp) {
+        List<Long> ports = new ArrayList<>();
+        Iterator<Long> iter = cfg.portIterator(devid);
+        iter.forEachRemaining(ports::add);
+        print(ports);
+        verifyPorts(ports, exp);
+    }
+
+    @Test
+    public void rangeIterators() {
+        verifyPortIterator(DEV1, 1, 2, 3, 4, 7, 8, 9);
+        verifyPortIterator(DEV2);
+        verifyPortIterator(DEV3, 7, 8, 9);
+    }
+
+    @Test
+    public void singlePorts() {
+        List<String> devIds = cfg.deviceIds();
+        print(devIds);
+        assertEquals("wrong dev id count", 3, devIds.size());
+        assertEquals("missing dev 4", true, devIds.contains(DEV4));
+
+        List<String> d1ranges = cfg.portRanges(DEV4);
+        print(d1ranges);
+        assertEquals("wrong d4 range count", 3, d1ranges.size());
+        assertEquals("missing 1", true, d1ranges.contains(P1));
+        assertEquals("missing 5", true, d1ranges.contains(P5));
+        assertEquals("missing 9", true, d1ranges.contains(P9));
+
+        verifyPortIterator(DEV4, 1, 5, 9);
+    }
+
+
+    // test Range inner class
+
+    @Test
+    public void rangeBadFormat() {
+        try {
+            range = new BlockedPortsConfig.Range("not-a-range-format");
+            fail("no exception thrown");
+        } catch (IllegalArgumentException iar) {
+            print(iar);
+            assertEquals("wrong msg", "Bad Range Format not-a-range-format", iar.getMessage());
+        }
+    }
+
+    @Test
+    public void rangeBadHi() {
+        try {
+            range = new BlockedPortsConfig.Range("2-nine");
+            fail("no exception thrown");
+        } catch (IllegalArgumentException iar) {
+            print(iar);
+            assertEquals("wrong msg", "Bad Range Format 2-nine", iar.getMessage());
+        }
+    }
+
+    @Test
+    public void rangeHiLessThanLo() {
+        try {
+            range = new BlockedPortsConfig.Range("9-5");
+            fail("no exception thrown");
+        } catch (IllegalArgumentException iar) {
+            print(iar);
+            assertEquals("wrong msg", "Bad Range Format 9-5", iar.getMessage());
+        }
+    }
+
+    @Test
+    public void rangeNegative() {
+        try {
+            range = new BlockedPortsConfig.Range("-2-4");
+            fail("no exception thrown");
+        } catch (IllegalArgumentException iar) {
+            print(iar);
+            assertEquals("wrong msg", "Bad Range Format -2-4", iar.getMessage());
+        }
+    }
+
+    @Test
+    public void rangeGood() {
+        range = new BlockedPortsConfig.Range("100-104");
+        List<Long> values = new ArrayList<>();
+        range.iterator().forEachRemaining(values::add);
+        print(values);
+        verifyPorts(values, 100, 101, 102, 103, 104);
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java
new file mode 100644
index 0000000..72b1ef5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingAppConfigTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+
+import java.io.InputStream;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for class {@link SegmentRoutingAppConfig}.
+ */
+public class SegmentRoutingAppConfigTest {
+    private SegmentRoutingAppConfig config;
+    private SegmentRoutingAppConfig invalidConfig;
+    private SegmentRoutingAppConfig mplsEcmpConfig;
+
+    private static final MacAddress ROUTER_MAC_1 = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final MacAddress ROUTER_MAC_2 = MacAddress.valueOf("00:00:00:00:00:02");
+    private static final MacAddress ROUTER_MAC_3 = MacAddress.valueOf("00:00:00:00:00:03");
+    private static final ConnectPoint PORT_1 = ConnectPoint.deviceConnectPoint("of:1/1");
+    private static final ConnectPoint PORT_2 = ConnectPoint.deviceConnectPoint("of:1/2");
+    private static final ConnectPoint PORT_3 = ConnectPoint.deviceConnectPoint("of:1/3");
+    private static final DeviceId VROUTER_ID_1 = DeviceId.deviceId("of:1");
+    private static final DeviceId VROUTER_ID_2 = DeviceId.deviceId("of:2");
+    private static final String PROVIDER_1 = "org.onosproject.provider.host";
+    private static final String PROVIDER_2 = "org.onosproject.netcfghost";
+    private static final String PROVIDER_3 = "org.onosproject.anotherprovider";
+
+    /**
+     * Initialize test related variables.
+     *
+     * @throws Exception
+     */
+    @Before
+    public void setUp() throws Exception {
+        InputStream jsonStream = SegmentRoutingAppConfigTest.class
+                .getResourceAsStream("/app.json");
+        InputStream invalidJsonStream = SegmentRoutingAppConfigTest.class
+                .getResourceAsStream("/app-invalid.json");
+        InputStream mplsEcmpJsonStream = SegmentRoutingAppConfigTest.class
+                .getResourceAsStream("/app-ecmp.json");
+
+        String key = SegmentRoutingManager.APP_NAME;
+        ApplicationId subject = new TestApplicationId(key);
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode jsonNode = mapper.readTree(jsonStream);
+        JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+        JsonNode mplsEcmpJsonNode = mapper.readTree(mplsEcmpJsonStream);
+        ConfigApplyDelegate delegate = new MockDelegate();
+
+        config = new SegmentRoutingAppConfig();
+        config.init(subject, key, jsonNode, mapper, delegate);
+        invalidConfig = new SegmentRoutingAppConfig();
+        invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+        mplsEcmpConfig = new SegmentRoutingAppConfig();
+        mplsEcmpConfig.init(subject, key, mplsEcmpJsonNode, mapper, delegate);
+    }
+
+    /**
+     * Tests config validity.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testIsValid() throws Exception {
+        assertTrue(config.isValid());
+        assertFalse(invalidConfig.isValid());
+        assertTrue(mplsEcmpConfig.isValid());
+    }
+
+    /**
+     * Test MPLS-ECMP default getter. By-default
+     * MPLS-ECMPS is false.
+     */
+    @Test
+    public void testDefaultMplsEcmp() {
+        boolean mplsEcmp = config.mplsEcmp();
+        assertThat(mplsEcmp, is(false));
+    }
+
+    /**
+     * Test MPLS-ECMP getter.
+     */
+    @Test
+    public void testMplsEcmp() {
+        boolean mplsEcmp = mplsEcmpConfig.mplsEcmp();
+        assertThat(mplsEcmp, is(true));
+    }
+
+    /**
+     * Test MPLS-ECMP setter.
+     */
+    @Test
+    public void testSetMplsEcmp() {
+        /*
+         * In the config the value is not set.
+         */
+        boolean mplsEcmp = config.mplsEcmp();
+        assertThat(mplsEcmp, is(false));
+        config.setMplsEcmp(true);
+        mplsEcmp = config.mplsEcmp();
+        assertThat(mplsEcmp, is(true));
+        /*
+         * In the mplsEcmpConfig the value is true,
+         */
+        mplsEcmp = mplsEcmpConfig.mplsEcmp();
+        assertThat(mplsEcmp, is(true));
+        config.setMplsEcmp(false);
+        mplsEcmp = config.mplsEcmp();
+        assertThat(mplsEcmp, is(false));
+    }
+
+    /**
+     * Tests vRouterMacs getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testVRouterMacs() throws Exception {
+        Set<MacAddress> vRouterMacs = config.vRouterMacs();
+        assertNotNull("vRouterMacs should not be null", vRouterMacs);
+        assertThat(vRouterMacs.size(), is(2));
+        assertTrue(vRouterMacs.contains(ROUTER_MAC_1));
+        assertTrue(vRouterMacs.contains(ROUTER_MAC_2));
+    }
+
+    /**
+     * Tests vRouterMacs setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetVRouterMacs() throws Exception {
+        ImmutableSet.Builder<MacAddress> builder = ImmutableSet.builder();
+        builder.add(ROUTER_MAC_3);
+        config.setVRouterMacs(builder.build());
+
+        Set<MacAddress> vRouterMacs = config.vRouterMacs();
+        assertThat(vRouterMacs.size(), is(1));
+        assertTrue(vRouterMacs.contains(ROUTER_MAC_3));
+    }
+
+    /**
+     * Tests suppressSubnet getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSuppressSubnet() throws Exception {
+        Set<ConnectPoint> suppressSubnet = config.suppressSubnet();
+        assertNotNull("suppressSubnet should not be null", suppressSubnet);
+        assertThat(suppressSubnet.size(), is(2));
+        assertTrue(suppressSubnet.contains(PORT_1));
+        assertTrue(suppressSubnet.contains(PORT_2));
+    }
+
+    /**
+     * Tests suppressSubnet setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetSuppressSubnet() throws Exception {
+        ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+        builder.add(PORT_3);
+        config.setSuppressSubnet(builder.build());
+
+        Set<ConnectPoint> suppressSubnet = config.suppressSubnet();
+        assertNotNull("suppressSubnet should not be null", suppressSubnet);
+        assertThat(suppressSubnet.size(), is(1));
+        assertTrue(suppressSubnet.contains(PORT_3));
+    }
+
+    /**
+     * Tests suppressHostByPort getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSuppressHostByPort() throws Exception {
+        Set<ConnectPoint> suppressHostByPort = config.suppressHostByPort();
+        assertNotNull("suppressHostByPort should not be null", suppressHostByPort);
+        assertThat(suppressHostByPort.size(), is(2));
+        assertTrue(suppressHostByPort.contains(PORT_1));
+        assertTrue(suppressHostByPort.contains(PORT_2));
+    }
+
+    /**
+     * Tests suppressHostByPort setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetSuppressHostByPort() throws Exception {
+        ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
+        builder.add(PORT_3);
+        config.setSuppressHostByPort(builder.build());
+
+        Set<ConnectPoint> suppressHostByPort = config.suppressHostByPort();
+        assertNotNull("suppressHostByPort should not be null", suppressHostByPort);
+        assertThat(suppressHostByPort.size(), is(1));
+        assertTrue(suppressHostByPort.contains(PORT_3));
+    }
+
+    /**
+     * Tests suppressHostByProvider getter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSuppressHostByProvider() throws Exception {
+        Set<String> supprsuppressHostByProvider = config.suppressHostByProvider();
+        assertNotNull("suppressHostByProvider should not be null", supprsuppressHostByProvider);
+        assertThat(supprsuppressHostByProvider.size(), is(2));
+        assertTrue(supprsuppressHostByProvider.contains(PROVIDER_1));
+        assertTrue(supprsuppressHostByProvider.contains(PROVIDER_2));
+    }
+
+    /**
+     * Tests suppressHostByProvider setter.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testSetHostLearning() throws Exception {
+        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+        builder.add(PROVIDER_3);
+        config.setSuppressHostByProvider(builder.build());
+
+        Set<String> supprsuppressHostByProvider = config.suppressHostByProvider();
+        assertNotNull("suppressHostByProvider should not be null", supprsuppressHostByProvider);
+        assertThat(supprsuppressHostByProvider.size(), is(1));
+        assertTrue(supprsuppressHostByProvider.contains(PROVIDER_3));
+    }
+
+    private class MockDelegate implements ConfigApplyDelegate {
+        @Override
+        public void onApply(Config config) {
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java
new file mode 100644
index 0000000..ddf90bb
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/SegmentRoutingDeviceConfigTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for class {@link SegmentRoutingDeviceConfig}.
+ */
+public class SegmentRoutingDeviceConfigTest {
+    private SegmentRoutingDeviceConfig config;
+    private SegmentRoutingDeviceConfig ipv6Config;
+    private SegmentRoutingDeviceConfig pairConfig;
+    private SegmentRoutingDeviceConfig invalidConfig;
+    private Map<Integer, Set<Integer>> adjacencySids1;
+    private Map<Integer, Set<Integer>> adjacencySids2;
+    private static final DeviceId PAIR_DEVICE_ID = DeviceId.deviceId("of:123456789ABCDEF0");
+    private static final PortNumber PAIR_LOCAL_PORT = PortNumber.portNumber(10);
+
+    @Before
+    public void setUp() throws Exception {
+        InputStream jsonStream = SegmentRoutingDeviceConfigTest.class
+                .getResourceAsStream("/device.json");
+        InputStream ipv6JsonStream = SegmentRoutingDeviceConfigTest.class
+                .getResourceAsStream("/device-ipv6.json");
+        InputStream pairJsonStream = SegmentRoutingDeviceConfigTest.class
+                .getResourceAsStream("/device-pair.json");
+        InputStream invalidJsonStream = SegmentRoutingDeviceConfigTest.class
+                .getResourceAsStream("/device-invalid.json");
+
+        adjacencySids1 = new HashMap<>();
+        Set<Integer> ports1 = new HashSet<>();
+        ports1.add(2);
+        ports1.add(3);
+        adjacencySids1.put(100, ports1);
+        Set<Integer> ports2 = new HashSet<>();
+        ports2.add(4);
+        ports2.add(5);
+        adjacencySids1.put(200, ports2);
+
+        adjacencySids2 = new HashMap<>();
+        Set<Integer> ports3 = new HashSet<>();
+        ports3.add(6);
+        adjacencySids2.put(300, ports3);
+
+        DeviceId subject = DeviceId.deviceId("of:0000000000000001");
+        String key = "segmentrouting";
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode jsonNode = mapper.readTree(jsonStream);
+        JsonNode ipv6JsonNode = mapper.readTree(ipv6JsonStream);
+        JsonNode pairJsonNode = mapper.readTree(pairJsonStream);
+        JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+        ConfigApplyDelegate delegate = new MockDelegate();
+
+        config = new SegmentRoutingDeviceConfig();
+        config.init(subject, key, jsonNode, mapper, delegate);
+
+        ipv6Config = new SegmentRoutingDeviceConfig();
+        ipv6Config.init(subject, key, ipv6JsonNode, mapper, delegate);
+
+        pairConfig = new SegmentRoutingDeviceConfig();
+        pairConfig.init(subject, key, pairJsonNode, mapper, delegate);
+
+        invalidConfig = new SegmentRoutingDeviceConfig();
+        invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+    }
+
+    @Test
+    public void testIsValid() {
+        assertTrue(config.isValid());
+        assertTrue(ipv6Config.isValid());
+        assertTrue(pairConfig.isValid());
+        assertFalse(invalidConfig.isValid());
+    }
+
+    @Test
+    public void testName() throws Exception {
+        assertTrue(config.name().isPresent());
+        assertThat(config.name().get(), is("Leaf-R1"));
+    }
+
+    @Test
+    public void testSetName() throws Exception {
+        config.setName("Spine-R1");
+        assertTrue(config.name().isPresent());
+        assertThat(config.name().get(), is("Spine-R1"));
+    }
+
+    @Test
+    public void testRouterIp() throws Exception {
+        assertThat(config.routerIpv4(), is(IpAddress.valueOf("10.0.1.254")));
+        assertThat(ipv6Config.routerIpv4(), is(IpAddress.valueOf("10.0.1.254")));
+        assertThat(ipv6Config.routerIpv6(), is(IpAddress.valueOf("2000::c0a8:0101")));
+    }
+
+    @Test
+    public void testSetRouterIp() throws Exception {
+        config.setRouterIpv4("10.0.2.254");
+        assertThat(config.routerIpv4(), is(IpAddress.valueOf("10.0.2.254")));
+        ipv6Config.setRouterIpv4("10.0.2.254");
+        assertThat(ipv6Config.routerIpv4(), is(IpAddress.valueOf("10.0.2.254")));
+        ipv6Config.setRouterIpv6("2000::c0a9:0101");
+        assertThat(ipv6Config.routerIpv6(), is(IpAddress.valueOf("2000::c0a9:0101")));
+    }
+
+    @Test
+    public void testRouterMac() throws Exception {
+        assertThat(config.routerMac(), is(MacAddress.valueOf("00:00:00:00:01:80")));
+    }
+
+    @Test
+    public void testSetRouterMac() throws Exception {
+        config.setRouterMac("00:00:00:00:02:80");
+        assertThat(config.routerMac(), is(MacAddress.valueOf("00:00:00:00:02:80")));
+    }
+
+    @Test
+    public void testNodeSid() throws Exception {
+        assertThat(config.nodeSidIPv4(), is(101));
+        assertThat(ipv6Config.nodeSidIPv4(), is(101));
+        assertThat(ipv6Config.nodeSidIPv6(), is(111));
+    }
+
+    @Test
+    public void testSetNodeSid() throws Exception {
+        config.setNodeSidIPv4(200);
+        assertThat(config.nodeSidIPv4(), is(200));
+        ipv6Config.setNodeSidIPv4(200);
+        assertThat(ipv6Config.nodeSidIPv4(), is(200));
+        ipv6Config.setNodeSidIPv6(201);
+        assertThat(ipv6Config.nodeSidIPv6(), is(201));
+    }
+
+    @Test
+    public void testIsEdgeRouter() throws Exception {
+        assertThat(config.isEdgeRouter(), is(true));
+    }
+
+    @Test
+    public void testSetIsEdgeRouter() throws Exception {
+        config.setIsEdgeRouter(false);
+        assertThat(config.isEdgeRouter(), is(false));
+    }
+
+    @Test
+    public void testAdjacencySids() throws Exception {
+        assertThat(config.adjacencySids(), is(adjacencySids1));
+    }
+
+    @Test
+    public void testSetAdjacencySids() throws Exception {
+        config.setAdjacencySids(adjacencySids2);
+        assertThat(config.adjacencySids(), is(adjacencySids2));
+    }
+
+    @Test
+    public void testPairDeviceId() throws Exception {
+        assertNull(config.pairDeviceId());
+        assertNull(ipv6Config.pairDeviceId());
+        assertThat(pairConfig.pairDeviceId(), is(PAIR_DEVICE_ID));
+    }
+
+    @Test
+    public void testSetPairDeviceId() throws Exception {
+        config.setPairDeviceId(PAIR_DEVICE_ID);
+        assertThat(config.pairDeviceId(), is(PAIR_DEVICE_ID));
+    }
+
+    @Test
+    public void testPairLocalPort() throws Exception {
+        assertNull(config.pairLocalPort());
+        assertNull(ipv6Config.pairLocalPort());
+        assertThat(pairConfig.pairLocalPort(), is(PAIR_LOCAL_PORT));
+    }
+
+    @Test
+    public void testSetPairLocalPort() throws Exception {
+        config.setPairLocalPort(PAIR_LOCAL_PORT);
+        assertThat(config.pairLocalPort(), is(PAIR_LOCAL_PORT));
+    }
+
+    private class MockDelegate implements ConfigApplyDelegate {
+        @Override
+        public void onApply(Config configFile) {
+        }
+    }
+}
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java
new file mode 100644
index 0000000..62d2d86
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/config/XConnectConfigTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016-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.segmentrouting.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.VlanId;
+import org.onosproject.TestApplicationId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.Config;
+import org.onosproject.net.config.ConfigApplyDelegate;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.storekey.XConnectStoreKey;
+import java.io.InputStream;
+import java.util.Set;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests for class {@link XConnectConfig}.
+ */
+public class XConnectConfigTest {
+    private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+    private static final VlanId VLAN10 = VlanId.vlanId((short) 10);
+    private static final VlanId VLAN20 = VlanId.vlanId((short) 20);
+    private static final PortNumber PORT3 = PortNumber.portNumber(3);
+    private static final PortNumber PORT4 = PortNumber.portNumber(4);
+    private static final PortNumber PORT5 = PortNumber.portNumber(5);
+    private static final XConnectStoreKey KEY1 = new XConnectStoreKey(DEV1, VLAN10);
+    private static final XConnectStoreKey KEY2 = new XConnectStoreKey(DEV2, VLAN10);
+    private static final XConnectStoreKey KEY3 = new XConnectStoreKey(DEV2, VLAN20);
+    private static final XConnectStoreKey KEY4 = new XConnectStoreKey(DEV2, VlanId.NONE);
+
+    private XConnectConfig config;
+    private XConnectConfig invalidConfig;
+
+    @Before
+    public void setUp() throws Exception {
+        InputStream jsonStream = SegmentRoutingAppConfigTest.class
+                .getResourceAsStream("/xconnect.json");
+        InputStream invalidJsonStream = SegmentRoutingAppConfigTest.class
+                .getResourceAsStream("/xconnect-invalid.json");
+
+        String key = SegmentRoutingManager.APP_NAME;
+        ApplicationId subject = new TestApplicationId(key);
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode jsonNode = mapper.readTree(jsonStream);
+        JsonNode invalidJsonNode = mapper.readTree(invalidJsonStream);
+        ConfigApplyDelegate delegate = new XConnectConfigTest.MockDelegate();
+
+        config = new XConnectConfig();
+        config.init(subject, key, jsonNode, mapper, delegate);
+        invalidConfig = new XConnectConfig();
+        invalidConfig.init(subject, key, invalidJsonNode, mapper, delegate);
+    }
+
+    /**
+     * Tests config validity.
+     */
+    @Test
+    public void testIsValid() {
+        assertTrue(config.isValid());
+        assertFalse(invalidConfig.isValid());
+    }
+
+    /**
+     * Tests getXconnects.
+     */
+    @Test
+    public void testGetXconnects() {
+        Set<XConnectStoreKey> xconnects = config.getXconnects();
+        assertThat(xconnects.size(), is(3));
+        assertTrue(xconnects.contains(KEY1));
+        assertTrue(xconnects.contains(KEY2));
+        assertTrue(xconnects.contains(KEY3));
+        assertFalse(xconnects.contains(KEY4));
+    }
+
+    /**
+     * Tests getPorts.
+     */
+    @Test
+    public void testGetPorts() {
+        Set<PortNumber> ports;
+
+        ports = config.getPorts(KEY1);
+        assertThat(ports.size(), is(2));
+        assertTrue(ports.contains(PORT3));
+        assertTrue(ports.contains(PORT4));
+
+        ports = config.getPorts(KEY2);
+        assertThat(ports.size(), is(2));
+        assertTrue(ports.contains(PORT3));
+        assertTrue(ports.contains(PORT4));
+
+        ports = config.getPorts(KEY3);
+        assertThat(ports.size(), is(2));
+        assertTrue(ports.contains(PORT4));
+        assertTrue(ports.contains(PORT5));
+    }
+
+    private class MockDelegate implements ConfigApplyDelegate {
+        @Override
+        public void onApply(Config config) {
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java
new file mode 100644
index 0000000..f890852
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/java/org/onosproject/segmentrouting/grouphandler/DestinationSetTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.segmentrouting.grouphandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.net.DeviceId;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class DestinationSetTest {
+    DestinationSet ds1, ds2, ds3, ds4;
+    DeviceId d201, d202;
+    int el201, el202;
+
+    @Before
+    public void setUp() {
+        d201 = DeviceId.deviceId("of:0000000000000201");
+        d202 = DeviceId.deviceId("of:0000000000000202");
+        el201 = 201;
+        el202 = 202;
+        ds1 = new DestinationSet(false, d201);
+        ds2 = new DestinationSet(false, el201, d201);
+        ds3 = new DestinationSet(false, el201, d201, el202, d202);
+        ds4 = new DestinationSet(false,
+                                 DestinationSet.NO_EDGE_LABEL, d201,
+                                 DestinationSet.NO_EDGE_LABEL, d202);
+    }
+
+    @Test
+    public void testIsValid() {
+        assertTrue(!ds1.mplsSet());
+        assertTrue(ds1.getEdgeLabel(d201) == DestinationSet.NO_EDGE_LABEL);
+        assertTrue(ds1.getDestinationSwitches().size() == 1);
+
+        assertTrue(!ds2.mplsSet());
+        assertTrue(ds2.getEdgeLabel(d201) == el201);
+        assertTrue(ds2.getDestinationSwitches().size() == 1);
+
+        assertTrue(!ds3.mplsSet());
+        assertTrue(ds3.getEdgeLabel(d201) == el201);
+        assertTrue(ds3.getEdgeLabel(d202) == el202);
+        assertTrue(ds3.getDestinationSwitches().size() == 2);
+
+        assertTrue(!ds4.mplsSet());
+        assertTrue(ds4.getEdgeLabel(d201) == DestinationSet.NO_EDGE_LABEL);
+        assertTrue(ds4.getEdgeLabel(d202) == DestinationSet.NO_EDGE_LABEL);
+        assertTrue(ds4.getDestinationSwitches().size() == 2);
+
+        assertFalse(ds1.equals(ds2));
+        assertFalse(ds1.equals(ds4));
+        assertFalse(ds3.equals(ds4));
+        assertFalse(ds2.equals(ds3));
+    }
+
+
+    @Test
+    public void testOneDestinationWithoutLabel() {
+        DestinationSet testds = new DestinationSet(false, d201);
+        assertTrue(testds.equals(ds1)); // match
+
+        testds = new DestinationSet(true, d201);
+        assertFalse(testds.equals(ds1)); //wrong mplsSet
+
+        testds = new DestinationSet(false, d202);
+        assertFalse(testds.equals(ds1)); //wrong device
+
+        testds = new DestinationSet(false, el201, d201);
+        assertFalse(testds.equals(ds1)); // wrong label
+
+        testds = new DestinationSet(false, -1, d201, -1, d202);
+        assertFalse(testds.equals(ds1)); // 2-devs should not match
+    }
+
+
+
+    @Test
+    public void testOneDestinationWithLabel() {
+        DestinationSet testds = new DestinationSet(false, 203, d202);
+        assertFalse(testds.equals(ds2)); //wrong label
+
+        testds = new DestinationSet(true, 201, d201);
+        assertFalse(testds.equals(ds2)); //wrong mplsSet
+
+        testds = new DestinationSet(false, 201, d202);
+        assertFalse(testds.equals(ds2)); //wrong device
+
+        testds = new DestinationSet(false, 201, DeviceId.deviceId("of:0000000000000201"));
+        assertTrue(testds.equals(ds2)); // match
+
+        testds = new DestinationSet(false, d201);
+        assertFalse(testds.equals(ds2)); // wrong label
+
+        testds = new DestinationSet(false, el201, d201, el202, d202);
+        assertFalse(testds.equals(ds1)); // 2-devs should not match
+    }
+
+    @Test
+    public void testDestPairWithLabel() {
+        DestinationSet testds = new DestinationSet(false, el201, d201, el202, d202);
+        assertTrue(testds.equals(ds3)); // match same switches, same order
+        assertTrue(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el202, d202, el201, d201);
+        assertTrue(testds.equals(ds3)); // match same switches, order reversed
+        assertTrue(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el202, d202);
+        assertFalse(testds.equals(ds3)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el201, d201);
+        assertFalse(testds.equals(ds3)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el201, d201, 0, DeviceId.NONE);
+        assertFalse(testds.equals(ds3)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el201, d202, el201, d201);
+        assertFalse(testds.equals(ds3)); // wrong labels
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(true, el202, d202, el201, d201);
+        assertFalse(testds.equals(ds3)); // wrong mpls set
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false, el202, d202, el201, d202);
+        assertFalse(testds.equals(ds3)); // wrong device
+        assertFalse(testds.hashCode() == ds3.hashCode());
+
+        testds = new DestinationSet(false,
+                                    el202, DeviceId.deviceId("of:0000000000000205"),
+                                    el201, d201);
+        assertFalse(testds.equals(ds3)); // wrong device
+        assertFalse(testds.hashCode() == ds3.hashCode());
+    }
+
+    @Test
+    public void testDestPairWithoutLabel() {
+        DestinationSet testds = new DestinationSet(false, -1, d201, -1, d202);
+        assertTrue(testds.equals(ds4)); // match same switches, same order
+        assertTrue(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, -1, d202, -1, d201);
+        assertTrue(testds.equals(ds4)); // match same switches, order reversed
+        assertTrue(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, -1, d202);
+        assertFalse(testds.equals(ds4)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, -1, d201);
+        assertFalse(testds.equals(ds4)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, -1, d201, 0, DeviceId.NONE);
+        assertFalse(testds.equals(ds4)); // one less switch should not match
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, el201, d201, -1, d202);
+        assertFalse(testds.equals(ds4)); // wrong labels
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(true, -1, d202, -1, d201);
+        assertFalse(testds.equals(ds4)); // wrong mpls set
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false, -1, d202, -1, d202);
+        assertFalse(testds.equals(ds4)); // wrong device
+        assertFalse(testds.hashCode() == ds4.hashCode());
+
+        testds = new DestinationSet(false,
+                                    -1, DeviceId.deviceId("of:0000000000000205"),
+                                    -1, d201);
+        assertFalse(testds.equals(ds4)); // wrong device
+        assertFalse(testds.hashCode() == ds4.hashCode());
+    }
+
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app-ecmp.json b/apps/segmentrouting/app/src/test/resources/app-ecmp.json
new file mode 100644
index 0000000..e94b1c5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app-ecmp.json
@@ -0,0 +1,19 @@
+{
+  "vRouterMacs" : [
+      "00:00:00:00:00:01",
+      "00:00:00:00:00:02"
+  ],
+  "suppressSubnet" : [
+      "of:1/1",
+      "of:1/2"
+  ],
+  "suppressHostByPort" : [
+      "of:1/1",
+      "of:1/2"
+  ],
+  "suppressHostByProvider" : [
+      "org.onosproject.provider.host",
+      "org.onosproject.netcfghost"
+  ],
+  "MPLS-ECMP" : true
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app-invalid.json b/apps/segmentrouting/app/src/test/resources/app-invalid.json
new file mode 100644
index 0000000..01508f8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app-invalid.json
@@ -0,0 +1,15 @@
+{
+  "vRouterMacs" : [
+      "00:00:00:00:00:01",
+      "00:00:00:00:00:02"
+  ],
+  "suppressSubnet" : [
+      "of:1/1",
+      "of:1/2"
+  ],
+  "suppressHostByPort" : [
+      "of:1/1",
+      "wrongPort"
+  ],
+  "suppressHostByProvider" : []
+}
diff --git a/apps/segmentrouting/app/src/test/resources/app.json b/apps/segmentrouting/app/src/test/resources/app.json
new file mode 100644
index 0000000..dab6384
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/app.json
@@ -0,0 +1,18 @@
+{
+  "vRouterMacs" : [
+      "00:00:00:00:00:01",
+      "00:00:00:00:00:02"
+  ],
+  "suppressSubnet" : [
+      "of:1/1",
+      "of:1/2"
+  ],
+  "suppressHostByPort" : [
+      "of:1/1",
+      "of:1/2"
+  ],
+  "suppressHostByProvider" : [
+      "org.onosproject.provider.host",
+      "org.onosproject.netcfghost"
+  ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json b/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json
new file mode 100644
index 0000000..3d8749e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/blocked-ports-alt.json
@@ -0,0 +1,5 @@
+{
+  "of:0000000000000001": ["1-9"],
+  "of:0000000000000003": ["7"],
+  "of:0000000000000004": ["1"]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/blocked-ports.json b/apps/segmentrouting/app/src/test/resources/blocked-ports.json
new file mode 100644
index 0000000..2543f3d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/blocked-ports.json
@@ -0,0 +1,5 @@
+{
+  "of:0000000000000001": ["1-4", "7-9"],
+  "of:0000000000000003": ["7-9"],
+  "of:0000000000000004": ["1", "5", "9"]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-invalid.json b/apps/segmentrouting/app/src/test/resources/device-invalid.json
new file mode 100644
index 0000000..dfcbdb8
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-invalid.json
@@ -0,0 +1,12 @@
+{
+  "name" : "Leaf-R1",
+  "ipv4NodeSid" : 101,
+  "ipv4Loopback" : "10.0.1.254",
+  "routerMac" : "00:00:00:00:01:80",
+  "isEdgeRouter" : true,
+  "adjacencySids" : [
+    { "adjSid" : 100, "ports" : [2, 3] },
+    { "adjSid" : 200, "ports" : [4, 5] }
+  ],
+  "pairDeviceId" : "of:123456789ABCDEF0"
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-ipv6.json b/apps/segmentrouting/app/src/test/resources/device-ipv6.json
new file mode 100644
index 0000000..9832f8c
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-ipv6.json
@@ -0,0 +1,13 @@
+{
+  "name" : "Leaf-R1",
+  "ipv4NodeSid" : 101,
+  "ipv4Loopback" : "10.0.1.254",
+  "ipv6NodeSid" : 111,
+  "ipv6Loopback" : "2000::c0a8:0101",
+  "routerMac" : "00:00:00:00:01:80",
+  "isEdgeRouter" : true,
+  "adjacencySids" : [
+    { "adjSid" : 100, "ports" : [2, 3] },
+    { "adjSid" : 200, "ports" : [4, 5] }
+  ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device-pair.json b/apps/segmentrouting/app/src/test/resources/device-pair.json
new file mode 100644
index 0000000..c699ff5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device-pair.json
@@ -0,0 +1,13 @@
+{
+  "name" : "Leaf-R1",
+  "ipv4NodeSid" : 101,
+  "ipv4Loopback" : "10.0.1.254",
+  "routerMac" : "00:00:00:00:01:80",
+  "isEdgeRouter" : true,
+  "adjacencySids" : [
+    { "adjSid" : 100, "ports" : [2, 3] },
+    { "adjSid" : 200, "ports" : [4, 5] }
+  ],
+  "pairDeviceId" : "of:123456789ABCDEF0",
+  "pairLocalPort" : "10"
+}
diff --git a/apps/segmentrouting/app/src/test/resources/device.json b/apps/segmentrouting/app/src/test/resources/device.json
new file mode 100644
index 0000000..83aec6e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/device.json
@@ -0,0 +1,11 @@
+{
+  "name" : "Leaf-R1",
+  "ipv4NodeSid" : 101,
+  "ipv4Loopback" : "10.0.1.254",
+  "routerMac" : "00:00:00:00:01:80",
+  "isEdgeRouter" : true,
+  "adjacencySids" : [
+    { "adjSid" : 100, "ports" : [2, 3] },
+    { "adjSid" : 200, "ports" : [4, 5] }
+  ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json b/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json
new file mode 100644
index 0000000..fac162d
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-conflicting-vlan.json
@@ -0,0 +1,24 @@
+{
+  "1": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/2",
+    "cP1InnerTag": "10",
+    "cP1OuterTag": "20",
+    "cP2InnerTag": "11",
+    "cP2OuterTag": "21",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "255"
+  },
+  "20": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/2",
+    "cP1InnerTag": "100",
+    "cP1OuterTag": "",
+    "cP2InnerTag": "21",
+    "cP2OuterTag": "",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "1255"
+  }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json
new file mode 100644
index 0000000..af0a9d1
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-mode.json
@@ -0,0 +1,13 @@
+{
+  "1": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000011/1",
+    "cP1InnerTag": "10",
+    "cP1OuterTag": "20",
+    "cP2InnerTag": "11",
+    "cP2OuterTag": "",
+    "mode": "UNDEFINED_MODED",
+    "sdTag": "40",
+    "pwLabel": "255"
+  }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json
new file mode 100644
index 0000000..66697f5
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-pwlabel.json
@@ -0,0 +1,13 @@
+{
+  "1": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000011/1",
+    "cP1InnerTag": "10",
+    "cP1OuterTag": "20",
+    "cP2InnerTag": "11",
+    "cP2OuterTag": "",
+    "mode": "1",
+    "sdTag": "40",
+    "pwLabel": "255.555551"
+  }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json
new file mode 100644
index 0000000..b556e06
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas-invalid-vlan.json
@@ -0,0 +1,24 @@
+{
+  "1": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/2",
+    "cP1InnerTag": "10",
+    "cP1OuterTag": "20",
+    "cP2InnerTag": "11",
+    "cP2OuterTag": "21",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "255"
+  },
+  "20": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/2",
+    "cP1InnerTag": "100",
+    "cP1OuterTag": "200.55",
+    "cP2InnerTag": "1100",
+    "cP2OuterTag": "210",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "1255"
+  }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/pwaas.json b/apps/segmentrouting/app/src/test/resources/pwaas.json
new file mode 100644
index 0000000..2a58b4e
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/pwaas.json
@@ -0,0 +1,24 @@
+{
+  "1": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/1",
+    "cP1InnerTag": "10",
+    "cP1OuterTag": "20",
+    "cP2InnerTag": "10",
+    "cP2OuterTag": "21",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "255"
+  },
+  "20": {
+    "cP1": "of:0000000000000001/1",
+    "cP2": "of:0000000000000002/1",
+    "cP1InnerTag": "100",
+    "cP1OuterTag": "200",
+    "cP2InnerTag": "100",
+    "cP2OuterTag": "210",
+    "mode": "RAW",
+    "sdTag": "",
+    "pwLabel": "1255"
+  }
+}
diff --git a/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json b/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json
new file mode 100644
index 0000000..e468271
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/xconnect-invalid.json
@@ -0,0 +1,18 @@
+{
+  "of:0000000000000001": [
+    {
+      "vlan": 10,
+      "ports": [3, 4]
+    }
+  ],
+  "of:0000000000000002": [
+    {
+      "vlan": 10,
+      "ports": [3, 4]
+    },
+    {
+      "vlan": 20,
+      "ports": [4, 5, 6]
+    }
+  ]
+}
diff --git a/apps/segmentrouting/app/src/test/resources/xconnect.json b/apps/segmentrouting/app/src/test/resources/xconnect.json
new file mode 100644
index 0000000..ebd61b3
--- /dev/null
+++ b/apps/segmentrouting/app/src/test/resources/xconnect.json
@@ -0,0 +1,19 @@
+{
+  "of:0000000000000001": [
+    {
+      "vlan": 10,
+      "ports": [3, 4],
+      "name": "OLT1"
+    }
+  ],
+  "of:0000000000000002": [
+    {
+      "vlan": 10,
+      "ports": [3, 4]
+    },
+    {
+      "vlan": 20,
+      "ports": [4, 5]
+    }
+  ]
+}