blob: 52232b93612c70831f9288a5c98be181c6c6df86 [file] [log] [blame]
/*
* 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.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
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.Link;
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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.onosproject.net.flow.criteria.Criterion.Type.VLAN_VID;
/**
* 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;
// Hashing function for the multicast hasher
private static final HashFunction HASH_FN = Hashing.md5();
// Read only cache of the Mcast leader
private Map<IpAddress, NodeId> mcastLeaderCache;
/**
* 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;
this.mcastLeaderCache = Maps.newConcurrentMap();
}
/**
* Clean up when deactivating the application.
*/
public void terminate() {
mcastLeaderCache.clear();
}
/**
* 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
: srManager.getDefaultInternalVlan();
}
// Use DEFAULT_VLAN if none of the above matches
return srManager.getDefaultInternalVlan();
}
/**
* Gets source connect point of given multicast group.
*
* @param mcastIp multicast IP
* @return source connect point or null if not found
*
* @deprecated in 1.12 ("Magpie") release.
*/
@Deprecated
ConnectPoint getSource(IpAddress mcastIp) {
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 sources connect points of given multicast group.
*
* @param mcastIp multicast IP
* @return sources connect points or empty set if not found
*/
Set<ConnectPoint> getSources(IpAddress mcastIp) {
// TODO 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 ? ImmutableSet.of() :
srManager.multicastRouteService.sources(mcastRoute);
}
/**
* 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) {
// TODO 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 ?
ImmutableMap.of() :
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();
}
/**
* Returns the hash of the group address.
*
* @param ipAddress the ip address
* @return the hash of the address
*/
private Long hasher(IpAddress ipAddress) {
return HASH_FN.newHasher()
.putBytes(ipAddress.toOctets())
.hash()
.asLong();
}
/**
* Given a multicast group define a leader for it.
*
* @param mcastIp the group address
* @return true if the instance is the leader of the group
*/
boolean isLeader(IpAddress mcastIp) {
// Get our id
final NodeId currentNodeId = srManager.clusterService.getLocalNode().id();
// Get the leader for this group using the ip address as key
final NodeId leader = srManager.workPartitionService.getLeader(mcastIp, this::hasher);
// If there is not a leader, let's send an error
if (leader == null) {
log.error("Fail to elect a leader for {}.", mcastIp);
return false;
}
// Update cache and return operation result
mcastLeaderCache.put(mcastIp, leader);
return currentNodeId.equals(leader);
}
/**
* Given a multicast group withdraw its leader.
*
* @param mcastIp the group address
*/
void withdrawLeader(IpAddress mcastIp) {
// For now just update the cache
mcastLeaderCache.remove(mcastIp);
}
Map<IpAddress, NodeId> getMcastLeaders(IpAddress mcastIp) {
// If mcast ip is present
if (mcastIp != null) {
return mcastLeaderCache.entrySet().stream()
.filter(entry -> entry.getKey().equals(mcastIp))
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue));
}
// Otherwise take all the groups
return ImmutableMap.copyOf(mcastLeaderCache);
}
/**
* Build recursively the mcast paths.
*
* @param mcastNextObjStore mcast next obj store
* @param toVisit the node to visit
* @param visited the visited nodes
* @param mcastPaths the current mcast paths
* @param currentPath the current path
* @param mcastIp the group ip
* @param source the source
*/
void buildMcastPaths(Map<McastStoreKey, NextObjective> mcastNextObjStore,
DeviceId toVisit, Set<DeviceId> visited,
Map<ConnectPoint, List<ConnectPoint>> mcastPaths,
List<ConnectPoint> currentPath, IpAddress mcastIp,
ConnectPoint source) {
// If we have visited the node to visit there is a loop
if (visited.contains(toVisit)) {
return;
}
// Visit next-hop
visited.add(toVisit);
VlanId assignedVlan = assignedVlan(toVisit.equals(source.deviceId()) ? source : null);
McastStoreKey mcastStoreKey = new McastStoreKey(mcastIp, toVisit, assignedVlan);
// Looking for next-hops
if (mcastNextObjStore.containsKey(mcastStoreKey)) {
// Build egress connect points, get ports and build relative cps
NextObjective nextObjective = mcastNextObjStore.get(mcastStoreKey);
Set<PortNumber> outputPorts = getPorts(nextObjective.next());
ImmutableSet.Builder<ConnectPoint> cpBuilder = ImmutableSet.builder();
outputPorts.forEach(portNumber -> cpBuilder.add(new ConnectPoint(toVisit, portNumber)));
Set<ConnectPoint> egressPoints = cpBuilder.build();
Set<Link> egressLinks;
List<ConnectPoint> newCurrentPath;
Set<DeviceId> newVisited;
DeviceId newToVisit;
for (ConnectPoint egressPoint : egressPoints) {
egressLinks = srManager.linkService.getEgressLinks(egressPoint);
// If it does not have egress links, stop
if (egressLinks.isEmpty()) {
// Add the connect points to the path
newCurrentPath = Lists.newArrayList(currentPath);
newCurrentPath.add(0, egressPoint);
mcastPaths.put(egressPoint, newCurrentPath);
} else {
newVisited = Sets.newHashSet(visited);
// Iterate over the egress links for the next hops
for (Link egressLink : egressLinks) {
newToVisit = egressLink.dst().deviceId();
newCurrentPath = Lists.newArrayList(currentPath);
newCurrentPath.add(0, egressPoint);
newCurrentPath.add(0, egressLink.dst());
buildMcastPaths(mcastNextObjStore, newToVisit, newVisited, mcastPaths, newCurrentPath, mcastIp,
source);
}
}
}
}
}
}