blob: c5304399789c171b75d3f19e2d85b81c6654ef9a [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;
23import org.onosproject.net.Device;
24import org.onosproject.net.DeviceId;
25import org.onosproject.net.HostLocation;
26import org.onosproject.net.Link;
27import org.onosproject.net.PortNumber;
28import org.onosproject.net.link.LinkService;
29import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
30import org.onosproject.segmentrouting.config.DeviceConfiguration;
31import org.onosproject.segmentrouting.grouphandler.DefaultGroupHandler;
32import org.onosproject.store.service.EventuallyConsistentMap;
33import org.onosproject.store.service.EventuallyConsistentMapBuilder;
34import org.onosproject.store.service.WallClockTimestamp;
35import org.slf4j.Logger;
36import org.slf4j.LoggerFactory;
37
38import com.google.common.collect.Sets;
39
40public class LinkHandler {
41 private static final Logger log = LoggerFactory.getLogger(LinkHandler.class);
42 protected final SegmentRoutingManager srManager;
43 protected LinkService linkService;
44
45 // Local store for all links seen and their present status, used for
46 // optimized routing. The existence of the link in the keys is enough to
47 // know
48 // if the link has been "seen-before" by this instance of the controller.
49 // The boolean value indicates if the link is currently up or not.
50 // XXX Currently the optimized routing logic depends on "forgetting" a link
51 // when a switch goes down, but "remembering" it when only the link goes
52 // down.
53 // Consider changing this logic so we can use the Link Service instead of
54 // a local cache.
55 private Map<Link, Boolean> seenLinks = new ConcurrentHashMap<>();
56
57 private EventuallyConsistentMap<DeviceId, Set<PortNumber>> downedPortStore = null;
58
59 /**
60 * Constructs the LinkHandler.
61 *
62 * @param srManager Segment Routing manager
63 */
64 LinkHandler(SegmentRoutingManager srManager) {
65 this.srManager = srManager;
66 linkService = srManager.linkService;
67 log.debug("Creating EC map downedportstore");
68 EventuallyConsistentMapBuilder<DeviceId, Set<PortNumber>> downedPortsMapBuilder
69 = srManager.storageService.eventuallyConsistentMapBuilder();
70 downedPortStore = downedPortsMapBuilder.withName("downedportstore")
71 .withSerializer(srManager.createSerializer())
72 .withTimestampProvider((k, v) -> new WallClockTimestamp())
73 .build();
74 log.trace("Current size {}", downedPortStore.size());
75 }
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 /**
89 * Preprocessing of added link before being sent for route-path handling.
90 * Also performs post processing of link.
91 *
92 * @param link the link to be processed
93 */
94 void processLinkAdded(Link link) {
95 log.info("** LINK ADDED {}", link.toString());
96 if (!isLinkValid(link)) {
97 return;
98 }
99 if (!srManager.deviceConfiguration
100 .isConfigured(link.src().deviceId())) {
101 updateSeenLink(link, true);
102 // XXX revisit - what about devicePortMap
103 log.warn("Source device of this link is not configured.. "
104 + "not processing further");
105 return;
106 }
107
108 // Irrespective of whether the local is a MASTER or not for this device,
109 // create group handler instance and push default TTP flow rules if
110 // needed,
111 // as in a multi-instance setup, instances can initiate groups for any
112 // device.
113 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
114 .get(link.src().deviceId());
115 if (groupHandler != null) {
116 groupHandler.portUpForLink(link);
117 } else {
118 // XXX revisit/cleanup
119 Device device = srManager.deviceService.getDevice(link.src().deviceId());
120 if (device != null) {
121 log.warn("processLinkAdded: Link Added "
122 + "Notification without Device Added "
123 + "event, still handling it");
124 srManager.processDeviceAdded(device);
125 groupHandler = srManager.groupHandlerMap.get(link.src().deviceId());
126 groupHandler.portUpForLink(link);
127 }
128 }
129
130 /*
131 // process link only if it is bidirectional
132 if (!isBidirectional(link)) {
133 log.debug("Link not bidirectional.. waiting for other direction " +
134 "src {} --> dst {} ", link.dst(), link.src());
135 // note that if we are not processing for routing, it should at least
136 // be considered a seen-link
137 updateSeenLink(link, true); return;
138 }
139 //TODO ensure that rehash is still done correctly even if link is not processed for
140 //rerouting - perhaps rehash in both directions when it ultimately becomes bidi?
141 */
142
143 log.debug("Starting optimized route-path processing for added link "
144 + "{} --> {}", link.src(), link.dst());
145 boolean seenBefore = isSeenLink(link);
146 // seenLink updates will be done after route-path changes
147 srManager.defaultRoutingHandler
148 .populateRoutingRulesForLinkStatusChange(null, link, null);
149
150 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
151 // handle edge-ports for dual-homed hosts
152 updateDualHomedHostPorts(link, true);
153
154 // It's possible that linkUp causes no route-path change as ECMP
155 // graph does
156 // not change if the link is a parallel link (same src-dst as
157 // another link.
158 // However we still need to update ECMP hash groups to include new
159 // buckets
160 // for the link that has come up.
161 if (!seenBefore && isParallelLink(link)) {
162 // if link seen first time, we need to ensure hash-groups have
163 // all ports
164 log.debug("Attempting retryHash for paralled first-time link {}",
165 link);
166 groupHandler.retryHash(link, false, true);
167 } else {
168 // seen before-link
169 if (isParallelLink(link)) {
170 log.debug("Attempting retryHash for paralled seen-before "
171 + "link {}", link);
172 groupHandler.retryHash(link, false, false);
173 }
174 }
175 }
176
177 srManager.mcastHandler.init();
178 }
179
180 /**
181 * Preprocessing of removed link before being sent for route-path handling.
182 * Also performs post processing of link.
183 *
184 * @param link the link to be processed
185 */
186 void processLinkRemoved(Link link) {
187 log.info("** LINK REMOVED {}", link.toString());
188 if (!isLinkValid(link)) {
189 return;
190 }
191 // when removing links, update seen links first, before doing route-path
192 // changes
193 updateSeenLink(link, false);
194
195 // device availability check helps to ensure that multiple link-removed
196 // events are actually treated as a single switch removed event.
197 // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
198 // when switch comes back.
199 if (link.src().elementId() instanceof DeviceId
200 && !srManager.deviceService.isAvailable(link.src().deviceId())) {
201 purgeSeenLink(link);
202 return;
203 }
204 if (link.dst().elementId() instanceof DeviceId
205 && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
206 purgeSeenLink(link);
207 return;
208 }
209
210 // handle edge-ports for dual-homed hosts
211 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
212 updateDualHomedHostPorts(link, false);
213 }
214
215 log.debug("Starting optimized route-path processing for removed link "
216 + "{} --> {}", link.src(), link.dst());
217 srManager.defaultRoutingHandler
218 .populateRoutingRulesForLinkStatusChange(link, null, null);
219
220 // update local groupHandler stores
221 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
222 .get(link.src().deviceId());
223 if (groupHandler != null) {
224 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
225 && isParallelLink(link)) {
226 log.debug("* retrying hash for parallel link removed:{}", link);
227 groupHandler.retryHash(link, true, false);
228 } else {
229 log.debug("Not attempting retry-hash for link removed: {} .. {}",
230 link,
231 (srManager.mastershipService.isLocalMaster(link.src()
232 .deviceId())) ? "not parallel"
233 : "not master");
234 }
235 // ensure local stores are updated
236 groupHandler.portDown(link.src().port());
237 } else {
238 log.warn("group handler not found for dev:{} when removing link: {}",
239 link.src().deviceId(), link);
240 }
241
242 srManager.mcastHandler.processLinkDown(link);
243 }
244
245 /**
246 * Checks validity of link. Examples of invalid links include
247 * indirect-links, links between ports on the same switch, and more.
248 *
249 * @param link the link to be processed
250 * @return true if valid link
251 */
252 private boolean isLinkValid(Link link) {
253 if (link.type() != Link.Type.DIRECT) {
254 // NOTE: A DIRECT link might be transiently marked as INDIRECT
255 // if BDDP is received before LLDP. We can safely ignore that
256 // until the LLDP is received and the link is marked as DIRECT.
257 log.info("Ignore link {}->{}. Link type is {} instead of DIRECT.",
258 link.src(), link.dst(), link.type());
259 return false;
260 }
261 DeviceId srcId = link.src().deviceId();
262 DeviceId dstId = link.dst().deviceId();
263 if (srcId.equals(dstId)) {
264 log.warn("Links between ports on the same switch are not "
265 + "allowed .. ignoring link {}", link);
266 return false;
267 }
268 DeviceConfiguration devConfig = srManager.deviceConfiguration;
269 try {
270 if (!devConfig.isEdgeDevice(srcId)
271 && !devConfig.isEdgeDevice(dstId)) {
272 // ignore links between spines
273 // XXX revisit when handling multi-stage fabrics
274 log.warn("Links between spines not allowed...ignoring "
275 + "link {}", link);
276 return false;
277 }
278 if (devConfig.isEdgeDevice(srcId)
279 && devConfig.isEdgeDevice(dstId)) {
280 // ignore links between leaves if they are not pair-links
281 // XXX revisit if removing pair-link config or allowing more than
282 // one pair-link
283 if (devConfig.getPairDeviceId(srcId).equals(dstId)
284 && devConfig.getPairLocalPort(srcId)
285 .equals(link.src().port())
286 && devConfig.getPairLocalPort(dstId)
287 .equals(link.dst().port())) {
288 // found pair link - allow it
289 return true;
290 } else {
291 log.warn("Links between leaves other than pair-links are "
292 + "not allowed...ignoring link {}", link);
293 return false;
294 }
295 }
296 } catch (DeviceConfigNotFoundException e) {
297 // We still want to count the links in seenLinks even though there
298 // is no config. So we let it return true
299 log.warn("Could not check validity of link {} as subtending devices "
300 + "are not yet configured", link);
301 }
302 return true;
303 }
304
305 /**
306 * Administratively enables or disables edge ports if the link that was
307 * added or removed was the only uplink port from an edge device. Only edge
308 * ports that belong to dual-homed hosts are considered.
309 *
310 * @param link the link to be processed
311 * @param added true if link was added, false if link was removed
312 */
313 private void updateDualHomedHostPorts(Link link, boolean added) {
314 if (!lastUplink(link)) {
315 return;
316 }
317 if (added) {
318 // re-enable previously disabled ports on this dev
319 Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
320 if (p != null) {
321 log.warn("Link src {} -->dst {} added is the first uplink, "
322 + "enabling dual homed ports: {}", link.src().deviceId(),
323 link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
324 p.forEach(pnum -> srManager.deviceAdminService
325 .changePortState(link.src().deviceId(), pnum, true));
326 }
327 } else {
328 // find dual homed hosts on this dev to disable
329 Set<PortNumber> dhp = srManager.hostHandler
330 .getDualHomedHostPorts(link.src().deviceId());
331 log.warn("Link src {} -->dst {} removed was the last uplink, "
332 + "disabling dual homed ports: {}", link.src().deviceId(),
333 link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
334 dhp.forEach(pnum -> srManager.deviceAdminService
335 .changePortState(link.src().deviceId(), pnum, false));
336 if (!dhp.isEmpty()) {
337 // update global store
338 Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
339 if (p == null) {
340 p = dhp;
341 } else {
342 p.addAll(dhp);
343 }
344 downedPortStore.put(link.src().deviceId(), p);
345 }
346 }
347 }
348
349 /**
350 * Returns true if given link is the last active uplink from src-device of
351 * link. An uplink is defined as a unidirectional link with src as
352 * edgeRouter and dst as non-edgeRouter.
353 *
354 * @param link
355 * @return true if given link is-the-first/was-the-last uplink from the src
356 * device
357 */
358 private boolean lastUplink(Link link) {
359 DeviceConfiguration devConfig = srManager.deviceConfiguration;
360 try {
361 if (!devConfig.isEdgeDevice(link.src().deviceId())) {
362 return false;
363 }
364 Set<Link> devLinks = srManager.linkService
365 .getDeviceLinks(link.src().deviceId());
366 boolean foundOtherUplink = false;
367 for (Link l : devLinks) {
368 if (devConfig.isEdgeDevice(l.dst().deviceId()) || l.equals(link)
369 || l.state() == Link.State.INACTIVE) {
370 continue;
371 }
372 foundOtherUplink = true;
373 break;
374 }
375 if (!foundOtherUplink) {
376 return true;
377 }
378 } catch (DeviceConfigNotFoundException e) {
379 log.warn("Unable to determine if link is last uplink"
380 + e.getMessage());
381 }
382 return false;
383 }
384
385 /**
386 * Returns true if this controller instance has seen this link before. The
387 * link may not be currently up, but as long as the link had been seen
388 * before this method will return true. The one exception is when the link
389 * was indeed seen before, but this controller instance was forced to forget
390 * it by a call to purgeSeenLink method.
391 *
392 * @param link the infrastructure link being queried
393 * @return true if this controller instance has seen this link before
394 */
395 boolean isSeenLink(Link link) {
396 return seenLinks.containsKey(link);
397 }
398
399 /**
400 * Updates the seen link store. Updates can be for links that are currently
401 * available or not.
402 *
403 * @param link the link to update in the seen-link local store
404 * @param up the status of the link, true if up, false if down
405 */
406 void updateSeenLink(Link link, boolean up) {
407 seenLinks.put(link, up);
408 }
409
410 /**
411 * Returns the status of a seen-link (up or down). If the link has not been
412 * seen-before, a null object is returned.
413 *
414 * @param link the infrastructure link being queried
415 * @return null if the link was not seen-before; true if the seen-link is
416 * up; false if the seen-link is down
417 */
418 private Boolean isSeenLinkUp(Link link) {
419 return seenLinks.get(link);
420 }
421
422 /**
423 * Makes this controller instance forget a previously seen before link.
424 *
425 * @param link the infrastructure link to purge
426 */
427 private void purgeSeenLink(Link link) {
428 seenLinks.remove(link);
429 }
430
431 /**
432 * Returns the status of a link as parallel link. A parallel link is defined
433 * as a link which has common src and dst switches as another seen-link that
434 * is currently enabled. It is not necessary for the link being queried to
435 * be a seen-link.
436 *
437 * @param link the infrastructure link being queried
438 * @return true if a seen-link exists that is up, and shares the same src
439 * and dst switches as the link being queried
440 */
441 private boolean isParallelLink(Link link) {
442 for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
443 Link seenLink = seen.getKey();
444 if (seenLink.equals(link)) {
445 continue;
446 }
447 if (seenLink.src().deviceId().equals(link.src().deviceId())
448 && seenLink.dst().deviceId().equals(link.dst().deviceId())
449 && seen.getValue()) {
450 return true;
451 }
452 }
453 return false;
454 }
455
456 /**
457 * Returns true if the link being queried is a bidirectional link. A bidi
458 * link is defined as a link, whose reverse link - ie. the link in the
459 * reverse direction - has been seen-before and is up. It is not necessary
460 * for the link being queried to be a seen-link.
461 *
462 * @param link the infrastructure link being queried
463 * @return true if another unidirectional link exists in the reverse
464 * direction, has been seen-before and is up
465 */
466 boolean isBidirectional(Link link) {
467 Link reverseLink = linkService.getLink(link.dst(), link.src());
468 if (reverseLink == null) {
469 return false;
470 }
471 Boolean result = isSeenLinkUp(reverseLink);
472 if (result == null) {
473 return false;
474 }
475 return result.booleanValue();
476 }
477
478 /**
479 * Determines if the given link should be avoided in routing calculations by
480 * policy or design.
481 *
482 * @param link the infrastructure link being queried
483 * @return true if link should be avoided
484 */
485 boolean avoidLink(Link link) {
486 // XXX currently only avoids all pair-links. In the future can be
487 // extended to avoid any generic link
488 DeviceId src = link.src().deviceId();
489 PortNumber srcPort = link.src().port();
490 DeviceConfiguration devConfig = srManager.deviceConfiguration;
491 if (devConfig == null || !devConfig.isConfigured(src)) {
492 log.warn("Device {} not configured..cannot avoid link {}", src,
493 link);
494 return false;
495 }
496 DeviceId pairDev;
497 PortNumber pairLocalPort, pairRemotePort = null;
498 try {
499 pairDev = devConfig.getPairDeviceId(src);
500 pairLocalPort = devConfig.getPairLocalPort(src);
501 if (pairDev != null) {
502 pairRemotePort = devConfig
503 .getPairLocalPort(pairDev);
504 }
505 } catch (DeviceConfigNotFoundException e) {
506 log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
507 src, link);
508 return false;
509 }
510
511 return srcPort.equals(pairLocalPort)
512 && link.dst().deviceId().equals(pairDev)
513 && link.dst().port().equals(pairRemotePort);
514 }
515
516 /**
517 * Cleans up internal LinkHandler stores.
518 *
519 * @param device the device that has been removed
520 */
521 void processDeviceRemoved(Device device) {
522 seenLinks.keySet()
523 .removeIf(key -> key.src().deviceId().equals(device.id())
524 || key.dst().deviceId().equals(device.id()));
525 }
526
527 /**
528 * Administratively disables the host location switchport if the edge device
529 * has no viable uplinks.
530 *
531 * @param loc one of the locations of the dual-homed host
532 */
533 void checkUplinksForDualHomedHosts(HostLocation loc) {
534 try {
535 for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
536 if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
537 || l.state() == Link.State.INACTIVE) {
538 continue;
539 }
540 // found valid uplink - so, nothing to do
541 return;
542 }
543 } catch (DeviceConfigNotFoundException e) {
544 log.warn("Could not check for valid uplinks due to missing device"
545 + "config " + e.getMessage());
546 return;
547 }
548 log.warn("Dual homed host location {} has no valid uplinks; "
549 + "disabling dual homed port", loc);
550 srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
551 false);
552 Set<PortNumber> p = downedPortStore.get(loc.deviceId());
553 if (p == null) {
554 p = Sets.newHashSet(loc.port());
555 } else {
556 p.add(loc.port());
557 }
558 downedPortStore.put(loc.deviceId(), p);
559 }
560
561}