Introduced HostMoveTracker to suspend hosts that moves too frequently

Change-Id: I3037c626657790ce6817feddba7dbbfac203b188
diff --git a/core/api/src/main/java/org/onosproject/net/DefaultHost.java b/core/api/src/main/java/org/onosproject/net/DefaultHost.java
index ec0d9b5..8e6c195 100644
--- a/core/api/src/main/java/org/onosproject/net/DefaultHost.java
+++ b/core/api/src/main/java/org/onosproject/net/DefaultHost.java
@@ -41,6 +41,7 @@
     private final VlanId innerVlan;
     private final EthType tpid;
     private final boolean configured;
+    private final boolean suspended;
 
     // TODO consider moving this constructor to a builder pattern.
     /**
@@ -95,7 +96,7 @@
                        VlanId vlan, Set<HostLocation> locations, Set<IpAddress> ips,
                        boolean configured, Annotations... annotations) {
         this(providerId, id, mac, vlan, locations, ips, VlanId.NONE,
-             EthType.EtherType.UNKNOWN.ethType(), configured, annotations);
+                EthType.EtherType.UNKNOWN.ethType(), configured, annotations);
     }
 
     /**
@@ -123,6 +124,36 @@
         this.configured = configured;
         this.innerVlan = innerVlan;
         this.tpid = tpid;
+        this.suspended = false;
+    }
+
+    /**
+     * Creates an end-station host using the supplied information.
+     *
+     * @param providerId  provider identity
+     * @param id          host identifier
+     * @param mac         host MAC address
+     * @param vlan        host VLAN identifier
+     * @param locations   set of host locations
+     * @param ips         host IP addresses
+     * @param configured  true if configured via NetworkConfiguration
+     * @param innerVlan   host inner VLAN identifier
+     * @param tpid        outer TPID of a host
+     * @param suspended   true if the host is suspended due to policy violation.
+     * @param annotations optional key/value annotations
+     */
+    public DefaultHost(ProviderId providerId, HostId id, MacAddress mac,
+                       VlanId vlan, Set<HostLocation> locations, Set<IpAddress> ips, VlanId innerVlan,
+                       EthType tpid, boolean configured, boolean suspended, Annotations... annotations) {
+        super(providerId, id, annotations);
+        this.mac = mac;
+        this.vlan = vlan;
+        this.locations = new HashSet<>(locations);
+        this.ips = new HashSet<>(ips);
+        this.configured = configured;
+        this.innerVlan = innerVlan;
+        this.tpid = tpid;
+        this.suspended = suspended;
     }
 
     @Override
@@ -177,8 +208,14 @@
     }
 
     @Override
+    public boolean suspended() {
+        return this.suspended;
+    }
+
+
+    @Override
     public int hashCode() {
-        return Objects.hash(id, mac, vlan, locations);
+        return Objects.hash(id, mac, vlan, locations, suspended);
     }
 
     @Override
@@ -195,7 +232,8 @@
                     Objects.equals(this.ipAddresses(), other.ipAddresses()) &&
                     Objects.equals(this.innerVlan, other.innerVlan) &&
                     Objects.equals(this.tpid, other.tpid) &&
-                    Objects.equals(this.annotations(), other.annotations());
+                    Objects.equals(this.annotations(), other.annotations()) &&
+                    Objects.equals(this.suspended, other.suspended);
         }
         return false;
     }
@@ -212,6 +250,7 @@
                 .add("configured", configured())
                 .add("innerVlanId", innerVlan())
                 .add("outerTPID", tpid())
+                .add("suspended", suspended())
                 .toString();
     }
 
diff --git a/core/api/src/main/java/org/onosproject/net/Host.java b/core/api/src/main/java/org/onosproject/net/Host.java
index 1c2e50a..d4a922e 100644
--- a/core/api/src/main/java/org/onosproject/net/Host.java
+++ b/core/api/src/main/java/org/onosproject/net/Host.java
@@ -73,6 +73,7 @@
 
     /**
      * Returns true if configured by NetworkConfiguration.
+     *
      * @return configured/learnt dynamically
      */
     default boolean configured() {
@@ -98,4 +99,12 @@
     }
     // TODO: explore capturing list of recent locations to aid in mobility
 
