Introduced HostMoveTracker to suspend hosts that moves too frequently

Change-Id: I29b233bb2185a712eec122b142fc0111f98e8f09
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..1c81561 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.
     /**
@@ -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/host/impl/HostManager.java b/core/net/src/main/java/org/onosproject/net/host/impl/HostManager.java
index 6cb1cb6..feb4c1d 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.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -56,14 +57,22 @@
 import org.onosproject.net.packet.PacketService;
 import org.onosproject.net.provider.AbstractProviderService;
 import org.osgi.service.component.ComponentContext;
+
 import org.slf4j.Logger;
 
 import java.util.Dictionary;
+import java.util.Map;
 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.security.AppGuard.checkPermission;
 import static org.onosproject.security.AppPermission.Type.HOST_EVENT;
 import static org.onosproject.security.AppPermission.Type.HOST_READ;
@@ -123,7 +132,38 @@
             label = "Enable/Disable greedy learning of IPv6 link local address")
     private boolean greedyLearningIpv6 = false;
 
+    @Property(name = "hostMoveTrackerEnabled", boolValue = false,
+            label = "Enable tracking of rogue host moves")
+    private boolean hostMoveTrackerEnabled = false;
+
+    private static final int HOST_MOVED_THRESHOLD_IN_MILLIS = 200000;
+    // If the host move happening within given threshold then increment the host move counter
+    private static final int HOST_MOVE_COUNTER = 3;
+    // Max value of the counter after which the host will not be considered as offending host
+    private static final long OFFENDING_HOST_EXPIRY_IN_MINS = 1;
+    // Default pool size of offending host clear executor thread
+    private static final int DEFAULT_OFFENDING_HOST_THREADS_POOL_SIZE = 10;
+    private Map<HostId, HostMoveTracker> hostMoveTracker = new ConcurrentHashMap<>();
+
+    @Property(name = "hostMoveThresholdInMillis", intValue = HOST_MOVED_THRESHOLD_IN_MILLIS,
+            label = "Host move threshold in milliseconds")
+    private int hostMoveThresholdInMillis = HOST_MOVED_THRESHOLD_IN_MILLIS;
+
+    @Property(name = "hostMoveCounter", intValue = HOST_MOVE_COUNTER,
+            label = "Host move counter before host is marked as rogue host")
+    private int hostMoveCounter = HOST_MOVE_COUNTER;
+
+    @Property(name = "offendingHostExpiryInMins", longValue = OFFENDING_HOST_EXPIRY_IN_MINS,
+            label = "Expiry time after which the host is cleared from being rogue host")
+    private long offendingHostExpiryInMins = OFFENDING_HOST_EXPIRY_IN_MINS;
+
+    @Property(name = "offendingHostClearThreadPool", intValue = DEFAULT_OFFENDING_HOST_THREADS_POOL_SIZE,
+            label = "Thread pool capacity")
+    protected int offendingHostClearThreadPool = DEFAULT_OFFENDING_HOST_THREADS_POOL_SIZE;
+
     private HostMonitor monitor;
+    private ScheduledExecutorService offendingHostUnblockExecutor = null;
+
 
 
     @Activate
@@ -135,8 +175,8 @@
         monitor = new HostMonitor(packetService, this, interfaceService, edgePortService);
         monitor.setProbeRate(probeRate);
         monitor.start();
-        modified(context);
         cfgService.registerProperties(getClass());
+        modified(context);
         log.info("Started");
     }
 
@@ -147,6 +187,9 @@
         networkConfigService.removeListener(networkConfigListener);
         cfgService.unregisterProperties(getClass(), false);
         monitor.shutdown();
+        if (offendingHostUnblockExecutor != null) {
+            offendingHostUnblockExecutor.shutdown();
+        }
         log.info("Stopped");
     }
 
@@ -177,15 +220,19 @@
     private void readComponentConfiguration(ComponentContext context) {
         Dictionary<?, ?> properties = context.getProperties();
         Boolean flag;
+        int newHostMoveThresholdInMillis;
+        int newHostMoveCounter;
+        int newOffendinghostPoolSize;
+        long newOffendingHostExpiryInMins;
 
         flag = Tools.isPropertyEnabled(properties, "monitorHosts");
         if (flag == null) {
             log.info("monitorHosts is not enabled " +
-                             "using current value of {}", monitorHosts);
+                    "using current value of {}", monitorHosts);
         } else {
             monitorHosts = flag;
             log.info("Configured. monitorHosts {}",
-                     monitorHosts ? "enabled" : "disabled");
+                    monitorHosts ? "enabled" : "disabled");
         }
 
         Long longValue = Tools.getLongProperty(properties, "probeRate");
@@ -202,19 +249,85 @@
         } else {
             allowDuplicateIps = flag;
             log.info("Removal of duplicate ip address is {}",
-                     allowDuplicateIps ? "disabled" : "enabled");
+                    allowDuplicateIps ? "disabled" : "enabled");
         }
 
         flag = Tools.isPropertyEnabled(properties, "greedyLearningIpv6");
         if (flag == null) {
             log.info("greedy learning is not enabled " +
-                             "using current value of {}", greedyLearningIpv6);
+                    "using current value of {}", greedyLearningIpv6);
         } else {
             greedyLearningIpv6 = flag;
             log.info("Configured. greedyLearningIpv6 {}",
-                     greedyLearningIpv6 ? "enabled" : "disabled");
+                    greedyLearningIpv6 ? "enabled" : "disabled");
         }
 
+        flag = Tools.isPropertyEnabled(properties, "hostMoveTrackerEnabled");
+        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, "hostMoveThresholdInMillis");
+                newHostMoveThresholdInMillis = isNullOrEmpty(s) ?
+                        hostMoveThresholdInMillis : Integer.parseInt(s.trim());
+
+                s = get(properties, "hostMoveCounter");
+                newHostMoveCounter = isNullOrEmpty(s) ? hostMoveCounter : Integer.parseInt(s.trim());
+
+                s = get(properties, "offendingHostExpiryInMins");
+                newOffendingHostExpiryInMins = isNullOrEmpty(s) ?
+                        offendingHostExpiryInMins : Integer.parseInt(s.trim());
+
+                s = get(properties, "offendingHostClearThreadPool");
+                newOffendinghostPoolSize = isNullOrEmpty(s) ?
+                        offendingHostClearThreadPool : Integer.parseInt(s.trim());
+            } catch (NumberFormatException | ClassCastException e) {
+                newHostMoveThresholdInMillis = HOST_MOVED_THRESHOLD_IN_MILLIS;
+                newHostMoveCounter = HOST_MOVE_COUNTER;
+                newOffendingHostExpiryInMins = OFFENDING_HOST_EXPIRY_IN_MINS;
+                newOffendinghostPoolSize = DEFAULT_OFFENDING_HOST_THREADS_POOL_SIZE;
+            }
+            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);
     }
 
     /**
@@ -338,8 +451,17 @@
             if (!allowDuplicateIps) {
                 removeDuplicates(hostId, hostDescription);
             }
-            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 -> {
@@ -391,7 +513,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);
                     }
                 });
@@ -476,8 +598,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 72f91be..80d8e24 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
@@ -361,6 +361,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 +464,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));