CORD-483 Made virtual network gateway MAC address configurable

- Added 'gatewayMAC' field to network config for cordvtn
- Implemented to send gratuitous ARP when gateway MAC is updated

Change-Id: I4f9050f4be64f04e0568515bbb95474513bbe057
diff --git a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtn.java b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtn.java
index c4894bf..19c9e7d 100644
--- a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtn.java
+++ b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtn.java
@@ -36,6 +36,12 @@
 import org.onosproject.net.HostLocation;
 import org.onosproject.net.Port;
 import org.onosproject.net.SparseAnnotations;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.config.basics.SubjectFactories;
 import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.driver.DriverService;
 import org.onosproject.net.flow.FlowRuleService;
@@ -62,10 +68,10 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 import static org.onlab.util.Tools.groupedThreads;
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -83,6 +89,12 @@
     protected CoreService coreService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry configRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigService configService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected HostProviderRegistry hostProviderRegistry;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -109,21 +121,31 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected OpenstackSwitchingService openstackService;
 
-    private static final int NUM_THREADS = 1;
+    private final ConfigFactory configFactory =
+            new ConfigFactory(SubjectFactories.APP_SUBJECT_FACTORY, CordVtnConfig.class, "cordvtn") {
+                @Override
+                public CordVtnConfig createConfig() {
+                    return new CordVtnConfig();
+                }
+            };
+
     private static final String DEFAULT_TUNNEL = "vxlan";
     private static final String SERVICE_ID = "serviceId";
     private static final String LOCATION_IP = "locationIp";
     private static final String OPENSTACK_VM_ID = "openstackVmId";
 
-    private final ExecutorService eventExecutor = Executors
-            .newFixedThreadPool(NUM_THREADS, groupedThreads("onos/cordvtn", "event-handler"));
+    private final ExecutorService eventExecutor =
+            newSingleThreadScheduledExecutor(groupedThreads("onos/cordvtn", "event-handler"));
 
     private final PacketProcessor packetProcessor = new InternalPacketProcessor();
     private final HostListener hostListener = new InternalHostListener();
+    private final NetworkConfigListener configListener = new InternalConfigListener();
 
+    private ApplicationId appId;
     private HostProviderService hostProvider;
     private CordVtnRuleInstaller ruleInstaller;
     private CordVtnArpProxy arpProxy;
