blob: c6439910a5f362bee76004282228eb084d3c69e0 [file] [log] [blame]
Saurav Dase9c8971e2018-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 Das95047002018-01-25 09:49:01 -080023import java.util.stream.Collectors;
24
Saurav Dase9c8971e2018-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 Das95047002018-01-25 09:49:01 -080040import com.google.common.collect.ImmutableMap;
Saurav Dase9c8971e2018-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 Das95047002018-01-25 09:49:01 -080049 // optimized routing. The existence of the link in the keys is enough to know
Saurav Dase9c8971e2018-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 Das95047002018-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 Dase9c8971e2018-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 Das07a34aa2018-02-09 17:26:45 -080074 init();
Saurav Dase9c8971e2018-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 Das07a34aa2018-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 Dase9c8971e2018-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 Das07a34aa2018-02-09 17:26:45 -0800108 if (srManager.deviceConfiguration == null ||
109 !srManager.deviceConfiguration.isConfigured(link.src().deviceId())) {
Saurav Dase9c8971e2018-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 Das95047002018-01-25 09:49:01 -0800118 // create group handler instance and push default TTP flow rules if needed,
Saurav Dase9c8971e2018-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 Das95047002018-01-25 09:49:01 -0800162 // It's possible that linkUp causes no route-path change as ECMP graph does
Saurav Dase9c8971e2018-01-18 12:07:33 -0800163 // not change if the link is a parallel link (same src-dst as
Saurav Das95047002018-01-25 09:49:01 -0800164 // another link). However we still need to update ECMP hash groups to include new buckets
Saurav Dase9c8971e2018-01-18 12:07:33 -0800165 // for the link that has come up.
Ray Milkeyd8615942018-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 Dase9c8971e2018-01-18 12:07:33 -0800180 }
181 }
182 }
183
184 srManager.mcastHandler.init();
185 }
186
187 /**
188 * Preprocessing of removed link before being sent for route-path handling.
189 * Also performs post processing of link.
190 *
191 * @param link the link to be processed
192 */
193 void processLinkRemoved(Link link) {
194 log.info("** LINK REMOVED {}", link.toString());
195 if (!isLinkValid(link)) {
196 return;
197 }
198 // when removing links, update seen links first, before doing route-path
199 // changes
200 updateSeenLink(link, false);
Saurav Das95047002018-01-25 09:49:01 -0800201 // handle edge-ports for dual-homed hosts
202 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
203 updateDualHomedHostPorts(link, false);
204 }
Saurav Dase9c8971e2018-01-18 12:07:33 -0800205
206 // device availability check helps to ensure that multiple link-removed
207 // events are actually treated as a single switch removed event.
208 // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
209 // when switch comes back.
210 if (link.src().elementId() instanceof DeviceId
211 && !srManager.deviceService.isAvailable(link.src().deviceId())) {
212 purgeSeenLink(link);
213 return;
214 }
215 if (link.dst().elementId() instanceof DeviceId
216 && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
217 purgeSeenLink(link);
218 return;
219 }
220
Saurav Dase9c8971e2018-01-18 12:07:33 -0800221 log.debug("Starting optimized route-path processing for removed link "
222 + "{} --> {}", link.src(), link.dst());
223 srManager.defaultRoutingHandler
224 .populateRoutingRulesForLinkStatusChange(link, null, null);
225
226 // update local groupHandler stores
227 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
228 .get(link.src().deviceId());
229 if (groupHandler != null) {
230 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
231 && isParallelLink(link)) {
232 log.debug("* retrying hash for parallel link removed:{}", link);
233 groupHandler.retryHash(link, true, false);
234 } else {
235 log.debug("Not attempting retry-hash for link removed: {} .. {}",
236 link,
237 (srManager.mastershipService.isLocalMaster(link.src()
238 .deviceId())) ? "not parallel"
239 : "not master");
240 }
241 // ensure local stores are updated
242 groupHandler.portDown(link.src().port());
243 } else {
244 log.warn("group handler not found for dev:{} when removing link: {}",
245 link.src().deviceId(), link);
246 }
247
248 srManager.mcastHandler.processLinkDown(link);
249 }
250
251 /**
252 * Checks validity of link. Examples of invalid links include
253 * indirect-links, links between ports on the same switch, and more.
254 *
255 * @param link the link to be processed
256 * @return true if valid link
257 */
258 private boolean isLinkValid(Link link) {
259 if (link.type() != Link.Type.DIRECT) {
260 // NOTE: A DIRECT link might be transiently marked as INDIRECT
261 // if BDDP is received before LLDP. We can safely ignore that
262 // until the LLDP is received and the link is marked as DIRECT.
263 log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
264 link.src(), link.dst(), link.type());
265 return false;
266 }
267 DeviceId srcId = link.src().deviceId();
268 DeviceId dstId = link.dst().deviceId();
269 if (srcId.equals(dstId)) {
270 log.warn("Links between ports on the same switch are not "
271 + "allowed .. ignoring link {}", link);
272 return false;
273 }
274 DeviceConfiguration devConfig = srManager.deviceConfiguration;
Saurav Das07a34aa2018-02-09 17:26:45 -0800275 if (devConfig == null) {
276 log.warn("Cannot check validity of link without device config");
277 return true;
278 }
Saurav Dase9c8971e2018-01-18 12:07:33 -0800279 try {
280 if (!devConfig.isEdgeDevice(srcId)
281 && !devConfig.isEdgeDevice(dstId)) {
282 // ignore links between spines
283 // XXX revisit when handling multi-stage fabrics
284 log.warn("Links between spines not allowed...ignoring "
285 + "link {}", link);
286 return false;
287 }
288 if (devConfig.isEdgeDevice(srcId)
289 && devConfig.isEdgeDevice(dstId)) {
290 // ignore links between leaves if they are not pair-links
291 // XXX revisit if removing pair-link config or allowing more than
292 // one pair-link
293 if (devConfig.getPairDeviceId(srcId).equals(dstId)
294 && devConfig.getPairLocalPort(srcId)
295 .equals(link.src().port())
296 && devConfig.getPairLocalPort(dstId)
297 .equals(link.dst().port())) {
298 // found pair link - allow it
299 return true;
300 } else {
301 log.warn("Links between leaves other than pair-links are "
302 + "not allowed...ignoring link {}", link);
303 return false;
304 }
305 }
306 } catch (DeviceConfigNotFoundException e) {
307 // We still want to count the links in seenLinks even though there
308 // is no config. So we let it return true
309 log.warn("Could not check validity of link {} as subtending devices "
310 + "are not yet configured", link);
311 }
312 return true;
313 }
314
315 /**
316 * Administratively enables or disables edge ports if the link that was
317 * added or removed was the only uplink port from an edge device. Only edge
318 * ports that belong to dual-homed hosts are considered.
319 *
320 * @param link the link to be processed
321 * @param added true if link was added, false if link was removed
322 */
323 private void updateDualHomedHostPorts(Link link, boolean added) {
Saurav Das95047002018-01-25 09:49:01 -0800324 if (!onlyUplink(link)) {
Saurav Dase9c8971e2018-01-18 12:07:33 -0800325 return;
326 }
327 if (added) {
328 // re-enable previously disabled ports on this dev
329 Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
330 if (p != null) {
331 log.warn("Link src {} -->dst {} added is the first uplink, "
332 + "enabling dual homed ports: {}", link.src().deviceId(),
333 link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
334 p.forEach(pnum -> srManager.deviceAdminService
335 .changePortState(link.src().deviceId(), pnum, true));
336 }
337 } else {
338 // find dual homed hosts on this dev to disable
339 Set<PortNumber> dhp = srManager.hostHandler
340 .getDualHomedHostPorts(link.src().deviceId());
341 log.warn("Link src {} -->dst {} removed was the last uplink, "
342 + "disabling dual homed ports: {}", link.src().deviceId(),
343 link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
344 dhp.forEach(pnum -> srManager.deviceAdminService
345 .changePortState(link.src().deviceId(), pnum, false));
346 if (!dhp.isEmpty()) {
347 // update global store
348 Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
349 if (p == null) {
350 p = dhp;
351 } else {
352 p.addAll(dhp);
353 }
354 downedPortStore.put(link.src().deviceId(), p);
355 }
356 }
357 }
358
359 /**
Saurav Das95047002018-01-25 09:49:01 -0800360 * Returns true if given link is the only active uplink from src-device of
Saurav Dase9c8971e2018-01-18 12:07:33 -0800361 * link. An uplink is defined as a unidirectional link with src as
362 * edgeRouter and dst as non-edgeRouter.
363 *
364 * @param link
365 * @return true if given link is-the-first/was-the-last uplink from the src
366 * device
367 */
Saurav Das95047002018-01-25 09:49:01 -0800368 private boolean onlyUplink(Link link) {
Saurav Dase9c8971e2018-01-18 12:07:33 -0800369 DeviceConfiguration devConfig = srManager.deviceConfiguration;
370 try {
Saurav Das95047002018-01-25 09:49:01 -0800371 if (!devConfig.isEdgeDevice(link.src().deviceId())
372 || devConfig.isEdgeDevice(link.dst().deviceId())) {
Saurav Dase9c8971e2018-01-18 12:07:33 -0800373 return false;
374 }
Saurav Das95047002018-01-25 09:49:01 -0800375 // note that using linkservice here would cause race conditions as
376 // more links can show up while the app is still processing the first one
377 Set<Link> devLinks = seenLinks.entrySet().stream()
378 .filter(entry -> entry.getKey().src().deviceId()
379 .equals(link.src().deviceId()))
380 .filter(entry -> entry.getValue())
381 .filter(entry -> !entry.getKey().equals(link))
382 .map(entry -> entry.getKey())
383 .collect(Collectors.toSet());
384
Saurav Dase9c8971e2018-01-18 12:07:33 -0800385 for (Link l : devLinks) {
Saurav Das95047002018-01-25 09:49:01 -0800386 if (devConfig.isEdgeDevice(l.dst().deviceId())) {
Saurav Dase9c8971e2018-01-18 12:07:33 -0800387 continue;
388 }
Saurav Das95047002018-01-25 09:49:01 -0800389 log.debug("Link {} is not the only active uplink. Found another"
390 + "link {}", link, l);
391 return false;
Saurav Dase9c8971e2018-01-18 12:07:33 -0800392 }
Saurav Das95047002018-01-25 09:49:01 -0800393 log.debug("Link {} is the only uplink", link);
394 return true;
Saurav Dase9c8971e2018-01-18 12:07:33 -0800395 } catch (DeviceConfigNotFoundException e) {
Saurav Das95047002018-01-25 09:49:01 -0800396 log.warn("Unable to determine if link is only uplink"
Saurav Dase9c8971e2018-01-18 12:07:33 -0800397 + e.getMessage());
398 }
399 return false;
400 }
401
402 /**
403 * Returns true if this controller instance has seen this link before. The
404 * link may not be currently up, but as long as the link had been seen
405 * before this method will return true. The one exception is when the link
406 * was indeed seen before, but this controller instance was forced to forget
407 * it by a call to purgeSeenLink method.
408 *
409 * @param link the infrastructure link being queried
410 * @return true if this controller instance has seen this link before
411 */
412 boolean isSeenLink(Link link) {
413 return seenLinks.containsKey(link);
414 }
415
416 /**
417 * Updates the seen link store. Updates can be for links that are currently
418 * available or not.
419 *
420 * @param link the link to update in the seen-link local store
421 * @param up the status of the link, true if up, false if down
422 */
423 void updateSeenLink(Link link, boolean up) {
424 seenLinks.put(link, up);
425 }
426
427 /**
428 * Returns the status of a seen-link (up or down). If the link has not been
429 * seen-before, a null object is returned.
430 *
431 * @param link the infrastructure link being queried
432 * @return null if the link was not seen-before; true if the seen-link is
433 * up; false if the seen-link is down
434 */
435 private Boolean isSeenLinkUp(Link link) {
436 return seenLinks.get(link);
437 }
438
439 /**
440 * Makes this controller instance forget a previously seen before link.
441 *
442 * @param link the infrastructure link to purge
443 */
444 private void purgeSeenLink(Link link) {
445 seenLinks.remove(link);
446 }
447
448 /**
449 * Returns the status of a link as parallel link. A parallel link is defined
450 * as a link which has common src and dst switches as another seen-link that
451 * is currently enabled. It is not necessary for the link being queried to
452 * be a seen-link.
453 *
454 * @param link the infrastructure link being queried
455 * @return true if a seen-link exists that is up, and shares the same src
456 * and dst switches as the link being queried
457 */
458 private boolean isParallelLink(Link link) {
459 for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
460 Link seenLink = seen.getKey();
461 if (seenLink.equals(link)) {
462 continue;
463 }
464 if (seenLink.src().deviceId().equals(link.src().deviceId())
465 && seenLink.dst().deviceId().equals(link.dst().deviceId())
466 && seen.getValue()) {
467 return true;
468 }
469 }
470 return false;
471 }
472
473 /**
474 * Returns true if the link being queried is a bidirectional link. A bidi
475 * link is defined as a link, whose reverse link - ie. the link in the
476 * reverse direction - has been seen-before and is up. It is not necessary
477 * for the link being queried to be a seen-link.
478 *
479 * @param link the infrastructure link being queried
480 * @return true if another unidirectional link exists in the reverse
481 * direction, has been seen-before and is up
482 */
483 boolean isBidirectional(Link link) {
484 Link reverseLink = linkService.getLink(link.dst(), link.src());
485 if (reverseLink == null) {
486 return false;
487 }
488 Boolean result = isSeenLinkUp(reverseLink);
489 if (result == null) {
490 return false;
491 }
492 return result.booleanValue();
493 }
494
495 /**
496 * Determines if the given link should be avoided in routing calculations by
497 * policy or design.
498 *
499 * @param link the infrastructure link being queried
500 * @return true if link should be avoided
501 */
502 boolean avoidLink(Link link) {
503 // XXX currently only avoids all pair-links. In the future can be
504 // extended to avoid any generic link
505 DeviceId src = link.src().deviceId();
506 PortNumber srcPort = link.src().port();
507 DeviceConfiguration devConfig = srManager.deviceConfiguration;
508 if (devConfig == null || !devConfig.isConfigured(src)) {
509 log.warn("Device {} not configured..cannot avoid link {}", src,
510 link);
511 return false;
512 }
513 DeviceId pairDev;
514 PortNumber pairLocalPort, pairRemotePort = null;
515 try {
516 pairDev = devConfig.getPairDeviceId(src);
517 pairLocalPort = devConfig.getPairLocalPort(src);
518 if (pairDev != null) {
519 pairRemotePort = devConfig
520 .getPairLocalPort(pairDev);
521 }
522 } catch (DeviceConfigNotFoundException e) {
523 log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
524 src, link);
525 return false;
526 }
527
528 return srcPort.equals(pairLocalPort)
529 && link.dst().deviceId().equals(pairDev)
530 && link.dst().port().equals(pairRemotePort);
531 }
532
533 /**
534 * Cleans up internal LinkHandler stores.
535 *
536 * @param device the device that has been removed
537 */
538 void processDeviceRemoved(Device device) {
539 seenLinks.keySet()
540 .removeIf(key -> key.src().deviceId().equals(device.id())
541 || key.dst().deviceId().equals(device.id()));
542 }
543
544 /**
545 * Administratively disables the host location switchport if the edge device
546 * has no viable uplinks.
547 *
548 * @param loc one of the locations of the dual-homed host
549 */
550 void checkUplinksForDualHomedHosts(HostLocation loc) {
551 try {
552 for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
553 if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
554 || l.state() == Link.State.INACTIVE) {
555 continue;
556 }
557 // found valid uplink - so, nothing to do
558 return;
559 }
560 } catch (DeviceConfigNotFoundException e) {
561 log.warn("Could not check for valid uplinks due to missing device"
562 + "config " + e.getMessage());
563 return;
564 }
565 log.warn("Dual homed host location {} has no valid uplinks; "
566 + "disabling dual homed port", loc);
567 srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
568 false);
569 Set<PortNumber> p = downedPortStore.get(loc.deviceId());
570 if (p == null) {
571 p = Sets.newHashSet(loc.port());
572 } else {
573 p.add(loc.port());
574 }
575 downedPortStore.put(loc.deviceId(), p);
576 }
577
Saurav Das95047002018-01-25 09:49:01 -0800578 ImmutableMap<Link, Boolean> getSeenLinks() {
579 return ImmutableMap.copyOf(seenLinks);
580 }
581
582 ImmutableMap<DeviceId, Set<PortNumber>> getDownedPorts() {
583 return ImmutableMap.copyOf(downedPortStore.entrySet());
584 }
585
Saurav Dase9c8971e2018-01-18 12:07:33 -0800586}