+    /**
+     * Returns the state of host whether it is in suspended state(offending host due to frequent movement.).
+     *
+     * @return state true if suspended else false.
+     */
+    boolean suspended();
+
+
 }
diff --git a/core/api/src/main/java/org/onosproject/net/host/HostEvent.java b/core/api/src/main/java/org/onosproject/net/host/HostEvent.java
index 0ed491f..0fecf2b 100644
--- a/core/api/src/main/java/org/onosproject/net/host/HostEvent.java
+++ b/core/api/src/main/java/org/onosproject/net/host/HostEvent.java
@@ -48,7 +48,15 @@
         /**
          * Signifies that a host location has changed.
          */
-        HOST_MOVED
+        HOST_MOVED,
+        /**
+         * Signifies that a host is in offending state, eg: frequent  host movement.
+         */
+        HOST_SUSPENDED,
+        /**
+         * Signifies that host state in non offending state.
+         */
+        HOST_UNSUSPENDED
     }
 
     private Host prevSubject;
diff --git a/core/api/src/main/java/org/onosproject/net/host/HostStore.java b/core/api/src/main/java/org/onosproject/net/host/HostStore.java
index 0b6238b..6d6eb11 100644
--- a/core/api/src/main/java/org/onosproject/net/host/HostStore.java
+++ b/core/api/src/main/java/org/onosproject/net/host/HostStore.java
@@ -161,4 +161,21 @@
      * @param probeMac the source MAC address ONOS uses to probe the host
      */
     default void removePendingHostLocation(MacAddress probeMac) {}
+
+    /**
+     * Update the host to suspended state to true
+     * denotes host is in suspended state.
+     *
+     * @param id ID of the host
+     */
+    default void suspend(HostId id){}
+
+    /**
+     * Update the host suspended state to false
+     * denotes host is in unsuspended state.
+     *
+     * @param id ID of the host
+     */
+    default void unsuspend(HostId id){}
+
 }
diff --git a/core/net/src/main/java/org/onosproject/net/OsgiPropertyConstants.java b/core/net/src/main/java/org/onosproject/net/OsgiPropertyConstants.java
index a88754f..6afb15c 100644
--- a/core/net/src/main/java/org/onosproject/net/OsgiPropertyConstants.java
+++ b/core/net/src/main/java/org/onosproject/net/OsgiPropertyConstants.java
@@ -62,6 +62,21 @@
     public static final String HM_PROBE_RATE = "probeRate";
     public static final long HM_PROBE_RATE_DEFAULT = 30000;
 
+    public static final String HM_HOST_MOVE_TRACKER_ENABLE = "hostMoveTrackerEnabled";
+    public static final boolean HM_HOST_MOVE_TRACKER_ENABLE_DEFAULT = false;
+
+    public static final String HM_HOST_MOVED_THRESHOLD_IN_MILLIS = "hostMoveThresholdInMillis";
+    public static final int HM_HOST_MOVED_THRESHOLD_IN_MILLIS_DEFAULT = 200000;
+
+    public static final String HM_HOST_MOVE_COUNTER = "hostMoveCounter";
+    public static final int HM_HOST_MOVE_COUNTER_DEFAULT = 3;
+
+    public static final String HM_OFFENDING_HOST_EXPIRY_IN_MINS = "offendingHostExpiryInMins";
+    public static final long HM_OFFENDING_HOST_EXPIRY_IN_MINS_DEFAULT = 1;
+
+    public static final String HM_OFFENDING_HOST_THREADS_POOL_SIZE = "offendingHostClearThreadPool";
+    public static final int HM_OFFENDING_HOST_THREADS_POOL_SIZE_DEFAULT = 10;
+
     public static final String HM_GREEDY_LEARNING_IPV6 = "greedyLearningIpv6";
     public static final boolean HM_GREEDY_LEARNING_IPV6_DEFAULT = false;
 
diff --git a/core/net/src/main/java/org/onosproject/net/host/impl/HostManager.java b/core/net/src/main/java/org/onosproject/net/host/impl/HostManager.java
index df25281..3bacd7f 100644
--- a/core/net/src/main/java/org/onosproject/net/host/impl/HostManager.java
+++ b/core/net/src/main/java/org/onosproject/net/host/impl/HostManager.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.net.host.impl;
 
