Introduced HostMoveTracker to suspend hosts that moves too frequently

Change-Id: I3037c626657790ce6817feddba7dbbfac203b188
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();
+    }
+}