+    private volatile MacAddress gatewayMac = MacAddress.NONE;
 
     /**
      * Creates an cordvtn host location provider.
@@ -134,8 +156,7 @@
 
     @Activate
     protected void activate() {
-        ApplicationId appId = coreService.registerApplication("org.onosproject.cordvtn");
-
+        appId = coreService.registerApplication("org.onosproject.cordvtn");
         ruleInstaller = new CordVtnRuleInstaller(appId, flowRuleService,
                                                  deviceService,
                                                  driverService,
@@ -150,17 +171,24 @@
         hostService.addListener(hostListener);
         hostProvider = hostProviderRegistry.register(this);
 
+        configRegistry.registerConfigFactory(configFactory);
+        configService.addListener(configListener);
+        readConfiguration();
+
         log.info("Started");
     }
 
     @Deactivate
     protected void deactivate() {
+        hostProviderRegistry.unregister(this);
         hostService.removeListener(hostListener);
+
         packetService.removeProcessor(packetProcessor);
 
-        eventExecutor.shutdown();
-        hostProviderRegistry.unregister(this);
+        configRegistry.unregisterConfigFactory(configFactory);
+        configService.removeListener(configListener);
 
+        eventExecutor.shutdown();
         log.info("Stopped");
     }
 
@@ -379,6 +407,10 @@
             // TODO check if the service needs an update on its group buckets after done CORD-433
             ruleInstaller.updateServiceGroup(service);
             arpProxy.addServiceIp(service.serviceIp());
+
+            // sends gratuitous ARP here for the case of adding existing VMs
+            // when ONOS or cordvtn app is restarted
+            arpProxy.sendGratuitousArp(service.serviceIp(), gatewayMac, Sets.newHashSet(host));
         }
 
         ruleInstaller.populateBasicConnectionRules(host, getTunnelIp(host), vNet);
@@ -418,6 +450,47 @@
         }
     }
 
+    /**
+     * Sets service network gateway MAC address and sends out gratuitous ARP to all
+     * VMs to update the gateway MAC address.
+     *
+     * @param mac mac address
+     */
+    private void setServiceGatewayMac(MacAddress mac) {
+        if (mac != null && !mac.equals(gatewayMac)) {
+            gatewayMac = mac;
+            log.debug("Set service gateway MAC address to {}", gatewayMac.toString());
+        }
+
+        // TODO get existing service list from XOS and replace the loop below
+        Set<String> vNets = Sets.newHashSet();
+        hostService.getHosts().forEach(host -> vNets.add(host.annotations().value(SERVICE_ID)));
+        vNets.remove(null);
+
+        vNets.stream().forEach(vNet -> {
+            CordService service = getCordService(CordServiceId.of(vNet));
+            if (service != null) {
+                arpProxy.sendGratuitousArp(
+                        service.serviceIp(),
+                        gatewayMac,
+                        service.hosts().keySet());
+            }
+        });
+    }
+
+    /**
+     * Updates configurations.
+     */
+    private void readConfiguration() {
+        CordVtnConfig config = configRegistry.getConfig(appId, CordVtnConfig.class);
+        if (config == null) {
+            log.debug("No configuration found");
+            return;
+        }
+
+        setServiceGatewayMac(config.gatewayMac());
+   }
+
     private class InternalHostListener implements HostListener {
 
         @Override
@@ -446,15 +519,31 @@
             }
 
             Ethernet ethPacket = context.inPacket().parsed();
-            if (ethPacket == null) {
+            if (ethPacket == null || ethPacket.getEtherType() != Ethernet.TYPE_ARP) {
                 return;
             }
 
-            if (ethPacket.getEtherType() != Ethernet.TYPE_ARP) {
+            arpProxy.processArpPacket(context, ethPacket, gatewayMac);
+        }
+    }
+
+    private class InternalConfigListener implements NetworkConfigListener {
+
+        @Override
+        public void event(NetworkConfigEvent event) {
+            if (!event.configClass().equals(CordVtnConfig.class)) {
                 return;
             }
 
-            arpProxy.processArpPacket(context, ethPacket);
+            switch (event.type()) {
+                case CONFIG_ADDED:
+                case CONFIG_UPDATED:
+                    log.info("Network configuration changed");
+                    eventExecutor.execute(CordVtn.this::readConfiguration);
+                    break;
+                default:
+                    break;
+            }
         }
     }
 }
diff --git a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnArpProxy.java b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnArpProxy.java
index 4cef148..917bd6c 100644
--- a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnArpProxy.java
+++ b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnArpProxy.java
@@ -23,6 +23,7 @@
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onosproject.core.ApplicationId;
+import org.onosproject.net.Host;
 import org.onosproject.net.flow.DefaultTrafficSelector;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.TrafficSelector;