+import com.google.common.collect.Sets;
 import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
@@ -59,12 +60,19 @@
 import org.slf4j.Logger;
 
 import java.util.Dictionary;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.isNullOrEmpty;
 import static org.onlab.packet.IPv6.getLinkLocalAddress;
+import static org.onlab.util.Tools.get;
 import static org.onosproject.net.OsgiPropertyConstants.HM_ALLOW_DUPLICATE_IPS;
 import static org.onosproject.net.OsgiPropertyConstants.HM_ALLOW_DUPLICATE_IPS_DEFAULT;
 import static org.onosproject.net.OsgiPropertyConstants.HM_GREEDY_LEARNING_IPV6;
@@ -73,6 +81,17 @@
 import static org.onosproject.net.OsgiPropertyConstants.HM_MONITOR_HOSTS_DEFAULT;
 import static org.onosproject.net.OsgiPropertyConstants.HM_PROBE_RATE;
 import static org.onosproject.net.OsgiPropertyConstants.HM_PROBE_RATE_DEFAULT;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVED_THRESHOLD_IN_MILLIS;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVED_THRESHOLD_IN_MILLIS_DEFAULT;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVE_COUNTER;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVE_COUNTER_DEFAULT;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVE_TRACKER_ENABLE;
+import static org.onosproject.net.OsgiPropertyConstants.HM_HOST_MOVE_TRACKER_ENABLE_DEFAULT;
+import static org.onosproject.net.OsgiPropertyConstants.HM_OFFENDING_HOST_EXPIRY_IN_MINS;
+import static org.onosproject.net.OsgiPropertyConstants.HM_OFFENDING_HOST_EXPIRY_IN_MINS_DEFAULT;
+import static org.onosproject.net.OsgiPropertyConstants.HM_OFFENDING_HOST_THREADS_POOL_SIZE;
+import static org.onosproject.net.OsgiPropertyConstants.HM_OFFENDING_HOST_THREADS_POOL_SIZE_DEFAULT;
+
 import static org.onosproject.security.AppGuard.checkPermission;
 import static org.onosproject.security.AppPermission.Type.HOST_EVENT;
 import static org.onosproject.security.AppPermission.Type.HOST_READ;
@@ -89,10 +108,17 @@
             HostProviderRegistry.class
         },
         property = {
-            HM_ALLOW_DUPLICATE_IPS + ":Boolean=" + HM_ALLOW_DUPLICATE_IPS_DEFAULT,
-            HM_MONITOR_HOSTS + ":Boolean=" + HM_MONITOR_HOSTS_DEFAULT,
-            HM_PROBE_RATE + ":Integer=" + HM_PROBE_RATE_DEFAULT,
-            HM_GREEDY_LEARNING_IPV6 + ":Boolean=" + HM_GREEDY_LEARNING_IPV6_DEFAULT
+                HM_ALLOW_DUPLICATE_IPS + ":Boolean=" + HM_ALLOW_DUPLICATE_IPS_DEFAULT,
+                HM_MONITOR_HOSTS + ":Boolean=" + HM_MONITOR_HOSTS_DEFAULT,
+                HM_PROBE_RATE + ":Integer=" + HM_PROBE_RATE_DEFAULT,
+                HM_GREEDY_LEARNING_IPV6 + ":Boolean=" + HM_GREEDY_LEARNING_IPV6_DEFAULT,
+                HM_HOST_MOVE_TRACKER_ENABLE + ":Boolean=" + HM_HOST_MOVE_TRACKER_ENABLE_DEFAULT,
+                HM_HOST_MOVED_THRESHOLD_IN_MILLIS + ":Integer=" + HM_HOST_MOVED_THRESHOLD_IN_MILLIS_DEFAULT,
+                HM_HOST_MOVE_COUNTER + ":Integer=" + HM_HOST_MOVE_COUNTER_DEFAULT,
+                HM_OFFENDING_HOST_EXPIRY_IN_MINS + ":Long=" + HM_OFFENDING_HOST_EXPIRY_IN_MINS_DEFAULT,
+                HM_OFFENDING_HOST_THREADS_POOL_SIZE + ":Integer=" + HM_OFFENDING_HOST_THREADS_POOL_SIZE_DEFAULT
+
+
         }
 )
 public class HostManager
