| /* |
| * Copyright 2017-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.ui.impl; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import org.onosproject.net.ConnectPoint; |
| import org.onosproject.net.DeviceId; |
| import org.onosproject.net.ElementId; |
| import org.onosproject.net.HostId; |
| import org.onosproject.net.Link; |
| import org.onosproject.net.flow.criteria.Criterion; |
| import org.onosproject.net.flow.criteria.PortCriterion; |
| import org.onosproject.net.flow.instructions.Instructions.OutputInstruction; |
| import org.onosproject.net.intent.FlowRuleIntent; |
| import org.onosproject.net.intent.Intent; |
| import org.onosproject.net.intent.IntentService; |
| import org.onosproject.net.intent.OpticalConnectivityIntent; |
| import org.onosproject.net.intent.ProtectionEndpointIntent; |
| import org.onosproject.net.link.LinkService; |
| import org.onosproject.ui.impl.topo.util.ServicesBundle; |
| import org.onosproject.ui.impl.topo.util.TrafficLink; |
| import org.onosproject.ui.impl.topo.util.TrafficLink.StatsType; |
| import org.onosproject.ui.impl.topo.util.TrafficLinkMap; |
| import org.onosproject.ui.topo.AbstractTopoMonitor; |
| import org.onosproject.ui.topo.DeviceHighlight; |
| import org.onosproject.ui.topo.Highlights; |
| import org.onosproject.ui.topo.HostHighlight; |
| import org.onosproject.ui.topo.LinkHighlight.Flavor; |
| import org.onosproject.ui.topo.Mod; |
| import org.onosproject.ui.topo.NodeHighlight; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.stream.Collectors; |
| |
| import static org.onosproject.net.MarkerResource.marker; |
| import static org.onosproject.ui.impl.ProtectedIntentMonitor.ProtectedMode.IDLE; |
| import static org.onosproject.ui.impl.ProtectedIntentMonitor.ProtectedMode.SELECTED_INTENT; |
| |
| /** |
| * Encapsulates the behavior of monitoring protected intents. |
| */ |
| //TODO refactor duplicated methods from here and the TrafficMonitor to AbstractTopoMonitor |
| public class ProtectedIntentMonitor extends AbstractTopoMonitor { |
| |
| private static final Logger log = |
| LoggerFactory.getLogger(ProtectedIntentMonitor.class); |
| private static final String PRIMARY_PATH_TAG = "protection1"; |
| |
| private static final String PROT_PRIMARY = "protPrimary"; |
| private static final String PROT_BACKUP = "protBackup"; |
| |
| |
| private static final Mod MOD_PROT_PRIMARY = new Mod(PROT_PRIMARY); |
| private static final Set<Mod> PROTECTED_MOD_PRIMARY_SET = |
| ImmutableSet.of(MOD_PROT_PRIMARY); |
| |
| private static final Mod MOD_PROT_BACKUP = new Mod(PROT_BACKUP); |
| private static final Set<Mod> PROTECTED_MOD_BACKUP_SET = |
| ImmutableSet.of(MOD_PROT_BACKUP); |
| |
| |
| /** |
| * Designates the different modes of operation. |
| */ |
| public enum ProtectedMode { |
| IDLE, |
| SELECTED_INTENT |
| } |
| |
| private final long trafficPeriod; |
| private final ServicesBundle services; |
| private final TopologyViewMessageHandler msgHandler; |
| |
| private final Timer timer = new Timer("topo-protected-intents"); |
| |
| private TimerTask trafficTask = null; |
| private ProtectedMode mode = ProtectedMode.IDLE; |
| private Intent selectedIntent = null; |
| |
| |
| /** |
| * Constructs a protected intent monitor. |
| * |
| * @param trafficPeriod traffic task period in ms |
| * @param services bundle of services |
| * @param msgHandler our message handler |
| */ |
| public ProtectedIntentMonitor(long trafficPeriod, ServicesBundle services, |
| TopologyViewMessageHandler msgHandler) { |
| this.trafficPeriod = trafficPeriod; |
| this.services = services; |
| this.msgHandler = msgHandler; |
| } |
| |
| // ======================================================================= |
| // === API === |
| |
| // TODO: move this out to the "h2h/multi-intent app" |
| |
| /** |
| * Monitor for protected intent data to be sent back to the web client, |
| * for the given intent. |
| * |
| * @param intent the intent to monitor |
| */ |
| public synchronized void monitor(Intent intent) { |
| log.debug("monitor intent: {}", intent.id()); |
| selectedIntent = intent; |
| mode = SELECTED_INTENT; |
| scheduleTask(); |
| sendSelectedIntents(); |
| } |
| |
| /** |
| * Stop all traffic monitoring. |
| */ |
| public synchronized void stopMonitoring() { |
| log.debug("STOP monitoring"); |
| if (mode != IDLE) { |
| sendClearAll(); |
| } |
| } |
| |
| |
| // ======================================================================= |
| // === Helper methods === |
| private void sendClearAll() { |
| clearAll(); |
| sendClearHighlights(); |
| } |
| |
| private void clearAll() { |
| this.mode = IDLE; |
| clearSelection(); |
| cancelTask(); |
| } |
| |
| private void clearSelection() { |
| selectedIntent = null; |
| } |
| |
| //TODO duplicate and can be brought in abstract upper class. |
| private synchronized void scheduleTask() { |
| if (trafficTask == null) { |
| log.debug("Starting up background protected intent task..."); |
| trafficTask = new TrafficUpdateTask(); |
| timer.schedule(trafficTask, trafficPeriod, trafficPeriod); |
| } else { |
| log.debug("(protected intent task already running)"); |
| } |
| } |
| |
| private synchronized void cancelTask() { |
| if (trafficTask != null) { |
| trafficTask.cancel(); |
| trafficTask = null; |
| } |
| } |
| |
| private void sendSelectedIntents() { |
| log.debug("sendSelectedIntents: {}", selectedIntent); |
| msgHandler.sendHighlights(protectedIntentHighlights()); |
| } |
| |
| private void sendClearHighlights() { |
| log.debug("sendClearHighlights"); |
| msgHandler.sendHighlights(new Highlights()); |
| } |
| |
| // ======================================================================= |
| // === Generate messages in JSON object node format |
| private Highlights protectedIntentHighlights() { |
| Highlights highlights = new Highlights(); |
| TrafficLinkMap linkMap = new TrafficLinkMap(); |
| IntentService intentService = services.intent(); |
| if (selectedIntent != null) { |
| List<Intent> installables = intentService.getInstallableIntents(selectedIntent.key()); |
| |
| if (installables != null) { |
| ProtectionEndpointIntent ep1 = installables.stream() |
| .filter(ProtectionEndpointIntent.class::isInstance) |
| .map(ProtectionEndpointIntent.class::cast) |
| .findFirst().orElse(null); |
| ProtectionEndpointIntent ep2 = installables.stream() |
| .filter(ii -> !ii.equals(ep1)) |
| .filter(ProtectionEndpointIntent.class::isInstance) |
| .map(ProtectionEndpointIntent.class::cast) |
| .findFirst().orElse(null); |
| if (ep1 == null || ep2 == null) { |
| log.warn("Selected Intent {} didn't have 2 protection endpoints", |
| selectedIntent.key()); |
| stopMonitoring(); |
| return highlights; |
| } |
| Set<Link> primary = new LinkedHashSet<>(); |
| Set<Link> backup = new LinkedHashSet<>(); |
| |
| Map<Boolean, List<FlowRuleIntent>> transits = installables.stream() |
| .filter(FlowRuleIntent.class::isInstance) |
| .map(FlowRuleIntent.class::cast) |
| // only consider fwd links so that ants march in one direction |
| // TODO: didn't help need further investigation. |
| //.filter(i -> !i.resources().contains(marker("rev"))) |
| .collect(Collectors.groupingBy(this::isPrimary)); |
| |
| // walk primary |
| ConnectPoint primHead = ep1.description().paths().get(0).output().connectPoint(); |
| ConnectPoint primTail = ep2.description().paths().get(0).output().connectPoint(); |
| List<FlowRuleIntent> primTransit = transits.getOrDefault(true, ImmutableList.of()); |
| populateLinks(primary, primHead, primTail, primTransit); |
| |
| // walk backup |
| ConnectPoint backHead = ep1.description().paths().get(1).output().connectPoint(); |
| ConnectPoint backTail = ep2.description().paths().get(1).output().connectPoint(); |
| List<FlowRuleIntent> backTransit = transits.getOrDefault(false, ImmutableList.of()); |
| populateLinks(backup, backHead, backTail, backTransit); |
| |
| // Add packet to optical links |
| if (!usingBackup(primary)) { |
| primary.addAll(protectedIntentMultiLayer(primHead, primTail)); |
| } |
| backup.addAll(protectedIntentMultiLayer(backHead, backTail)); |
| |
| boolean isOptical = selectedIntent instanceof OpticalConnectivityIntent; |
| //last parameter (traffic) signals if the link is highlighted with ants or solid line |
| //Flavor is swapped so green is primary path. |
| if (usingBackup(primary)) { |
| //the backup becomes in use so we have a dotted line |
| processLinks(linkMap, backup, Flavor.PRIMARY_HIGHLIGHT, |
| isOptical, true, PROTECTED_MOD_BACKUP_SET); |
| } else { |
| processLinks(linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, |
| isOptical, true, PROTECTED_MOD_PRIMARY_SET); |
| processLinks(linkMap, backup, Flavor.SECONDARY_HIGHLIGHT, |
| isOptical, false, PROTECTED_MOD_BACKUP_SET); |
| } |
| |
| updateHighlights(highlights, primary); |
| updateHighlights(highlights, backup); |
| colorLinks(highlights, linkMap); |
| highlights.subdueAllElse(Highlights.Amount.MINIMALLY); |
| } else { |
| log.debug("Selected Intent has no installable intents"); |
| } |
| } else { |
| log.debug("Selected Intent is null"); |
| } |
| return highlights; |
| } |
| |
| /** |
| * Returns the packet to optical mapping given a head and tail of a protection path. |
| * |
| * @param head head of path |
| * @param tail tail of path |
| */ |
| private Set<Link> protectedIntentMultiLayer(ConnectPoint head, ConnectPoint tail) { |
| List<Link> links = new LinkedList<>(); |
| LinkService linkService = services.link(); |
| IntentService intentService = services.intent(); |
| |
| // Ingress cross connect link |
| links.addAll( |
| linkService.getEgressLinks(head).stream() |
| .filter(l -> l.type() == Link.Type.OPTICAL) |
| .collect(Collectors.toList()) |
| ); |
| |
| // Egress cross connect link |
| links.addAll( |
| linkService.getIngressLinks(tail).stream() |
| .filter(l -> l.type() == Link.Type.OPTICAL) |
| .collect(Collectors.toList()) |
| ); |
| |
| // The protected intent does not rely on a multi-layer mapping |
| if (links.size() != 2) { |
| return Collections.emptySet(); |
| } |
| |
| // Expected head and tail of optical circuit (not connectivity!) intent |
| ConnectPoint ocHead = links.get(0).dst(); |
| ConnectPoint ocTail = links.get(1).src(); |
| |
| // Optical connectivity |
| // FIXME: assumes that transponder (OTN device) is a one-to-one mapping |
| // We need to track the multi-layer aspects better |
| intentService.getIntents().forEach(intent -> { |
| if (intent instanceof OpticalConnectivityIntent) { |
| OpticalConnectivityIntent ocIntent = (OpticalConnectivityIntent) intent; |
| if (ocHead.deviceId().equals(ocIntent.getSrc().deviceId()) && |
| ocTail.deviceId().equals(ocIntent.getDst().deviceId())) { |
| intentService.getInstallableIntents(ocIntent.key()).forEach(i -> { |
| if (i instanceof FlowRuleIntent) { |
| FlowRuleIntent fr = (FlowRuleIntent) i; |
| links.addAll(linkResources(fr)); |
| } |
| }); |
| } |
| } |
| }); |
| |
| return new LinkedHashSet<>(links); |
| } |
| |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| |
| /** |
| * Populate Links along the primary/backup path. |
| * |
| * @param links link collection to populate [output] |
| * @param head head-end of primary/backup path |
| * @param tail tail-end of primary/backup path |
| * @param transit Intents if any |
| */ |
| private void populateLinks(Set<Link> links, |
| ConnectPoint head, |
| ConnectPoint tail, |
| List<FlowRuleIntent> transit) { |
| // find first hop link |
| Link first = transit.stream() |
| // search for Link head -> transit Intent head |
| // as first candidate of 1st hop Link |
| .flatMap(fri -> fri.flowRules().stream()) |
| .map(fr -> |
| // find first input port from FlowRule |
| Optional.ofNullable(fr.selector().getCriterion(Criterion.Type.IN_PORT)) |
| .filter(PortCriterion.class::isInstance) |
| .map(PortCriterion.class::cast) |
| .map(PortCriterion::port) |
| .map(pn -> new ConnectPoint(fr.deviceId(), pn)) |
| .orElse(null) |
| ).filter(Objects::nonNull) |
| .map(dst -> services.link().getLink(head, dst)) |
| .filter(Objects::nonNull) |
| .findFirst() |
| // if there isn't one probably 1 hop to the tail |
| .orElse(services.link().getLink(head, tail)); |
| |
| // add first link |
| if (first != null) { |
| links.add(first); |
| } |
| |
| // add links in the middle if any |
| transit.forEach(fri -> links.addAll(linkResources(fri))); |
| |
| // add last hop if any |
| Lists.reverse(transit).stream() |
| // search for Link transit Intent tail -> tail |
| // as candidate of last hop Link |
| .flatMap(fri -> ImmutableList.copyOf(fri.flowRules()).reverse().stream()) |
| .map(fr -> |
| // find first output port from FlowRule |
| fr.treatment().allInstructions().stream() |
| .filter(OutputInstruction.class::isInstance).findFirst() |
| .map(OutputInstruction.class::cast) |
| .map(OutputInstruction::port) |
| .map(pn -> new ConnectPoint(fr.deviceId(), pn)) |
| .orElse(null) |
| ).filter(Objects::nonNull) |
| .map(src -> services.link().getLink(src, tail)) |
| .filter(Objects::nonNull) |
| .findFirst() |
| .ifPresent(links::add); |
| } |
| |
| /** |
| * Returns true if specified intent is marked with primary marker resource. |
| * |
| * @param intent to test |
| * @return true if it is an Intent taking part of primary transit path |
| */ |
| private boolean isPrimary(Intent intent) { |
| return intent.resources() |
| .contains(marker(PRIMARY_PATH_TAG)); |
| } |
| |
| // returns true if the backup path is the one where the traffic is currently flowing |
| private boolean usingBackup(Set<Link> primary) { |
| Set<Link> activeLinks = Sets.newHashSet(services.link().getActiveLinks()); |
| return primary.isEmpty() || !activeLinks.containsAll(primary); |
| } |
| |
| private void updateHighlights(Highlights highlights, Iterable<Link> links) { |
| for (Link link : links) { |
| ensureNodePresent(highlights, link.src().elementId()); |
| ensureNodePresent(highlights, link.dst().elementId()); |
| } |
| } |
| |
| //TODO duplicate and can be brought in abstract upper class. |
| private void ensureNodePresent(Highlights highlights, ElementId eid) { |
| String id = eid.toString(); |
| NodeHighlight nh = highlights.getNode(id); |
| if (nh == null) { |
| if (eid instanceof DeviceId) { |
| nh = new DeviceHighlight(id); |
| highlights.add((DeviceHighlight) nh); |
| } else if (eid instanceof HostId) { |
| nh = new HostHighlight(id); |
| highlights.add((HostHighlight) nh); |
| } |
| } |
| } |
| |
| private void processLinks(TrafficLinkMap linkMap, Iterable<Link> links, |
| Flavor flavor, boolean isOptical, |
| boolean showTraffic, Set<Mod> mods) { |
| if (links != null) { |
| for (Link link : links) { |
| TrafficLink tlink = linkMap.add(link); |
| tlink.tagFlavor(flavor); |
| tlink.optical(isOptical); |
| if (showTraffic) { |
| tlink.antMarch(true); |
| } |
| tlink.tagMods(mods); |
| } |
| } |
| } |
| |
| //TODO duplicate and can be brought in abstract upper class. |
| private void colorLinks(Highlights highlights, TrafficLinkMap linkMap) { |
| for (TrafficLink tlink : linkMap.biLinks()) { |
| highlights.add(tlink.highlight(StatsType.TAGGED)); |
| } |
| } |
| |
| //TODO duplicate and can be brought in abstract upper class. |
| // Extracts links from the specified flow rule intent resources |
| private Collection<Link> linkResources(Intent installable) { |
| ImmutableList.Builder<Link> builder = ImmutableList.builder(); |
| installable.resources().stream().filter(r -> r instanceof Link) |
| .forEach(r -> builder.add((Link) r)); |
| return builder.build(); |
| } |
| |
| // ======================================================================= |
| // === Background Task |
| |
| // Provides periodic update of traffic information to the client |
| private class TrafficUpdateTask extends TimerTask { |
| @Override |
| public void run() { |
| try { |
| switch (mode) { |
| case SELECTED_INTENT: |
| sendSelectedIntents(); |
| break; |
| |
| default: |
| // RELATED_INTENTS and IDLE modes should never invoke |
| // the background task, but if they do, they have |
| // nothing to do |
| break; |
| } |
| |
| } catch (Exception e) { |
| log.warn("Unable to process protected intent task due to {}", e.getMessage()); |
| log.warn("Boom!", e); |
| } |
| } |
| } |
| } |