[ONOS-5283] Arbitrary connect points, support multiple vlans

Change-Id: I9bd3536c08dfd8a637293460395de7e2a1dc1dc1
diff --git a/apps/vpls/src/main/java/org/onosproject/vpls/Vpls.java b/apps/vpls/src/main/java/org/onosproject/vpls/Vpls.java
index 2951d13..4e4bd4b 100644
--- a/apps/vpls/src/main/java/org/onosproject/vpls/Vpls.java
+++ b/apps/vpls/src/main/java/org/onosproject/vpls/Vpls.java
@@ -15,10 +15,10 @@
  */
 package org.onosproject.vpls;
 
-import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import com.google.common.collect.SetMultimap;
-
-import org.apache.commons.lang3.tuple.Pair;
+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;
@@ -29,29 +29,56 @@
 import org.onosproject.app.ApplicationService;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.intf.Interface;
 import org.onosproject.incubator.net.intf.InterfaceEvent;
 import org.onosproject.incubator.net.intf.InterfaceListener;
 import org.onosproject.incubator.net.intf.InterfaceService;
-import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.FilteredConnectPoint;
 import org.onosproject.net.Host;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.host.HostListener;
 import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.Key;
 import org.onosproject.routing.IntentSynchronizationService;
+import org.onosproject.vpls.config.VplsConfigurationService;
 import org.slf4j.Logger;
 
-import java.util.Map;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.slf4j.LoggerFactory.getLogger;
+import static org.onosproject.vpls.IntentInstaller.PREFIX_BROADCAST;
+import static org.onosproject.vpls.IntentInstaller.PREFIX_UNICAST;
 
 /**
  * Application to create L2 broadcast overlay networks using VLAN.
  */
 @Component(immediate = true)
 public class Vpls {
-    protected static final String VPLS_APP = "org.onosproject.vpls";
+    /**
+     * Application name of VPLS.
+     */
+    static final String VPLS_APP = "org.onosproject.vpls";
+
+    private static final String HOST_FCP_NOT_FOUND =
+            "Filtered connected point for host {} not found";
+    private static final String HOST_EVENT = "Received HostEvent {}";
+    private static final String INTF_CONF_EVENT =
+            "Received InterfaceConfigEvent {}";
+    private static final String NET_CONF_EVENT =
+            "Received NetworkConfigEvent {}";
 
     private final Logger log = getLogger(getClass());
 
@@ -73,11 +100,20 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected IntentSynchronizationService intentSynchronizer;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigService configService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected VplsConfigurationService vplsConfigService;
+
     private final HostListener hostListener = new InternalHostListener();
 
     private final InternalInterfaceListener interfaceListener
             = new InternalInterfaceListener();
 
+    private final InternalNetworkConfigListener configListener =
+            new InternalNetworkConfigListener();
+
     private IntentInstaller intentInstaller;
 
     private ApplicationId appId;
@@ -97,111 +133,250 @@
 
         hostService.addListener(hostListener);
         interfaceService.addListener(interfaceListener);
+        configService.addListener(configListener);
 
-        setupConnectivity();
+        setupConnectivity(false);
 
         log.info("Activated");
     }
 
     @Deactivate
     public void deactivate() {
+        configService.removeListener(configListener);
         intentSynchronizer.removeIntentsByAppId(appId);
         log.info("Deactivated");
     }
 