@@ -140,8 +166,25 @@
     /** Enable/Disable greedy learning of IPv6 link local address. */
     private boolean greedyLearningIpv6 = HM_GREEDY_LEARNING_IPV6_DEFAULT;
 
+    /** Enable/Disable tracking of rogue host moves. */
+    private boolean hostMoveTrackerEnabled = HM_HOST_MOVE_TRACKER_ENABLE_DEFAULT;
+
+    /** Host move threshold in milli seconds. */
+    private int hostMoveThresholdInMillis = HM_HOST_MOVED_THRESHOLD_IN_MILLIS_DEFAULT;
+
+    /** If the host move happening within given threshold then increment the host move counter. */
+    private int hostMoveCounter = HM_HOST_MOVE_COUNTER_DEFAULT;
+
+    /** Max value of the counter after which the host will not be considered as offending host. */
+    private long offendingHostExpiryInMins = HM_OFFENDING_HOST_EXPIRY_IN_MINS_DEFAULT;
+
+    /** Default pool size of offending host clear executor thread. */
+    private int offendingHostClearThreadPool = HM_OFFENDING_HOST_THREADS_POOL_SIZE_DEFAULT;
+
     private HostMonitor monitor;
     private HostAnnotationOperator hostAnnotationOperator;
+    private ScheduledExecutorService offendingHostUnblockExecutor = null;
+    private Map<HostId, HostMoveTracker> hostMoveTracker = new ConcurrentHashMap<>();
 
 
     @Activate
@@ -154,8 +197,8 @@
         monitor = new HostMonitor(packetService, this, interfaceService, edgePortService);
         monitor.setProbeRate(probeRate);
         monitor.start();
-        modified(context);
         cfgService.registerProperties(getClass());
+        modified(context);
         log.info("Started");
     }
 
@@ -166,6 +209,9 @@
         networkConfigService.removeListener(networkConfigListener);
         cfgService.unregisterProperties(getClass(), false);
         monitor.shutdown();
+        if (offendingHostUnblockExecutor != null) {
+            offendingHostUnblockExecutor.shutdown();
+        }
         log.info("Stopped");
     }
 
