[CORD-2834] Handling of dual-homed sinks

Includes also McastUtils to move some code out of the McastHandler

Change-Id: I101637ee600c9d524f17e9f3fc29d63256844956
diff --git a/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastUtils.java b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastUtils.java
new file mode 100644
index 0000000..37f273b
--- /dev/null
+++ b/apps/segmentrouting/app/src/main/java/org/onosproject/segmentrouting/mcast/McastUtils.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright 2018-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.segmentrouting.mcast;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.mcast.api.McastRoute;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.config.basics.McastConfig;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criteria;
+import org.onosproject.net.flow.criteria.VlanIdCriterion;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flowobjective.DefaultFilteringObjective;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.DefaultNextObjective;
+import org.onosproject.net.flowobjective.DefaultObjectiveContext;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.segmentrouting.SegmentRoutingManager;
+import org.onosproject.segmentrouting.SegmentRoutingService;
+import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
+import static org.onosproject.segmentrouting.SegmentRoutingManager.INTERNAL_VLAN;
+
+/**
+ * Utility class for Multicast Handler.
+ */
+class McastUtils {
+
+    // Internal reference to the log
+    private final Logger log;
+    // Internal reference to SR Manager
+    private SegmentRoutingManager srManager;
+    // Internal reference to the app id
+    private ApplicationId coreAppId;
+
+    /**
+     * Builds a new McastUtils object.
+     *
+     * @param srManager the SR manager
+     * @param coreAppId the core application id
+     * @param log log reference of the McastHandler
+     */
+    McastUtils(SegmentRoutingManager srManager, ApplicationId coreAppId, Logger log) {
+        this.srManager = srManager;
+        this.coreAppId = coreAppId;
+        this.log = log;
+    }
+
+    /**
+     * Given a connect point define a leader for it.
+     *
+     * @param source the source connect point
+     * @return true if this instance is the leader, otherwise false
+     */
+    boolean isLeader(ConnectPoint source) {
+        // Continue only when we have the mastership on the operation
+        if (!srManager.mastershipService.isLocalMaster(source.deviceId())) {
+            // When the source is available we just check the mastership
+            if (srManager.deviceService.isAvailable(source.deviceId())) {
+                return false;
+            }
+            // Fallback with Leadership service
+            // source id is used a topic
+            NodeId leader = srManager.leadershipService.runForLeadership(
+                    source.deviceId().toString()).leaderNodeId();
+            // Verify if this node is the leader
+            if (!srManager.clusterService.getLocalNode().id().equals(leader)) {
+                return false;
+            }
+        }
+        // Done
+        return true;
+    }
+
+    /**
+     * Get router mac using application config and the connect point.
+     *
+     * @param deviceId the device id
+     * @param port the port number
+     * @return the router mac if the port is configured, otherwise null
+     */
+    private MacAddress getRouterMac(DeviceId deviceId, PortNumber port) {
+        // Do nothing if the port is configured as suppressed
+        ConnectPoint connectPoint = new ConnectPoint(deviceId, port);
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId(), SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(connectPoint)) {
+            log.info("Ignore suppressed port {}", connectPoint);
+            return MacAddress.NONE;
+        }
+        // Get the router mac using the device configuration
+        MacAddress routerMac;
+        try {
+            routerMac = srManager.deviceConfiguration().getDeviceMac(deviceId);
+        } catch (DeviceConfigNotFoundException dcnfe) {
+            log.warn("Fail to push filtering objective since device is not configured. Abort");
+            return MacAddress.NONE;
+        }
+        return routerMac;
+    }
+
+    /**
+     * Adds filtering objective for given device and port.
+     *
+     * @param deviceId device ID
+     * @param port ingress port number
+     * @param assignedVlan assigned VLAN ID
+     * @param mcastIp the group address
+     * @param mcastRole the role of the device
+     */
+    void addFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan,
+                           IpAddress mcastIp, McastRole mcastRole) {
+
+        MacAddress routerMac = getRouterMac(deviceId, port);
+        if (routerMac.equals(MacAddress.NONE)) {
+            return;
+        }
+
+        FilteringObjective.Builder filtObjBuilder = filterObjBuilder(port, assignedVlan, mcastIp,
+                                                                     routerMac, mcastRole);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully add filter on {}/{}, vlan {}",
+                                         deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to add filter on {}/{}, vlan {}: {}",
+                                 deviceId, port.toLong(), assignedVlan, error));
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.add(context));
+    }
+
+    /**
+     * Removes filtering objective for given device and port.
+     *
+     * @param deviceId device ID
+     * @param port ingress port number
+     * @param assignedVlan assigned VLAN ID
+     * @param mcastIp multicast IP address
+     * @param mcastRole the multicast role of the device
+     */
+    void removeFilterToDevice(DeviceId deviceId, PortNumber port, VlanId assignedVlan,
+                              IpAddress mcastIp, McastRole mcastRole) {
+
+        MacAddress routerMac = getRouterMac(deviceId, port);
+        if (routerMac.equals(MacAddress.NONE)) {
+            return;
+        }
+
+        FilteringObjective.Builder filtObjBuilder =
+                filterObjBuilder(port, assignedVlan, mcastIp, routerMac, mcastRole);
+        ObjectiveContext context = new DefaultObjectiveContext(
+                (objective) -> log.debug("Successfully removed filter on {}/{}, vlan {}",
+                                         deviceId, port.toLong(), assignedVlan),
+                (objective, error) ->
+                        log.warn("Failed to remove filter on {}/{}, vlan {}: {}",
+                                 deviceId, port.toLong(), assignedVlan, error));
+        srManager.flowObjectiveService.filter(deviceId, filtObjBuilder.remove(context));
+    }
+
+    /**
+     * Gets assigned VLAN according to the value in the meta.
+     *
+     * @param nextObjective nextObjective to analyze
+     * @return assigned VLAN ID
+     */
+    VlanId assignedVlanFromNext(NextObjective nextObjective) {
+        return ((VlanIdCriterion) nextObjective.meta().getCriterion(VLAN_VID)).vlanId();
+    }
+
+    /**
+     * Gets ingress VLAN from McastConfig.
+     *
+     * @return ingress VLAN or VlanId.NONE if not configured
+     */
+    private VlanId ingressVlan() {
+        McastConfig mcastConfig =
+                srManager.cfgService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.ingressVlan() : VlanId.NONE;
+    }
+
+    /**
+     * Gets egress VLAN from McastConfig.
+     *
+     * @return egress VLAN or VlanId.NONE if not configured
+     */
+    private VlanId egressVlan() {
+        McastConfig mcastConfig =
+                srManager.cfgService.getConfig(coreAppId, McastConfig.class);
+        return (mcastConfig != null) ? mcastConfig.egressVlan() : VlanId.NONE;
+    }
+
+    /**
+     * Gets assigned VLAN according to the value of egress VLAN.
+     * If connect point is specified, try to reuse the assigned VLAN on the connect point.
+     *
+     * @param cp connect point; Can be null if not specified
+     * @return assigned VLAN ID
+     */
+    VlanId assignedVlan(ConnectPoint cp) {
+        // Use the egressVlan if it is tagged
+        if (!egressVlan().equals(VlanId.NONE)) {
+            return egressVlan();
+        }
+        // Reuse unicast VLAN if the port has subnet configured
+        if (cp != null) {
+            VlanId untaggedVlan = srManager.getInternalVlanId(cp);
+            return (untaggedVlan != null) ? untaggedVlan : INTERNAL_VLAN;
+        }
+        // Use DEFAULT_VLAN if none of the above matches
+        return SegmentRoutingManager.INTERNAL_VLAN;
+    }
+
+    /**
+     * Gets source connect point of given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @return source connect point or null if not found
+     */
+    // FIXME To be addressed with multiple sources support
+    ConnectPoint getSource(IpAddress mcastIp) {
+        // FIXME we should support different types of routes
+        McastRoute mcastRoute = srManager.multicastRouteService.getRoutes().stream()
+                .filter(mcastRouteInternal -> mcastRouteInternal.group().equals(mcastIp))
+                .findFirst().orElse(null);
+        return mcastRoute == null ? null : srManager.multicastRouteService.sources(mcastRoute)
+                .stream()
+                .findFirst().orElse(null);
+    }
+
+    /**
+     * Gets sinks of given multicast group.
+     *
+     * @param mcastIp multicast IP
+     * @return map of sinks or empty map if not found
+     */
+    Map<HostId, Set<ConnectPoint>> getSinks(IpAddress mcastIp) {
+        // FIXME we should support different types of routes
+        McastRoute mcastRoute = srManager.multicastRouteService.getRoutes().stream()
+                .filter(mcastRouteInternal -> mcastRouteInternal.group().equals(mcastIp))
+                .findFirst().orElse(null);
+        return mcastRoute == null ?
+                Collections.emptyMap() :
+                srManager.multicastRouteService.routeData(mcastRoute).sinks();
+    }
+
+    /**
+     * Get sinks affected by this egress device.
+     *
+     * @param egressDevice the egress device
+     * @param mcastIp the mcast ip address
+     * @return the map of the sinks affected
+     */
+    Map<HostId, Set<ConnectPoint>> getAffectedSinks(DeviceId egressDevice,
+                                                            IpAddress mcastIp) {
+        return getSinks(mcastIp).entrySet()
+                .stream()
+                .filter(hostIdSetEntry -> hostIdSetEntry.getValue().stream()
+                        .map(ConnectPoint::deviceId)
+                        .anyMatch(deviceId -> deviceId.equals(egressDevice))
+                ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    /**
+     * Creates a next objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param outPorts set of output port numbers
+     * @param nextId the next id
+     * @return next objective builder
+     */
+    NextObjective.Builder nextObjBuilder(IpAddress mcastIp, VlanId assignedVlan,
+                                         Set<PortNumber> outPorts, Integer nextId) {
+        // If nextId is null allocate a new one
+        if (nextId == null) {
+            nextId = srManager.flowObjectiveService.allocateNextId();
+        }
+        // Build the meta selector with the fwd objective info
+        TrafficSelector metadata =
+                DefaultTrafficSelector.builder()
+                        .matchVlanId(assignedVlan)
+                        .matchIPDst(mcastIp.toIpPrefix())
+                        .build();
+        // Define the nextobjective type
+        NextObjective.Builder nextObjBuilder = DefaultNextObjective
+                .builder().withId(nextId)
+                .withType(NextObjective.Type.BROADCAST)
+                .fromApp(srManager.appId())
+                .withMeta(metadata);
+        // Add the output ports
+        outPorts.forEach(port -> {
+            TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
+            if (egressVlan().equals(VlanId.NONE)) {
+                tBuilder.popVlan();
+            }
+            tBuilder.setOutput(port);
+            nextObjBuilder.addTreatment(tBuilder.build());
+        });
+        // Done return the complete builder
+        return nextObjBuilder;
+    }
+
+    /**
+     * Creates a forwarding objective builder for multicast.
+     *
+     * @param mcastIp multicast group
+     * @param assignedVlan assigned VLAN ID
+     * @param nextId next ID of the L3 multicast group
+     * @return forwarding objective builder
+     */
+    ForwardingObjective.Builder fwdObjBuilder(IpAddress mcastIp,
+                                                      VlanId assignedVlan, int nextId) {
+        TrafficSelector.Builder sbuilder = DefaultTrafficSelector.builder();
+        // Let's the matching on the group address
+        // TODO SSM support in future
+        if (mcastIp.isIp6()) {
+            sbuilder.matchEthType(Ethernet.TYPE_IPV6);
+            sbuilder.matchIPv6Dst(mcastIp.toIpPrefix());
+        } else {
+            sbuilder.matchEthType(Ethernet.TYPE_IPV4);
+            sbuilder.matchIPDst(mcastIp.toIpPrefix());
+        }
+        // Then build the meta selector
+        TrafficSelector.Builder metabuilder = DefaultTrafficSelector.builder();
+        metabuilder.matchVlanId(assignedVlan);
+        // Finally return the completed builder
+        ForwardingObjective.Builder fwdBuilder = DefaultForwardingObjective.builder();
+        fwdBuilder.withSelector(sbuilder.build())
+                .withMeta(metabuilder.build())
+                .nextStep(nextId)
+                .withFlag(ForwardingObjective.Flag.SPECIFIC)
+                .fromApp(srManager.appId())
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        return fwdBuilder;
+    }
+
+    /**
+     * Creates a filtering objective builder for multicast.
+     *
+     * @param ingressPort ingress port of the multicast stream
+     * @param assignedVlan assigned VLAN ID
+     * @param mcastIp the group address
+     * @param routerMac router MAC. This is carried in metadata and used from some switches that
+     *                  need to put unicast entry before multicast entry in TMAC table.
+     * @param mcastRole the Multicast role
+     * @return filtering objective builder
+     */
+    private FilteringObjective.Builder filterObjBuilder(PortNumber ingressPort, VlanId assignedVlan,
+                                                IpAddress mcastIp, MacAddress routerMac, McastRole mcastRole) {
+        FilteringObjective.Builder filtBuilder = DefaultFilteringObjective.builder();
+        // Let's add the in port matching and the priority
+        filtBuilder.withKey(Criteria.matchInPort(ingressPort))
+                .withPriority(SegmentRoutingService.DEFAULT_PRIORITY);
+        // According to the mcast role we match on the proper vlan
+        // If the role is null we are on the transit or on the egress
+        if (mcastRole == null) {
+            filtBuilder.addCondition(Criteria.matchVlanId(egressVlan()));
+        } else {
+            filtBuilder.addCondition(Criteria.matchVlanId(ingressVlan()));
+        }
+        // According to the IP type we set the proper match on the mac address
+        if (mcastIp.isIp4()) {
+            filtBuilder.addCondition(Criteria.matchEthDstMasked(MacAddress.IPV4_MULTICAST,
+                                                                MacAddress.IPV4_MULTICAST_MASK));
+        } else {
+            filtBuilder.addCondition(Criteria.matchEthDstMasked(MacAddress.IPV6_MULTICAST,
+                                                                MacAddress.IPV6_MULTICAST_MASK));
+        }
+        // We finally build the meta treatment
+        TrafficTreatment tt = DefaultTrafficTreatment.builder()
+                .pushVlan().setVlanId(assignedVlan)
+                .setEthDst(routerMac)
+                .build();
+        filtBuilder.withMeta(tt);
+        // Done, we return a permit filtering objective
+        return filtBuilder.permit().fromApp(srManager.appId());
+    }
+
+    /**
+     * Gets output ports information from treatments.
+     *
+     * @param treatments collection of traffic treatments
+     * @return set of output port numbers
+     */
+    Set<PortNumber> getPorts(Collection<TrafficTreatment> treatments) {
+        ImmutableSet.Builder<PortNumber> builder = ImmutableSet.builder();
+        treatments.forEach(treatment -> treatment.allInstructions().stream()
+                    .filter(instr -> instr instanceof Instructions.OutputInstruction)
+                    .forEach(instr -> builder.add(((Instructions.OutputInstruction) instr).port())));
+        return builder.build();
+    }
+}