blob: 2bbac10d1948daf90f751db5163ab752b13cf397 [file] [log] [blame]
/*
* Copyright 2016-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.net.intent.impl.compiler;
import com.google.common.annotations.Beta;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.onlab.packet.VlanId;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultLink;
import org.onosproject.net.DefaultPath;
import org.onosproject.net.DeviceId;
import org.onosproject.net.DisjointPath;
import org.onosproject.net.FilteredConnectPoint;
import org.onosproject.net.Link;
import org.onosproject.net.Link.State;
import org.onosproject.net.NetworkResource;
import org.onosproject.net.Path;
import org.onosproject.net.behaviour.protection.TransportEndpointDescription;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.IntentCompilationException;
import org.onosproject.net.intent.LinkCollectionIntent;
import org.onosproject.net.intent.ProtectedTransportIntent;
import org.onosproject.net.intent.ProtectionEndpointIntent;
import org.onosproject.net.resource.DiscreteResourceId;
import org.onosproject.net.resource.Resource;
import org.onosproject.net.resource.Resources;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.transform;
import static java.util.stream.Stream.concat;
import static org.onosproject.net.MarkerResource.marker;
import static org.onosproject.net.behaviour.protection.ProtectedTransportEndpointDescription.buildDescription;
import static org.slf4j.LoggerFactory.getLogger;
/**
* IntentCompiler for {@link ProtectedTransportIntent}.
*/
@Beta
@Component(immediate = true)
public class ProtectedTransportIntentCompiler
extends ConnectivityIntentCompiler<ProtectedTransportIntent> {
/**
* Marker value for forward path.
*/
private static final String FWD = "fwd";
/**
* Marker value for reverse path.
*/
private static final String REV = "rev";
private final Logger log = getLogger(getClass());
@Activate
public void activate() {
intentManager.registerCompiler(ProtectedTransportIntent.class, this);
log.info("started");
}
@Deactivate
public void deactivate() {
intentManager.unregisterCompiler(ProtectedTransportIntent.class);
log.info("stopped");
}
@Override
public List<Intent> compile(ProtectedTransportIntent intent,
List<Intent> installable) {
log.trace("compiling {} {}", intent, installable);
// case 0 hop, same device
final DeviceId did1 = intent.one();
final DeviceId did2 = intent.two();
if (Objects.equals(did1, did2)) {
// Doesn't really make sense to create 0 hop protected path, but
// can generate Flow for the device, just to provide connectivity.
// future work.
log.error("0 hop not supported yet.");
throw new IntentCompilationException("0 hop not supported yet.");
}
List<Intent> reusable = Optional.ofNullable(installable).orElse(ImmutableList.of())
.stream()
.filter(this::isIntact)
.collect(Collectors.toList());
if (reusable.isEmpty() ||
reusable.stream().allMatch(ProtectionEndpointIntent.class::isInstance)) {
// case provisioning new protected path
// or
// case re-compilation (total failure -> restoration)
return createFreshProtectedPaths(intent, did1, did2);
} else {
// case re-compilation (partial failure)
log.warn("Re-computing adding new backup path not supported yet. No-Op.");
// TODO This part needs to be flexible to support various use case
// - non-revertive behavior (Similar to PartialFailureConstraint)
// - revertive behavior
// - compute third path
// ...
// Require further input what they actually need.
// TODO handle PartialFailureConstraint
/// case only need to update transit portion
/// case head and/or tail needs to be updated
// TODO do we need to prune broken
return installable;
}
}
/**
* Test if resources used by specified Intent is intact.
*
* @param installed Intent to test
* @return true if Intent is intact
*/
private boolean isIntact(Intent installed) {
return installed.resources().stream()
.filter(Link.class::isInstance)
.map(Link.class::cast)
.allMatch(this::isLive);
}
/**
* Test if specified Link is intact.
*
* @param link to test
* @return true if link is intact
*/
private boolean isLive(Link link) {
// Only testing link state for now
// in the long run, consider verifying OAM state on ports
return link.state() != State.INACTIVE;
}
/**
* Creates new protected paths.
*
* @param intent original intention
* @param did1 identifier of first device
* @param did2 identifier of second device
* @return compilation result
* @throws IntentCompilationException when there's no satisfying path.
*/
private List<Intent> createFreshProtectedPaths(ProtectedTransportIntent intent,
DeviceId did1,
DeviceId did2) {
DisjointPath disjointPath = getDisjointPath(intent, did1, did2);
if (disjointPath == null || disjointPath.backup() == null) {
log.error("Unable to find disjoint path between {}, {}", did1, did2);
throw new IntentCompilationException("Unable to find disjoint paths.");
}
Path primary = disjointPath.primary();
Path secondary = disjointPath.backup();
String fingerprint = intent.key().toString();
// pick and allocate Vlan to use as S-tag
Pair<VlanId, VlanId> vlans = allocateEach(intent, primary, secondary, VlanId.class);
VlanId primaryVlan = vlans.getLeft();
VlanId secondaryVlan = vlans.getRight();
// Build edge Intents for head/tail
// resource for head/tail
Collection<NetworkResource> oneResources = new ArrayList<>();
Collection<NetworkResource> twoResources = new ArrayList<>();
List<TransportEndpointDescription> onePaths = new ArrayList<>();
onePaths.add(TransportEndpointDescription.builder()
.withOutput(vlanPort(primary.src(), primaryVlan))
.build());
onePaths.add(TransportEndpointDescription.builder()
.withOutput(vlanPort(secondary.src(), secondaryVlan))
.build());
List<TransportEndpointDescription> twoPaths = new ArrayList<>();
twoPaths.add(TransportEndpointDescription.builder()
.withOutput(vlanPort(primary.dst(), primaryVlan))
.build());
twoPaths.add(TransportEndpointDescription.builder()
.withOutput(vlanPort(secondary.dst(), secondaryVlan))
.build());
ProtectionEndpointIntent oneIntent = ProtectionEndpointIntent.builder()
.key(intent.key())
.appId(intent.appId())
.priority(intent.priority())
.resources(oneResources)
.deviceId(did1)
.description(buildDescription(onePaths, did2, fingerprint))
.build();
ProtectionEndpointIntent twoIntent = ProtectionEndpointIntent.builder()
.key(intent.key())
.appId(intent.appId())
.resources(twoResources)
.deviceId(did2)
.description(buildDescription(twoPaths, did1, fingerprint))
.build();
// Build transit intent for primary/secondary path
Collection<NetworkResource> resources1 = ImmutableList.of(marker("protection1"));
Collection<NetworkResource> resources2 = ImmutableList.of(marker("protection2"));
ImmutableList<Intent> result = ImmutableList.<Intent>builder()
// LinkCollection for primary and backup paths
.addAll(createTransitIntent(intent, primary, primaryVlan, resources1))
.addAll(createTransitIntent(intent, secondary, secondaryVlan, resources2))
.add(oneIntent)
.add(twoIntent)
.build();
log.trace("createFreshProtectedPaths result: {}", result);
return result;
}
/**
* Creates required Intents required to transit bi-directionally the network.
*
* @param intent parent IntentId
* @param path whole path
* @param vid VlanId to use as tunnel labels
* @param resources to be passed down to generated Intents
* @return List on transit Intents, if any is required.
*/
List<LinkCollectionIntent> createTransitIntent(Intent intent,
Path path,
VlanId vid,
Collection<NetworkResource> resources) {
if (path.links().size() <= 1) {
// There's no need for transit Intents
return ImmutableList.of();
}
Collection<NetworkResource> fwd = ImmutableList.<NetworkResource>builder()
.addAll(resources)
.add(marker(FWD))
.build();
Collection<NetworkResource> rev = ImmutableList.<NetworkResource>builder()
.addAll(resources)
.add(marker(REV))
.build();
return ImmutableList.of(createSubTransitIntent(intent, path, vid, fwd),
createSubTransitIntent(intent, reverse(path), vid, rev));
}
/**
* Returns a path in reverse direction.
*
* @param path to reverse
* @return reversed path
*/
Path reverse(Path path) {
List<Link> revLinks = Lists.reverse(transform(path.links(), this::reverse));
return new DefaultPath(path.providerId(),
revLinks,
path.weight(),
path.annotations());
}
// TODO consider adding equivalent to Link/DefaultLink.
/**
* Returns a link in reverse direction.
*
* @param link to revese
* @return reversed link
*/
Link reverse(Link link) {
return DefaultLink.builder()
.providerId(link.providerId())
.src(link.dst())
.dst(link.src())
.type(link.type())
.state(link.state())
.isExpected(link.isExpected())
.annotations(link.annotations())
.build();
}
/**
* Creates required Intents required to transit uni-directionally along the Path.
*
* @param intent parent IntentId
* @param path whole path
* @param vid VlanId to use as tunnel labels
* @param resources to be passed down to generated Intents
* @return List of transit Intents, if any is required.
*/
LinkCollectionIntent createSubTransitIntent(Intent intent,
Path path,
VlanId vid,
Collection<NetworkResource> resources) {
checkArgument(path.links().size() > 1);
// transit ingress/egress
ConnectPoint one = path.links().get(0).dst();
ConnectPoint two = path.links().get(path.links().size() - 1).src();
return LinkCollectionIntent.builder()
// TODO there should probably be .parent(intent)
// which copies key, appId, priority, ...
.key(intent.key())
.appId(intent.appId())
.priority(intent.priority())
//.constraints(intent.constraints())
// VLAN tunnel
//.selector(DefaultTrafficSelector.builder().matchVlanId(vid).build())
//.treatment(intent.treatment())
.resources(resources)
.links(ImmutableSet.copyOf(path.links()))
.filteredIngressPoints(ImmutableSet.of(vlanPort(one, vid)))
.filteredEgressPoints(ImmutableSet.of(vlanPort(two, vid)))
// magic flag required for p2p type
.applyTreatmentOnEgress(true)
.cost(path.cost())
.build();
}
/**
* Creates VLAN filtered-ConnectPoint.
*
* @param cp ConnectPoint
* @param vid VLAN ID
* @return filtered-ConnectPoint
*/
static FilteredConnectPoint vlanPort(ConnectPoint cp, VlanId vid) {
return new FilteredConnectPoint(cp, DefaultTrafficSelector.builder()
.matchVlanId(vid)
.build());
}
/**
* Creates ResourceId for a port.
*
* @param cp ConnectPoint
* @return ResourceId
*/
static DiscreteResourceId resourceId(ConnectPoint cp) {
return Resources.discrete(cp.deviceId(), cp.port()).id();
}
/**
* Allocate resource for each {@link Path}s.
*
* @param intent to allocate resource to
* @param primary path
* @param secondary path
* @param klass label resource class
* @return Pair of chosen resource (primary, secondary)
* @param <T> label resource type
* @throws IntentCompilationException when there is no resource available
*/
<T> Pair<T, T> allocateEach(Intent intent, Path primary, Path secondary, Class<T> klass) {
log.trace("allocateEach({}, {}, {}, {})", intent, primary, secondary, klass);
Pair<T, T> vlans = null;
do {
Set<T> primaryVlans = commonLabelResource(primary, klass);
Set<T> secondaryVlans = commonLabelResource(secondary, klass);
Pair<T, T> candidates = pickEach(primaryVlans, secondaryVlans);
T primaryT = candidates.getLeft();
T secondaryT = candidates.getRight();
// try to allocate candidates along each path
Stream<Resource> primaryResources = primary.links().stream()
.flatMap(link -> Stream.of(link.src(), link.dst()))
.distinct()
.map(cp -> Resources.discrete(resourceId(cp), primaryT).resource());
Stream<Resource> secondaryResources = secondary.links().stream()
.flatMap(link -> Stream.of(link.src(), link.dst()))
.distinct()
.map(cp -> Resources.discrete(resourceId(cp), secondaryT).resource());
List<Resource> resources = concat(primaryResources, secondaryResources)
.collect(Collectors.toList());
log.trace("Calling allocate({},{})", intent.key(), resources);
if (resourceService.allocate(intent.key(), resources).isEmpty()) {
log.warn("Allocation failed, retrying");
continue;
}
vlans = candidates;
} while (false);
log.trace("allocation done.");
return vlans;
}
/**
* Randomly pick one resource from candidates.
*
* @param set of candidates
* @return chosen one
* @param <T> label resource type
*/
<T> T pickOne(Set<T> set) {
// Note: Set returned by commonLabelResource(..) assures,
// there is at least one element.
// FIXME more reasonable selection logic
return Iterables.get(set, RandomUtils.nextInt(0, set.size()));
}
/**
* Select resource from available Resources.
*
* @param primary Set of resource to pick from
* @param secondary Set of resource to pick from
* @return Pair of chosen resource (primary, secondary)
* @param <T> label resource type
*/
<T> Pair<T, T> pickEach(Set<T> primary, Set<T> secondary) {
Set<T> intersection = Sets.intersection(primary, secondary);
if (!intersection.isEmpty()) {
// favor common
T picked = pickOne(intersection);
return Pair.of(picked, picked);
}
T pickedP = pickOne(primary);
T pickedS = pickOne(secondary);
return Pair.of(pickedP, pickedS);
}
/**
* Finds label resource, which can be used in common along the path.
*
* @param path path
* @param klass Label class
* @return Set of common resources
* @throws IntentCompilationException when there is no resource available
* @param <T> label resource type
*/
<T> Set<T> commonLabelResource(Path path, Class<T> klass) {
Optional<Set<T>> common = path.links().stream()
.flatMap(link -> Stream.of(link.src(), link.dst()))
.distinct()
.map(cp -> getAvailableResourceValues(cp, klass))
.reduce(Sets::intersection);
if (!common.isPresent() || common.get().isEmpty()) {
log.error("No common label available for: {}", path);
throw new IntentCompilationException("No common label available for: " + path);
}
return common.get();
}
<T> Set<T> getAvailableResourceValues(ConnectPoint cp, Class<T> klass) {
return resourceService.getAvailableResourceValues(
resourceId(cp),
klass);
}
}