-    protected void setupConnectivity() {
-        /*
-         * Parse Configuration and get Connect Point by VlanId.
-         */
-        SetMultimap<VlanId, ConnectPoint> confCPointsByVlan = getConfigCPoints();
+    /**
+     * Sets up connectivity for all VPLSs.
+     *
+     * @param isNetworkConfigEvent true if this function is triggered
+     *                             by NetworkConfigEvent; false otherwise
+     */
+    private void setupConnectivity(boolean isNetworkConfigEvent) {
+        SetMultimap<String, Interface> networkInterfaces =
+                vplsConfigService.getVplsNetworks();
 
-        /*
-         * Check that configured Connect Points have hosts attached and
-         * associate their Mac Address to the Connect Points configured.
-         */
-        SetMultimap<VlanId, Pair<ConnectPoint, MacAddress>> confHostPresentCPoint =
-                pairAvailableHosts(confCPointsByVlan);
+        Set<String> vplsAffectedByApi =
+                new HashSet<>(vplsConfigService.getVplsAffectedByApi());
 
-        /*
-         * Create and submit intents between the Connect Points.
-         * Intents for broadcast between all the configured Connect Points.
-         * Intents for unicast between all the configured Connect Points with
-         * hosts attached.
-         */
-        intentInstaller.installIntents(confHostPresentCPoint);
+        if (isNetworkConfigEvent && vplsAffectedByApi.isEmpty()) {
+            vplsAffectedByApi.addAll(vplsConfigService.getOldVpls());
+        }
 
+        networkInterfaces.asMap().forEach((networkName, interfaces) -> {
+            Set<Host> hosts = Sets.newHashSet();
+            interfaces.forEach(intf -> {
+                // Add hosts that belongs to the specific VPLS
+                hostService.getConnectedHosts(intf.connectPoint())
+                        .stream()
+                        .filter(host -> host.vlan().equals(intf.vlan()))
+                        .forEach(hosts::add);
+            });
+
+            setupConnectivity(networkName, interfaces, hosts,
+                    vplsAffectedByApi.contains(networkName));
+            vplsAffectedByApi.remove(networkName);
+        });
+
+        if (!vplsAffectedByApi.isEmpty()) {
+            for (String networkName:vplsAffectedByApi) {
+                withdrawIntents(networkName, Lists.newArrayList());
+            }
+        }
     }
 
     /**
-     * Computes the list of configured interfaces with a VLAN Id.
+     * Sets up connectivity for specific VPLS.
      *
-     * @return the interfaces grouped by vlan id
+     * @param networkName the VPLS name
+     * @param interfaces the interfaces that belong to the VPLS
+     * @param hosts the hosts that belong to the VPLS
+     * @param affectedByApi true if this function is triggered from the APIs;
+     *                      false otherwise
      */
-    protected SetMultimap<VlanId, ConnectPoint> getConfigCPoints() {
-        log.debug("Checking interface configuration");
+    private void setupConnectivity(String networkName,
+                                   Collection<Interface> interfaces,
+                                   Set<Host> hosts,
+                                   boolean affectedByApi) {
+        List<Intent> intents = Lists.newArrayList();
+        List<Key> keys = Lists.newArrayList();
+        Set<FilteredConnectPoint> fcPoints = buildFCPoints(interfaces);
 
-        SetMultimap<VlanId, ConnectPoint> confCPointsByVlan =
-                HashMultimap.create();
+        intents.addAll(buildUnicastIntents(
+                networkName, hosts, fcPoints, affectedByApi));
+        intents.addAll(buildBroadcastIntents(
+                networkName, fcPoints, affectedByApi));
 
-        interfaceService.getInterfaces()
-                .stream()
-                .filter(intf -> intf.ipAddressesList().isEmpty())
-                .forEach(intf -> confCPointsByVlan.put(intf.vlan(), intf.connectPoint()));
-        return confCPointsByVlan;
+        if (affectedByApi) {
+            intents.forEach(intent -> keys.add(intent.key()));
+            withdrawIntents(networkName, keys);
+        }
+
+        intentInstaller.submitIntents(intents);
     }
 
     /**
-     * Checks if for any ConnectPoint configured there's an host presents
-     * and in case it associates them together.
+     * Withdraws intents belonging to a VPLS, given a VPLS name.
      *
-     * @param confCPointsByVlan the configured ConnectPoints grouped by VLAN Id
-     * @return the configured ConnectPoints with eventual hosts associated.
+     * @param networkName the VPLS name
+     * @param keys the keys of the intents to be installed
      */
-    protected SetMultimap<VlanId, Pair<ConnectPoint, MacAddress>> pairAvailableHosts(
-            SetMultimap<VlanId, ConnectPoint> confCPointsByVlan) {
-        log.debug("Binding connected hosts MAC addresses");
+    private void withdrawIntents(String networkName,
+                                 List<Key> keys) {
+        List<Intent> intents = Lists.newArrayList();
 
-        SetMultimap<VlanId, Pair<ConnectPoint, MacAddress>> confHostPresentCPoint =
-                HashMultimap.create();
+        intentInstaller.getIntentsFromVpls(networkName)
+                .forEach(intent -> {
+                    if (!keys.contains(intent.key())) {
+                        intents.add(intent);
+                    }
+        });
 
-        confCPointsByVlan.entries()
-                .forEach(e -> bindMacAddr(e, confHostPresentCPoint));
-
-        return confHostPresentCPoint;
+        intentInstaller.withdrawIntents(intents);
     }
 
-    // Bind VLAN Id with hosts and connect points
-    private void bindMacAddr(Map.Entry<VlanId, ConnectPoint> e,
-                             SetMultimap<VlanId, Pair<ConnectPoint,
-                                     MacAddress>> confHostPresentCPoint) {
-        VlanId vlanId = e.getKey();
-        ConnectPoint cp = e.getValue();
-        Set<Host> connectedHosts = hostService.getConnectedHosts(cp);
-        connectedHosts.forEach(host -> {
-            if (host.vlan().equals(vlanId)) {
-                confHostPresentCPoint.put(vlanId, Pair.of(cp, host.mac()));
-            } else {
-                confHostPresentCPoint.put(vlanId, Pair.of(cp, null));
+    /**
+     * Sets up broadcast intents between any given filtered connect point.
+     *
+     * @param networkName the VPLS name
+     * @param fcPoints the set of filtered connect points
+     * @param affectedByApi true if the function triggered from APIs;
+     *                      false otherwise
+     * @return the set of broadcast intents
+     */
+    private Set<Intent> buildBroadcastIntents(String networkName,
+                                              Set<FilteredConnectPoint> fcPoints,
+                                              boolean affectedByApi) {
+        Set<Intent> intents = Sets.newHashSet();
+        fcPoints.forEach(point -> {
+            Set<FilteredConnectPoint> otherPoints =
+                    fcPoints.stream()
+                            .filter(fcp -> !fcp.equals(point))
+                            .collect(Collectors.toSet());
+
+            Key brcKey = intentInstaller.buildKey(PREFIX_BROADCAST,
+                                                  point.connectPoint(),
+                                                  networkName,
+                                                  MacAddress.BROADCAST);
+
+            if ((!intentInstaller.intentExists(brcKey) || affectedByApi) &&
+                    !otherPoints.isEmpty()) {
+                intents.add(intentInstaller.buildBrcIntent(brcKey,
+                                                           point,
+                                                           otherPoints));
             }
         });
-        if (connectedHosts.isEmpty()) {
-            confHostPresentCPoint.put(vlanId, Pair.of(cp, null));
-        }
+
+        return ImmutableSet.copyOf(intents);
+    }
+
+    /**
+     * Sets up unicast intents between any given filtered connect point.
+     *
+     * @param networkName the VPLS name
+     * @param hosts the set of destination hosts
+     * @param fcPoints the set of filtered connect points
+     * @param affectedByApi true if the function triggered from APIs;
+     *                      false otherwise
+     * @return the set of unicast intents
+     */
+    private Set<Intent> buildUnicastIntents(String networkName,
+                                            Set<Host> hosts,
+                                            Set<FilteredConnectPoint> fcPoints,
+                                            boolean affectedByApi) {
+        Set<Intent> intents = Sets.newHashSet();
+        hosts.forEach(host -> {
+            FilteredConnectPoint hostPoint = getHostPoint(host, fcPoints);
+
+            if (hostPoint == null) {
+                log.warn(HOST_FCP_NOT_FOUND, host);
+                return;
+            }
+
+            Set<FilteredConnectPoint> otherPoints =
+                    fcPoints.stream()
+                            .filter(fcp -> !fcp.equals(hostPoint))
+                            .collect(Collectors.toSet());
+
+            Key uniKey = intentInstaller.buildKey(PREFIX_UNICAST,
+                                                  host.location(),
+                                                  networkName,
+                                                  host.mac());
+
+            if ((!intentInstaller.intentExists(uniKey) || affectedByApi) &&
+                    !otherPoints.isEmpty()) {
+                intents.add(intentInstaller.buildUniIntent(uniKey,
+                                                           otherPoints,
+                                                           hostPoint,
+                                                           host));
+            }
+        });
+
+        return ImmutableSet.copyOf(intents);
+    }
+
+    /**
+     * Finds the filtered connect point a host is attached to.
+     *
+     * @param host the target host
+     * @param fcps the filtered connected points
+     * @return null if not found; the filtered connect point otherwise
+     */
+    private FilteredConnectPoint getHostPoint(Host host,
+                                              Set<FilteredConnectPoint> fcps) {
+        return fcps.stream()
+                .filter(fcp -> fcp.connectPoint().equals(host.location()))
+                .filter(fcp -> {
+                    VlanIdCriterion vlanCriterion =
+                            (VlanIdCriterion) fcp.trafficSelector().
+                                    getCriterion(Criterion.Type.VLAN_VID);
+
+                    return vlanCriterion != null &&
+                            vlanCriterion.vlanId().equals(host.vlan());
+                })
+                .findFirst()
+                .orElse(null);
+    }
+
+    /**
+     * Computes a set of filtered connect points from a list of given interfaces.
+     *
+     * @param interfaces the interfaces to compute
+     * @return the set of filtered connect points
+     */
+    private Set<FilteredConnectPoint> buildFCPoints(Collection<Interface> interfaces) {
+        // Build all filtered connected points in the network
+        return interfaces
+                .stream()
+                .map(intf -> {
+                    TrafficSelector.Builder selectorBuilder =
+                            DefaultTrafficSelector.builder();
+
+                    if (!intf.vlan().equals(VlanId.NONE)) {
+                        selectorBuilder.matchVlanId(intf.vlan());
+                    }
+
+                    return new FilteredConnectPoint(intf.connectPoint(),
+                                                    selectorBuilder.build());
+                })
+                .collect(Collectors.toSet());
     }
 
     /**
      * Listener for host events.
      */
-    class InternalHostListener implements HostListener {
+    private class InternalHostListener implements HostListener {
         @Override
         public void event(HostEvent event) {
-            log.debug("Received HostEvent {}", event);
+            log.debug(HOST_EVENT, event);
             switch (event.type()) {
                 case HOST_ADDED:
                 case HOST_UPDATED:
                 case HOST_REMOVED:
-                    setupConnectivity();
+                    setupConnectivity(false);
                     break;
+
                 default:
                     break;
             }
@@ -214,16 +389,39 @@
     private class InternalInterfaceListener implements InterfaceListener {
         @Override
         public void event(InterfaceEvent event) {
-            log.debug("Received InterfaceConfigEvent {}", event);
+            log.debug(INTF_CONF_EVENT, event);
             switch (event.type()) {
                 case INTERFACE_ADDED:
                 case INTERFACE_UPDATED:
                 case INTERFACE_REMOVED:
-                    setupConnectivity();
+                    setupConnectivity(false);
                     break;
+
                 default:
                     break;
             }
         }
     }
+
+    /**
+     * Listener for VPLS configuration events.
+     */
+    private class InternalNetworkConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            if (event.configClass() == VplsConfigurationService.CONFIG_CLASS) {
+                log.debug(NET_CONF_EVENT, event.configClass());
+                switch (event.type()) {
+                    case CONFIG_ADDED:
+                    case CONFIG_UPDATED:
+                    case CONFIG_REMOVED:
+                        setupConnectivity(true);
+                        break;
+
+                    default:
+                        break;
+                }
+            }
+        }
+    }
 }