@@ -196,6 +242,10 @@
     private void readComponentConfiguration(ComponentContext context) {
         Dictionary<?, ?> properties = context.getProperties();
         Boolean flag;
+        int newHostMoveThresholdInMillis;
+        int newHostMoveCounter;
+        int newOffendinghostPoolSize;
+        long newOffendingHostExpiryInMins;
 
         flag = Tools.isPropertyEnabled(properties, HM_MONITOR_HOSTS);
         if (flag == null) {
@@ -233,7 +283,71 @@
             log.info("Configured. greedyLearningIpv6 {}",
                      greedyLearningIpv6 ? "enabled" : "disabled");
         }
+        flag = Tools.isPropertyEnabled(properties, HM_HOST_MOVE_TRACKER_ENABLE);
+        if (flag == null) {
+            log.info("Host move tracker is not configured " +
+                    "using current value of {}", hostMoveTrackerEnabled);
+        } else {
+            hostMoveTrackerEnabled = flag;
+            log.info("Configured. hostMoveTrackerEnabled {}",
+                    hostMoveTrackerEnabled ? "enabled" : "disabled");
 
+            //On enable cfg ,sets default configuration vales added , else use the default values
+            properties = context.getProperties();
+            try {
+                String s = get(properties, HM_HOST_MOVED_THRESHOLD_IN_MILLIS);
+                newHostMoveThresholdInMillis = isNullOrEmpty(s) ?
+                        hostMoveThresholdInMillis : Integer.parseInt(s.trim());
+
+                s = get(properties, HM_HOST_MOVE_COUNTER);
+                newHostMoveCounter = isNullOrEmpty(s) ? hostMoveCounter : Integer.parseInt(s.trim());
+
+                s = get(properties, HM_OFFENDING_HOST_EXPIRY_IN_MINS);
+                newOffendingHostExpiryInMins = isNullOrEmpty(s) ?
+                        offendingHostExpiryInMins : Integer.parseInt(s.trim());
+
+                s = get(properties, HM_OFFENDING_HOST_THREADS_POOL_SIZE);
+                newOffendinghostPoolSize = isNullOrEmpty(s) ?
+                        offendingHostClearThreadPool : Integer.parseInt(s.trim());
+            } catch (NumberFormatException | ClassCastException e) {
+                newHostMoveThresholdInMillis = HM_HOST_MOVED_THRESHOLD_IN_MILLIS_DEFAULT;
+                newHostMoveCounter = HM_HOST_MOVE_COUNTER_DEFAULT;
+                newOffendingHostExpiryInMins = HM_OFFENDING_HOST_EXPIRY_IN_MINS_DEFAULT;
+                newOffendinghostPoolSize = HM_OFFENDING_HOST_THREADS_POOL_SIZE_DEFAULT;
+            }
+            if (newHostMoveThresholdInMillis != hostMoveThresholdInMillis) {
+                hostMoveThresholdInMillis = newHostMoveThresholdInMillis;
+            }
+            if (newHostMoveCounter != hostMoveCounter) {
+                hostMoveCounter = newHostMoveCounter;
+            }
+            if (newOffendingHostExpiryInMins != offendingHostExpiryInMins) {
+                offendingHostExpiryInMins = newOffendingHostExpiryInMins;
+            }
+            if (hostMoveTrackerEnabled && offendingHostUnblockExecutor == null) {
+                setupThreadPool();
+            } else if (newOffendinghostPoolSize != offendingHostClearThreadPool
+                    && offendingHostUnblockExecutor != null) {
+                offendingHostClearThreadPool = newOffendinghostPoolSize;
+                offendingHostUnblockExecutor.shutdown();
+                offendingHostUnblockExecutor = null;
+                setupThreadPool();
+            } else if (!hostMoveTrackerEnabled && offendingHostUnblockExecutor != null) {
+                offendingHostUnblockExecutor.shutdown();
+                offendingHostUnblockExecutor = null;
+            }
+            if (newOffendinghostPoolSize != offendingHostClearThreadPool) {
+                offendingHostClearThreadPool = newOffendinghostPoolSize;
+            }
+
+            log.debug("modified hostMoveThresholdInMillis: {}, hostMoveCounter: {}, " +
+                            "offendingHostExpiryInMins: {} ", hostMoveThresholdInMillis,
+                    hostMoveCounter, offendingHostExpiryInMins);
+        }
+    }
+
+    private synchronized void setupThreadPool() {
+        offendingHostUnblockExecutor = Executors.newScheduledThreadPool(offendingHostClearThreadPool);
     }
 
     /**
@@ -371,8 +485,16 @@
                 hostDescription = hostAnnotationOperator.combine(hostId, hostDescription, Optional.of(annoConfig));
             }
 
-            store.createOrUpdateHost(provider().id(), hostId,
-                                     hostDescription, replaceIps);
+            if (!hostMoveTrackerEnabled) {
+                store.createOrUpdateHost(provider().id(), hostId,
+                        hostDescription, replaceIps);
+            } else if (!shouldBlock(hostId, hostDescription.locations())) {
+                log.debug("Host move is allowed for host with Id: {} ", hostId);
+                store.createOrUpdateHost(provider().id(), hostId,
+                        hostDescription, replaceIps);
+            } else {
+                log.info("Host move is NOT allowed for host with Id: {} , removing from host store ", hostId);
+            }
 
             if (monitorHosts) {
                 hostDescription.ipAddress().forEach(ip -> {
@@ -424,7 +546,7 @@
                 allHosts.forEach(eachHost -> {
                     if (!(eachHost.id().equals(hostId))) {
                         log.info("Duplicate ip {} found on host {} and {}", ip,
-                                 hostId.toString(), eachHost.id().toString());
+                                hostId.toString(), eachHost.id().toString());
                         store.removeIp(eachHost.id(), ip);
                     }
                 });
@@ -509,8 +631,83 @@
             Host host = store.getHost(hostId);
             return host == null || !host.configured() || host.providerId().equals(provider().id());
         }
+
+
+        /**
+         * Deny host move if happening within the threshold time,
+         * track moved host to identify offending hosts.
+         *
+         * @param hostId    host identifier
+         * @param locations host locations
+         */
+        private boolean shouldBlock(HostId hostId, Set<HostLocation> locations) {
+            Host host = store.getHost(hostId);
+            // If host is not present in host store means host added for hte first time.
+            if (host != null) {
+                if (host.suspended()) {
+                    // Checks host is marked as offending in other onos cluster instance/local instance
+                    log.debug("Host id {} is moving frequently hence host moving " +
+                            "processing is ignored", hostId);
+                    return true;
+                }
+            } else {
+                //host added for the first time.
+                return false;
+            }
+            HostMoveTracker hostMove = hostMoveTracker.computeIfAbsent(hostId, id -> new HostMoveTracker(locations));
+            if (Sets.difference(hostMove.getLocations(), locations).isEmpty() &&
+                    Sets.difference(locations, hostMove.getLocations()).isEmpty()) {
+                log.debug("Not hostmove scenario: Host id: {}, Old Host Location: {}, New host Location: {}",
+                        hostId, hostMove.getLocations(), locations);
+                return false; // It is not a host move scenario
+            } else if (hostMove.getCounter() >= hostMoveCounter && System.currentTimeMillis() - hostMove.getTimeStamp()
+                    < hostMoveThresholdInMillis) {
+                //Check host move is crossed the threshold, then to mark as offending Host
+                log.debug("Host id {} is identified as offending host and entry is added in cache", hostId);
+                hostMove.resetHostMoveTracker(locations);
+                store.suspend(hostId);
+                //Set host suspended flag to false after given offendingHostExpiryInMins
+                offendingHostUnblockExecutor.schedule(new UnblockOffendingHost(hostId),
+                        offendingHostExpiryInMins,
+                        TimeUnit.MINUTES);
+                return true;
+            } else if (System.currentTimeMillis() - hostMove.getTimeStamp()
+                    < hostMoveThresholdInMillis) {
+                //Increment the host move count as hostmove occured within the hostMoveThresholdInMillis time
+                hostMove.updateHostMoveTracker(locations);
+                log.debug("Updated the tracker with the host move registered for host: {}", hostId);
+            } else if (System.currentTimeMillis() - hostMove.getTimeStamp()
+                    > hostMoveThresholdInMillis) {
+                //Hostmove is happened after hostMoveThresholdInMillis time so remove from host tracker.
+                hostMove.resetHostMoveTracker(locations);
+                store.unsuspend(hostId);
+                log.debug("Reset the tracker with the host move registered for host: {}", hostId);
+            }
+            return false;
+        }
+
+        // Set host suspended flag to false after given offendingHostExpiryInMins.
+        private final class UnblockOffendingHost implements Runnable {
+            private HostId hostId;
+
+            UnblockOffendingHost(HostId hostId) {
+                this.hostId = hostId;
+            }
+
+            @Override
+            public void run() {
+                // Set the host suspended flag to false
+                try {
+                    store.unsuspend(hostId);
+                    log.debug("Host {}: Marked host as unsuspended", hostId);
+                } catch (Exception ex) {
+                    log.debug("Host {}: not present in host list", hostId);
+                }
+            }
+        }
     }
 
