Support multiple host locations in HostHandler

Also include refactoring and some unit tests

Change-Id: I8e213d0ebff0cc8c87569f515a72007f63d85a14
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/HostHandler.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/HostHandler.java
index 6709efd..0ca1ba9 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/HostHandler.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/HostHandler.java
@@ -35,12 +35,14 @@
 import org.onosproject.net.flowobjective.ObjectiveContext;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.host.HostService;
-import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Sets;
 import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
 
 /**
  * Handles host-related events.
@@ -56,120 +58,158 @@
      *
      * @param srManager Segment Routing manager
      */
-    public HostHandler(SegmentRoutingManager srManager) {
+    HostHandler(SegmentRoutingManager srManager) {
         this.srManager = srManager;
         hostService = srManager.hostService;
         flowObjectiveService = srManager.flowObjectiveService;
     }
 
     protected void init(DeviceId devId) {
-        hostService.getHosts().forEach(host -> {
-            DeviceId deviceId = host.location().deviceId();
-            // The host does not attach to this device
-            if (!deviceId.equals(devId)) {
-                return;
-            }
-            processHostAdded(host);
-        });
+        hostService.getHosts().forEach(host ->
+            host.locations().stream()
+                    .filter(location -> location.deviceId().equals(devId))
+                    .forEach(location -> processHostAddedAtLocation(host, location))
+        );
     }
 
-    protected void processHostAddedEvent(HostEvent event) {
+    void processHostAddedEvent(HostEvent event) {
         processHostAdded(event.subject());
     }
 
-    protected void processHostAdded(Host host) {
-        MacAddress mac = host.mac();
-        VlanId vlanId = host.vlan();
-        HostLocation location = host.location();
-        DeviceId deviceId = location.deviceId();
-        PortNumber port = location.port();
-        Set<IpAddress> ips = host.ipAddresses();
-        log.info("Host {}/{} is added at {}:{}", mac, vlanId, deviceId, port);
-
-        if (accepted(host)) {
-            processBridgingRule(deviceId, port, mac, vlanId, false);
-            ips.forEach(ip -> {
-                processRoutingRule(deviceId, port, mac, vlanId, ip, false);
-            });
-        }
+    private void processHostAdded(Host host) {
+        host.locations().forEach(location -> processHostAddedAtLocation(host, location));
     }
 
-    protected void processHostRemoveEvent(HostEvent event) {
+    void processHostAddedAtLocation(Host host, HostLocation location) {
+        checkArgument(host.locations().contains(location), "{} is not a location of {}", location, host);
+
+        MacAddress mac = host.mac();
+        VlanId vlanId = host.vlan();
+        Set<HostLocation> locations = host.locations();
+        Set<IpAddress> ips = host.ipAddresses();
+        log.info("Host {}/{} is added at {}", mac, vlanId, locations);
+
+        processBridgingRule(location.deviceId(), location.port(), mac, vlanId, false);
+        ips.forEach(ip ->
+                processRoutingRule(location.deviceId(), location.port(), mac, vlanId, ip, false)
+        );
+    }
+
+    void processHostRemovedEvent(HostEvent event) {
         processHostRemoved(event.subject());
     }
 
-    protected void processHostRemoved(Host host) {
+    private void processHostRemoved(Host host) {
         MacAddress mac = host.mac();
         VlanId vlanId = host.vlan();
-        HostLocation location = host.location();
-        DeviceId deviceId = location.deviceId();
-        PortNumber port = location.port();
+        Set<HostLocation> locations = host.locations();
         Set<IpAddress> ips = host.ipAddresses();
-        log.info("Host {}/{} is removed from {}:{}", mac, vlanId, deviceId, port);
+        log.info("Host {}/{} is removed from {}", mac, vlanId, locations);
 
-        if (accepted(host)) {
-            processBridgingRule(deviceId, port, mac, vlanId, true);
-            ips.forEach(ip -> {
-                processRoutingRule(deviceId, port, mac, vlanId, ip, true);
-            });
-        }
+        locations.forEach(location -> {
+            processBridgingRule(location.deviceId(), location.port(), mac, vlanId, true);
+            ips.forEach(ip ->
+                processRoutingRule(location.deviceId(), location.port(), mac, vlanId, ip, true)
+            );
+        });
     }
 
-    protected void processHostMovedEvent(HostEvent event) {
+    void processHostMovedEvent(HostEvent event) {
         MacAddress mac = event.subject().mac();
         VlanId vlanId = event.subject().vlan();
-        HostLocation prevLocation = event.prevSubject().location();
-        DeviceId prevDeviceId = prevLocation.deviceId();
-        PortNumber prevPort = prevLocation.port();
+        Set<HostLocation> prevLocations = event.prevSubject().locations();
         Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
-        HostLocation newLocation = event.subject().location();
-        DeviceId newDeviceId = newLocation.deviceId();
-        PortNumber newPort = newLocation.port();
+        Set<HostLocation> newLocations = event.subject().locations();
         Set<IpAddress> newIps = event.subject().ipAddresses();
-        log.info("Host {}/{} is moved from {}:{} to {}:{}",
-                mac, vlanId, prevDeviceId, prevPort, newDeviceId, newPort);
+        log.info("Host {}/{} is moved from {} to {}", mac, vlanId, prevLocations, newLocations);
 
-        if (accepted(event.prevSubject())) {
-            processBridgingRule(prevDeviceId, prevPort, mac, vlanId, true);
-            prevIps.forEach(ip -> {
-                processRoutingRule(prevDeviceId, prevPort, mac, vlanId, ip, true);
-            });
-        }
+        Set<DeviceId> newDeviceIds = newLocations.stream().map(HostLocation::deviceId)
+                .collect(Collectors.toSet());
 
-        if (accepted(event.subject())) {
-            processBridgingRule(newDeviceId, newPort, mac, vlanId, false);
-            newIps.forEach(ip -> {
-                processRoutingRule(newDeviceId, newPort, mac, vlanId, ip, false);
+        // For each old location
+        Sets.difference(prevLocations, newLocations).forEach(prevLocation -> {
+            // TODO Switch to backup link when pair device is configured
+
+            // 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(), mac, vlanId, true);
+                Sets.intersection(prevIps, newIps).forEach(ip ->
+                        processRoutingRule(prevLocation.deviceId(), prevLocation.port(), mac, vlanId,
+                                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(vlanId) ||
+                        (oldAssignedVlan != null && newAssignedVlan != null &&
+                        // Host is untagged and the new location has the same assigned vlan
+                        oldAssignedVlan.equals(newAssignedVlan));
+            })) {
+                processBridgingRule(prevLocation.deviceId(), prevLocation.port(), mac, vlanId, 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(), mac, vlanId,
+                            ip, true);
+                }
             });
-        }
+
+            // Remove routing rules for old IPs
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                processRoutingRule(prevLocation.deviceId(), prevLocation.port(), mac, vlanId,
+                        ip, true)
+            );
+        });
+
+        // For each new location, add all new IPs.
+        Sets.difference(newLocations, prevLocations).forEach(newLocation -> {
+            processBridgingRule(newLocation.deviceId(), newLocation.port(), mac, vlanId, false);
+            newIps.forEach(ip ->
+                    processRoutingRule(newLocation.deviceId(), newLocation.port(), mac, vlanId,
+                            ip, false)
+            );
+        });
+
+        // For each unchanged location, add new IPs and remove old IPs.
+        Sets.intersection(newLocations, prevLocations).forEach(unchangedLocation -> {
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), mac,
+                            vlanId, ip, true)
+            );
+
+            Sets.difference(newIps, prevIps).forEach(ip ->
+                    processRoutingRule(unchangedLocation.deviceId(), unchangedLocation.port(), mac,
+                        vlanId, ip, false)
+            );
+        });
     }
 
-    protected void processHostUpdatedEvent(HostEvent event) {
+    void processHostUpdatedEvent(HostEvent event) {
         MacAddress mac = event.subject().mac();
         VlanId vlanId = event.subject().vlan();
-        HostLocation prevLocation = event.prevSubject().location();
-        DeviceId prevDeviceId = prevLocation.deviceId();
-        PortNumber prevPort = prevLocation.port();
+        Set<HostLocation> locations = event.subject().locations();
         Set<IpAddress> prevIps = event.prevSubject().ipAddresses();
-        HostLocation newLocation = event.subject().location();
-        DeviceId newDeviceId = newLocation.deviceId();
-        PortNumber newPort = newLocation.port();
         Set<IpAddress> newIps = event.subject().ipAddresses();
         log.info("Host {}/{} is updated", mac, vlanId);
 
-        if (accepted(event.prevSubject())) {
-            // Revoke previous IP table entry
-            Sets.difference(prevIps, newIps).forEach(ip -> {
-                processRoutingRule(prevDeviceId, prevPort, mac, vlanId, ip, true);
-            });
-        }
-
-        if (accepted(event.subject())) {
-            // Populate new IP table entry
-            Sets.difference(newIps, prevIps).forEach(ip -> {
-                processRoutingRule(newDeviceId, newPort, mac, vlanId, ip, false);
-            });
-        }
+        locations.forEach(location -> {
+            Sets.difference(prevIps, newIps).forEach(ip ->
+                    processRoutingRule(location.deviceId(), location.port(), mac, vlanId, ip, true)
+            );
+            Sets.difference(newIps, prevIps).forEach(ip ->
+                    processRoutingRule(location.deviceId(), location.port(), mac, vlanId, ip, false)
+            );
+        });
     }
 
     /**
@@ -185,7 +225,7 @@
      * @param revoke true if forwarding objective is meant to revoke forwarding rule
      * @return Forwarding objective builder
      */
-    private ForwardingObjective.Builder bridgingFwdObjBuilder(
+    ForwardingObjective.Builder bridgingFwdObjBuilder(
             DeviceId deviceId, MacAddress mac, VlanId hostVlanId,
             PortNumber outport, boolean revoke) {
         ConnectPoint connectPoint = new ConnectPoint(deviceId, outport);
@@ -290,33 +330,16 @@
     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("{} routing rule for {} at {}", revoke ? "Revoking" : "Populating",
-                    ip, location);
-            if (revoke) {
-                srManager.routingRulePopulator.revokeRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
-            } else {
-                srManager.routingRulePopulator.populateRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
-            }
+        if (!srManager.deviceConfiguration.inSameSubnet(location, ip)) {
+            log.info("{} is not included in the subnet config of {}/{}. Ignored.", ip, deviceId, port);
+            return;
         }
-    }
 
-    /**
-     * Determines whether a host should be accepted by SR or not.
-     *
-     * @param host host to be checked
-     * @return true if segment routing accepts the host
-     */
-    private boolean accepted(Host host) {
-        SegmentRoutingAppConfig appConfig = srManager.cfgService
-                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
-
-        boolean accepted = appConfig == null ||
-                (!appConfig.suppressHostByProvider().contains(host.providerId().id()) &&
-                !appConfig.suppressHostByPort().contains(host.location()));
-        if (!accepted) {
-            log.info("Ignore suppressed host {}", host.id());
+        log.info("{} routing rule for {} at {}", revoke ? "Revoking" : "Populating", ip, location);
+        if (revoke) {
+            srManager.routingRulePopulator.revokeRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
+        } else {
+            srManager.routingRulePopulator.populateRoute(deviceId, ip.toIpPrefix(), mac, vlanId, port);
         }
-        return accepted;
     }
 }
diff --git a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
index 1f126c6..9ac8d1a 100644
--- a/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
+++ b/apps/segmentrouting/src/main/java/org/onosproject/segmentrouting/SegmentRoutingManager.java
@@ -648,6 +648,21 @@
     }
 
     /**
+     * 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
+     */
+    public VlanId getInternalVlanId(ConnectPoint connectPoint) {
+        VlanId untaggedVlanId = getUntaggedVlanId(connectPoint);
+        VlanId nativeVlanId = getNativeVlanId(connectPoint);
+        return untaggedVlanId != null ? untaggedVlanId : nativeVlanId;
+    }
+
+    /**
      * Returns vlan port map of given device.
      *
      * @param deviceId device id
@@ -1465,7 +1480,7 @@
                     hostHandler.processHostMovedEvent(event);
                     break;
                 case HOST_REMOVED:
-                    hostHandler.processHostRemoveEvent(event);
+                    hostHandler.processHostRemovedEvent(event);
                     break;
                 case HOST_UPDATED:
                     hostHandler.processHostUpdatedEvent(event);
diff --git a/apps/segmentrouting/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java b/apps/segmentrouting/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
index ba9bf8f..d9900c2 100644
--- a/apps/segmentrouting/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
+++ b/apps/segmentrouting/src/test/java/org/onosproject/segmentrouting/HostHandlerTest.java
@@ -48,6 +48,7 @@
 import org.onosproject.net.flowobjective.ForwardingObjective;
 import org.onosproject.net.flowobjective.Objective;
 import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostServiceAdapter;
 import org.onosproject.net.host.InterfaceIpAddress;
 import org.onosproject.net.provider.ProviderId;
 import org.onosproject.segmentrouting.config.DeviceConfiguration;
@@ -73,40 +74,52 @@
     private Map<Integer, TrafficTreatment> nextTable = Maps.newConcurrentMap();
     private AtomicInteger atomicNextId = new AtomicInteger();
 
-    // Host information
+    // 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);
-    private static final IpAddress HOST_IP1 = IpAddress.valueOf("10.0.1.1");
-    private static final IpAddress HOST_IP2 = IpAddress.valueOf("10.0.2.1");
-    private static final IpAddress HOST_IP3 = IpAddress.valueOf("10.0.1.2");
-
-    // Untagged interface
-    private static final ConnectPoint CP1 = new ConnectPoint(DeviceId.deviceId("of:0000000000000001"),
-            PortNumber.portNumber(10));
-    private static final HostLocation HOST_LOC1 = new HostLocation(CP1, 0);
-    private static final IpPrefix INTF_PREFIX1 = IpPrefix.valueOf("10.0.1.254/24");
-    private static final InterfaceIpAddress INTF_IP1 = new InterfaceIpAddress(INTF_PREFIX1.address(),
-            INTF_PREFIX1);
+    // 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");
+    // Device
+    private static final DeviceId DEV1 = DeviceId.deviceId("of:0000000000000001");
+    private static final DeviceId DEV2 = DeviceId.deviceId("of:0000000000000002");
+    // 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);
+    // 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);
+    // Interface VLAN
     private static final VlanId INTF_VLAN_UNTAGGED = VlanId.vlanId((short) 10);
-
-    // Another untagged interface with same subnet and vlan
-    private static final ConnectPoint CP3 = new ConnectPoint(DeviceId.deviceId("of:0000000000000002"),
-            PortNumber.portNumber(30));
-    private static final HostLocation HOST_LOC3 = new HostLocation(CP3, 0);
-
-    // Tagged/Native interface
-    private static final ConnectPoint CP2 = new ConnectPoint(DeviceId.deviceId("of:0000000000000001"),
-            PortNumber.portNumber(20));
-    private static final HostLocation HOST_LOC2 = new HostLocation(CP2, 0);
-    private static final IpPrefix INTF_PREFIX2 = IpPrefix.valueOf("10.0.2.254/24");
-    private static final InterfaceIpAddress INTF_IP2 = new  InterfaceIpAddress(INTF_PREFIX2.address(),
-            INTF_PREFIX2);
     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);
+    // 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 InterfaceIpAddress INTF_IP1 =
+            new InterfaceIpAddress(INTF_PREFIX1.address(), INTF_PREFIX1);
+    private static final InterfaceIpAddress INTF_IP2 =
+            new InterfaceIpAddress(INTF_PREFIX2.address(), INTF_PREFIX2);
+    // 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);
 
     @Before
     public void setUp() throws Exception {
@@ -116,6 +129,7 @@
         srManager.flowObjectiveService = new MockFlowObjectiveService();
         srManager.routingRulePopulator = new MockRoutingRulePopulator();
         srManager.interfaceService = new MockInterfaceService();
+        srManager.hostService = new MockHostService();
 
         hostHandler = new HostHandler(srManager);
 
@@ -125,7 +139,34 @@
 
     @Test
     public void init() throws Exception {
-        // TODO Implement test for init()
+        hostHandler.init(DEV1);
+        assertEquals(1, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+
+        hostHandler.init(DEV2);
+        assertEquals(2, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertEquals(1, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
     }
 
     @Test
@@ -135,27 +176,27 @@
         // 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_LOC1), Sets.newHashSet(HOST_IP1), false);
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
         assertEquals(1, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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_LOC2), Sets.newHashSet(HOST_IP2), false);
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
         assertEquals(2, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC2.deviceId(), HOST_IP2.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
         assertEquals(2, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC2.deviceId(), HOST_MAC, INTF_VLAN_NATIVE)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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_LOC1), Sets.newHashSet(HOST_IP1), false);
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP11), false);
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
         assertEquals(2, routingTable.size());
         assertEquals(2, bridgingTable.size());
@@ -163,119 +204,265 @@
         // 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_LOC2), Sets.newHashSet(HOST_IP2), false);
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP21), false);
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, subject));
         assertEquals(2, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC2.deviceId(), HOST_IP2.toIpPrefix())));
-        assertEquals(HOST_VLAN_TAGGED, routingTable.get(new RoutingTableKey(HOST_LOC2.deviceId(),
-                HOST_IP2.toIpPrefix())).vlanId);
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertEquals(HOST_VLAN_TAGGED, routingTable.get(new RoutingTableKey(HOST_LOC13.deviceId(),
+                HOST_IP21.toIpPrefix())).vlanId);
         assertEquals(3, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC2.deviceId(), HOST_MAC, HOST_VLAN_TAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
     }
 
     @Test
     public void testHostRemoved() throws Exception {
         Host subject = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
-                Sets.newHashSet(HOST_LOC1), Sets.newHashSet(HOST_IP1), false);
+                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, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
 
         // Remove the host
         // Expect: add the routing rule and the bridging rule
-        hostHandler.processHostRemoveEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
+        hostHandler.processHostRemovedEvent(new HostEvent(HostEvent.Type.HOST_REMOVED, subject));
         assertEquals(0, routingTable.size());
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP2.toIpPrefix())));
         assertEquals(0, bridgingTable.size());
-        assertNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @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, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertEquals(0, bridgingTable.size());
     }
 
     @Test
     public void testHostMoved() throws Exception {
         Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
-                Sets.newHashSet(HOST_LOC1), Sets.newHashSet(HOST_IP1), false);
+                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_LOC2), Sets.newHashSet(HOST_IP1), false);
+                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_LOC3), Sets.newHashSet(HOST_IP1), false);
+                Sets.newHashSet(HOST_LOC13), Sets.newHashSet(HOST_IP11), false);
 
         // Add a host
         // Expect: add a new routing rule. no change to bridging rule.
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
         assertEquals(1, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP2.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP3.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
-        assertNull(bridgingTable.get(new BridingTableKey(HOST_LOC3.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNull(bridgingTable.get(new BridingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)));
 
-        // Move the host to CP2, which has different subnet setting
+        // 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, host2, host1));
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host3, host1));
         assertEquals(0, routingTable.size());
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC2.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC3.deviceId(), HOST_IP1.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC2.deviceId(), HOST_MAC, INTF_VLAN_NATIVE)));
-        assertNull(bridgingTable.get(new BridingTableKey(HOST_LOC3.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_NATIVE)));
+        assertNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
 
-        // Move the host to CP3, which has same subnet setting
+        // 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, host3, host2));
+        hostHandler.processHostMovedEvent(new HostEvent(HostEvent.Type.HOST_MOVED, host2, host3));
         assertEquals(1, routingTable.size());
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC2.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC3.deviceId(), HOST_IP1.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC3.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, bridgingTable.size());
+        assertEquals(P1, bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())).portNumber);
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(2, bridgingTable.size());
+        assertEquals(P2, bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P2, bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP14.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV2, HOST_IP14.toIpPrefix())).portNumber);
+        assertEquals(2, bridgingTable.size());
+        assertEquals(P1, bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P1, bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P1, routingTable.get(new RoutingTableKey(DEV1, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())).portNumber);
+        assertEquals(P2, routingTable.get(new RoutingTableKey(DEV2, HOST_IP13.toIpPrefix())).portNumber);
+        assertEquals(2, bridgingTable.size());
+        assertEquals(P1, bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
+        assertEquals(P2, bridgingTable.get(new BridingTableKey(DEV2, HOST_MAC, INTF_VLAN_UNTAGGED)).portNumber);
     }
 
     @Test
     public void testHostUpdated() throws Exception {
         Host host1 = new DefaultHost(PROVIDER_ID, HOST_ID_UNTAGGED, HOST_MAC, HOST_VLAN_UNTAGGED,
-                Sets.newHashSet(HOST_LOC1), Sets.newHashSet(HOST_IP1), false);
+                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_LOC1), Sets.newHashSet(HOST_IP2), false);
+                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_LOC1), Sets.newHashSet(HOST_IP3), false);
+                Sets.newHashSet(HOST_LOC11), Sets.newHashSet(HOST_IP12), false);
 
         // Add a host
-        // Expect: add a new routing rule. no change to bridging rule.
+        // Expect: add one new routing rule. Add one new bridging rule.
         hostHandler.processHostAddedEvent(new HostEvent(HostEvent.Type.HOST_ADDED, host1));
         assertEquals(1, routingTable.size());
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP2.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP3.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP2.toIpPrefix())));
-        assertNotNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP3.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP1.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP2.toIpPrefix())));
-        assertNull(routingTable.get(new RoutingTableKey(HOST_LOC1.deviceId(), HOST_IP3.toIpPrefix())));
         assertEquals(1, bridgingTable.size());
-        assertNotNull(bridgingTable.get(new BridingTableKey(HOST_LOC1.deviceId(), HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+    }
+
+    @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, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP13.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP14.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP13.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP14.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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, routingTable.size());
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP21.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV1, HOST_IP12.toIpPrefix())));
+        assertNotNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP11.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP21.toIpPrefix())));
+        assertNull(routingTable.get(new RoutingTableKey(DEV2, HOST_IP12.toIpPrefix())));
+        assertEquals(2, bridgingTable.size());
+        assertNotNull(bridgingTable.get(new BridingTableKey(DEV1, HOST_MAC, INTF_VLAN_UNTAGGED)));
+        assertNotNull(bridgingTable.get(new BridingTableKey(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));
     }
 
     class MockSegmentRoutingManager extends SegmentRoutingManager {
@@ -299,14 +486,20 @@
         public Set<Interface> getInterfacesByPort(ConnectPoint cp) {
             Interface intf = null;
 
-            if (CP1.equals(cp)) {
-                intf = new Interface(null, CP1, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+            if (CP11.equals(cp)) {
+                intf = new Interface(null, CP11, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
                         INTF_VLAN_UNTAGGED, null, null);
-            } else if (CP2.equals(cp)) {
-                intf = new Interface(null, CP2, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
+            } else if (CP12.equals(cp)) {
+                intf = new Interface(null, CP12, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                        INTF_VLAN_UNTAGGED, null, null);
+            } else if (CP13.equals(cp)) {
+                intf = new Interface(null, CP13, Lists.newArrayList(INTF_IP2), MacAddress.NONE, null,
                         null, INTF_VLAN_TAGGED, INTF_VLAN_NATIVE);
-            } else if (CP3.equals(cp)) {
-                intf = new Interface(null, CP3, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+            } else if (CP21.equals(cp)) {
+                intf = new Interface(null, CP21, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
+                        INTF_VLAN_UNTAGGED, null, null);
+            } else if (CP22.equals(cp)) {
+                intf = new Interface(null, CP22, Lists.newArrayList(INTF_IP1), MacAddress.NONE, null,
                         INTF_VLAN_UNTAGGED, null, null);
             }
 
@@ -348,6 +541,13 @@
         }
     }
 
+    class MockHostService extends HostServiceAdapter {
+        @Override
+        public Set<Host> getHosts() {
+            return Sets.newHashSet(HOST1);
+        }
+    }
+
     class MockRoutingRulePopulator extends RoutingRulePopulator {
         MockRoutingRulePopulator() {
             super(srManager);