blob: 2961e505fa2f6137e82ebe6c1ced3a9e77921bfd [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());
74 }
75
76 /**
77 * Constructs the LinkHandler for unit-testing.
78 *
79 * @param srManager SegmentRoutingManager
80 * @param linkService LinkService
81 */
82 LinkHandler(SegmentRoutingManager srManager, LinkService linkService) {
83 this.srManager = srManager;
84 this.linkService = linkService;
85 }
86
87 /**
88 * Preprocessing of added link before being sent for route-path handling.
89 * Also performs post processing of link.
90 *
91 * @param link the link to be processed
92 */
93 void processLinkAdded(Link link) {
94 log.info("** LINK ADDED {}", link.toString());
95 if (!isLinkValid(link)) {
96 return;
97 }
98 if (!srManager.deviceConfiguration
99 .isConfigured(link.src().deviceId())) {
100 updateSeenLink(link, true);
101 // XXX revisit - what about devicePortMap
102 log.warn("Source device of this link is not configured.. "
103 + "not processing further");
104 return;
105 }
106
107 // Irrespective of whether the local is a MASTER or not for this device,
Saurav Das6430f412018-01-25 09:49:01 -0800108 // create group handler instance and push default TTP flow rules if needed,
Saurav Dase6c448a2018-01-18 12:07:33 -0800109 // as in a multi-instance setup, instances can initiate groups for any
110 // device.
111 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
112 .get(link.src().deviceId());
113 if (groupHandler != null) {
114 groupHandler.portUpForLink(link);
115 } else {
116 // XXX revisit/cleanup
117 Device device = srManager.deviceService.getDevice(link.src().deviceId());
118 if (device != null) {
119 log.warn("processLinkAdded: Link Added "
120 + "Notification without Device Added "
121 + "event, still handling it");
122 srManager.processDeviceAdded(device);
123 groupHandler = srManager.groupHandlerMap.get(link.src().deviceId());
124 groupHandler.portUpForLink(link);
125 }
126 }
127
128 /*
129 // process link only if it is bidirectional
130 if (!isBidirectional(link)) {
131 log.debug("Link not bidirectional.. waiting for other direction " +
132 "src {} --> dst {} ", link.dst(), link.src());
133 // note that if we are not processing for routing, it should at least
134 // be considered a seen-link
135 updateSeenLink(link, true); return;
136 }
137 //TODO ensure that rehash is still done correctly even if link is not processed for
138 //rerouting - perhaps rehash in both directions when it ultimately becomes bidi?
139 */
140
141 log.debug("Starting optimized route-path processing for added link "
142 + "{} --> {}", link.src(), link.dst());
143 boolean seenBefore = isSeenLink(link);
144 // seenLink updates will be done after route-path changes
145 srManager.defaultRoutingHandler
146 .populateRoutingRulesForLinkStatusChange(null, link, null);
147
148 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
149 // handle edge-ports for dual-homed hosts
150 updateDualHomedHostPorts(link, true);
151
Saurav Das6430f412018-01-25 09:49:01 -0800152 // It's possible that linkUp causes no route-path change as ECMP graph does
Saurav Dase6c448a2018-01-18 12:07:33 -0800153 // not change if the link is a parallel link (same src-dst as
Saurav Das6430f412018-01-25 09:49:01 -0800154 // another link). However we still need to update ECMP hash groups to include new buckets
Saurav Dase6c448a2018-01-18 12:07:33 -0800155 // for the link that has come up.
Ray Milkey43969b92018-01-24 10:41:14 -0800156 if (groupHandler != null) {
157 if (!seenBefore && isParallelLink(link)) {
158 // if link seen first time, we need to ensure hash-groups have
159 // all ports
160 log.debug("Attempting retryHash for paralled first-time link {}",
161 link);
162 groupHandler.retryHash(link, false, true);
163 } else {
164 // seen before-link
165 if (isParallelLink(link)) {
166 log.debug("Attempting retryHash for paralled seen-before "
167 + "link {}", link);
168 groupHandler.retryHash(link, false, false);
169 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800170 }
171 }
172 }
173
174 srManager.mcastHandler.init();
175 }
176
177 /**
178 * Preprocessing of removed link before being sent for route-path handling.
179 * Also performs post processing of link.
180 *
181 * @param link the link to be processed
182 */
183 void processLinkRemoved(Link link) {
184 log.info("** LINK REMOVED {}", link.toString());
185 if (!isLinkValid(link)) {
186 return;
187 }
188 // when removing links, update seen links first, before doing route-path
189 // changes
190 updateSeenLink(link, false);
Saurav Das6430f412018-01-25 09:49:01 -0800191 // handle edge-ports for dual-homed hosts
192 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
193 updateDualHomedHostPorts(link, false);
194 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800195
196 // device availability check helps to ensure that multiple link-removed
197 // events are actually treated as a single switch removed event.
198 // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
199 // when switch comes back.
200 if (link.src().elementId() instanceof DeviceId
201 && !srManager.deviceService.isAvailable(link.src().deviceId())) {
202 purgeSeenLink(link);
203 return;
204 }
205 if (link.dst().elementId() instanceof DeviceId
206 && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
207 purgeSeenLink(link);
208 return;
209 }
210
Saurav Dase6c448a2018-01-18 12:07:33 -0800211 log.debug("Starting optimized route-path processing for removed link "
212 + "{} --> {}", link.src(), link.dst());
213 srManager.defaultRoutingHandler
214 .populateRoutingRulesForLinkStatusChange(link, null, null);
215
216 // update local groupHandler stores
217 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
218 .get(link.src().deviceId());
219 if (groupHandler != null) {
220 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
221 && isParallelLink(link)) {
222 log.debug("* retrying hash for parallel link removed:{}", link);
223 groupHandler.retryHash(link, true, false);
224 } else {
225 log.debug("Not attempting retry-hash for link removed: {} .. {}",
226 link,
227 (srManager.mastershipService.isLocalMaster(link.src()
228 .deviceId())) ? "not parallel"
229 : "not master");
230 }
231 // ensure local stores are updated
232 groupHandler.portDown(link.src().port());
233 } else {
234 log.warn("group handler not found for dev:{} when removing link: {}",
235 link.src().deviceId(), link);
236 }
237
238 srManager.mcastHandler.processLinkDown(link);
239 }
240
241 /**
242 * Checks validity of link. Examples of invalid links include
243 * indirect-links, links between ports on the same switch, and more.
244 *
245 * @param link the link to be processed
246 * @return true if valid link
247 */
248 private boolean isLinkValid(Link link) {
249 if (link.type() != Link.Type.DIRECT) {
250 // NOTE: A DIRECT link might be transiently marked as INDIRECT
251 // if BDDP is received before LLDP. We can safely ignore that
252 // until the LLDP is received and the link is marked as DIRECT.
253 log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
254 link.src(), link.dst(), link.type());
255 return false;
256 }
257 DeviceId srcId = link.src().deviceId();
258 DeviceId dstId = link.dst().deviceId();
259 if (srcId.equals(dstId)) {
260 log.warn("Links between ports on the same switch are not "
261 + "allowed .. ignoring link {}", link);
262 return false;
263 }
264 DeviceConfiguration devConfig = srManager.deviceConfiguration;
265 try {
266 if (!devConfig.isEdgeDevice(srcId)
267 && !devConfig.isEdgeDevice(dstId)) {
268 // ignore links between spines
269 // XXX revisit when handling multi-stage fabrics
270 log.warn("Links between spines not allowed...ignoring "
271 + "link {}", link);
272 return false;
273 }
274 if (devConfig.isEdgeDevice(srcId)
275 && devConfig.isEdgeDevice(dstId)) {
276 // ignore links between leaves if they are not pair-links
277 // XXX revisit if removing pair-link config or allowing more than
278 // one pair-link
279 if (devConfig.getPairDeviceId(srcId).equals(dstId)
280 && devConfig.getPairLocalPort(srcId)
281 .equals(link.src().port())
282 && devConfig.getPairLocalPort(dstId)
283 .equals(link.dst().port())) {
284 // found pair link - allow it
285 return true;
286 } else {
287 log.warn("Links between leaves other than pair-links are "
288 + "not allowed...ignoring link {}", link);
289 return false;
290 }
291 }
292 } catch (DeviceConfigNotFoundException e) {
293 // We still want to count the links in seenLinks even though there
294 // is no config. So we let it return true
295 log.warn("Could not check validity of link {} as subtending devices "
296 + "are not yet configured", link);
297 }
298 return true;
299 }
300
301 /**
302 * Administratively enables or disables edge ports if the link that was
303 * added or removed was the only uplink port from an edge device. Only edge
304 * ports that belong to dual-homed hosts are considered.
305 *
306 * @param link the link to be processed
307 * @param added true if link was added, false if link was removed
308 */
309 private void updateDualHomedHostPorts(Link link, boolean added) {
Saurav Das6430f412018-01-25 09:49:01 -0800310 if (!onlyUplink(link)) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800311 return;
312 }
313 if (added) {
314 // re-enable previously disabled ports on this dev
315 Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
316 if (p != null) {
317 log.warn("Link src {} -->dst {} added is the first uplink, "
318 + "enabling dual homed ports: {}", link.src().deviceId(),
319 link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
320 p.forEach(pnum -> srManager.deviceAdminService
321 .changePortState(link.src().deviceId(), pnum, true));
322 }
323 } else {
324 // find dual homed hosts on this dev to disable
325 Set<PortNumber> dhp = srManager.hostHandler
326 .getDualHomedHostPorts(link.src().deviceId());
327 log.warn("Link src {} -->dst {} removed was the last uplink, "
328 + "disabling dual homed ports: {}", link.src().deviceId(),
329 link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
330 dhp.forEach(pnum -> srManager.deviceAdminService
331 .changePortState(link.src().deviceId(), pnum, false));
332 if (!dhp.isEmpty()) {
333 // update global store
334 Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
335 if (p == null) {
336 p = dhp;
337 } else {
338 p.addAll(dhp);
339 }
340 downedPortStore.put(link.src().deviceId(), p);
341 }
342 }
343 }
344
345 /**
Saurav Das6430f412018-01-25 09:49:01 -0800346 * Returns true if given link is the only active uplink from src-device of
Saurav Dase6c448a2018-01-18 12:07:33 -0800347 * link. An uplink is defined as a unidirectional link with src as
348 * edgeRouter and dst as non-edgeRouter.
349 *
350 * @param link
351 * @return true if given link is-the-first/was-the-last uplink from the src
352 * device
353 */
Saurav Das6430f412018-01-25 09:49:01 -0800354 private boolean onlyUplink(Link link) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800355 DeviceConfiguration devConfig = srManager.deviceConfiguration;
356 try {
Saurav Das6430f412018-01-25 09:49:01 -0800357 if (!devConfig.isEdgeDevice(link.src().deviceId())
358 || devConfig.isEdgeDevice(link.dst().deviceId())) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800359 return false;
360 }
Saurav Das6430f412018-01-25 09:49:01 -0800361 // note that using linkservice here would cause race conditions as
362 // more links can show up while the app is still processing the first one
363 Set<Link> devLinks = seenLinks.entrySet().stream()
364 .filter(entry -> entry.getKey().src().deviceId()
365 .equals(link.src().deviceId()))
366 .filter(entry -> entry.getValue())
367 .filter(entry -> !entry.getKey().equals(link))
368 .map(entry -> entry.getKey())
369 .collect(Collectors.toSet());
370
Saurav Dase6c448a2018-01-18 12:07:33 -0800371 for (Link l : devLinks) {
Saurav Das6430f412018-01-25 09:49:01 -0800372 if (devConfig.isEdgeDevice(l.dst().deviceId())) {
Saurav Dase6c448a2018-01-18 12:07:33 -0800373 continue;
374 }
Saurav Das6430f412018-01-25 09:49:01 -0800375 log.debug("Link {} is not the only active uplink. Found another"
376 + "link {}", link, l);
377 return false;
Saurav Dase6c448a2018-01-18 12:07:33 -0800378 }
Saurav Das6430f412018-01-25 09:49:01 -0800379 log.debug("Link {} is the only uplink", link);
380 return true;
Saurav Dase6c448a2018-01-18 12:07:33 -0800381 } catch (DeviceConfigNotFoundException e) {
Saurav Das6430f412018-01-25 09:49:01 -0800382 log.warn("Unable to determine if link is only uplink"
Saurav Dase6c448a2018-01-18 12:07:33 -0800383 + e.getMessage());
384 }
385 return false;
386 }
387
388 /**
389 * Returns true if this controller instance has seen this link before. The
390 * link may not be currently up, but as long as the link had been seen
391 * before this method will return true. The one exception is when the link
392 * was indeed seen before, but this controller instance was forced to forget
393 * it by a call to purgeSeenLink method.
394 *
395 * @param link the infrastructure link being queried
396 * @return true if this controller instance has seen this link before
397 */
398 boolean isSeenLink(Link link) {
399 return seenLinks.containsKey(link);
400 }
401
402 /**
403 * Updates the seen link store. Updates can be for links that are currently
404 * available or not.
405 *
406 * @param link the link to update in the seen-link local store
407 * @param up the status of the link, true if up, false if down
408 */
409 void updateSeenLink(Link link, boolean up) {
410 seenLinks.put(link, up);
411 }
412
413 /**
414 * Returns the status of a seen-link (up or down). If the link has not been
415 * seen-before, a null object is returned.
416 *
417 * @param link the infrastructure link being queried
418 * @return null if the link was not seen-before; true if the seen-link is
419 * up; false if the seen-link is down
420 */
421 private Boolean isSeenLinkUp(Link link) {
422 return seenLinks.get(link);
423 }
424
425 /**
426 * Makes this controller instance forget a previously seen before link.
427 *
428 * @param link the infrastructure link to purge
429 */
430 private void purgeSeenLink(Link link) {
431 seenLinks.remove(link);
432 }
433
434 /**
435 * Returns the status of a link as parallel link. A parallel link is defined
436 * as a link which has common src and dst switches as another seen-link that
437 * is currently enabled. It is not necessary for the link being queried to
438 * be a seen-link.
439 *
440 * @param link the infrastructure link being queried
441 * @return true if a seen-link exists that is up, and shares the same src
442 * and dst switches as the link being queried
443 */
444 private boolean isParallelLink(Link link) {
445 for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
446 Link seenLink = seen.getKey();
447 if (seenLink.equals(link)) {
448 continue;
449 }
450 if (seenLink.src().deviceId().equals(link.src().deviceId())
451 && seenLink.dst().deviceId().equals(link.dst().deviceId())
452 && seen.getValue()) {
453 return true;
454 }
455 }
456 return false;
457 }
458
459 /**
460 * Returns true if the link being queried is a bidirectional link. A bidi
461 * link is defined as a link, whose reverse link - ie. the link in the
462 * reverse direction - has been seen-before and is up. It is not necessary
463 * for the link being queried to be a seen-link.
464 *
465 * @param link the infrastructure link being queried
466 * @return true if another unidirectional link exists in the reverse
467 * direction, has been seen-before and is up
468 */
469 boolean isBidirectional(Link link) {
470 Link reverseLink = linkService.getLink(link.dst(), link.src());
471 if (reverseLink == null) {
472 return false;
473 }
474 Boolean result = isSeenLinkUp(reverseLink);
475 if (result == null) {
476 return false;
477 }
478 return result.booleanValue();
479 }
480
481 /**
482 * Determines if the given link should be avoided in routing calculations by
483 * policy or design.
484 *
485 * @param link the infrastructure link being queried
486 * @return true if link should be avoided
487 */
488 boolean avoidLink(Link link) {
489 // XXX currently only avoids all pair-links. In the future can be
490 // extended to avoid any generic link
491 DeviceId src = link.src().deviceId();
492 PortNumber srcPort = link.src().port();
493 DeviceConfiguration devConfig = srManager.deviceConfiguration;
494 if (devConfig == null || !devConfig.isConfigured(src)) {
495 log.warn("Device {} not configured..cannot avoid link {}", src,
496 link);
497 return false;
498 }
499 DeviceId pairDev;
500 PortNumber pairLocalPort, pairRemotePort = null;
501 try {
502 pairDev = devConfig.getPairDeviceId(src);
503 pairLocalPort = devConfig.getPairLocalPort(src);
504 if (pairDev != null) {
505 pairRemotePort = devConfig
506 .getPairLocalPort(pairDev);
507 }
508 } catch (DeviceConfigNotFoundException e) {
509 log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
510 src, link);
511 return false;
512 }
513
514 return srcPort.equals(pairLocalPort)
515 && link.dst().deviceId().equals(pairDev)
516 && link.dst().port().equals(pairRemotePort);
517 }
518
519 /**
520 * Cleans up internal LinkHandler stores.
521 *
522 * @param device the device that has been removed
523 */
524 void processDeviceRemoved(Device device) {
525 seenLinks.keySet()
526 .removeIf(key -> key.src().deviceId().equals(device.id())
527 || key.dst().deviceId().equals(device.id()));
528 }
529
530 /**
531 * Administratively disables the host location switchport if the edge device
532 * has no viable uplinks.
533 *
534 * @param loc one of the locations of the dual-homed host
535 */
536 void checkUplinksForDualHomedHosts(HostLocation loc) {
537 try {
538 for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
539 if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
540 || l.state() == Link.State.INACTIVE) {
541 continue;
542 }
543 // found valid uplink - so, nothing to do
544 return;
545 }
546 } catch (DeviceConfigNotFoundException e) {
547 log.warn("Could not check for valid uplinks due to missing device"
548 + "config " + e.getMessage());
549 return;
550 }
551 log.warn("Dual homed host location {} has no valid uplinks; "
552 + "disabling dual homed port", loc);
553 srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
554 false);
555 Set<PortNumber> p = downedPortStore.get(loc.deviceId());
556 if (p == null) {
557 p = Sets.newHashSet(loc.port());
558 } else {
559 p.add(loc.port());
560 }
561 downedPortStore.put(loc.deviceId(), p);
562 }
563
Saurav Das6430f412018-01-25 09:49:01 -0800564 ImmutableMap<Link, Boolean> getSeenLinks() {
565 return ImmutableMap.copyOf(seenLinks);
566 }
567
568 ImmutableMap<DeviceId, Set<PortNumber>> getDownedPorts() {
569 return ImmutableMap.copyOf(downedPortStore.entrySet());
570 }
571
Saurav Dase6c448a2018-01-18 12:07:33 -0800572}