+
     // Store delegate to re-post events emitted from the store.
     private class InternalStoreDelegate implements HostStoreDelegate {
         @Override
diff --git a/core/net/src/main/java/org/onosproject/net/host/impl/HostMoveTracker.java b/core/net/src/main/java/org/onosproject/net/host/impl/HostMoveTracker.java
new file mode 100644
index 0000000..05a8f77
--- /dev/null
+++ b/core/net/src/main/java/org/onosproject/net/host/impl/HostMoveTracker.java
@@ -0,0 +1,115 @@
+/*
+ * 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.net.host.impl;
+
+import org.onosproject.net.HostLocation;
+
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Used for tracking of the host move.
+ */
+public class HostMoveTracker {
+    private Integer counter;
+    private Long timeStamp;
+    private Set<HostLocation> locations;
+
+    /**
+     * Initialize the instance of HostMoveTracker.
+     *
+     * @param locations List of locations where host is present
+     */
+    public HostMoveTracker(Set<HostLocation> locations) {
+        counter = 1;
+        timeStamp = System.currentTimeMillis();
+        this.locations = locations;
+    }
+
+    /**
+     * Updates locations in HostMoveTracker.
+     *
+     * @param locations List of locations where host is present
+     */
+    public void updateHostMoveTracker(Set<HostLocation> locations) {
+        counter += 1;
+        timeStamp = System.currentTimeMillis();
+        this.locations = locations;
+    }
+
+    /**
+     * Reset hostmove count,timestamp and updated locations.
+     *
+     * @param locations List of locations where host is present
+     */
+    public void resetHostMoveTracker(Set<HostLocation> locations) {
+        counter = 0;
+        timeStamp = System.currentTimeMillis();
+        this.locations = locations;
+    }
+
+    /**
+     * Reset hostmove count and timestamp.
+     */
+    public void resetHostMoveTracker() {
+        counter = 0;
+        timeStamp = System.currentTimeMillis();
+    }
+
+    public Long getTimeStamp() {
+        return timeStamp;
+    }
+
+    public Integer getCounter() {
+        return counter;
+    }
+
+
+    public Set<HostLocation> getLocations() {
+        return locations;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        HostMoveTracker that = (HostMoveTracker) o;
+        return Objects.equals(locations, that.locations) &&
+                Objects.equals(counter, that.counter);
+    }
+
+    @Override
+    public int hashCode() {
+
+        return Objects.hash(locations, counter);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("counter", getCounter())
+                .add("timeStamp", getTimeStamp())
+                .add("locations", getLocations())
+                .toString();
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onosproject/store/host/impl/DistributedHostStore.java b/core/store/dist/src/main/java/org/onosproject/store/host/impl/DistributedHostStore.java
index 16ad729..8cf463d 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/host/impl/DistributedHostStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/host/impl/DistributedHostStore.java
@@ -37,11 +37,11 @@
 import org.onosproject.store.AbstractStore;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.ConsistentMap;
-import org.onosproject.store.service.DistributedPrimitive.Status;
 import org.onosproject.store.service.MapEvent;
 import org.onosproject.store.service.MapEventListener;
 import org.onosproject.store.service.Serializer;
 import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.DistributedPrimitive.Status;
 import org.onosproject.store.service.Versioned;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -66,10 +66,7 @@
 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.net.DefaultAnnotations.merge;
-import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
-import static org.onosproject.net.host.HostEvent.Type.HOST_MOVED;
-import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
-import static org.onosproject.net.host.HostEvent.Type.HOST_UPDATED;
+import static org.onosproject.net.host.HostEvent.Type.*;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -164,7 +161,7 @@
 
         if (replaceIPs) {
             if (!Objects.equals(hostDescription.ipAddress(),
-                                existingHost.ipAddresses())) {
+                    existingHost.ipAddresses())) {
                 return true;
             }
         } else {
@@ -176,8 +173,8 @@
         // check to see if any of the annotations provided by hostDescription
         // differ from those in the existing host
         return hostDescription.annotations().keys().stream()
-                   .anyMatch(k -> !Objects.equals(hostDescription.annotations().value(k),
-                                                  existingHost.annotations().value(k)));
+                .anyMatch(k -> !Objects.equals(hostDescription.annotations().value(k),
+                        existingHost.annotations().value(k)));
 
 
     }
@@ -189,37 +186,37 @@
                                         HostDescription hostDescription,
                                         boolean replaceIPs) {
         hostsConsistentMap.computeIf(hostId,
-                       existingHost -> shouldUpdate(existingHost, providerId,
-                                                    hostDescription, replaceIPs),
-                       (id, existingHost) -> {
+                existingHost -> shouldUpdate(existingHost, providerId,
+                        hostDescription, replaceIPs),
+                (id, existingHost) -> {
 
-                           final Set<IpAddress> addresses;
-                           if (existingHost == null || replaceIPs) {
-                               addresses = ImmutableSet.copyOf(hostDescription.ipAddress());
-                           } else {
-                               addresses = Sets.newHashSet(existingHost.ipAddresses());
-                               addresses.addAll(hostDescription.ipAddress());
-                           }
+                    final Set<IpAddress> addresses;
+                    if (existingHost == null || replaceIPs) {
+                        addresses = ImmutableSet.copyOf(hostDescription.ipAddress());
+                    } else {
+                        addresses = Sets.newHashSet(existingHost.ipAddresses());
+                        addresses.addAll(hostDescription.ipAddress());
+                    }
 
-                           final Annotations annotations;
-                           if (existingHost != null) {
-                               annotations = merge((DefaultAnnotations) existingHost.annotations(),
-                                       hostDescription.annotations());
-                           } else {
-                               annotations = hostDescription.annotations();
-                           }
+                    final Annotations annotations;
+                    if (existingHost != null) {
+                        annotations = merge((DefaultAnnotations) existingHost.annotations(),
+                                hostDescription.annotations());
+                    } else {
+                        annotations = hostDescription.annotations();
+                    }
 
-                           return new DefaultHost(providerId,
-                                                  hostId,
-                                                  hostDescription.hwAddress(),
-                                                  hostDescription.vlan(),
-                                                  hostDescription.locations(),
-                                                  addresses,
-                                                  hostDescription.innerVlan(),
-                                                  hostDescription.tpid(),
-                                                  hostDescription.configured(),
-                                                  annotations);
-                       });
+                    return new DefaultHost(providerId,
+                            hostId,
+                            hostDescription.hwAddress(),
+                            hostDescription.vlan(),
+                            hostDescription.locations(),
+                            addresses,
+                            hostDescription.innerVlan(),
+                            hostDescription.tpid(),
+                            hostDescription.configured(),
+                            annotations);
+                });
         return null;
     }
 
