Automated RPD host relearn.

Change-Id: Ia501ca80c448a7f8f9c31dc2b6513c739d69be6d
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 a8541a6..ad67681 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
@@ -26,10 +26,12 @@
 import org.onlab.packet.IPv4;
 import org.onlab.packet.IPv6;
 import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
 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;
@@ -40,8 +42,10 @@
 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.IgnoreDhcpConfig;
 import org.onosproject.dhcprelay.config.IndirectDhcpRelayConfig;
+import org.onosproject.mastership.MastershipService;
 import org.onosproject.dhcprelay.store.DhcpFpmPrefixStore;
 import org.onosproject.dhcprelay.store.DhcpRecord;
 import org.onosproject.dhcprelay.store.DhcpRelayStore;
@@ -49,6 +53,7 @@
 import org.onosproject.net.Device;
 import org.onosproject.net.Host;
 import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
 import org.onosproject.net.config.Config;
 import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
@@ -62,6 +67,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.intf.Interface;
 import org.onosproject.net.intf.InterfaceService;
 import org.onosproject.net.packet.DefaultOutboundPacket;
@@ -93,6 +99,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -103,6 +110,10 @@
 import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_FPM_ENABLED_DEFAULT;
 import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_POLL_INTERVAL;
 import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_POLL_INTERVAL_DEFAULT;
+import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_PROBE_INTERVAL;
+import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_PROBE_INTERVAL_DEFAULT;
+import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_PROBE_COUNT;
+import static org.onosproject.dhcprelay.OsgiPropertyConstants.DHCP_PROBE_COUNT_DEFAULT;
 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
 
@@ -115,7 +126,9 @@
     property = {
         ARP_ENABLED + ":Boolean=" + ARP_ENABLED_DEFAULT,
         DHCP_POLL_INTERVAL + ":Integer=" + DHCP_POLL_INTERVAL_DEFAULT,
-        DHCP_FPM_ENABLED + ":Boolean=" + DHCP_FPM_ENABLED_DEFAULT
+        DHCP_FPM_ENABLED + ":Boolean=" + DHCP_FPM_ENABLED_DEFAULT,
+        DHCP_PROBE_INTERVAL + ":Integer=" + DHCP_PROBE_INTERVAL_DEFAULT,
+        DHCP_PROBE_COUNT + ":Integer=" + DHCP_PROBE_COUNT_DEFAULT
     }
 )
 public class DhcpRelayManager implements DhcpRelayService {
@@ -129,6 +142,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,
@@ -166,6 +180,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();
+                }
             }
     );
 
@@ -174,6 +197,9 @@
     protected NetworkConfigRegistry cfgService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
     protected CoreService coreService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
@@ -214,13 +240,22 @@
     /** Enable DhcpRelay Fpm. */
     protected boolean dhcpFpmEnabled = DHCP_FPM_ENABLED_DEFAULT;
 
+    /** Dhcp host relearn probe interval in millis. */
+    protected int dhcpHostRelearnProbeInterval = DHCP_PROBE_INTERVAL_DEFAULT;
+
+    /** Dhcp host relearn probe count. */
+    protected int dhcpHostRelearnProbeCount = DHCP_PROBE_COUNT_DEFAULT;
+
     private ScheduledExecutorService timerExecutor;
+    private ScheduledExecutorService executorService = null;
     protected ExecutorService devEventExecutor;
     private ExecutorService packetExecutor;
 
     protected DeviceListener deviceListener = new InternalDeviceListener();
     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.
@@ -261,6 +296,8 @@
                                "distributed", Boolean.TRUE.toString());
         compCfgService.registerProperties(getClass());
 
+        executorService = Executors.newScheduledThreadPool(POOL_SIZE);
+
         deviceService.addListener(deviceListener);
 
         log.info("DHCP-RELAY Started");
@@ -280,6 +317,7 @@
         packetExecutor.shutdown();
         timerExecutor = null;
         packetExecutor = null;
+        executorService.shutdown();
 
         log.info("DHCP-RELAY Stopped");
     }
@@ -352,6 +390,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);
@@ -362,6 +402,9 @@
         if (ignoreDhcpConfig != null) {
             updateConfig(ignoreDhcpConfig);
         }
+        if (hostAutoRelearnConfig != null) {
+            updateConfig(hostAutoRelearnConfig);
+        }
     }
 
     /**
@@ -383,6 +426,9 @@
             v4Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
             v6Handler.updateIgnoreVlanConfig((IgnoreDhcpConfig) config);
         }
+        if (config instanceof HostAutoRelearnConfig) {
+            setHostAutoRelearnConfig((HostAutoRelearnConfig) config);
+        }
     }
 
     protected void removeConfig(Config config) {
@@ -624,7 +670,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());
@@ -637,7 +684,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);
@@ -645,12 +692,44 @@
                 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 (!mastershipService.isLocalMaster(device.id())) {
+                log.warn("This instance is not the master for the device {}", device.id());
+                return;
+            }
+            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 msg on CP {} Mac {} Vlan{} DeviceId {}",
+                            i, cp, i.macAddress(), i.vlanId(), device.id());
+                    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();
@@ -672,6 +751,83 @@
         }
     }
 
+    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;
+            }
+
+            Interface senderInterface = interfaceService.getInterfacesByPort(connectPoint)
+                    .stream().filter(iface -> Dhcp6HandlerUtil.interfaceContainsVlan(iface, vlanId))
+                    .findFirst().orElse(null);
+            if (senderInterface == null) {
+                log.warn("Cannot get sender interface for from packet, abort... vlan {}", vlanId.toString());
+            }
+            MacAddress senderMacAddress = senderInterface.mac();
+            byte[] senderIpAddress = IPv6.getLinkLocalAddress(senderMacAddress.toBytes());
+            byte[] destIp = IPv6.getSolicitNodeAddress(ipv6Address.toOctets());
+
+            Ethernet ethernet = NeighborSolicitation.buildNdpSolicit(
+                    this.ipv6Address,
+                    Ip6Address.valueOf(senderIpAddress),
+                    Ip6Address.valueOf(destIp), //destip
+                    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) {