Automated RPD host relearn.

Change-Id: Ie35e2c51cc4d2aa62a7c230744ae54c05def207c
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java
index 08ba908..7f935dc 100644
--- a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/DhcpRelayManager.java
@@ -45,10 +45,12 @@
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IPv4;
 import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.UDP;
 import org.onlab.packet.VlanId;
+import org.onlab.packet.ndp.NeighborSolicitation;
 import org.onlab.util.Tools;
 import org.onosproject.cfg.ComponentConfigService;
 import org.onosproject.core.ApplicationId;
@@ -59,6 +61,7 @@
 import org.onosproject.dhcprelay.config.DefaultDhcpRelayConfig;
 import org.onosproject.dhcprelay.config.DhcpServerConfig;
 import org.onosproject.dhcprelay.config.EnableDhcpFpmConfig;
+import org.onosproject.dhcprelay.config.HostAutoRelearnConfig;
 import org.onosproject.dhcprelay.config.IndirectDhcpRelayConfig;
 import org.onosproject.dhcprelay.config.IgnoreDhcpConfig;
 import org.onosproject.dhcprelay.store.DhcpRecord;
@@ -75,6 +78,7 @@
 import org.onosproject.net.intf.InterfaceService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
 import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
 import org.onosproject.net.config.NetworkConfigListener;
@@ -84,6 +88,7 @@
 import org.onosproject.net.flow.TrafficSelector;
 import org.onosproject.net.flow.TrafficTreatment;
 import org.onosproject.net.host.HostService;
+import org.onosproject.net.Port;
 import org.onosproject.net.packet.DefaultOutboundPacket;
 import org.onosproject.net.packet.OutboundPacket;
 import org.onosproject.net.packet.PacketContext;
@@ -98,6 +103,8 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.CopyOnWriteArraySet;
+
 import static org.onlab.util.Tools.groupedThreads;
 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
@@ -110,7 +117,6 @@
 public class DhcpRelayManager implements DhcpRelayService {
     public static final String DHCP_RELAY_APP = "org.onosproject.dhcprelay";
     public static final String ROUTE_STORE_IMPL = "org.onosproject.routeservice.store.RouteStoreImpl";
-
     private static final int DEFAULT_POOL_SIZE = 32;
 
     private static final TrafficSelector ARP_SELECTOR = DefaultTrafficSelector.builder()
@@ -118,6 +124,7 @@
             .build();
     private final Logger log = LoggerFactory.getLogger(getClass());
     private final InternalConfigListener cfgListener = new InternalConfigListener();
+    protected CopyOnWriteArraySet hostAutoRelearnEnabledDevices = new CopyOnWriteArraySet();
 
     private final Set<ConfigFactory> factories = ImmutableSet.of(
             new ConfigFactory<ApplicationId, DefaultDhcpRelayConfig>(APP_SUBJECT_FACTORY,
@@ -155,6 +162,15 @@
                 public EnableDhcpFpmConfig createConfig() {
                     return new EnableDhcpFpmConfig();
                 }
+            },
+            new ConfigFactory<ApplicationId, HostAutoRelearnConfig>(APP_SUBJECT_FACTORY,
+                    HostAutoRelearnConfig.class,
+                    HostAutoRelearnConfig.KEY,
+                    true) {
+                @Override
+                public HostAutoRelearnConfig createConfig() {
+                    return new HostAutoRelearnConfig();
+                }
             }
     );
 
@@ -206,7 +222,16 @@
             label = "Enable DhcpRelay Fpm")
     protected boolean dhcpFpmEnabled = false;
 
+    @Property(name = "dhcpHostRelearnProbeInterval", intValue = 500,
+            label = "dhcp host relearn probe interval in millis")
+    protected int dhcpHostRelearnProbeInterval = 500;
+
+    @Property(name = "dhcpHostRelearnProbeCount", intValue = 3,
+            label = "dhcp host relearn probe count")
+    protected int dhcpHostRelearnProbeCount = 3;
+
     private ScheduledExecutorService timerExecutor;
+    private ScheduledExecutorService executorService = null;
     protected ExecutorService devEventExecutor;
     private ExecutorService packetExecutor;
 
@@ -214,6 +239,9 @@
     private DhcpRelayPacketProcessor dhcpRelayPacketProcessor = new DhcpRelayPacketProcessor();
     private ApplicationId appId;
 
+    private static final int POOL_SIZE = 10;
+    private static final int HOST_PROBE_INIT_DELAY = 500;
+
     /**
      *   One second timer.
      */
@@ -224,6 +252,7 @@
         }
     };
 
+
     @Activate
     protected void activate(ComponentContext context) {
         //start the dhcp relay agent
@@ -253,6 +282,8 @@
                                "distributed", Boolean.TRUE.toString());
         compCfgService.registerProperties(getClass());
 