@@ -279,9 +276,9 @@
                         .forEach(newLocations::add);
 
                 return new DefaultHost(existingHost.providerId(),
-                                hostId, existingHost.mac(), existingHost.vlan(),
-                                newLocations, existingHost.ipAddresses(),
-                                existingHost.configured(), existingHost.annotations());
+                        hostId, existingHost.mac(), existingHost.vlan(),
+                        newLocations, existingHost.ipAddresses(),
+                        existingHost.configured(), existingHost.annotations());
             }
             return null;
         });
@@ -361,6 +358,52 @@
         return ImmutableSet.copyOf(filtered);
     }
 
+    @Override
+    public void suspend(HostId hostId) {
+        hosts.compute(hostId, (id, existingHost) -> {
+            if (existingHost != null) {
+                if (!existingHost.suspended()) {
+                    return new DefaultHost(existingHost.providerId(),
+                            hostId,
+                            existingHost.mac(),
+                            existingHost.vlan(),
+                            existingHost.locations(),
+                            existingHost.ipAddresses(),
+                            existingHost.innerVlan(),
+                            existingHost.tpid(),
+                            existingHost.configured(),
+                            true,
+                            existingHost.annotations());
+                }
+
+            }
+            return null;
+        });
+    }
+
+    @Override
+    public void unsuspend(HostId hostId) {
+        hosts.compute(hostId, (id, existingHost) -> {
+            if (existingHost != null) {
+                if (existingHost.suspended()) {
+                    return new DefaultHost(existingHost.providerId(),
+                            hostId,
+                            existingHost.mac(),
+                            existingHost.vlan(),
+                            existingHost.locations(),
+                            existingHost.ipAddresses(),
+                            existingHost.innerVlan(),
+                            existingHost.tpid(),
+                            existingHost.configured(),
+                            false,
+                            existingHost.annotations());
+
+                }
+            }
+            return null;
+        });
+    }
+
     private Set<Host> filter(Collection<DefaultHost> collection, Predicate<DefaultHost> predicate) {
         return collection.stream().filter(predicate).collect(Collectors.toSet());
     }
@@ -418,7 +461,11 @@
                     break;
                 case UPDATE:
                     updateHostsByIp(host, prevHost);
-                    if (!Objects.equals(prevHost.locations(), host.locations())) {
+                    if (host.suspended() && !prevHost.suspended()) {
+                        notifyDelegate(new HostEvent(HOST_SUSPENDED, host, prevHost));
+                    } else if (!host.suspended() && prevHost.suspended()) {
+                        notifyDelegate(new HostEvent(HOST_UNSUSPENDED, host, prevHost));
+                    } else if (!Objects.equals(prevHost.locations(), host.locations())) {
                         notifyDelegate(new HostEvent(HOST_MOVED, host, prevHost));
                     } else if (!Objects.equals(prevHost, host)) {
                         notifyDelegate(new HostEvent(HOST_UPDATED, host, prevHost));