blob: ef9fb382d1d947647ad88f6bd867b42dcc6c16f3 [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;
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.
Ray Milkey43969b92018-01-24 10:41:14 -0800161 if (groupHandler != null) {
162 if (!seenBefore && isParallelLink(link)) {
163 // if link seen first time, we need to ensure hash-groups have
164 // all ports
165 log.debug("Attempting retryHash for paralled first-time link {}",
166 link);
167 groupHandler.retryHash(link, false, true);
168 } else {
169 // seen before-link
170 if (isParallelLink(link)) {
171 log.debug("Attempting retryHash for paralled seen-before "
172 + "link {}", link);
173 groupHandler.retryHash(link, false, false);
174 }
Saurav Dase6c448a2018-01-18 12:07:33 -0800175 }
176 }
177 }
178
179 srManager.mcastHandler.init();
180 }
181
182 /**
183 * Preprocessing of removed link before being sent for route-path handling.
184 * Also performs post processing of link.
185 *
186 * @param link the link to be processed
187 */
188 void processLinkRemoved(Link link) {
189 log.info("** LINK REMOVED {}", link.toString());
190 if (!isLinkValid(link)) {
191 return;
192 }
193 // when removing links, update seen links first, before doing route-path
194 // changes
195 updateSeenLink(link, false);
196
197 // device availability check helps to ensure that multiple link-removed
198 // events are actually treated as a single switch removed event.
199 // purgeSeenLink is necessary so we do rerouting (instead of rehashing)
200 // when switch comes back.
201 if (link.src().elementId() instanceof DeviceId
202 && !srManager.deviceService.isAvailable(link.src().deviceId())) {
203 purgeSeenLink(link);
204 return;
205 }
206 if (link.dst().elementId() instanceof DeviceId
207 && !srManager.deviceService.isAvailable(link.dst().deviceId())) {
208 purgeSeenLink(link);
209 return;
210 }
211
212 // handle edge-ports for dual-homed hosts
213 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())) {
214 updateDualHomedHostPorts(link, false);
215 }
216
217 log.debug("Starting optimized route-path processing for removed link "
218 + "{} --> {}", link.src(), link.dst());
219 srManager.defaultRoutingHandler
220 .populateRoutingRulesForLinkStatusChange(link, null, null);
221
222 // update local groupHandler stores
223 DefaultGroupHandler groupHandler = srManager.groupHandlerMap
224 .get(link.src().deviceId());
225 if (groupHandler != null) {
226 if (srManager.mastershipService.isLocalMaster(link.src().deviceId())
227 && isParallelLink(link)) {
228 log.debug("* retrying hash for parallel link removed:{}", link);
229 groupHandler.retryHash(link, true, false);
230 } else {
231 log.debug("Not attempting retry-hash for link removed: {} .. {}",
232 link,
233 (srManager.mastershipService.isLocalMaster(link.src()
234 .deviceId())) ? "not parallel"
235 : "not master");
236 }
237 // ensure local stores are updated
238 groupHandler.portDown(link.src().port());
239 } else {
240 log.warn("group handler not found for dev:{} when removing link: {}",
241 link.src().deviceId(), link);
242 }
243
244 srManager.mcastHandler.processLinkDown(link);
245 }
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 */
254 private boolean isLinkValid(Link link) {
255 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;
271 try {
272 if (!devConfig.isEdgeDevice(srcId)
273 && !devConfig.isEdgeDevice(dstId)) {
274 // ignore links between spines
275 // XXX revisit when handling multi-stage fabrics
276 log.warn("Links between spines not allowed...ignoring "
277 + "link {}", link);
278 return false;
279 }
280 if (devConfig.isEdgeDevice(srcId)
281 && devConfig.isEdgeDevice(dstId)) {
282 // ignore links between leaves if they are not pair-links
283 // XXX revisit if removing pair-link config or allowing more than
284 // one pair-link
285 if (devConfig.getPairDeviceId(srcId).equals(dstId)
286 && devConfig.getPairLocalPort(srcId)
287 .equals(link.src().port())
288 && devConfig.getPairLocalPort(dstId)
289 .equals(link.dst().port())) {
290 // found pair link - allow it
291 return true;
292 } else {
293 log.warn("Links between leaves other than pair-links are "
294 + "not allowed...ignoring link {}", link);
295 return false;
296 }
297 }
298 } catch (DeviceConfigNotFoundException e) {
299 // We still want to count the links in seenLinks even though there
300 // is no config. So we let it return true
301 log.warn("Could not check validity of link {} as subtending devices "
302 + "are not yet configured", link);
303 }
304 return true;
305 }
306
307 /**
308 * Administratively enables or disables edge ports if the link that was
309 * added or removed was the only uplink port from an edge device. Only edge
310 * ports that belong to dual-homed hosts are considered.
311 *
312 * @param link the link to be processed
313 * @param added true if link was added, false if link was removed
314 */
315 private void updateDualHomedHostPorts(Link link, boolean added) {
316 if (!lastUplink(link)) {
317 return;
318 }
319 if (added) {
320 // re-enable previously disabled ports on this dev
321 Set<PortNumber> p = downedPortStore.remove(link.src().deviceId());
322 if (p != null) {
323 log.warn("Link src {} -->dst {} added is the first uplink, "
324 + "enabling dual homed ports: {}", link.src().deviceId(),
325 link.dst().deviceId(), (p.isEmpty()) ? "no ports" : p);
326 p.forEach(pnum -> srManager.deviceAdminService
327 .changePortState(link.src().deviceId(), pnum, true));
328 }
329 } else {
330 // find dual homed hosts on this dev to disable
331 Set<PortNumber> dhp = srManager.hostHandler
332 .getDualHomedHostPorts(link.src().deviceId());
333 log.warn("Link src {} -->dst {} removed was the last uplink, "
334 + "disabling dual homed ports: {}", link.src().deviceId(),
335 link.dst().deviceId(), (dhp.isEmpty()) ? "no ports" : dhp);
336 dhp.forEach(pnum -> srManager.deviceAdminService
337 .changePortState(link.src().deviceId(), pnum, false));
338 if (!dhp.isEmpty()) {
339 // update global store
340 Set<PortNumber> p = downedPortStore.get(link.src().deviceId());
341 if (p == null) {
342 p = dhp;
343 } else {
344 p.addAll(dhp);
345 }
346 downedPortStore.put(link.src().deviceId(), p);
347 }
348 }
349 }
350
351 /**
352 * Returns true if given link is the last active uplink from src-device of
353 * link. An uplink is defined as a unidirectional link with src as
354 * edgeRouter and dst as non-edgeRouter.
355 *
356 * @param link
357 * @return true if given link is-the-first/was-the-last uplink from the src
358 * device
359 */
360 private boolean lastUplink(Link link) {
361 DeviceConfiguration devConfig = srManager.deviceConfiguration;
362 try {
363 if (!devConfig.isEdgeDevice(link.src().deviceId())) {
364 return false;
365 }
366 Set<Link> devLinks = srManager.linkService
367 .getDeviceLinks(link.src().deviceId());
368 boolean foundOtherUplink = false;
369 for (Link l : devLinks) {
370 if (devConfig.isEdgeDevice(l.dst().deviceId()) || l.equals(link)
371 || l.state() == Link.State.INACTIVE) {
372 continue;
373 }
374 foundOtherUplink = true;
375 break;
376 }
377 if (!foundOtherUplink) {
378 return true;
379 }
380 } catch (DeviceConfigNotFoundException e) {
381 log.warn("Unable to determine if link is last uplink"
382 + e.getMessage());
383 }
384 return false;
385 }
386
387 /**
388 * Returns true if this controller instance has seen this link before. The
389 * link may not be currently up, but as long as the link had been seen
390 * before this method will return true. The one exception is when the link
391 * was indeed seen before, but this controller instance was forced to forget
392 * it by a call to purgeSeenLink method.
393 *
394 * @param link the infrastructure link being queried
395 * @return true if this controller instance has seen this link before
396 */
397 boolean isSeenLink(Link link) {
398 return seenLinks.containsKey(link);
399 }
400
401 /**
402 * Updates the seen link store. Updates can be for links that are currently
403 * available or not.
404 *
405 * @param link the link to update in the seen-link local store
406 * @param up the status of the link, true if up, false if down
407 */
408 void updateSeenLink(Link link, boolean up) {
409 seenLinks.put(link, up);
410 }
411
412 /**
413 * Returns the status of a seen-link (up or down). If the link has not been
414 * seen-before, a null object is returned.
415 *
416 * @param link the infrastructure link being queried
417 * @return null if the link was not seen-before; true if the seen-link is
418 * up; false if the seen-link is down
419 */
420 private Boolean isSeenLinkUp(Link link) {
421 return seenLinks.get(link);
422 }
423
424 /**
425 * Makes this controller instance forget a previously seen before link.
426 *
427 * @param link the infrastructure link to purge
428 */
429 private void purgeSeenLink(Link link) {
430 seenLinks.remove(link);
431 }
432
433 /**
434 * Returns the status of a link as parallel link. A parallel link is defined
435 * as a link which has common src and dst switches as another seen-link that
436 * is currently enabled. It is not necessary for the link being queried to
437 * be a seen-link.
438 *
439 * @param link the infrastructure link being queried
440 * @return true if a seen-link exists that is up, and shares the same src
441 * and dst switches as the link being queried
442 */
443 private boolean isParallelLink(Link link) {
444 for (Entry<Link, Boolean> seen : seenLinks.entrySet()) {
445 Link seenLink = seen.getKey();
446 if (seenLink.equals(link)) {
447 continue;
448 }
449 if (seenLink.src().deviceId().equals(link.src().deviceId())
450 && seenLink.dst().deviceId().equals(link.dst().deviceId())
451 && seen.getValue()) {
452 return true;
453 }
454 }
455 return false;
456 }
457
458 /**
459 * Returns true if the link being queried is a bidirectional link. A bidi
460 * link is defined as a link, whose reverse link - ie. the link in the
461 * reverse direction - has been seen-before and is up. It is not necessary
462 * for the link being queried to be a seen-link.
463 *
464 * @param link the infrastructure link being queried
465 * @return true if another unidirectional link exists in the reverse
466 * direction, has been seen-before and is up
467 */
468 boolean isBidirectional(Link link) {
469 Link reverseLink = linkService.getLink(link.dst(), link.src());
470 if (reverseLink == null) {
471 return false;
472 }
473 Boolean result = isSeenLinkUp(reverseLink);
474 if (result == null) {
475 return false;
476 }
477 return result.booleanValue();
478 }
479
480 /**
481 * Determines if the given link should be avoided in routing calculations by
482 * policy or design.
483 *
484 * @param link the infrastructure link being queried
485 * @return true if link should be avoided
486 */
487 boolean avoidLink(Link link) {
488 // XXX currently only avoids all pair-links. In the future can be
489 // extended to avoid any generic link
490 DeviceId src = link.src().deviceId();
491 PortNumber srcPort = link.src().port();
492 DeviceConfiguration devConfig = srManager.deviceConfiguration;
493 if (devConfig == null || !devConfig.isConfigured(src)) {
494 log.warn("Device {} not configured..cannot avoid link {}", src,
495 link);
496 return false;
497 }
498 DeviceId pairDev;
499 PortNumber pairLocalPort, pairRemotePort = null;
500 try {
501 pairDev = devConfig.getPairDeviceId(src);
502 pairLocalPort = devConfig.getPairLocalPort(src);
503 if (pairDev != null) {
504 pairRemotePort = devConfig
505 .getPairLocalPort(pairDev);
506 }
507 } catch (DeviceConfigNotFoundException e) {
508 log.warn("Pair dev for dev {} not configured..cannot avoid link {}",
509 src, link);
510 return false;
511 }
512
513 return srcPort.equals(pairLocalPort)
514 && link.dst().deviceId().equals(pairDev)
515 && link.dst().port().equals(pairRemotePort);
516 }
517
518 /**
519 * Cleans up internal LinkHandler stores.
520 *
521 * @param device the device that has been removed
522 */
523 void processDeviceRemoved(Device device) {
524 seenLinks.keySet()
525 .removeIf(key -> key.src().deviceId().equals(device.id())
526 || key.dst().deviceId().equals(device.id()));
527 }
528
529 /**
530 * Administratively disables the host location switchport if the edge device
531 * has no viable uplinks.
532 *
533 * @param loc one of the locations of the dual-homed host
534 */
535 void checkUplinksForDualHomedHosts(HostLocation loc) {
536 try {
537 for (Link l : srManager.linkService.getDeviceLinks(loc.deviceId())) {
538 if (srManager.deviceConfiguration.isEdgeDevice(l.dst().deviceId())
539 || l.state() == Link.State.INACTIVE) {
540 continue;
541 }
542 // found valid uplink - so, nothing to do
543 return;
544 }
545 } catch (DeviceConfigNotFoundException e) {
546 log.warn("Could not check for valid uplinks due to missing device"
547 + "config " + e.getMessage());
548 return;
549 }
550 log.warn("Dual homed host location {} has no valid uplinks; "
551 + "disabling dual homed port", loc);
552 srManager.deviceAdminService.changePortState(loc.deviceId(), loc.port(),
553 false);
554 Set<PortNumber> p = downedPortStore.get(loc.deviceId());
555 if (p == null) {
556 p = Sets.newHashSet(loc.port());
557 } else {
558 p.add(loc.port());
559 }
560 downedPortStore.put(loc.deviceId(), p);
561 }
562
563}