blob: be99764857d41a16daf810dd4a2f904ed82cf0c9 [file] [log] [blame]
Saurav Dase6c448a2018-01-18 12:07:33 -08001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.segmentrouting;
18
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.Set;
22import java.util.concurrent.ConcurrentHashMap;
Saurav Das6430f412018-01-25 09:49:01 -080023import java.util.stream.Collectors;
24
Saurav Dase6c448a2018-01-18 12:07:33 -080025import org.onosproject.net.Device;
26import org.onosproject.net.DeviceId;
27import org.onosproject.net.HostLocation;
28import org.onosproject.net.Link;
29import org.onosproject.net.PortNumber;
30import org.onosproject.net.link.LinkService;
31import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
32import org.onosproject.segmentrouting.config.DeviceConfiguration;
33import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
34import org.onosproject.store.service.EventuallyConsistentMap;
35import org.onosproject.store.service.EventuallyConsistentMapBuilder;
36import org.onosproject.store.service.WallClockTimestamp;
37import org.slf4j.Logger;
38import org.slf4j.LoggerFactory;
39
Saurav Das6430f412018-01-25 09:49:01 -080040import com.google.common.collect.ImmutableMap;
Saurav Dase6c448a2018-01-18 12:07:33 -080041import com.google.common.collect.Sets;
42
43public class LinkHandler {
44 private static final Logger log = LoggerFactory.getLogger(LinkHandler.class);
45 protected final SegmentRoutingManager srManager;
46 protected LinkService linkService;
47
48 // Local store for all links seen and their present status, used for
Saurav Das6430f412018-01-25 09:49:01 -080049 // optimized routing. The existence of the link in the keys is enough to know
Saurav Dase6c448a2018-01-18 12:07:33 -080050 // if the link has been "seen-before" by this instance of the controller.
51 // The boolean value indicates if the link is currently up or not.
Saurav Das6430f412018-01-25 09:49:01 -080052 // Currently the optimized routing logic depends on "forgetting" a link
53 // when a switch goes down, but "remembering" it when only the link goes down.
Saurav Dase6c448a2018-01-18 12:07:33 -080054 private Map<Link, Boolean> seenLinks = new ConcurrentHashMap<>();
55
56 private EventuallyConsistentMap<DeviceId, Set<PortNumber>> downedPortStore = null;
57
58 /**
59 * Constructs the LinkHandler.
60 *
61 * @param srManager Segment Routing manager
62 */
63 LinkHandler(SegmentRoutingManager srManager) {
64 this.srManager = srManager;
65 linkService = srManager.linkService;
66 log.debug("Creating EC map downedportstore");
67 EventuallyConsistentMapBuilder<DeviceId, Set<PortNumber>> downedPortsMapBuilder
68 = srManager.storageService.eventuallyConsistentMapBuilder();
69 downedPortStore = downedPortsMapBuilder.withName("downedportstore")
70 .withSerializer(srManager.createSerializer())
71 .withTimestampProvider((k, v) -> new WallClockTimestamp())
72 .build();
73 log.trace("Current size {}", downedPortStore.size());
Saurav Dase321cff2018-02-09 17:26:45 -080074 init();
Saurav Dase6c448a2018-01-18 12:07:33 -080075 }
76
77 /**
78 * Constructs the LinkHandler for unit-testing.
79 *
80 * @param srManager SegmentRoutingManager
81 * @param linkService LinkService
82 */
83 LinkHandler(SegmentRoutingManager srManager, LinkService linkService) {
84 this.srManager = srManager;
85 this.linkService = linkService;
86 }
87
88 /**
Saurav Dase321cff2018-02-09 17:26:45 -080089 * Initialize LinkHandler.
90 */
91 private void init() {
92 log.info("Loading stored links");
93 srManager.linkService.getActiveLinks()
94 .forEach(link -> processLinkAdded(link));
95 }
96
97 /**
Saurav Dase6c448a2018-01-18 12:07:33 -080098 * Preprocessing of added link before being sent for route-path handling.
99 * Also performs post processing of link.
100 *
101 * @param link the link to be processed
102 */
103 void processLinkAdded(Link link) {
104 log.info("** LINK ADDED {}", link.toString());
105 if (!isLinkValid(link)) {
106 return;
107 }
Saurav Dase321cff2018-02-09 17:26:45 -0800108 if (srManager.deviceConfiguration == null ||
109 !srManager.deviceConfiguration.isConfigured(link.src().deviceId())) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800110 updateSeenLink(link, true);
111 // XXX revisit - what about devicePortMap
112 log.warn("Source device of this link is not configured.. "
113 + "not processing further");
114 return;
115 }
116
117 // Irrespective of whether the local is a MASTER or not for this device,
Saurav Das6430f412018-01-25 09:49:01 -0800118 // create group handler instance and push default TTP flow rules if needed,
Saurav Dase6c448a2018-01-18 12:07:33 -0800119 // as in a multi-instance setup, instances can initiate groups for any
120 // device.
121 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
122 .get(link.src().deviceId());
123 if (groupHandler != null) {
124 groupHandler.portUpForLink(link);
125 } else {
126 // XXX revisit/cleanup
127 Device device = srManager.deviceService.getDevice(link.src().deviceId());
128 if (device != null) {
129 log.warn("processLinkAdded: Link Added "
130 + "Notification without Device Added "
131 + "event, still handling it");
132 srManager.processDeviceAdded(device);
133 groupHandler = srManager.groupHandlerMap.get(link.src().deviceId());
134 groupHandler.portUpForLink(link);
135 }
136 }
137
138 /*
139 // process link only if it is bidirectional
140 if (!isBidirectional(link)) {
141 log.debug("Link not bidirectional.. waiting for other direction " +
142 "src {} --> dst {} ", link.dst(), link.src());
143 // note that if we are not processing for routing, it should at least
144 // be considered a seen-link
145 updateSeenLink(link, true); return;
146 }
147 //TODO ensure that rehash is still done correctly even if link is not processed for
148 //rerouting - perhaps rehash in both directions when it ultimately becomes bidi?
149 */
150
151 log.debug("Starting optimized route-path processing for added link "
152 + "{} --> {}", link.src(), link.dst());
153 boolean seenBefore = isSeenLink(link);
154 // seenLink updates will be done after route-path changes
155 srManager.defaultRoutingHandler
156 .populateRoutingRulesForLinkStatusChange(null, link, null);
157
158 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
159 // handle edge-ports for dual-homed hosts
160 updateDualHomedHostPorts(link, true);
161
Saurav Das6430f412018-01-25 09:49:01 -0800162 // It's possible that linkUp causes no route-path change as ECMP graph does
Saurav Dase6c448a2018-01-18 12:07:33 -0800163 // not change if the link is a parallel link (same src-dst as
Saurav Das6430f412018-01-25 09:49:01 -0800164 // another link). However we still need to update ECMP hash groups to include new buckets
Saurav Dase6c448a2018-01-18 12:07:33 -0800165 // for the link that has come up.
Ray Milkey43969b92018-01-24 10:41:14 -0800166 if (groupHandler != null) {
167 if (!seenBefore && isParallelLink(link)) {
168 // if link seen first time, we need to ensure hash-groups have
169 // all ports
170 log.debug("Attempting retryHash for paralled first-time link {}",
171 link);
172 groupHandler.retryHash(link, false, true);
173 } else {
174 // seen before-link
175 if (isParallelLink(link)) {
176 log.debug("Attempting retryHash for paralled seen-before "
177 + "link {}", link);
178 groupHandler.retryHash(link, false, false);
179 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800180 }
181 }
182 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800183 }
184
185 /**
186 * Preprocessing of removed link before being sent for route-path handling.
187 * Also performs post processing of link.
188 *
189 * @param link the link to be processed
190 */
191 void processLinkRemoved(Link link) {
192 log.info("** LINK REMOVED {}", link.toString());
193 if (!isLinkValid(link)) {
194 return;
195 }
196 // when removing links, update seen links first, before doing route-path
197 // changes
198 updateSeenLink(link, false);
Saurav Das6430f412018-01-25 09:49:01 -0800199 // handle edge-ports for dual-homed hosts
200 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
201 updateDualHomedHostPorts(link, false);
202 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800203
204 // device availability check helps to ensure that multiple link-removed
205 // events are actually treated as a single switch removed event.
206 // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
207 // when switch comes back.
208 if (link.src().elementId() instanceof DeviceId
209 && !srManager.deviceService.isAvailable(link.src().deviceId())) {
210 purgeSeenLink(link);
211 return;
212 }
213 if (link.dst().elementId() instanceof DeviceId
214 && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
215 purgeSeenLink(link);
216 return;
217 }
218
Saurav Dase6c448a2018-01-18 12:07:33 -0800219 log.debug("Starting optimized route-path processing for removed link "
220 + "{} --> {}", link.src(), link.dst());
221 srManager.defaultRoutingHandler
222 .populateRoutingRulesForLinkStatusChange(link, null, null);
223
224 // update local groupHandler stores
225 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
226 .get(link.src().deviceId());
227 if (groupHandler != null) {
228 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
229 && isParallelLink(link)) {
230 log.debug("* retrying hash for parallel link removed:{}", link);
231 groupHandler.retryHash(link, true, false);
232 } else {
233 log.debug("Not attempting retry-hash for link removed: {} .. {}",
234 link,
235 (srManager.mastershipService.isLocalMaster(link.src()
236 .deviceId())) ? "not parallel"
237 : "not master");
238 }
239 // ensure local stores are updated
240 groupHandler.portDown(link.src().port());
241 } else {
242 log.warn("group handler not found for dev:{} when removing link: {}",
243 link.src().deviceId(), link);
244 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800245 }
246
247 /**
248 * Checks validity of link. Examples of invalid links include
249 * indirect-links, links between ports on the same switch, and more.
250 *
251 * @param link the link to be processed
252 * @return true if valid link
253 */
Pier Luigid8a15162018-02-15 16:33:08 +0100254 boolean isLinkValid(Link link) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800255 if (link.type() != Link.Type.DIRECT) {
256 // NOTE: A DIRECT link might be transiently marked as INDIRECT
257 // if BDDP is received before LLDP. We can safely ignore that
258 // until the LLDP is received and the link is marked as DIRECT.
259 log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
260 link.src(), link.dst(), link.type());
261 return false;
262 }
263 DeviceId srcId = link.src().deviceId();
264 DeviceId dstId = link.dst().deviceId();
265 if (srcId.equals(dstId)) {
266 log.warn("Links between ports on the same switch are not "
267 + "allowed .. ignoring link {}", link);
268 return false;
269 }
270 DeviceConfiguration devConfig = srManager.deviceConfiguration;
Saurav Dase321cff2018-02-09 17:26:45 -0800271 if (devConfig == null) {
272 log.warn("Cannot check validity of link without device config");
273 return true;
274 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800275 try {
276 if (!devConfig.isEdgeDevice(srcId)
277 && !devConfig.isEdgeDevice(dstId)) {
278 // ignore links between spines
279 // XXX revisit when handling multi-stage fabrics
280 log.warn("Links between spines not allowed...ignoring "
281 + "link {}", link);
282 return false;
283 }
284 if (devConfig.isEdgeDevice(srcId)
285 && devConfig.isEdgeDevice(dstId)) {
286 // ignore links between leaves if they are not pair-links
287 // XXX revisit if removing pair-link config or allowing more than
288 // one pair-link
289 if (devConfig.getPairDeviceId(srcId).equals(dstId)
290 && devConfig.getPairLocalPort(srcId)
291 .equals(link.src().port())
292 && devConfig.getPairLocalPort(dstId)
293 .equals(link.dst().port())) {
294 // found pair link - allow it
295 return true;
296 } else {
297 log.warn("Links between leaves other than pair-links are "
298 + "not allowed...ignoring link {}", link);
299 return false;
300 }
301 }
302 } catch (DeviceConfigNotFoundException e) {
303 // We still want to count the links in seenLinks even though there
304 // is no config. So we let it return true
305 log.warn("Could not check validity of link {} as subtending devices "
306 + "are not yet configured", link);
307 }
308 return true;
309 }
310
311 /**
312 * Administratively enables or disables edge ports if the link that was
313 * added or removed was the only uplink port from an edge device. Only edge
314 * ports that belong to dual-homed hosts are considered.
315 *
316 * @param link the link to be processed
317 * @param added true if link was added, false if link was removed
318 */
319 private void updateDualHomedHostPorts(Link link, boolean added) {
Saurav Das6430f412018-01-25 09:49:01 -0800320 if (!onlyUplink(link)) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800321 return;
322 }
323 if (added) {
324 // re-enable previously disabled ports on this dev
325 Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
326 if (p != null) {
327 log.warn("Link src {} -->dst {} added is the first uplink, "
328 + "enabling dual homed ports: {}", link.src().deviceId(),
329 link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
330 p.forEach(pnum -> srManager.deviceAdminService
331 .changePortState(link.src().deviceId(), pnum, true));
332 }
333 } else {
334 // find dual homed hosts on this dev to disable
335 Set<PortNumber> dhp = srManager.hostHandler
336 .getDualHomedHostPorts(link.src().deviceId());
337 log.warn("Link src {} -->dst {} removed was the last uplink, "
338 + "disabling dual homed ports: {}", link.src().deviceId(),
339 link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
340 dhp.forEach(pnum -> srManager.deviceAdminService
341 .changePortState(link.src().deviceId(), pnum, false));
342 if (!dhp.isEmpty()) {
343 // update global store
344 Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
345 if (p == null) {
346 p = dhp;
347 } else {
348 p.addAll(dhp);
349 }
350 downedPortStore.put(link.src().deviceId(), p);
351 }
352 }
353 }
354
355 /**
Saurav Das6430f412018-01-25 09:49:01 -0800356 * Returns true if given link is the only active uplink from src-device of
Saurav Dase6c448a2018-01-18 12:07:33 -0800357 * link. An uplink is defined as a unidirectional link with src as
358 * edgeRouter and dst as non-edgeRouter.
359 *
360 * @param link
361 * @return true if given link is-the-first/was-the-last uplink from the src
362 * device
363 */
Saurav Das6430f412018-01-25 09:49:01 -0800364 private boolean onlyUplink(Link link) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800365 DeviceConfiguration devConfig = srManager.deviceConfiguration;
366 try {
Saurav Das6430f412018-01-25 09:49:01 -0800367 if (!devConfig.isEdgeDevice(link.src().deviceId())
368 || devConfig.isEdgeDevice(link.dst().deviceId())) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800369 return false;
370 }
Saurav Das6430f412018-01-25 09:49:01 -0800371 // note that using linkservice here would cause race conditions as
372 // more links can show up while the app is still processing the first one
373 Set<Link> devLinks = seenLinks.entrySet().stream()
374 .filter(entry -> entry.getKey().src().deviceId()
375 .equals(link.src().deviceId()))
376 .filter(entry -> entry.getValue())
377 .filter(entry -> !entry.getKey().equals(link))
378 .map(entry -> entry.getKey())
379 .collect(Collectors.toSet());
380
Saurav Dase6c448a2018-01-18 12:07:33 -0800381 for (Link l : devLinks) {
Saurav Das6430f412018-01-25 09:49:01 -0800382 if (devConfig.isEdgeDevice(l.dst().deviceId())) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800383 continue;
384 }
Saurav Das6430f412018-01-25 09:49:01 -0800385 log.debug("Link {} is not the only active uplink. Found another"
386 + "link {}", link, l);
387 return false;
Saurav Dase6c448a2018-01-18 12:07:33 -0800388 }
Saurav Das6430f412018-01-25 09:49:01 -0800389 log.debug("Link {} is the only uplink", link);
390 return true;
Saurav Dase6c448a2018-01-18 12:07:33 -0800391 } catch (DeviceConfigNotFoundException e) {
Saurav Das6430f412018-01-25 09:49:01 -0800392 log.warn("Unable to determine if link is only uplink"
Saurav Dase6c448a2018-01-18 12:07:33 -0800393 + e.getMessage());
394 }
395 return false;
396 }
397
398 /**
399 * Returns true if this controller instance has seen this link before. The
400 * link may not be currently up, but as long as the link had been seen
401 * before this method will return true. The one exception is when the link
402 * was indeed seen before, but this controller instance was forced to forget
403 * it by a call to purgeSeenLink method.
404 *
405 * @param link the infrastructure link being queried
406 * @return true if this controller instance has seen this link before
407 */
408 boolean isSeenLink(Link link) {
409 return seenLinks.containsKey(link);
410 }
411
412 /**
413 * Updates the seen link store. Updates can be for links that are currently
414 * available or not.
415 *
416 * @param link the link to update in the seen-link local store
417 * @param up the status of the link, true if up, false if down
418 */
419 void updateSeenLink(Link link, boolean up) {
420 seenLinks.put(link, up);
421 }
422
423 /**
424 * Returns the status of a seen-link (up or down). If the link has not been
425 * seen-before, a null object is returned.
426 *
427 * @param link the infrastructure link being queried
428 * @return null if the link was not seen-before; true if the seen-link is
429 * up; false if the seen-link is down
430 */
431 private Boolean isSeenLinkUp(Link link) {
432 return seenLinks.get(link);
433 }
434
435 /**
436 * Makes this controller instance forget a previously seen before link.
437 *
438 * @param link the infrastructure link to purge
439 */
440 private void purgeSeenLink(Link link) {
441 seenLinks.remove(link);
442 }
443
444 /**
445 * Returns the status of a link as parallel link. A parallel link is defined
446 * as a link which has common src and dst switches as another seen-link that
447 * is currently enabled. It is not necessary for the link being queried to
448 * be a seen-link.
449 *
450 * @param link the infrastructure link being queried
451 * @return true if a seen-link exists that is up, and shares the same src
452 * and dst switches as the link being queried
453 */
454 private boolean isParallelLink(Link link) {
455 for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
456 Link seenLink = seen.getKey();
457 if (seenLink.equals(link)) {
458 continue;
459 }
460 if (seenLink.src().deviceId().equals(link.src().deviceId())
461 && seenLink.dst().deviceId().equals(link.dst().deviceId())
462 && seen.getValue()) {
463 return true;
464 }
465 }
466 return false;
467 }
468
469 /**
470 * Returns true if the link being queried is a bidirectional link. A bidi
471 * link is defined as a link, whose reverse link - ie. the link in the
472 * reverse direction - has been seen-before and is up. It is not necessary
473 * for the link being queried to be a seen-link.
474 *
475 * @param link the infrastructure link being queried
476 * @return true if another unidirectional link exists in the reverse
477 * direction, has been seen-before and is up
478 */
479 boolean isBidirectional(Link link) {
480 Link reverseLink = linkService.getLink(link.dst(), link.src());
481 if (reverseLink == null) {
482 return false;
483 }
484 Boolean result = isSeenLinkUp(reverseLink);
485 if (result == null) {
486 return false;
487 }
488 return result.booleanValue();
489 }
490
491 /**
492 * Determines if the given link should be avoided in routing calculations by
493 * policy or design.
494 *
495 * @param link the infrastructure link being queried
496 * @return true if link should be avoided
497 */
498 boolean avoidLink(Link link) {
499 // XXX currently only avoids all pair-links. In the future can be
500 // extended to avoid any generic link
501 DeviceId src = link.src().deviceId();
502 PortNumber srcPort = link.src().port();
503 DeviceConfiguration devConfig = srManager.deviceConfiguration;
504 if (devConfig == null || !devConfig.isConfigured(src)) {
505 log.warn("Device {} not configured..cannot avoid link {}", src,
506 link);
507 return false;
508 }
509 DeviceId pairDev;
510 PortNumber pairLocalPort, pairRemotePort = null;
511 try {
512 pairDev = devConfig.getPairDeviceId(src);
513 pairLocalPort = devConfig.getPairLocalPort(src);
514 if (pairDev != null) {
515 pairRemotePort = devConfig
516 .getPairLocalPort(pairDev);
517 }
518 } catch (DeviceConfigNotFoundException e) {
519 log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
520 src, link);
521 return false;
522 }
523
524 return srcPort.equals(pairLocalPort)
525 && link.dst().deviceId().equals(pairDev)
526 && link.dst().port().equals(pairRemotePort);
527 }
528
529 /**
530 * Cleans up internal LinkHandler stores.
531 *
532 * @param device the device that has been removed
533 */
534 void processDeviceRemoved(Device device) {
535 seenLinks.keySet()
536 .removeIf(key -> key.src().deviceId().equals(device.id())
537 || key.dst().deviceId().equals(device.id()));
538 }
539
540 /**
541 * Administratively disables the host location switchport if the edge device
542 * has no viable uplinks.
543 *
544 * @param loc one of the locations of the dual-homed host
545 */
546 void checkUplinksForDualHomedHosts(HostLocation loc) {
547 try {
548 for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
549 if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
550 || l.state() == Link.State.INACTIVE) {
551 continue;
552 }
553 // found valid uplink - so, nothing to do
554 return;
555 }
556 } catch (DeviceConfigNotFoundException e) {
557 log.warn("Could not check for valid uplinks due to missing device"
558 + "config " + e.getMessage());
559 return;
560 }
561 log.warn("Dual homed host location {} has no valid uplinks; "
562 + "disabling dual homed port", loc);
563 srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
564 false);
565 Set<PortNumber> p = downedPortStore.get(loc.deviceId());
566 if (p == null) {
567 p = Sets.newHashSet(loc.port());
568 } else {
569 p.add(loc.port());
570 }
571 downedPortStore.put(loc.deviceId(), p);
572 }
573
Saurav Das6430f412018-01-25 09:49:01 -0800574 ImmutableMap<Link, Boolean> getSeenLinks() {
575 return ImmutableMap.copyOf(seenLinks);
576 }
577
578 ImmutableMap<DeviceId, Set<PortNumber>> getDownedPorts() {
579 return ImmutableMap.copyOf(downedPortStore.entrySet());
580 }
581
Saurav Dase6c448a2018-01-18 12:07:33 -0800582}