@@ -37,6 +38,7 @@
 import java.util.Optional;
 import java.util.Set;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -45,8 +47,6 @@
  */
 public class CordVtnArpProxy {
     protected final Logger log = getLogger(getClass());
-    // TODO make gateway MAC address configurable
-    private static final MacAddress DEFAULT_GATEWAY_MAC = MacAddress.valueOf("00:00:00:00:00:01");
 
     private final ApplicationId appId;
     private final PacketService packetService;
@@ -120,22 +120,21 @@
      *
      * @param context packet context
      * @param ethPacket ethernet packet
+     * @param gatewayMac gateway mac address
      */
-    public void processArpPacket(PacketContext context, Ethernet ethPacket) {
+    public void processArpPacket(PacketContext context, Ethernet ethPacket, MacAddress gatewayMac) {
+        checkArgument(!gatewayMac.equals(MacAddress.NONE));
+
         ARP arpPacket = (ARP) ethPacket.getPayload();
         Ip4Address targetIp = Ip4Address.valueOf(arpPacket.getTargetProtocolAddress());
 
-        if (arpPacket.getOpCode() != ARP.OP_REQUEST) {
+        if (arpPacket.getOpCode() != ARP.OP_REQUEST || !serviceIPs.contains(targetIp)) {
            return;
         }
 
-        if (!serviceIPs.contains(targetIp)) {
-            return;
-        }
-
         Ethernet ethReply = ARP.buildArpReply(
                 targetIp,
-                DEFAULT_GATEWAY_MAC,
+                gatewayMac,
                 ethPacket);
 
         TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -149,4 +148,57 @@
 
         context.block();
     }
+
+    /**
+     * Emits gratuitous ARP when a gateway mac address has been changed.
+     *
+     * @param ip ip address to update MAC
+     * @param mac new mac address
+     * @param hosts set of hosts to send gratuitous ARP packet
+     */
+    public void sendGratuitousArp(IpAddress ip, MacAddress mac, Set<Host> hosts) {
+        checkArgument(!mac.equals(MacAddress.NONE));
+
+        Ethernet ethArp = buildGratuitousArp(ip.getIp4Address(), mac);
+        hosts.stream().forEach(host -> {
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .setOutput(host.location().port())
+                    .build();
+
+            packetService.emit(new DefaultOutboundPacket(
+                    host.location().deviceId(),
+                    treatment,
+                    ByteBuffer.wrap(ethArp.serialize())));
+        });
+    }
+
+    /**
+     * Builds gratuitous ARP packet with a given IP and MAC address.
+     *
+     * @param ip ip address for TPA and SPA
+     * @param mac new mac address
+     * @return ethernet packet
+     */
+    private Ethernet buildGratuitousArp(IpAddress ip, MacAddress mac) {
+        Ethernet eth = new Ethernet();
+
+        eth.setEtherType(Ethernet.TYPE_ARP);
+        eth.setSourceMACAddress(mac);
+        eth.setDestinationMACAddress(MacAddress.BROADCAST);
+
+        ARP arp = new ARP();
+        arp.setOpCode(ARP.OP_REQUEST);
+        arp.setHardwareType(ARP.HW_TYPE_ETHERNET);
+        arp.setHardwareAddressLength((byte) Ethernet.DATALAYER_ADDRESS_LENGTH);
+        arp.setProtocolType(ARP.PROTO_TYPE_IP);
+        arp.setProtocolAddressLength((byte) Ip4Address.BYTE_LENGTH);
+
+        arp.setSenderHardwareAddress(mac.toBytes());
+        arp.setTargetHardwareAddress(MacAddress.BROADCAST.toBytes());
+        arp.setSenderProtocolAddress(ip.getIp4Address().toOctets());
+        arp.setTargetProtocolAddress(ip.getIp4Address().toOctets());
+
+        eth.setPayload(arp);
+        return eth;
+    }
 }
diff --git a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnConfig.java b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnConfig.java
index 57842dc..167d7bf 100644
--- a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnConfig.java
+++ b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnConfig.java
@@ -18,20 +18,25 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.collect.Sets;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
 import org.onlab.packet.TpPort;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.config.Config;
+import org.slf4j.Logger;
 
 import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Configuration object for CordVtn service.
  */
 public class CordVtnConfig extends Config<ApplicationId> {
 
+    protected final Logger log = getLogger(getClass());
+
     public static final String CORDVTN_NODES = "nodes";
     public static final String HOSTNAME = "hostname";
     public static final String OVSDB_IP = "ovsdbIp";
@@ -39,6 +44,7 @@
     public static final String BRIDGE_ID = "bridgeId";
     public static final String PHYSICAL_PORT_NAME = "phyPortName";
     public static final String LOCAL_IP = "localIp";
+    public static final String GATEWAY_MAC = "gatewayMac";
 
     /**
      * Returns the set of nodes read from network config.
@@ -64,6 +70,25 @@
     }
 
     /**
+     * Returns gateway MAC address.
+     *
+     * @return mac address, or null
+     */
+    public MacAddress gatewayMac() {
+        JsonNode jsonNode = object.get(GATEWAY_MAC);
+        if (jsonNode == null) {
+            return null;
+        }
+
+        try {
+            return MacAddress.valueOf(jsonNode.asText());
+        } catch (IllegalArgumentException e) {
+            log.error("Wrong MAC address format {}", jsonNode.asText());
+            return null;
+        }
+    }
+
+    /**
      * Configuration for CordVtn node.
      */
     public static class CordVtnNodeConfig {
diff --git a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnNodeManager.java b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnNodeManager.java
index 7e56a21..2a66263 100644
--- a/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnNodeManager.java
+++ b/apps/cordvtn/src/main/java/org/onosproject/cordvtn/CordVtnNodeManager.java
@@ -41,12 +41,10 @@
 import org.onosproject.net.behaviour.TunnelConfig;
 import org.onosproject.net.behaviour.TunnelDescription;
 import org.onosproject.net.behaviour.TunnelName;
-import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
 import org.onosproject.net.config.NetworkConfigListener;
 import org.onosproject.net.config.NetworkConfigRegistry;
 import org.onosproject.net.config.NetworkConfigService;
-import org.onosproject.net.config.basics.SubjectFactories;
 import org.onosproject.net.device.DeviceAdminService;
 import org.onosproject.net.device.DeviceEvent;
 import org.onosproject.net.device.DeviceListener;
@@ -151,14 +149,6 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected CordVtnService cordVtnService;
 
-    private final ConfigFactory configFactory =
-            new ConfigFactory(SubjectFactories.APP_SUBJECT_FACTORY, CordVtnConfig.class, "cordvtn") {
-                @Override
-                public CordVtnConfig createConfig() {
-                    return new CordVtnConfig();
-                }
-            };
-
     private final ExecutorService eventExecutor =
             newSingleThreadScheduledExecutor(groupedThreads("onos/cordvtncfg", "event-handler"));
 
@@ -231,7 +221,6 @@
     @Activate
     protected void active() {
         appId = coreService.getAppId(CordVtnService.CORDVTN_APP_ID);
-
         nodeStore = storageService.<CordVtnNode, NodeState>consistentMapBuilder()
                 .withSerializer(Serializer.using(NODE_SERIALIZER.build()))
                 .withName("cordvtn-nodestore")
@@ -247,12 +236,11 @@
 
         deviceService.addListener(deviceListener);
         configService.addListener(configListener);
-        configRegistry.registerConfigFactory(configFactory);
+        readConfiguration();
     }
 
     @Deactivate
     protected void deactivate() {
-        configRegistry.unregisterConfigFactory(configFactory);
         configService.removeListener(configListener);
         deviceService.removeListener(deviceListener);
 
@@ -820,13 +808,13 @@
     }
 
     /**
-     * Reads node configuration from config file.
+     * Reads cordvtn nodes from config file.
      */
     private void readConfiguration() {
         CordVtnConfig config = configRegistry.getConfig(appId, CordVtnConfig.class);
 
         if (config == null) {
-            log.warn("No configuration found");
+            log.debug("No configuration found");
             return;
         }
 
@@ -841,6 +829,8 @@
 
             addNode(cordVtnNode);
         });
+
+        // TODO remove nodes if needed
     }
 
     private class InternalConfigListener implements NetworkConfigListener {
@@ -853,11 +843,7 @@
 
             switch (event.type()) {
                 case CONFIG_ADDED:
-                    log.info("Network configuration added");
-                    eventExecutor.execute(CordVtnNodeManager.this::readConfiguration);
-                    break;
                 case CONFIG_UPDATED:
-                    log.info("Network configuration updated");
                     eventExecutor.execute(CordVtnNodeManager.this::readConfiguration);
                     break;
                 default: