Jonathan Hart

Port the Router functionality from SDN-IP.

As part of this we added an onlab-thirdparty artifact which allows us to
bring in dependencies that aren't bundles.
...@@ -36,6 +36,12 @@ ...@@ -36,6 +36,12 @@
36 <groupId>com.google.guava</groupId> 36 <groupId>com.google.guava</groupId>
37 <artifactId>guava</artifactId> 37 <artifactId>guava</artifactId>
38 </dependency> 38 </dependency>
39 +
40 + <dependency>
41 + <groupId>org.onlab.onos</groupId>
42 + <artifactId>onlab-thirdparty</artifactId>
43 + </dependency>
44 +
39 </dependencies> 45 </dependencies>
40 46
41 </project> 47 </project>
......
1 +package org.onlab.onos.sdnip;
2 +
3 +import static com.google.common.base.Preconditions.checkNotNull;
4 +
5 +import java.util.Objects;
6 +
7 +import org.onlab.packet.IpAddress;
8 +import org.onlab.packet.IpPrefix;
9 +
10 +import com.google.common.base.MoreObjects;
11 +
12 +/**
13 + * Represents a route entry for an IP prefix.
14 + */
15 +public class RouteEntry {
16 + private final IpPrefix prefix; // The IP prefix
17 + private final IpAddress nextHop; // Next-hop IP address
18 +
19 + /**
20 + * Class constructor.
21 + *
22 + * @param prefix the IP prefix of the route
23 + * @param nextHop the next hop IP address for the route
24 + */
25 + public RouteEntry(IpPrefix prefix, IpAddress nextHop) {
26 + this.prefix = checkNotNull(prefix);
27 + this.nextHop = checkNotNull(nextHop);
28 + }
29 +
30 + /**
31 + * Returns the IP prefix of the route.
32 + *
33 + * @return the IP prefix of the route
34 + */
35 + public IpPrefix prefix() {
36 + return prefix;
37 + }
38 +
39 + /**
40 + * Returns the next hop IP address for the route.
41 + *
42 + * @return the next hop IP address for the route
43 + */
44 + public IpAddress nextHop() {
45 + return nextHop;
46 + }
47 +
48 + /**
49 + * Creates the binary string representation of an IPv4 prefix.
50 + * The string length is equal to the prefix length.
51 + *
52 + * @param ip4Prefix the IPv4 prefix to use
53 + * @return the binary string representation
54 + */
55 + static String createBinaryString(IpPrefix ip4Prefix) {
56 + if (ip4Prefix.prefixLength() == 0) {
57 + return "";
58 + }
59 +
60 + StringBuilder result = new StringBuilder(ip4Prefix.prefixLength());
61 + long value = ip4Prefix.toRealInt();
62 + for (int i = 0; i < ip4Prefix.prefixLength(); i++) {
63 + long mask = 1 << (IpAddress.MAX_INET_MASK - 1 - i);
64 + result.append(((value & mask) == 0) ? "0" : "1");
65 + }
66 + return result.toString();
67 + }
68 +
69 + @Override
70 + public boolean equals(Object other) {
71 + if (this == other) {
72 + return true;
73 + }
74 +
75 + //
76 + // NOTE: Subclasses are considered as change of identity, hence
77 + // equals() will return false if the class type doesn't match.
78 + //
79 + if (other == null || getClass() != other.getClass()) {
80 + return false;
81 + }
82 +
83 + RouteEntry otherRoute = (RouteEntry) other;
84 + return Objects.equals(this.prefix, otherRoute.prefix) &&
85 + Objects.equals(this.nextHop, otherRoute.nextHop);
86 + }
87 +
88 + @Override
89 + public int hashCode() {
90 + return Objects.hash(prefix, nextHop);
91 + }
92 +
93 + @Override
94 + public String toString() {
95 + return MoreObjects.toStringHelper(getClass())
96 + .add("prefix", prefix)
97 + .add("nextHop", nextHop)
98 + .toString();
99 + }
100 +}
1 +package org.onlab.onos.sdnip;
2 +
3 +/**
4 + * An interface to receive route updates from route providers.
5 + */
6 +public interface RouteListener {
7 + /**
8 + * Receives a route update from a route provider.
9 + *
10 + * @param routeUpdate the updated route information
11 + */
12 + public void update(RouteUpdate routeUpdate);
13 +}
1 +package org.onlab.onos.sdnip;
2 +
3 +import static com.google.common.base.Preconditions.checkNotNull;
4 +
5 +import java.util.Objects;
6 +
7 +import com.google.common.base.MoreObjects;
8 +
9 +/**
10 + * Represents a change in routing information.
11 + */
12 +public class RouteUpdate {
13 + private final Type type; // The route update type
14 + private final RouteEntry routeEntry; // The updated route entry
15 +
16 + /**
17 + * Specifies the type of a route update.
18 + * <p/>
19 + * Route updates can either provide updated information for a route, or
20 + * withdraw a previously updated route.
21 + */
22 + public enum Type {
23 + /**
24 + * The update contains updated route information for a route.
25 + */
26 + UPDATE,
27 + /**
28 + * The update withdraws the route, meaning any previous information is
29 + * no longer valid.
30 + */
31 + DELETE
32 + }
33 +
34 + /**
35 + * Class constructor.
36 + *
37 + * @param type the type of the route update
38 + * @param routeEntry the route entry with the update
39 + */
40 + public RouteUpdate(Type type, RouteEntry routeEntry) {
41 + this.type = type;
42 + this.routeEntry = checkNotNull(routeEntry);
43 + }
44 +
45 + /**
46 + * Returns the type of the route update.
47 + *
48 + * @return the type of the update
49 + */
50 + public Type type() {
51 + return type;
52 + }
53 +
54 + /**
55 + * Returns the route entry the route update is for.
56 + *
57 + * @return the route entry the route update is for
58 + */
59 + public RouteEntry routeEntry() {
60 + return routeEntry;
61 + }
62 +
63 + @Override
64 + public boolean equals(Object other) {
65 + if (other == this) {
66 + return true;
67 + }
68 +
69 + if (!(other instanceof RouteUpdate)) {
70 + return false;
71 + }
72 +
73 + RouteUpdate otherUpdate = (RouteUpdate) other;
74 +
75 + return Objects.equals(this.type, otherUpdate.type) &&
76 + Objects.equals(this.routeEntry, otherUpdate.routeEntry);
77 + }
78 +
79 + @Override
80 + public int hashCode() {
81 + return Objects.hash(type, routeEntry);
82 + }
83 +
84 + @Override
85 + public String toString() {
86 + return MoreObjects.toStringHelper(getClass())
87 + .add("type", type)
88 + .add("routeEntry", routeEntry)
89 + .toString();
90 + }
91 +}
1 +package org.onlab.onos.sdnip;
2 +
3 +import java.util.Collection;
4 +import java.util.HashMap;
5 +import java.util.HashSet;
6 +import java.util.Iterator;
7 +import java.util.LinkedList;
8 +import java.util.List;
9 +import java.util.Map;
10 +import java.util.Set;
11 +import java.util.concurrent.BlockingQueue;
12 +import java.util.concurrent.ConcurrentHashMap;
13 +import java.util.concurrent.ExecutorService;
14 +import java.util.concurrent.Executors;
15 +import java.util.concurrent.LinkedBlockingQueue;
16 +import java.util.concurrent.Semaphore;
17 +
18 +import org.apache.commons.lang3.tuple.Pair;
19 +import org.onlab.onos.net.ConnectPoint;
20 +import org.onlab.onos.net.Host;
21 +import org.onlab.onos.net.flow.DefaultTrafficSelector;
22 +import org.onlab.onos.net.flow.DefaultTrafficTreatment;
23 +import org.onlab.onos.net.flow.TrafficSelector;
24 +import org.onlab.onos.net.flow.TrafficTreatment;
25 +import org.onlab.onos.net.flow.criteria.Criteria.IPCriterion;
26 +import org.onlab.onos.net.flow.criteria.Criterion;
27 +import org.onlab.onos.net.flow.criteria.Criterion.Type;
28 +import org.onlab.onos.net.host.HostEvent;
29 +import org.onlab.onos.net.host.HostListener;
30 +import org.onlab.onos.net.host.HostService;
31 +import org.onlab.onos.net.intent.Intent;
32 +import org.onlab.onos.net.intent.IntentId;
33 +import org.onlab.onos.net.intent.IntentService;
34 +import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
35 +import org.onlab.onos.sdnip.config.BgpPeer;
36 +import org.onlab.onos.sdnip.config.Interface;
37 +import org.onlab.onos.sdnip.config.SdnIpConfigService;
38 +import org.onlab.packet.Ethernet;
39 +import org.onlab.packet.IpAddress;
40 +import org.onlab.packet.IpPrefix;
41 +import org.onlab.packet.MacAddress;
42 +import org.slf4j.Logger;
43 +import org.slf4j.LoggerFactory;
44 +
45 +import com.google.common.base.Objects;
46 +import com.google.common.collect.HashMultimap;
47 +import com.google.common.collect.Multimaps;
48 +import com.google.common.collect.SetMultimap;
49 +import com.google.common.util.concurrent.ThreadFactoryBuilder;
50 +import com.googlecode.concurrenttrees.common.KeyValuePair;
51 +import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
52 +import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
53 +import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
54 +
55 +/**
56 + * This class processes BGP route update, translates each update into a intent
57 + * and submits the intent.
58 + *
59 + * TODO: Make it thread-safe.
60 + */
61 +public class Router implements RouteListener {
62 +
63 + private static final Logger log = LoggerFactory.getLogger(Router.class);
64 +
65 + // Store all route updates in a InvertedRadixTree.
66 + // The key in this Tree is the binary sting of prefix of route.
67 + // The Ip4Address is the next hop address of route, and is also the value
68 + // of each entry.
69 + private InvertedRadixTree<RouteEntry> bgpRoutes;
70 +
71 + // Stores all incoming route updates in a queue.
72 + private BlockingQueue<RouteUpdate> routeUpdates;
73 +
74 + // The Ip4Address is the next hop address of each route update.
75 + private SetMultimap<IpAddress, RouteEntry> routesWaitingOnArp;
76 + private ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> pushedRouteIntents;
77 +
78 + private IntentService intentService;
79 + //private IProxyArpService proxyArp;
80 + private HostService hostService;
81 + private SdnIpConfigService configInfoService;
82 + private InterfaceService interfaceService;
83 +
84 + private ExecutorService bgpUpdatesExecutor;
85 + private ExecutorService bgpIntentsSynchronizerExecutor;
86 +
87 + // TODO temporary
88 + private int intentId = Integer.MAX_VALUE / 2;
89 +
90 + //
91 + // State to deal with SDN-IP Leader election and pushing Intents
92 + //
93 + private Semaphore intentsSynchronizerSemaphore = new Semaphore(0);
94 + private volatile boolean isElectedLeader = false;
95 + private volatile boolean isActivatedLeader = false;
96 +
97 + // For routes announced by local BGP deamon in SDN network,
98 + // the next hop will be 0.0.0.0.
99 + public static final IpAddress LOCAL_NEXT_HOP = IpAddress.valueOf("0.0.0.0");
100 +
101 + /**
102 + * Class constructor.
103 + *
104 + * @param intentService the intent service
105 + * @param proxyArp the proxy ARP service
106 + * @param configInfoService the configuration service
107 + * @param interfaceService the interface service
108 + */
109 + public Router(IntentService intentService, HostService hostService,
110 + SdnIpConfigService configInfoService, InterfaceService interfaceService) {
111 +
112 + this.intentService = intentService;
113 + this.hostService = hostService;
114 + this.configInfoService = configInfoService;
115 + this.interfaceService = interfaceService;
116 +
117 + bgpRoutes = new ConcurrentInvertedRadixTree<>(
118 + new DefaultByteArrayNodeFactory());
119 + routeUpdates = new LinkedBlockingQueue<>();
120 + routesWaitingOnArp = Multimaps.synchronizedSetMultimap(
121 + HashMultimap.<IpAddress, RouteEntry>create());
122 + pushedRouteIntents = new ConcurrentHashMap<>();
123 +
124 + bgpUpdatesExecutor = Executors.newSingleThreadExecutor(
125 + new ThreadFactoryBuilder().setNameFormat("bgp-updates-%d").build());
126 + bgpIntentsSynchronizerExecutor = Executors.newSingleThreadExecutor(
127 + new ThreadFactoryBuilder()
128 + .setNameFormat("bgp-intents-synchronizer-%d").build());
129 + }
130 +
131 + /**
132 + * Starts the Router.
133 + */
134 + public void start() {
135 +
136 + bgpUpdatesExecutor.execute(new Runnable() {
137 + @Override
138 + public void run() {
139 + doUpdatesThread();
140 + }
141 + });
142 +
143 + bgpIntentsSynchronizerExecutor.execute(new Runnable() {
144 + @Override
145 + public void run() {
146 + doIntentSynchronizationThread();
147 + }
148 + });
149 + }
150 +
151 + //@Override TODO hook this up to something
152 + public void leaderChanged(boolean isLeader) {
153 + log.debug("Leader changed: {}", isLeader);
154 +
155 + if (!isLeader) {
156 + this.isElectedLeader = false;
157 + this.isActivatedLeader = false;
158 + return; // Nothing to do
159 + }
160 + this.isActivatedLeader = false;
161 + this.isElectedLeader = true;
162 +
163 + //
164 + // Tell the Intents Synchronizer thread to start the synchronization
165 + //
166 + intentsSynchronizerSemaphore.release();
167 + }
168 +
169 + @Override
170 + public void update(RouteUpdate routeUpdate) {
171 + log.debug("Received new route Update: {}", routeUpdate);
172 +
173 + try {
174 + routeUpdates.put(routeUpdate);
175 + } catch (InterruptedException e) {
176 + log.debug("Interrupted while putting on routeUpdates queue", e);
177 + Thread.currentThread().interrupt();
178 + }
179 + }
180 +
181 + /**
182 + * Thread for Intent Synchronization.
183 + */
184 + private void doIntentSynchronizationThread() {
185 + boolean interrupted = false;
186 + try {
187 + while (!interrupted) {
188 + try {
189 + intentsSynchronizerSemaphore.acquire();
190 + //
191 + // Drain all permits, because a single synchronization is
192 + // sufficient.
193 + //
194 + intentsSynchronizerSemaphore.drainPermits();
195 + } catch (InterruptedException e) {
196 + log.debug("Interrupted while waiting to become " +
197 + "Intent Synchronization leader");
198 + interrupted = true;
199 + break;
200 + }
201 + syncIntents();
202 + }
203 + } finally {
204 + if (interrupted) {
205 + Thread.currentThread().interrupt();
206 + }
207 + }
208 + }
209 +
210 + /**
211 + * Thread for handling route updates.
212 + */
213 + private void doUpdatesThread() {
214 + boolean interrupted = false;
215 + try {
216 + while (!interrupted) {
217 + try {
218 + RouteUpdate update = routeUpdates.take();
219 + switch (update.type()) {
220 + case UPDATE:
221 + processRouteAdd(update.routeEntry());
222 + break;
223 + case DELETE:
224 + processRouteDelete(update.routeEntry());
225 + break;
226 + default:
227 + log.error("Unknown update Type: {}", update.type());
228 + break;
229 + }
230 + } catch (InterruptedException e) {
231 + log.debug("Interrupted while taking from updates queue", e);
232 + interrupted = true;
233 + } catch (Exception e) {
234 + log.debug("exception", e);
235 + }
236 + }
237 + } finally {
238 + if (interrupted) {
239 + Thread.currentThread().interrupt();
240 + }
241 + }
242 + }
243 +
244 + /**
245 + * Performs Intents Synchronization between the internally stored Route
246 + * Intents and the installed Route Intents.
247 + */
248 + private void syncIntents() {
249 + synchronized (this) {
250 + if (!isElectedLeader) {
251 + return; // Nothing to do: not the leader anymore
252 + }
253 + log.debug("Syncing SDN-IP Route Intents...");
254 +
255 + Map<IpPrefix, MultiPointToSinglePointIntent> fetchedIntents =
256 + new HashMap<>();
257 +
258 + //
259 + // Fetch all intents, and classify the Multi-Point-to-Point Intents
260 + // based on the matching prefix.
261 + //
262 + for (Intent intent : intentService.getIntents()) {
263 + //
264 + // TODO: Ignore all intents that are not installed by
265 + // the SDN-IP application.
266 + //
267 + if (!(intent instanceof MultiPointToSinglePointIntent)) {
268 + continue;
269 + }
270 + MultiPointToSinglePointIntent mp2pIntent =
271 + (MultiPointToSinglePointIntent) intent;
272 + /*Match match = mp2pIntent.getMatch();
273 + if (!(match instanceof PacketMatch)) {
274 + continue;
275 + }
276 + PacketMatch packetMatch = (PacketMatch) match;
277 + Ip4Prefix prefix = packetMatch.getDstIpAddress();
278 + if (prefix == null) {
279 + continue;
280 + }
281 + fetchedIntents.put(prefix, mp2pIntent);*/
282 + for (Criterion criterion : mp2pIntent.selector().criteria()) {
283 + if (criterion.type() == Type.IPV4_DST) {
284 + IPCriterion ipCriterion = (IPCriterion) criterion;
285 + fetchedIntents.put(ipCriterion.ip(), mp2pIntent);
286 + }
287 + }
288 +
289 + }
290 +
291 + //
292 + // Compare for each prefix the local IN-MEMORY Intents with the
293 + // FETCHED Intents:
294 + // - If the IN-MEMORY Intent is same as the FETCHED Intent, store
295 + // the FETCHED Intent in the local memory (i.e., override the
296 + // IN-MEMORY Intent) to preserve the original Intent ID
297 + // - if the IN-MEMORY Intent is not same as the FETCHED Intent,
298 + // delete the FETCHED Intent, and push/install the IN-MEMORY
299 + // Intent.
300 + // - If there is an IN-MEMORY Intent for a prefix, but no FETCHED
301 + // Intent for same prefix, then push/install the IN-MEMORY
302 + // Intent.
303 + // - If there is a FETCHED Intent for a prefix, but no IN-MEMORY
304 + // Intent for same prefix, then delete/withdraw the FETCHED
305 + // Intent.
306 + //
307 + Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
308 + storeInMemoryIntents = new LinkedList<>();
309 + Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
310 + addIntents = new LinkedList<>();
311 + Collection<Pair<IpPrefix, MultiPointToSinglePointIntent>>
312 + deleteIntents = new LinkedList<>();
313 + for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
314 + pushedRouteIntents.entrySet()) {
315 + IpPrefix prefix = entry.getKey();
316 + MultiPointToSinglePointIntent inMemoryIntent =
317 + entry.getValue();
318 + MultiPointToSinglePointIntent fetchedIntent =
319 + fetchedIntents.get(prefix);
320 +
321 + if (fetchedIntent == null) {
322 + //
323 + // No FETCHED Intent for same prefix: push the IN-MEMORY
324 + // Intent.
325 + //
326 + addIntents.add(Pair.of(prefix, inMemoryIntent));
327 + continue;
328 + }
329 +
330 + //
331 + // If IN-MEMORY Intent is same as the FETCHED Intent,
332 + // store the FETCHED Intent in the local memory.
333 + //
334 + if (compareMultiPointToSinglePointIntents(inMemoryIntent,
335 + fetchedIntent)) {
336 + storeInMemoryIntents.add(Pair.of(prefix, fetchedIntent));
337 + } else {
338 + //
339 + // The IN-MEMORY Intent is not same as the FETCHED Intent,
340 + // hence delete the FETCHED Intent, and install the
341 + // IN-MEMORY Intent.
342 + //
343 + deleteIntents.add(Pair.of(prefix, fetchedIntent));
344 + addIntents.add(Pair.of(prefix, inMemoryIntent));
345 + }
346 + fetchedIntents.remove(prefix);
347 + }
348 +
349 + //
350 + // Any remaining FETCHED Intents have to be deleted/withdrawn
351 + //
352 + for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry :
353 + fetchedIntents.entrySet()) {
354 + IpPrefix prefix = entry.getKey();
355 + MultiPointToSinglePointIntent fetchedIntent = entry.getValue();
356 + deleteIntents.add(Pair.of(prefix, fetchedIntent));
357 + }
358 +
359 + //
360 + // Perform the actions:
361 + // 1. Store in memory fetched intents that are same. Can be done
362 + // even if we are not the leader anymore
363 + // 2. Delete intents: check if the leader before each operation
364 + // 3. Add intents: check if the leader before each operation
365 + //
366 + for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
367 + storeInMemoryIntents) {
368 + IpPrefix prefix = pair.getLeft();
369 + MultiPointToSinglePointIntent intent = pair.getRight();
370 + log.debug("Intent synchronization: updating in-memory " +
371 + "Intent for prefix: {}", prefix);
372 + pushedRouteIntents.put(prefix, intent);
373 + }
374 + //
375 + isActivatedLeader = true; // Allow push of Intents
376 + for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
377 + deleteIntents) {
378 + IpPrefix prefix = pair.getLeft();
379 + MultiPointToSinglePointIntent intent = pair.getRight();
380 + if (!isElectedLeader) {
381 + isActivatedLeader = false;
382 + return;
383 + }
384 + log.debug("Intent synchronization: deleting Intent for " +
385 + "prefix: {}", prefix);
386 + intentService.withdraw(intent);
387 + }
388 + //
389 + for (Pair<IpPrefix, MultiPointToSinglePointIntent> pair :
390 + addIntents) {
391 + IpPrefix prefix = pair.getLeft();
392 + MultiPointToSinglePointIntent intent = pair.getRight();
393 + if (!isElectedLeader) {
394 + isActivatedLeader = false;
395 + return;
396 + }
397 + log.debug("Intent synchronization: adding Intent for " +
398 + "prefix: {}", prefix);
399 + intentService.submit(intent);
400 + }
401 + if (!isElectedLeader) {
402 + isActivatedLeader = false;
403 + }
404 + log.debug("Syncing SDN-IP routes completed.");
405 + }
406 + }
407 +
408 + /**
409 + * Compares two Multi-point to Single Point Intents whether they represent
410 + * same logical intention.
411 + *
412 + * @param intent1 the first Intent to compare
413 + * @param intent2 the second Intent to compare
414 + * @return true if both Intents represent same logical intention, otherwise
415 + * false
416 + */
417 + private boolean compareMultiPointToSinglePointIntents(
418 + MultiPointToSinglePointIntent intent1,
419 + MultiPointToSinglePointIntent intent2) {
420 + /*Match match1 = intent1.getMatch();
421 + Match match2 = intent2.getMatch();
422 + Action action1 = intent1.getAction();
423 + Action action2 = intent2.getAction();
424 + Set<SwitchPort> ingressPorts1 = intent1.getIngressPorts();
425 + Set<SwitchPort> ingressPorts2 = intent2.getIngressPorts();
426 + SwitchPort egressPort1 = intent1.getEgressPort();
427 + SwitchPort egressPort2 = intent2.getEgressPort();
428 +
429 + return Objects.equal(match1, match2) &&
430 + Objects.equal(action1, action2) &&
431 + Objects.equal(egressPort1, egressPort2) &&
432 + Objects.equal(ingressPorts1, ingressPorts2);*/
433 + return Objects.equal(intent1.selector(), intent2.selector()) &&
434 + Objects.equal(intent1.treatment(), intent2.treatment()) &&
435 + Objects.equal(intent1.ingressPoints(), intent2.ingressPoints()) &&
436 + Objects.equal(intent1.egressPoint(), intent2.egressPoint());
437 + }
438 +
439 + /**
440 + * Processes adding a route entry.
441 + * <p/>
442 + * Put new route entry into InvertedRadixTree. If there was an existing
443 + * nexthop for this prefix, but the next hop was different, then execute
444 + * deleting old route entry. If the next hop is the SDN domain, we do not
445 + * handle it at the moment. Otherwise, execute adding a route.
446 + *
447 + * @param routeEntry the route entry to add
448 + */
449 + protected void processRouteAdd(RouteEntry routeEntry) {
450 + synchronized (this) {
451 + log.debug("Processing route add: {}", routeEntry);
452 +
453 + IpPrefix prefix = routeEntry.prefix();
454 + IpAddress nextHop = null;
455 + RouteEntry foundRouteEntry =
456 + bgpRoutes.put(RouteEntry.createBinaryString(prefix),
457 + routeEntry);
458 + if (foundRouteEntry != null) {
459 + nextHop = foundRouteEntry.nextHop();
460 + }
461 +
462 + if (nextHop != null && !nextHop.equals(routeEntry.nextHop())) {
463 + // There was an existing nexthop for this prefix. This update
464 + // supersedes that, so we need to remove the old flows for this
465 + // prefix from the switches
466 + executeRouteDelete(routeEntry);
467 + }
468 + if (nextHop != null && nextHop.equals(routeEntry.nextHop())) {
469 + return;
470 + }
471 +
472 + if (routeEntry.nextHop().equals(LOCAL_NEXT_HOP)) {
473 + // Route originated by SDN domain
474 + // We don't handle these at the moment
475 + log.debug("Own route {} to {}",
476 + routeEntry.prefix(), routeEntry.nextHop());
477 + return;
478 + }
479 +
480 + executeRouteAdd(routeEntry);
481 + }
482 + }
483 +
484 + /**
485 + * Executes adding a route entry.
486 + * <p/>
487 + * Find out the egress Interface and MAC address of next hop router for
488 + * this route entry. If the MAC address can not be found in ARP cache,
489 + * then this prefix will be put in routesWaitingOnArp queue. Otherwise,
490 + * new route intent will be created and installed.
491 + *
492 + * @param routeEntry the route entry to add
493 + */
494 + private void executeRouteAdd(RouteEntry routeEntry) {
495 + log.debug("Executing route add: {}", routeEntry);
496 +
497 + // See if we know the MAC address of the next hop
498 + //MacAddress nextHopMacAddress =
499 + //proxyArp.getMacAddress(routeEntry.getNextHop());
500 + MacAddress nextHopMacAddress = null;
501 + Set<Host> hosts = hostService.getHostsByIp(
502 + routeEntry.nextHop().toPrefix());
503 + if (!hosts.isEmpty()) {
504 + // TODO how to handle if multiple hosts are returned?
505 + nextHopMacAddress = hosts.iterator().next().mac();
506 + }
507 +
508 + if (nextHopMacAddress == null) {
509 + routesWaitingOnArp.put(routeEntry.nextHop(), routeEntry);
510 + //proxyArp.sendArpRequest(routeEntry.getNextHop(), this, true);
511 + // TODO maybe just do this for every prefix anyway
512 + hostService.startMonitoringIp(routeEntry.nextHop());
513 + return;
514 + }
515 +
516 + addRouteIntentToNextHop(routeEntry.prefix(),
517 + routeEntry.nextHop(),
518 + nextHopMacAddress);
519 + }
520 +
521 + /**
522 + * Adds a route intent given a prefix and a next hop IP address. This
523 + * method will find the egress interface for the intent.
524 + *
525 + * @param prefix IP prefix of the route to add
526 + * @param nextHopIpAddress IP address of the next hop
527 + * @param nextHopMacAddress MAC address of the next hop
528 + */
529 + private void addRouteIntentToNextHop(IpPrefix prefix,
530 + IpAddress nextHopIpAddress,
531 + MacAddress nextHopMacAddress) {
532 +
533 + // Find the attachment point (egress interface) of the next hop
534 + Interface egressInterface;
535 + if (configInfoService.getBgpPeers().containsKey(nextHopIpAddress)) {
536 + // Route to a peer
537 + log.debug("Route to peer {}", nextHopIpAddress);
538 + BgpPeer peer =
539 + configInfoService.getBgpPeers().get(nextHopIpAddress);
540 + egressInterface =
541 + interfaceService.getInterface(peer.connectPoint());
542 + } else {
543 + // Route to non-peer
544 + log.debug("Route to non-peer {}", nextHopIpAddress);
545 + egressInterface =
546 + interfaceService.getMatchingInterface(nextHopIpAddress);
547 + if (egressInterface == null) {
548 + log.warn("No outgoing interface found for {}",
549 + nextHopIpAddress);
550 + return;
551 + }
552 + }
553 +
554 + doAddRouteIntent(prefix, egressInterface, nextHopMacAddress);
555 + }
556 +
557 + /**
558 + * Installs a route intent for a prefix.
559 + * <p/>
560 + * Intent will match dst IP prefix and rewrite dst MAC address at all other
561 + * border switches, then forward packets according to dst MAC address.
562 + *
563 + * @param prefix IP prefix from route
564 + * @param egressInterface egress Interface connected to next hop router
565 + * @param nextHopMacAddress MAC address of next hop router
566 + */
567 + private void doAddRouteIntent(IpPrefix prefix, Interface egressInterface,
568 + MacAddress nextHopMacAddress) {
569 + log.debug("Adding intent for prefix {}, next hop mac {}",
570 + prefix, nextHopMacAddress);
571 +
572 + MultiPointToSinglePointIntent pushedIntent =
573 + pushedRouteIntents.get(prefix);
574 +
575 + // Just for testing.
576 + if (pushedIntent != null) {
577 + log.error("There should not be a pushed intent: {}", pushedIntent);
578 + }
579 +
580 + ConnectPoint egressPort = egressInterface.connectPoint();
581 +
582 + Set<ConnectPoint> ingressPorts = new HashSet<>();
583 +
584 + for (Interface intf : interfaceService.getInterfaces()) {
585 + if (!intf.equals(egressInterface)) {
586 + ConnectPoint srcPort = intf.connectPoint();
587 + ingressPorts.add(srcPort);
588 + }
589 + }
590 +
591 + // Match the destination IP prefix at the first hop
592 + //PacketMatchBuilder builder = new PacketMatchBuilder();
593 + //builder.setEtherType(Ethernet.TYPE_IPV4).setDstIpNet(prefix);
594 + //PacketMatch packetMatch = builder.build();
595 + TrafficSelector selector = DefaultTrafficSelector.builder()
596 + .matchEthType(Ethernet.TYPE_IPV4)
597 + .matchIPDst(prefix)
598 + .build();
599 +
600 + // Rewrite the destination MAC address
601 + //ModifyDstMacAction modifyDstMacAction =
602 + //new ModifyDstMacAction(nextHopMacAddress);
603 + TrafficTreatment treatment = DefaultTrafficTreatment.builder()
604 + .setEthDst(nextHopMacAddress)
605 + .build();
606 +
607 + MultiPointToSinglePointIntent intent =
608 + new MultiPointToSinglePointIntent(nextIntentId(),
609 + selector, treatment, ingressPorts, egressPort);
610 +
611 + if (isElectedLeader && isActivatedLeader) {
612 + log.debug("Intent installation: adding Intent for prefix: {}",
613 + prefix);
614 + intentService.submit(intent);
615 + }
616 +
617 + // Maintain the Intent
618 + pushedRouteIntents.put(prefix, intent);
619 + }
620 +
621 + /**
622 + * Executes deleting a route entry.
623 + * <p/>
624 + * Removes prefix from InvertedRadixTree, if success, then try to delete
625 + * the relative intent.
626 + *
627 + * @param routeEntry the route entry to delete
628 + */
629 + protected void processRouteDelete(RouteEntry routeEntry) {
630 + synchronized (this) {
631 + log.debug("Processing route delete: {}", routeEntry);
632 + IpPrefix prefix = routeEntry.prefix();
633 +
634 + // TODO check the change of logic here - remove doesn't check that
635 + // the route entry was what we expected (and we can't do this
636 + // concurrently)
637 +
638 + if (bgpRoutes.remove(RouteEntry.createBinaryString(prefix))) {
639 + //
640 + // Only delete flows if an entry was actually removed from the
641 + // tree. If no entry was removed, the <prefix, nexthop> wasn't
642 + // there so it's probably already been removed and we don't
643 + // need to do anything.
644 + //
645 + executeRouteDelete(routeEntry);
646 + }
647 +
648 + routesWaitingOnArp.remove(routeEntry.nextHop(), routeEntry);
649 + // TODO cancel the request in the ARP manager as well
650 + }
651 + }
652 +
653 + /**
654 + * Executed deleting a route entry.
655 + *
656 + * @param routeEntry the route entry to delete
657 + */
658 + private void executeRouteDelete(RouteEntry routeEntry) {
659 + log.debug("Executing route delete: {}", routeEntry);
660 +
661 + IpPrefix prefix = routeEntry.prefix();
662 +
663 + MultiPointToSinglePointIntent intent =
664 + pushedRouteIntents.remove(prefix);
665 +
666 + if (intent == null) {
667 + log.debug("There is no intent in pushedRouteIntents to delete " +
668 + "for prefix: {}", prefix);
669 + } else {
670 + if (isElectedLeader && isActivatedLeader) {
671 + log.debug("Intent installation: deleting Intent for prefix: {}",
672 + prefix);
673 + intentService.withdraw(intent);
674 + }
675 + }
676 + }
677 +
678 + /**
679 + * This method handles the prefixes which are waiting for ARP replies for
680 + * MAC addresses of next hops.
681 + *
682 + * @param ipAddress next hop router IP address, for which we sent ARP
683 + * request out
684 + * @param macAddress MAC address which is relative to the ipAddress
685 + */
686 + //@Override
687 + // TODO change name
688 + public void arpResponse(IpAddress ipAddress, MacAddress macAddress) {
689 + log.debug("Received ARP response: {} => {}", ipAddress, macAddress);
690 +
691 + // We synchronize on this to prevent changes to the InvertedRadixTree
692 + // while we're pushing intent. If the InvertedRadixTree changes, the
693 + // InvertedRadixTree and intent could get out of sync.
694 + synchronized (this) {
695 +
696 + Set<RouteEntry> routesToPush =
697 + routesWaitingOnArp.removeAll(ipAddress);
698 +
699 + for (RouteEntry routeEntry : routesToPush) {
700 + // These will always be adds
701 + IpPrefix prefix = routeEntry.prefix();
702 + String binaryString = RouteEntry.createBinaryString(prefix);
703 + RouteEntry foundRouteEntry =
704 + bgpRoutes.getValueForExactKey(binaryString);
705 + if (foundRouteEntry != null &&
706 + foundRouteEntry.nextHop().equals(routeEntry.nextHop())) {
707 + log.debug("Pushing prefix {} next hop {}",
708 + routeEntry.prefix(), routeEntry.nextHop());
709 + // We only push prefix flows if the prefix is still in the
710 + // InvertedRadixTree and the next hop is the same as our
711 + // update.
712 + // The prefix could have been removed while we were waiting
713 + // for the ARP, or the next hop could have changed.
714 + addRouteIntentToNextHop(prefix, ipAddress, macAddress);
715 + } else {
716 + log.debug("Received ARP response, but {}/{} is no longer in"
717 + + " InvertedRadixTree", routeEntry.prefix(),
718 + routeEntry.nextHop());
719 + }
720 + }
721 + }
722 + }
723 +
724 + /**
725 + * Gets the SDN-IP routes.
726 + *
727 + * @return the SDN-IP routes
728 + */
729 + public Collection<RouteEntry> getRoutes() {
730 + Iterator<KeyValuePair<RouteEntry>> it =
731 + bgpRoutes.getKeyValuePairsForKeysStartingWith("").iterator();
732 +
733 + List<RouteEntry> routes = new LinkedList<>();
734 +
735 + while (it.hasNext()) {
736 + KeyValuePair<RouteEntry> entry = it.next();
737 + routes.add(entry.getValue());
738 + }
739 +
740 + return routes;
741 + }
742 +
743 + /**
744 + * Generates a new unique intent ID.
745 + *
746 + * @return the new intent ID.
747 + */
748 + private IntentId nextIntentId() {
749 + return new IntentId(intentId++);
750 + }
751 +
752 + /**
753 + * Listener for host events.
754 + */
755 + class InternalHostListener implements HostListener {
756 + @Override
757 + public void event(HostEvent event) {
758 + if (event.type() == HostEvent.Type.HOST_ADDED ||
759 + event.type() == HostEvent.Type.HOST_UPDATED) {
760 + Host host = event.subject();
761 + for (IpPrefix ip : host.ipAddresses()) {
762 + arpResponse(ip.toIpAddress(), host.mac());
763 + }
764 + }
765 + }
766 + }
767 +}
...@@ -9,7 +9,10 @@ import org.apache.felix.scr.annotations.Reference; ...@@ -9,7 +9,10 @@ import org.apache.felix.scr.annotations.Reference;
9 import org.apache.felix.scr.annotations.ReferenceCardinality; 9 import org.apache.felix.scr.annotations.ReferenceCardinality;
10 import org.onlab.onos.net.host.HostService; 10 import org.onlab.onos.net.host.HostService;
11 import org.onlab.onos.net.intent.IntentService; 11 import org.onlab.onos.net.intent.IntentService;
12 +import org.onlab.onos.sdnip.RouteUpdate.Type;
12 import org.onlab.onos.sdnip.config.SdnIpConfigReader; 13 import org.onlab.onos.sdnip.config.SdnIpConfigReader;
14 +import org.onlab.packet.IpAddress;
15 +import org.onlab.packet.IpPrefix;
13 import org.slf4j.Logger; 16 import org.slf4j.Logger;
14 17
15 /** 18 /**
...@@ -28,6 +31,7 @@ public class SdnIp { ...@@ -28,6 +31,7 @@ public class SdnIp {
28 31
29 private SdnIpConfigReader config; 32 private SdnIpConfigReader config;
30 private PeerConnectivity peerConnectivity; 33 private PeerConnectivity peerConnectivity;
34 + private Router router;
31 35
32 @Activate 36 @Activate
33 protected void activate() { 37 protected void activate() {
...@@ -41,6 +45,14 @@ public class SdnIp { ...@@ -41,6 +45,14 @@ public class SdnIp {
41 peerConnectivity = new PeerConnectivity(config, interfaceService, intentService); 45 peerConnectivity = new PeerConnectivity(config, interfaceService, intentService);
42 peerConnectivity.start(); 46 peerConnectivity.start();
43 47
48 + router = new Router(intentService, hostService, config, interfaceService);
49 + router.start();
50 +
51 + // TODO need to disable link discovery on external ports
52 +
53 + router.update(new RouteUpdate(Type.UPDATE, new RouteEntry(
54 + IpPrefix.valueOf("172.16.20.0/24"),
55 + IpAddress.valueOf("192.168.10.1"))));
44 } 56 }
45 57
46 @Deactivate 58 @Deactivate
......
1 +package org.onlab.onos.sdnip;
2 +
3 +import static org.hamcrest.Matchers.is;
4 +import static org.hamcrest.Matchers.not;
5 +import static org.junit.Assert.assertThat;
6 +
7 +import org.junit.Test;
8 +import org.onlab.packet.IpAddress;
9 +import org.onlab.packet.IpPrefix;
10 +
11 +/**
12 + * Unit tests for the RouteEntry class.
13 + */
14 +public class RouteEntryTest {
15 + /**
16 + * Tests valid class constructor.
17 + */
18 + @Test
19 + public void testConstructor() {
20 + IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
21 + IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
22 +
23 + RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
24 + assertThat(routeEntry.toString(),
25 + is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
26 + }
27 +
28 + /**
29 + * Tests invalid class constructor for null IPv4 prefix.
30 + */
31 + @Test(expected = NullPointerException.class)
32 + public void testInvalidConstructorNullPrefix() {
33 + IpPrefix prefix = null;
34 + IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
35 +
36 + new RouteEntry(prefix, nextHop);
37 + }
38 +
39 + /**
40 + * Tests invalid class constructor for null IPv4 next-hop.
41 + */
42 + @Test(expected = NullPointerException.class)
43 + public void testInvalidConstructorNullNextHop() {
44 + IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
45 + IpAddress nextHop = null;
46 +
47 + new RouteEntry(prefix, nextHop);
48 + }
49 +
50 + /**
51 + * Tests getting the fields of a route entry.
52 + */
53 + @Test
54 + public void testGetFields() {
55 + IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
56 + IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
57 +
58 + RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
59 + assertThat(routeEntry.prefix(), is(prefix));
60 + assertThat(routeEntry.nextHop(), is(nextHop));
61 + }
62 +
63 + /**
64 + * Tests creating a binary string from IPv4 prefix.
65 + */
66 + @Test
67 + public void testCreateBinaryString() {
68 + IpPrefix prefix;
69 +
70 + prefix = IpPrefix.valueOf("0.0.0.0/0");
71 + assertThat(RouteEntry.createBinaryString(prefix), is(""));
72 +
73 + prefix = IpPrefix.valueOf("192.168.166.0/22");
74 + assertThat(RouteEntry.createBinaryString(prefix),
75 + is("1100000010101000101001"));
76 +
77 + prefix = IpPrefix.valueOf("192.168.166.0/23");
78 + assertThat(RouteEntry.createBinaryString(prefix),
79 + is("11000000101010001010011"));
80 +
81 + prefix = IpPrefix.valueOf("192.168.166.0/24");
82 + assertThat(RouteEntry.createBinaryString(prefix),
83 + is("110000001010100010100110"));
84 +
85 + prefix = IpPrefix.valueOf("130.162.10.1/25");
86 + assertThat(RouteEntry.createBinaryString(prefix),
87 + is("1000001010100010000010100"));
88 +
89 + prefix = IpPrefix.valueOf("255.255.255.255/32");
90 + assertThat(RouteEntry.createBinaryString(prefix),
91 + is("11111111111111111111111111111111"));
92 + }
93 +
94 + /**
95 + * Tests equality of {@link RouteEntry}.
96 + */
97 + @Test
98 + public void testEquality() {
99 + IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
100 + IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
101 + RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
102 +
103 + IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/24");
104 + IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
105 + RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
106 +
107 + assertThat(routeEntry1, is(routeEntry2));
108 + }
109 +
110 + /**
111 + * Tests non-equality of {@link RouteEntry}.
112 + */
113 + @Test
114 + public void testNonEquality() {
115 + IpPrefix prefix1 = IpPrefix.valueOf("1.2.3.0/24");
116 + IpAddress nextHop1 = IpAddress.valueOf("5.6.7.8");
117 + RouteEntry routeEntry1 = new RouteEntry(prefix1, nextHop1);
118 +
119 + IpPrefix prefix2 = IpPrefix.valueOf("1.2.3.0/25"); // Different
120 + IpAddress nextHop2 = IpAddress.valueOf("5.6.7.8");
121 + RouteEntry routeEntry2 = new RouteEntry(prefix2, nextHop2);
122 +
123 + IpPrefix prefix3 = IpPrefix.valueOf("1.2.3.0/24");
124 + IpAddress nextHop3 = IpAddress.valueOf("5.6.7.9"); // Different
125 + RouteEntry routeEntry3 = new RouteEntry(prefix3, nextHop3);
126 +
127 + assertThat(routeEntry1, is(not(routeEntry2)));
128 + assertThat(routeEntry1, is(not(routeEntry3)));
129 + }
130 +
131 + /**
132 + * Tests object string representation.
133 + */
134 + @Test
135 + public void testToString() {
136 + IpPrefix prefix = IpPrefix.valueOf("1.2.3.0/24");
137 + IpAddress nextHop = IpAddress.valueOf("5.6.7.8");
138 + RouteEntry routeEntry = new RouteEntry(prefix, nextHop);
139 +
140 + assertThat(routeEntry.toString(),
141 + is("RouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8}"));
142 + }
143 +}
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
30 30
31 <bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle> 31 <bundle>mvn:org.codehaus.jackson/jackson-core-asl/1.9.13</bundle>
32 <bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle> 32 <bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
33 + <bundle>mvn:org.onlab.onos/onlab-thirdparty/1.0.0-SNAPSHOT</bundle>
33 </feature> 34 </feature>
34 35
35 <feature name="onos-thirdparty-web" version="1.0.0" 36 <feature name="onos-thirdparty-web" version="1.0.0"
......
...@@ -107,7 +107,13 @@ ...@@ -107,7 +107,13 @@
107 </dependency> 107 </dependency>
108 108
109 <dependency> 109 <dependency>
110 - <groupId>commons-lang</groupId> 110 + <groupId>com.googlecode.concurrent-trees</groupId>
111 + <artifactId>concurrent-trees</artifactId>
112 + <version>2.4.0</version>
113 + </dependency>
114 +
115 + <dependency>
116 + <groupId>commons-lang</groupId>
111 <artifactId>commons-lang</artifactId> 117 <artifactId>commons-lang</artifactId>
112 <version>2.6</version> 118 <version>2.6</version>
113 </dependency> 119 </dependency>
...@@ -266,6 +272,13 @@ ...@@ -266,6 +272,13 @@
266 <artifactId>onos-of-api</artifactId> 272 <artifactId>onos-of-api</artifactId>
267 <version>${project.version}</version> 273 <version>${project.version}</version>
268 </dependency> 274 </dependency>
275 +
276 + <dependency>
277 + <groupId>org.onlab.onos</groupId>
278 + <artifactId>onlab-thirdparty</artifactId>
279 + <version>${project.version}</version>
280 + </dependency>
281 +
269 <dependency> 282 <dependency>
270 <groupId>org.onlab.onos</groupId> 283 <groupId>org.onlab.onos</groupId>
271 <artifactId>onos-of-api</artifactId> 284 <artifactId>onos-of-api</artifactId>
......
...@@ -191,6 +191,15 @@ public final class IpAddress { ...@@ -191,6 +191,15 @@ public final class IpAddress {
191 } 191 }
192 192
193 /** 193 /**
194 + * Converts the IP address to a /32 IP prefix.
195 + *
196 + * @return the new IP prefix
197 + */
198 + public IpPrefix toPrefix() {
199 + return IpPrefix.valueOf(octets, MAX_INET_MASK);
200 + }
201 +
202 + /**
194 * Helper for computing the mask value from CIDR. 203 * Helper for computing the mask value from CIDR.
195 * 204 *
196 * @return an integer bitmask 205 * @return an integer bitmask
......
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5 + <modelVersion>4.0.0</modelVersion>
6 +
7 + <parent>
8 + <groupId>org.onlab.onos</groupId>
9 + <artifactId>onlab-utils</artifactId>
10 + <version>1.0.0-SNAPSHOT</version>
11 + <relativePath>../pom.xml</relativePath>
12 + </parent>
13 +
14 + <artifactId>onlab-thirdparty</artifactId>
15 + <packaging>bundle</packaging>
16 +
17 + <description>ONLab third-party dependencies</description>
18 +
19 + <dependencies>
20 + <dependency>
21 + <groupId>com.googlecode.concurrent-trees</groupId>
22 + <artifactId>concurrent-trees</artifactId>
23 + <version>2.4.0</version>
24 + </dependency>
25 + </dependencies>
26 +
27 + <build>
28 + <plugins>
29 + <plugin>
30 + <groupId>org.apache.maven.plugins</groupId>
31 + <artifactId>maven-shade-plugin</artifactId>
32 + <version>2.3</version>
33 + <configuration>
34 + <filters>
35 + <filter>
36 + <artifact>com.googlecode.concurrent-trees:concurrent-trees</artifact>
37 + <includes>
38 + <include>com/googlecode/**</include>
39 + </includes>
40 +
41 + </filter>
42 + <filter>
43 + <artifact>com.google.guava:guava</artifact>
44 + <excludes>
45 + <exclude>**</exclude>
46 + </excludes>
47 + </filter>
48 + </filters>
49 + </configuration>
50 + <executions>
51 + <execution>
52 + <phase>package</phase>
53 + <goals>
54 + <goal>shade</goal>
55 + </goals>
56 + </execution>
57 + </executions>
58 + </plugin>
59 + <plugin>
60 + <groupId>org.apache.felix</groupId>
61 + <artifactId>maven-bundle-plugin</artifactId>
62 + <configuration>
63 + <instructions>
64 + <Export-Package>
65 + com.googlecode.concurrenttrees.*
66 + </Export-Package>
67 + </instructions>
68 + </configuration>
69 + </plugin>
70 + </plugins>
71 + </build>
72 +
73 +</project>
1 +package org.onlab.thirdparty;
2 +
3 +
4 +/**
5 + * Empty class required to get the onlab-thirdparty module to build properly.
6 + * <p/>
7 + * TODO Figure out how to remove this.
8 + */
9 +public class OnlabThirdparty {
10 +
11 +}