Implement path protection for point to point intents
Change-Id: I3f3627e7c2a7e3ab017e46655692ab70fdeae413
diff --git a/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PointToPointIntentCompiler.java b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PointToPointIntentCompiler.java
index 28d7ea5..0de641a 100644
--- a/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PointToPointIntentCompiler.java
+++ b/core/net/src/main/java/org/onosproject/net/intent/impl/compiler/PointToPointIntentCompiler.java
@@ -18,17 +18,51 @@
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.GroupService;
+import org.onosproject.net.intent.FlowRuleIntent;
import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentId;
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 java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
+import java.util.ListIterator;
import static java.util.Arrays.asList;
import static org.onosproject.net.DefaultEdgeLink.createEdgeLink;
@@ -45,6 +79,18 @@
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;
+ 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() {
@@ -63,20 +109,160 @@
ConnectPoint egressPoint = intent.egressPoint();
if (ingressPoint.deviceId().equals(egressPoint.deviceId())) {
- List<Link> links = asList(createEdgeLink(ingressPoint, true), createEdgeLink(egressPoint, false));
- return asList(createPathIntent(new DefaultPath(PID, links, DEFAULT_COST), intent));
+ return createZeroHopIntent(ingressPoint, egressPoint, intent);
}
+ // proceed with no protected paths
+ if (!ProtectionConstraint.requireProtectedPath(intent)) {
+ return createUnprotectedIntent(ingressPoint, egressPoint, intent);
+ }
+
+ try {
+ // attempt to compute and implement backup path
+ return createProtectedIntent(ingressPoint, egressPoint, intent, installable);
+ } catch (PathNotFoundException e) {
+ // 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> createUnprotectedIntent(ConnectPoint ingressPoint,
+ ConnectPoint egressPoint,
+ PointToPointIntent intent) {
List<Link> links = new ArrayList<>();
Path path = getPath(intent, ingressPoint.deviceId(),
- egressPoint.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));
+ path.annotations()), intent,
+ PathIntent.ProtectionType.PRIMARY));
+ }
+
+ //FIXME: Compatibility with EncapsulationConstraint
+ private List<Intent> createProtectedIntent(ConnectPoint ingressPoint,
+ ConnectPoint egressPoint,
+ PointToPointIntent intent,
+ List<Intent> installable) {
+ 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));
+ }
}
/**
@@ -85,9 +271,11 @@
*
* @param path path to create an intent for
* @param intent original intent
+ * @param type primary or backup
*/
private Intent createPathIntent(Path path,
- PointToPointIntent intent) {
+ PointToPointIntent intent,
+ PathIntent.ProtectionType type) {
return PathIntent.builder()
.appId(intent.appId())
.selector(intent.selector())
@@ -95,7 +283,262 @@
.path(path)
.constraints(intent.constraints())
.priority(intent.priority())
+ .setType(type)
.build();
}
+ /**
+ * Gets primary port number through failover group associated
+ * with this intent.
+ */
+ private PortNumber getPrimaryPort(PointToPointIntent intent) {
+ Group group = groupService.getGroup(intent.ingressPoint().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.ingressPoint().deviceId(),
+ tempPortNum);
+ if (port != null && port.isEnabled()) {
+ primaryPort = tempPortNum;
+ }
+ }
+ }
+ }
+ return primaryPort;
+ }
+
+ /**
+ * Creates group key unique to each intent.
+ */
+ 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());
+ 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;
+ }
+
+ private TrafficTreatment buildFailoverTreatment(DeviceId srcDevice,
+ GroupKey groupKey) {
+ Group group = groupService.getGroup(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.ingressPoint().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.ingressPoint().deviceId();
+ GroupKey groupKey = makeGroupKey(pointIntent.id());
+ Group group = groupService.getGroup(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());
+ }
}