| /* |
| * Copyright 2016-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 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; |
| |
| 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 org.apache.commons.lang3.RandomUtils; |
| import org.apache.commons.lang3.tuple.Pair; |
| 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.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.ResourceService; |
| import org.onosproject.net.resource.Resources; |
| import org.slf4j.Logger; |
| |
| 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; |
| |
| /** |
| * 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()); |
| |
| @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) |
| protected ResourceService resourceService; |
| |
| @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.cost(), |
| 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.id(), resources); |
| if (resourceService.allocate(intent.id(), 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); |
| } |
| |
| } |