blob: 02b8865d66666f03586a78f442d39ce8a6d4e17a [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Laboratory
*
* 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.net.intent.impl.compiler;
import com.google.common.collect.ImmutableSet;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultPath;
import org.onosproject.net.DeviceId;
import org.onosproject.net.DisjointPath;
import org.onosproject.net.EdgeLink;
import org.onosproject.net.Link;
import org.onosproject.net.Path;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.DefaultFlowRule;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.FlowRule;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.flow.instructions.Instruction;
import org.onosproject.net.flow.instructions.Instructions;
import org.onosproject.net.group.DefaultGroupBucket;
import org.onosproject.net.group.DefaultGroupDescription;
import org.onosproject.net.group.DefaultGroupKey;
import org.onosproject.net.group.Group;
import org.onosproject.net.group.GroupBucket;
import org.onosproject.net.group.GroupBuckets;
import org.onosproject.net.group.GroupDescription;
import org.onosproject.net.group.GroupKey;
import org.onosproject.net.group.GroupListener;
import org.onosproject.net.group.GroupService;
import org.onosproject.net.intent.FlowRuleIntent;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.IntentCompilationException;
import org.onosproject.net.intent.IntentId;
import org.onosproject.net.intent.LinkCollectionIntent;
import org.onosproject.net.intent.PathIntent;
import org.onosproject.net.intent.PointToPointIntent;
import org.onosproject.net.intent.constraint.ProtectionConstraint;
import org.onosproject.net.intent.impl.PathNotFoundException;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.provider.ProviderId;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static java.util.Arrays.asList;
import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
import static org.slf4j.LoggerFactory.getLogger;
/**
* An intent compiler for {@link org.onosproject.net.intent.PointToPointIntent}.
*/
@Component(immediate = true)
public class PointToPointIntentCompiler
extends ConnectivityIntentCompiler<PointToPointIntent> {
// TODO: use off-the-shell core provider ID
private static final ProviderId PID =
new ProviderId("core", "org.onosproject.core", true);
// TODO: consider whether the default cost is appropriate or not
public static final int DEFAULT_COST = 1;
protected static final int PRIORITY = Intent.DEFAULT_INTENT_PRIORITY;
private static final int GROUP_TIMEOUT = 5;
private final Logger log = getLogger(getClass());
protected boolean erasePrimary = false;
protected boolean eraseBackup = false;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected GroupService groupService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected LinkService linkService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected DeviceService deviceService;
@Activate
public void activate() {
intentManager.registerCompiler(PointToPointIntent.class, this);
}
@Deactivate
public void deactivate() {
intentManager.unregisterCompiler(PointToPointIntent.class);
}
@Override
public List<Intent> compile(PointToPointIntent intent, List<Intent> installable) {
log.trace("compiling {} {}", intent, installable);
ConnectPoint ingressPoint = intent.ingressPoint();
ConnectPoint egressPoint = intent.egressPoint();
if (ingressPoint.deviceId().equals(egressPoint.deviceId())) {
return createZeroHopLinkCollectionIntent(intent);
}
// proceed with no protected paths
if (!ProtectionConstraint.requireProtectedPath(intent)) {
return createUnprotectedLinkCollectionIntent(intent);
}
try {
// attempt to compute and implement backup path
return createProtectedIntent(ingressPoint, egressPoint, intent, installable);
} catch (PathNotFoundException e) {
log.warn("Could not find disjoint Path for {}", intent);
// no disjoint path extant -- maximum one path exists between devices
return createSinglePathIntent(ingressPoint, egressPoint, intent, installable);
}
}
private List<Intent> createZeroHopIntent(ConnectPoint ingressPoint,
ConnectPoint egressPoint,
PointToPointIntent intent) {
List<Link> links = asList(createEdgeLink(ingressPoint, true), createEdgeLink(egressPoint, false));
return asList(createPathIntent(new DefaultPath(PID, links, DEFAULT_COST),
intent, PathIntent.ProtectionType.PRIMARY));
}
private List<Intent> createZeroHopLinkCollectionIntent(PointToPointIntent intent) {
return asList(createLinkCollectionIntent(ImmutableSet.of(), DEFAULT_COST,
intent));
}
private List<Intent> createUnprotectedIntent(ConnectPoint ingressPoint,
ConnectPoint egressPoint,
PointToPointIntent intent) {
List<Link> links = new ArrayList<>();
Path path = getPath(intent, ingressPoint.deviceId(),
egressPoint.deviceId());
links.add(createEdgeLink(ingressPoint, true));
links.addAll(path.links());
links.add(createEdgeLink(egressPoint, false));
return asList(createPathIntent(new DefaultPath(PID, links, path.cost(),
path.annotations()), intent,
PathIntent.ProtectionType.PRIMARY));
}
private List<Intent> createUnprotectedLinkCollectionIntent(PointToPointIntent intent) {
Path path = getPath(intent, intent.filteredIngressPoint().connectPoint().deviceId(),
intent.filteredEgressPoint().connectPoint().deviceId());
return asList(createLinkCollectionIntent(ImmutableSet.copyOf(path.links()),
path.cost(),
intent));
}
//FIXME: Compatibility with EncapsulationConstraint
private List<Intent> createProtectedIntent(ConnectPoint ingressPoint,
ConnectPoint egressPoint,
PointToPointIntent intent,
List<Intent> installable) {
log.trace("createProtectedIntent");
DisjointPath path = getDisjointPath(intent, ingressPoint.deviceId(),
egressPoint.deviceId());
List<Intent> reusableIntents = null;
if (installable != null) {
reusableIntents = filterInvalidSubIntents(installable, intent);
if (reusableIntents.size() == installable.size()) {
// all old paths are still viable
return installable;
}
}
List<Intent> intentList = new ArrayList<>();
// primary path intent
List<Link> links = new ArrayList<>();
links.addAll(path.links());
links.add(createEdgeLink(egressPoint, false));
// backup path intent
List<Link> backupLinks = new ArrayList<>();
backupLinks.addAll(path.backup().links());
backupLinks.add(createEdgeLink(egressPoint, false));
/*
* One of the old paths is still entirely intact. This old path has
* already been made primary, so we must add a backup path intent
* and modify the failover group treatment accordingly.
*/
if (reusableIntents != null && reusableIntents.size() > 1) {
/*
* Ensures that the egress port on source device is different than
* that of existing path so that failover group will be useful
* (would not be useful if both output ports in group bucket were
* the same). Does not necessarily ensure that the new backup path
* is entirely disjoint from the old path.
*/
PortNumber primaryPort = getPrimaryPort(intent);
if (primaryPort != null && !links.get(0).src().port().equals(primaryPort)) {
reusableIntents.add(createPathIntent(new DefaultPath(PID, links,
path.cost(), path.annotations()),
intent, PathIntent.ProtectionType.BACKUP));
updateFailoverGroup(intent, links);
return reusableIntents;
} else {
reusableIntents.add(createPathIntent(new DefaultPath(PID, backupLinks, path.backup().cost(),
path.backup().annotations()), intent, PathIntent.ProtectionType.BACKUP));
updateFailoverGroup(intent, backupLinks);
return reusableIntents;
}
}
intentList.add(createPathIntent(new DefaultPath(PID, links, path.cost(),
path.annotations()),
intent, PathIntent.ProtectionType.PRIMARY));
intentList.add(createPathIntent(new DefaultPath(PID, backupLinks, path.backup().cost(),
path.backup().annotations()),
intent, PathIntent.ProtectionType.BACKUP));
// Create fast failover flow rule intent or, if it already exists,
// add contents appropriately.
if (groupService.getGroup(ingressPoint.deviceId(),
makeGroupKey(intent.id())) == null) {
// manufactured fast failover flow rule intent
createFailoverTreatmentGroup(path.links(), path.backup().links(), intent);
FlowRuleIntent frIntent = new FlowRuleIntent(intent.appId(),
createFailoverFlowRules(intent),
asList(ingressPoint.deviceId()),
PathIntent.ProtectionType.FAILOVER);
intentList.add(frIntent);
} else {
updateFailoverGroup(intent, links);
updateFailoverGroup(intent, backupLinks);
}
return intentList;
}
private List<Intent> createSinglePathIntent(ConnectPoint ingressPoint,
ConnectPoint egressPoint,
PointToPointIntent intent,
List<Intent> installable) {
List<Link> links = new ArrayList<>();
Path onlyPath = getPath(intent, ingressPoint.deviceId(),
egressPoint.deviceId());
List<Intent> reusableIntents = null;
if (installable != null) {
reusableIntents = filterInvalidSubIntents(installable, intent);
if (reusableIntents.size() == installable.size()) {
// all old paths are still viable
return installable;
}
}
// If there exists a full path from old installable intents,
// return the intents that comprise it.
if (reusableIntents != null && reusableIntents.size() > 1) {
return reusableIntents;
} else {
links.add(createEdgeLink(ingressPoint, true));
links.addAll(onlyPath.links());
links.add(createEdgeLink(egressPoint, false));
return asList(createPathIntent(new DefaultPath(PID, links, onlyPath.cost(),
onlyPath.annotations()),
intent, PathIntent.ProtectionType.PRIMARY));
}
}
/**
* Creates a path intent from the specified path and original
* connectivity intent.
*
* @param path path to create an intent for
* @param intent original intent
* @param type primary or backup
*/
private Intent createPathIntent(Path path,
PointToPointIntent intent,
PathIntent.ProtectionType type) {
return PathIntent.builder()
.appId(intent.appId())
.selector(intent.selector())
.treatment(intent.treatment())
.path(path)
.constraints(intent.constraints())
.priority(intent.priority())
.setType(type)
.build();
}
/**
* Creates a link collection intent from the specified path and original
* point to point intent.
*
* @param links the links of the packets
* @param cost the cost associated to the links
* @param intent the point to point intent we are compiling
* @return the link collection intent
*/
private Intent createLinkCollectionIntent(Set<Link> links,
double cost,
PointToPointIntent intent) {
return LinkCollectionIntent.builder()
.key(intent.key())
.appId(intent.appId())
.selector(intent.selector())
.treatment(intent.treatment())
.links(ImmutableSet.copyOf(links))
.filteredIngressPoints(ImmutableSet.of(
intent.filteredIngressPoint()
))
.filteredEgressPoints(ImmutableSet.of(
intent.filteredEgressPoint()
))
.applyTreatmentOnEgress(true)
.constraints(intent.constraints())
.priority(intent.priority())
.cost(cost)
.build();
}
/**
* Gets primary port number through failover group associated
* with this intent.
*/
private PortNumber getPrimaryPort(PointToPointIntent intent) {
Group group = groupService.getGroup(intent.filteredIngressPoint().connectPoint().deviceId(),
makeGroupKey(intent.id()));
PortNumber primaryPort = null;
if (group != null) {
List<GroupBucket> buckets = group.buckets().buckets();
Iterator<GroupBucket> iterator = buckets.iterator();
while (primaryPort == null && iterator.hasNext()) {
GroupBucket bucket = iterator.next();
Instruction individualInstruction = bucket.treatment().allInstructions().get(0);
if (individualInstruction instanceof Instructions.OutputInstruction) {
Instructions.OutputInstruction outInstruction =
(Instructions.OutputInstruction) individualInstruction;
PortNumber tempPortNum = outInstruction.port();
Port port = deviceService.getPort(intent.filteredIngressPoint().connectPoint().deviceId(),
tempPortNum);
if (port != null && port.isEnabled()) {
primaryPort = tempPortNum;
}
}
}
}
return primaryPort;
}
/**
* Creates group key unique to each intent.
*
* @param intentId identifier of intent to get a key for
* @return unique group key for the intent identifier
*/
public static GroupKey makeGroupKey(IntentId intentId) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(intentId.fingerprint());
return new DefaultGroupKey(buffer.array());
}
/**
* Creates a new failover group with the initial ports of the links
* from the primary and backup path.
*
* @param links links from the primary path
* @param backupLinks links from the backup path
* @param intent intent from which this call originates
*/
private void createFailoverTreatmentGroup(List<Link> links,
List<Link> backupLinks,
PointToPointIntent intent) {
List<GroupBucket> buckets = new ArrayList<>();
TrafficTreatment.Builder tBuilderIn = DefaultTrafficTreatment.builder();
ConnectPoint src = links.get(0).src();
tBuilderIn.setOutput(src.port());
TrafficTreatment.Builder tBuilderIn2 = DefaultTrafficTreatment.builder();
ConnectPoint src2 = backupLinks.get(0).src();
tBuilderIn2.setOutput(src2.port());
buckets.add(DefaultGroupBucket.createFailoverGroupBucket(tBuilderIn.build(), src.port(), null));
buckets.add(DefaultGroupBucket.createFailoverGroupBucket(tBuilderIn2.build(), src2.port(), null));
GroupBuckets groupBuckets = new GroupBuckets(buckets);
GroupDescription groupDesc = new DefaultGroupDescription(src.deviceId(), Group.Type.FAILOVER,
groupBuckets, makeGroupKey(intent.id()), null, intent.appId());
log.trace("adding failover group {}", groupDesc);
groupService.addGroup(groupDesc);
}
/**
* Manufactures flow rule with treatment that is defined by failover
* group and traffic selector determined by ingress port of the intent.
*
* @param intent intent which is being compiled (for appId)
* @return a list of a singular flow rule with fast failover
* outport traffic treatment
*/
private List<FlowRule> createFailoverFlowRules(PointToPointIntent intent) {
List<FlowRule> flowRules = new ArrayList<>();
ConnectPoint ingress = intent.ingressPoint();
DeviceId deviceId = ingress.deviceId();
// flow rule with failover traffic treatment
TrafficSelector trafficSelector = DefaultTrafficSelector.builder(intent.selector())
.matchInPort(ingress.port()).build();
FlowRule.Builder flowRuleBuilder = DefaultFlowRule.builder();
flowRules.add(flowRuleBuilder.withSelector(trafficSelector)
.withTreatment(buildFailoverTreatment(deviceId, makeGroupKey(intent.id())))
.fromApp(intent.appId())
.makePermanent()
.forDevice(deviceId)
.withPriority(PRIORITY)
.build());
return flowRules;
}
/**
* Waits for specified group to appear maximum of {@value #GROUP_TIMEOUT} seconds.
*
* @param deviceId {@link DeviceId}
* @param groupKey {@link GroupKey} to wait for.
* @return {@link Group}
* @throws IntentCompilationException on any error.
*/
private Group waitForGroup(DeviceId deviceId, GroupKey groupKey) {
return waitForGroup(deviceId, groupKey, GROUP_TIMEOUT, TimeUnit.SECONDS);
}
/**
* Waits for specified group to appear until timeout.
*
* @param deviceId {@link DeviceId}
* @param groupKey {@link GroupKey} to wait for.
* @param timeout timeout
* @param unit unit of timeout
* @return {@link Group}
* @throws IntentCompilationException on any error.
*/
private Group waitForGroup(DeviceId deviceId, GroupKey groupKey, long timeout, TimeUnit unit) {
Group group = groupService.getGroup(deviceId, groupKey);
if (group != null) {
return group;
}
final CompletableFuture<Group> future = new CompletableFuture<>();
final GroupListener listener = event -> {
if (event.subject().deviceId() == deviceId &&
event.subject().appCookie().equals(groupKey)) {
future.complete(event.subject());
return;
}
};
groupService.addListener(listener);
try {
group = groupService.getGroup(deviceId, groupKey);
if (group != null) {
return group;
}
return future.get(timeout, unit);
} catch (InterruptedException e) {
log.debug("Interrupted", e);
Thread.currentThread().interrupt();
throw new IntentCompilationException("Interrupted", e);
} catch (ExecutionException e) {
log.debug("ExecutionException", e);
throw new IntentCompilationException("ExecutionException caught", e);
} catch (TimeoutException e) {
// one last try
group = groupService.getGroup(deviceId, groupKey);
if (group != null) {
return group;
} else {
log.debug("Timeout", e);
throw new IntentCompilationException("Timeout", e);
}
} finally {
groupService.removeListener(listener);
}
}
private TrafficTreatment buildFailoverTreatment(DeviceId srcDevice,
GroupKey groupKey) {
Group group = waitForGroup(srcDevice, groupKey);
TrafficTreatment.Builder tBuilder = DefaultTrafficTreatment.builder();
TrafficTreatment trafficTreatment = tBuilder.group(group.id()).build();
return trafficTreatment;
}
/**
* Deletes intents from the given list if the ports or links the intent
* relies on are no longer viable. The failover flow rule intent is never
* deleted -- only its contents are updated.
*
* @param oldInstallables list of intents to examine
* @return list of reusable installable intents
*/
private List<Intent> filterInvalidSubIntents(List<Intent> oldInstallables,
PointToPointIntent pointIntent) {
List<Intent> intentList = new ArrayList<>();
intentList.addAll(oldInstallables);
erasePrimary = false;
eraseBackup = false;
if (intentList != null) {
Iterator<Intent> iterator = intentList.iterator();
while (iterator.hasNext() && !(erasePrimary && eraseBackup)) {
Intent intent = iterator.next();
intent.resources().forEach(resource -> {
if (resource instanceof Link) {
Link link = (Link) resource;
if (link.state() == Link.State.INACTIVE) {
setPathsToRemove(intent);
} else if (link instanceof EdgeLink) {
ConnectPoint connectPoint = (link.src().elementId() instanceof DeviceId)
? link.src() : link.dst();
Port port = deviceService.getPort(connectPoint.deviceId(), connectPoint.port());
if (port == null || !port.isEnabled()) {
setPathsToRemove(intent);
}
} else {
Port port1 = deviceService.getPort(link.src().deviceId(), link.src().port());
Port port2 = deviceService.getPort(link.dst().deviceId(), link.dst().port());
if (port1 == null || !port1.isEnabled() || port2 == null || !port2.isEnabled()) {
setPathsToRemove(intent);
}
}
}
});
}
removeAndUpdateIntents(intentList, pointIntent);
}
return intentList;
}
/**
* Sets instance variables erasePrimary and eraseBackup. If erasePrimary,
* the primary path is no longer viable and related intents will be deleted.
* If eraseBackup, the backup path is no longer viable and related intents
* will be deleted.
*
* @param intent intent whose resources are found to be disabled/inactive:
* if intent is part of primary path, primary path set for removal;
* if intent is part of backup path, backup path set for removal;
* if bad intent is of type failover, the ingress point is down,
* and both paths are rendered inactive.
* @return true if both primary and backup paths are to be removed
*/
private boolean setPathsToRemove(Intent intent) {
if (intent instanceof FlowRuleIntent) {
FlowRuleIntent frIntent = (FlowRuleIntent) intent;
PathIntent.ProtectionType type = frIntent.type();
if (type == PathIntent.ProtectionType.PRIMARY || type == PathIntent.ProtectionType.FAILOVER) {
erasePrimary = true;
}
if (type == PathIntent.ProtectionType.BACKUP || type == PathIntent.ProtectionType.FAILOVER) {
eraseBackup = true;
}
}
return erasePrimary && eraseBackup;
}
/**
* Removes intents from installables list, depending on the values
* of instance variables erasePrimary and eraseBackup. Flow rule intents
* that contain the manufactured fast failover flow rules are never deleted.
* The contents are simply modified as necessary. If cleanUpIntents size
* is greater than 1 (failover intent), then one whole path from previous
* installables must be still viable.
*
* @param cleanUpIntents list of installable intents
*/
private void removeAndUpdateIntents(List<Intent> cleanUpIntents,
PointToPointIntent pointIntent) {
ListIterator<Intent> iterator = cleanUpIntents.listIterator();
while (iterator.hasNext()) {
Intent cIntent = iterator.next();
if (cIntent instanceof FlowRuleIntent) {
FlowRuleIntent fIntent = (FlowRuleIntent) cIntent;
if (fIntent.type() == PathIntent.ProtectionType.PRIMARY && erasePrimary) {
// remove primary path's flow rule intents
iterator.remove();
} else if (fIntent.type() == PathIntent.ProtectionType.BACKUP && eraseBackup) {
//remove backup path's flow rule intents
iterator.remove();
} else if (fIntent.type() == PathIntent.ProtectionType.BACKUP && erasePrimary) {
// promote backup path's flow rule intents to primary
iterator.set(new FlowRuleIntent(fIntent, PathIntent.ProtectionType.PRIMARY));
}
}
}
// remove buckets whose watchports are disabled if the failover group exists
Group group = groupService.getGroup(pointIntent.filteredIngressPoint().connectPoint().deviceId(),
makeGroupKey(pointIntent.id()));
if (group != null) {
updateFailoverGroup(pointIntent);
}
}
// Removes buckets whose treatments rely on disabled ports from the
// failover group.
private void updateFailoverGroup(PointToPointIntent pointIntent) {
DeviceId deviceId = pointIntent.filteredIngressPoint().connectPoint().deviceId();
GroupKey groupKey = makeGroupKey(pointIntent.id());
Group group = waitForGroup(deviceId, groupKey);
Iterator<GroupBucket> groupIterator = group.buckets().buckets().iterator();
while (groupIterator.hasNext()) {
GroupBucket bucket = groupIterator.next();
Instruction individualInstruction = bucket.treatment().allInstructions().get(0);
if (individualInstruction instanceof Instructions.OutputInstruction) {
Instructions.OutputInstruction outInstruction =
(Instructions.OutputInstruction) individualInstruction;
Port port = deviceService.getPort(deviceId, outInstruction.port());
if (port == null || !port.isEnabled()) {
GroupBuckets removeBuckets = new GroupBuckets(Collections.singletonList(bucket));
groupService.removeBucketsFromGroup(deviceId, groupKey,
removeBuckets, groupKey,
pointIntent.appId());
}
}
}
}
// Adds failover group bucket with treatment outport determined by the
// ingress point of the links.
private void updateFailoverGroup(PointToPointIntent intent, List<Link> links) {
GroupKey groupKey = makeGroupKey(intent.id());
TrafficTreatment.Builder tBuilderIn = DefaultTrafficTreatment.builder();
ConnectPoint src = links.get(0).src();
tBuilderIn.setOutput(src.port());
GroupBucket bucket = DefaultGroupBucket.createFailoverGroupBucket(tBuilderIn.build(), src.port(), null);
GroupBuckets addBuckets = new GroupBuckets(Collections.singletonList(bucket));
groupService.addBucketsToGroup(src.deviceId(), groupKey, addBuckets, groupKey, intent.appId());
}
}