[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();
+ }
+}