+        executorService = Executors.newScheduledThreadPool(POOL_SIZE);
+
         deviceService.addListener(deviceListener);
 
         log.info("DHCP-RELAY Started");
@@ -272,7 +303,7 @@
         packetExecutor.shutdown();
         timerExecutor = null;
         packetExecutor = null;
-
+        executorService.shutdown();
         log.info("DHCP-RELAY Stopped");
     }
 
@@ -344,6 +375,8 @@
                 cfgService.getConfig(appId, IndirectDhcpRelayConfig.class);
         IgnoreDhcpConfig ignoreDhcpConfig =
                 cfgService.getConfig(appId, IgnoreDhcpConfig.class);
+        HostAutoRelearnConfig hostAutoRelearnConfig =
+                cfgService.getConfig(appId, HostAutoRelearnConfig.class);
 
         if (defaultConfig != null) {
             updateConfig(defaultConfig);
@@ -354,6 +387,9 @@
         if (ignoreDhcpConfig != null) {
             updateConfig(ignoreDhcpConfig);
         }
+        if (hostAutoRelearnConfig != null) {
+            updateConfig(hostAutoRelearnConfig);
+        }
     }
 
     /**
@@ -375,6 +411,9 @@
             v4Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
             v6Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
         }
+        if (config instanceof HostAutoRelearnConfig) {
+            setHostAutoRelearnConfig((HostAutoRelearnConfig) config);
+        }
     }
 
     protected void removeConfig(Config config) {
@@ -422,10 +461,12 @@
     public Collection<DhcpRecord> getDhcpRecords() {
         return dhcpRelayStore.getDhcpRecords();
     }
+
     @Override
     public void updateDhcpRecord(HostId hostId, DhcpRecord dhcpRecord) {
         dhcpRelayStore.updateDhcpRecord(hostId, dhcpRecord);
     }
+
     @Override
     public Optional<MacAddress> getDhcpServerMacAddress() {
         // TODO: depreated it
@@ -498,6 +539,7 @@
 
 
     private class DhcpRelayPacketProcessor implements PacketProcessor {
+
         @Override
         public void process(PacketContext context) {
             packetExecutor.execute(() -> processInternal(context));
@@ -616,7 +658,8 @@
         public boolean isRelevant(NetworkConfigEvent event) {
             if (event.configClass().equals(DefaultDhcpRelayConfig.class) ||
                     event.configClass().equals(IndirectDhcpRelayConfig.class) ||
-                    event.configClass().equals(IgnoreDhcpConfig.class)) {
+                    event.configClass().equals(IgnoreDhcpConfig.class) ||
+                    event.configClass().equals(HostAutoRelearnConfig.class)) {
                 return true;
             }
             log.debug("Ignore irrelevant event class {}", event.configClass().getName());
@@ -629,7 +672,7 @@
         @Override
         public void event(DeviceEvent event) {
           if (devEventExecutor != null) {
-            Device device = event.subject();
+            final Device device = event.subject();
             switch (event.type()) {
                 case DEVICE_ADDED:
                     devEventExecutor.execute(this::updateIgnoreVlanConfigs);
@@ -637,12 +680,40 @@
                 case DEVICE_AVAILABILITY_CHANGED:
                     devEventExecutor.execute(() -> deviceAvailabilityChanged(device));
                     break;
+                case PORT_UPDATED:
+                    Port port = event.port();
+                    devEventExecutor.execute(() -> portUpdatedEventHandler(device, port));
+                    break;
                 default:
                     break;
             }
           }
         }
 
+        private void portUpdatedEventHandler(Device device, Port port) {
+            if (hostAutoRelearnEnabledDevices.contains(device.id()) && port.isEnabled()) {
+                ConnectPoint cp = new ConnectPoint(device.id(), port.number());
+                HostLocation hostLocation = new HostLocation(cp, 0);
+                Set<DhcpRecord> records = dhcpRelayStore.getDhcpRecords()
+                                          .stream()
+                                          .filter(i -> i.directlyConnected())
+                                          .filter(i -> i.locations().contains(hostLocation))
+                                          .collect(Collectors.toSet());
+
+                for (DhcpRecord i : records) {
+                    //found a dhcprecord matching the connect point of the port event
+                    log.debug("portUpdatedEventHandler : DHCP record {}, sending message on CP {} Mac {} Vlan{}",
+                            i, cp, i.macAddress(), i.vlanId());
+                    if (i.ip4Address().isPresent()) {
+                        log.warn("Sending host relearn probe for v4 not supported for Mac {} Vlan{} ip {}",
+                             i.macAddress(), i.vlanId(), i.ip4Address());
+                    } else if (i.ip6Address().isPresent()) {
+                        sendHostRelearnProbe(cp, i.macAddress(), i.vlanId(), i.ip6Address());
+                    }
+                 }
+            }
+       }
+
         private void deviceAvailabilityChanged(Device device) {
             if (deviceService.isAvailable(device.id())) {
                 updateIgnoreVlanConfigs();
@@ -664,6 +735,76 @@
         }
     }
 
+    private void setHostAutoRelearnConfig(HostAutoRelearnConfig config) {
+        hostAutoRelearnEnabledDevices.clear();
+        if (config == null) {
+            return;
+        }
+        hostAutoRelearnEnabledDevices.addAll(config.hostAutoRelearnEnabledDevices());
+    }
+
+    //  Packet transmission class.
+    private class PktTransmitter implements Runnable {
+
+        MacAddress mac;
+        VlanId vlanId;
+        Ip6Address ipv6Address;
+        ConnectPoint connectPoint;
+
+        PktTransmitter(MacAddress mac, VlanId vlanId, Ip6Address ipv6Address, ConnectPoint connectPoint) {
+            this.mac = mac;
+            this.vlanId = vlanId;
+            this.ipv6Address = ipv6Address;
+            this.connectPoint = connectPoint;
+        }
+
+        @Override
+        public void run() {
+            log.debug("Host Relearn probe packet transmission activated for Mac {} Vlan {} Ip {} ConnectPt {}",
+                                     mac, vlanId, ipv6Address, connectPoint);
+            if (mac == null || vlanId == null || ipv6Address == null || connectPoint == null) {
+                return;
+            }
+
+            byte[] senderMacAddress = new byte[MacAddress.MAC_ADDRESS_LENGTH];
+            byte[] senderIpAddress = new byte[Ip6Address.BYTE_LENGTH];
+            Ethernet ethernet = NeighborSolicitation.buildNdpSolicit(
+                    this.ipv6Address,
+                    Ip6Address.valueOf(senderIpAddress),
+                    this.ipv6Address, //destip
+                    MacAddress.valueOf(senderMacAddress),
+                    this.mac,
+                    this.vlanId);
+
+            sendHostRelearnProbeToConnectPoint(ethernet, connectPoint);
+
+            log.debug("Host Relearn Probe transmission completed.");
+        }
+    }
+
+    //Create packet and schedule transmitter thread.
+    private void sendHostRelearnProbe(ConnectPoint connectPoint, MacAddress mac, VlanId vlanId,
+                                      Optional<Ip6Address> ipv6Address) {
+        PktTransmitter nsTransmitter = new PktTransmitter(mac, vlanId, ipv6Address.get(), connectPoint);
+        executorService.schedule(nsTransmitter, HOST_PROBE_INIT_DELAY, TimeUnit.MILLISECONDS);
+    }
+
+    // Send Host Relearn Probe packets to ConnectPoint
+    private void sendHostRelearnProbeToConnectPoint(Ethernet nsPacket, ConnectPoint connectPoint) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder().setOutput(connectPoint.port()).build();
+        OutboundPacket outboundPacket = new DefaultOutboundPacket(connectPoint.deviceId(),
+                treatment, ByteBuffer.wrap(nsPacket.serialize()));
+        int counter = 0;
+        try {
+            while (counter < dhcpHostRelearnProbeCount) {
+              packetService.emit(outboundPacket);
+              counter++;
+              Thread.sleep(dhcpHostRelearnProbeInterval);
+            }
+        } catch (Exception e) {
+            log.error("Exception while emmiting packet {}", e.getMessage(), e);
+        }
+    }
 
 
     public Optional<FpmRecord> getFpmRecord(IpPrefix prefix) {
@@ -684,5 +825,4 @@
         return dhcpFpmPrefixStore.removeFpmRecord(prefix);
     }
 
-
 }
diff --git a/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/HostAutoRelearnConfig.java b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/HostAutoRelearnConfig.java
new file mode 100644
index 0000000..85b8c9a
--- /dev/null
+++ b/apps/dhcprelay/app/src/main/java/org/onosproject/dhcprelay/config/HostAutoRelearnConfig.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019-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.dhcprelay.config;
+
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+import java.util.Set;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Dhcp Host Auto Relearn Config.
+ */
+public class HostAutoRelearnConfig extends Config<ApplicationId> {
+    public static final String KEY = "hostAutoRelearnEnabledDevices";
+
+    @Override
+    public boolean isValid() {
+        if (array == null) {
+            return false;
+        }
+        try {
+            for (JsonNode node : array) {
+               DeviceId.deviceId(node.asText());
+            }
+        } catch (IllegalArgumentException ex) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns Set of Devices on which Host Auto Relearn is enabled.
+     *
+     * @return Set of DeviceId where Host Auto Relearn is enabled.
+     */
+
+    public Set<DeviceId> hostAutoRelearnEnabledDevices() {
+        Set<DeviceId> enabledDevices = Sets.newHashSet();
+
+        array.forEach(node -> {
+            DeviceId deviceId = DeviceId.deviceId(node.asText());
+            enabledDevices.add(deviceId);
+        });
+
+        return enabledDevices;
+    }
+}