BUG-2383: add BestPathSelector and related classes 35/15935/8
authorRobert Varga <rovarga@cisco.com>
Mon, 2 Mar 2015 15:32:04 +0000 (16:32 +0100)
committerDana Kutenicsova <dkutenic@cisco.com>
Thu, 5 Mar 2015 08:49:51 +0000 (09:49 +0100)
Introduce a utility BestPath class along with a builder-style selector.
The selector can be run incrementally by initiating the state from
previous selection run, then feeding all candidates and finally
extracting the best-matching path.

Change-Id: I34aab7f0c2ab4b917597a77d54508f5d93e6a8c9
Signed-off-by: Robert Varga <rovarga@cisco.com>
bgp/rib-impl/pom.xml
bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPath.java [new file with mode: 0644]
bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathSelector.java [new file with mode: 0644]
bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathState.java [new file with mode: 0644]
bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/RouterIds.java [new file with mode: 0644]

index 1baf98207a631e50f6d38647493d58dd919bc969..558e9fa32099112afd05ed1242f5607154bcbd6b 100644 (file)
             <groupId>org.opendaylight.yangtools</groupId>
             <artifactId>yang-data-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>yang-model-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.opendaylight.yangtools</groupId>
             <artifactId>yang-data-impl</artifactId>
             <artifactId>binding-generator-impl</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.opendaylight.yangtools</groupId>
-            <artifactId>yang-model-api</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.opendaylight.yangtools</groupId>
             <artifactId>yang-parser-api</artifactId>
diff --git a/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPath.java b/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPath.java
new file mode 100644 (file)
index 0000000..b9d0177
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.bgp.rib.impl;
+
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.UnsignedInteger;
+import javax.annotation.Nonnull;
+
+final class BestPath {
+    private final UnsignedInteger routerId;
+    private final BestPathState state;
+
+    BestPath(@Nonnull final UnsignedInteger routerId, @Nonnull final BestPathState state) {
+        this.routerId = Preconditions.checkNotNull(routerId);
+        this.state = Preconditions.checkNotNull(state);
+    }
+
+    UnsignedInteger getRouterId() {
+        return routerId;
+    }
+
+    BestPathState getState() {
+        return state;
+    }
+}
diff --git a/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathSelector.java b/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathSelector.java
new file mode 100644 (file)
index 0000000..40fad6a
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.bgp.rib.impl;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.UnsignedInteger;
+import java.util.Collection;
+import javax.annotation.Nonnull;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.OriginatorId;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.types.rev130919.BgpOrigin;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
+
+final class BestPathSelector {
+    private static final Collection<PathArgument> ORIGINATOR_ID = ImmutableList.<PathArgument>of(new NodeIdentifier(OriginatorId.QNAME), new NodeIdentifier(QName.create(OriginatorId.QNAME, "originator")));
+
+    private final Long ourAs;
+    private UnsignedInteger bestOriginatorId = null;
+    private UnsignedInteger bestRouterId = null;
+    private BestPathState bestState = null;
+
+    BestPathSelector(final Long ourAs) {
+        this.ourAs = Preconditions.checkNotNull(ourAs);
+    }
+
+    void processPath(final UnsignedInteger routerId, final ContainerNode attrs) {
+        Preconditions.checkNotNull(routerId, "Router ID may not be null");
+
+        // Consider only non-null attributes
+        if (attrs != null) {
+            /*
+             * RFC 4456 mandates the use of Originator IDs instead of Router ID for
+             * selection purposes.
+             */
+            final Optional<NormalizedNode<?, ?>> maybeOriginatorId = NormalizedNodes.findNode(attrs, ORIGINATOR_ID);
+            final UnsignedInteger originatorId;
+            if (maybeOriginatorId.isPresent()) {
+                originatorId = RouterIds.routerIdForAddress(((LeafNode<String>)maybeOriginatorId.get()).getValue());
+            } else {
+                originatorId = routerId;
+            }
+
+            /*
+             * Store the new details if we have nothing stored or when the selection algorithm indicates new details
+             * are better.
+             */
+            final BestPathState state = new BestPathState(attrs);
+            if (this.bestOriginatorId == null || selectPath(originatorId, state)) {
+                this.bestOriginatorId = originatorId;
+                this.bestRouterId = routerId;
+                this.bestState = state;
+            }
+        }
+    }
+
+    BestPath result() {
+        return this.bestRouterId == null ? null : new BestPath(this.bestRouterId, this.bestState);
+    }
+
+    /**
+     * Chooses best route according to BGP best path selection.
+     *
+     * @param originatorId of the new route
+     * @param state attributes of the new route
+     * @return true if the existing path is better, false if the new path is better
+     */
+    private boolean selectPath(final @Nonnull UnsignedInteger originatorId, final @Nonnull BestPathState state) {
+        // 1. prefer path with accessible nexthop
+        // - we assume that all nexthops are accessible
+
+        /*
+         * 2. prefer path with higher LOCAL_PREF
+         *
+         * FIXME: for eBGP cases (when the LOCAL_PREF is missing), we should assign a policy-based preference
+         *        before we ever get here.
+         */
+        if (bestState.getLocalPref() == null && state.getLocalPref() != null) {
+            return true;
+        }
+        if (bestState.getLocalPref() != null && state.getLocalPref() == null) {
+            return false;
+        }
+        if (state.getLocalPref() != null) {
+            if (state.getLocalPref() > this.bestState.getLocalPref()) {
+                return true;
+            }
+        }
+
+        // 3. prefer learned path
+        // - we assume that all paths are learned
+
+        // 4. prefer the path with the shortest AS_PATH.
+        if (this.bestState.getAsPathLength() > state.getAsPathLength()) {
+            return true;
+        }
+
+        // 5. prefer the path with the lowest origin type
+        // - IGP is lower than Exterior Gateway Protocol (EGP), and EGP is lower than INCOMPLETE
+        if (!this.bestState.getOrigin().equals(state.getOrigin())) {
+            final BgpOrigin bo = this.bestState.getOrigin();
+            final BgpOrigin no = state.getOrigin();
+
+            // This trick relies on the order in which the values are declared in the model.
+            if (no.ordinal() < bo.ordinal()) {
+                return true;
+            }
+        }
+
+        // FIXME: we should be able to cache the best AS
+        final Long bestAs = this.bestState.getPeerAs();
+        final Long newAs = state.getPeerAs();
+
+        /*
+         * Checks 6 and 7 are mutually-exclusive, as MEDs are comparable
+         * only when the routes originated from the same AS. On the other
+         * hand, when they are from the same AS, they are in the same iBGP/eBGP
+         * relationship.
+         *
+         */
+        if (bestAs.equals(newAs)) {
+            // 6. prefer the path with the lowest multi-exit discriminator (MED)
+            if (this.bestState.getMultiExitDisc() != null || state.getMultiExitDisc() != null) {
+                final Long bmed = this.bestState.getMultiExitDisc();
+                final Long nmed = state.getMultiExitDisc();
+                if (nmed < bmed) {
+                    return true;
+                }
+            }
+        } else {
+            /*
+             * 7. prefer eBGP over iBGP paths
+             *
+             * EBGP is peering between two different AS, whereas IBGP is between same AS (Autonomous System),
+             * so we just compare the AS numbers to our AS.
+             *
+             * FIXME: we should know this information from the peer directly.
+             */
+            if (!this.ourAs.equals(bestAs) && this.ourAs.equals(newAs)) {
+                return true;
+            }
+        }
+
+        // 8. Prefer the path with the lowest IGP metric to the BGP next hop.
+        // - no next hop metric is advertised
+
+        /*
+         * 9. When both paths are external, prefer the path that was received first (the oldest one).
+         *
+         * FIXME: we do not want to store an explicit timer for each set due to performance/memory
+         *        constraints. Our caller has the information about which attributes have changed
+         *        since the selection process has ran last time, which may be a good enough approximation,
+         *        but its properties need to be analyzed.
+         */
+
+        /*
+         * 10. Prefer the route that comes from the BGP router with the lowest router ID.
+         *
+         * This is normally guaranteed by the iteration order of our caller, which runs selection
+         * in the order of increasing router ID, but RFC-4456 Route Reflection throws a wrench into that.
+         *
+         * With RFC-5004, this gets a bit easier, because it completely eliminates step f) and later :-)
+         *
+         * RFC-5004 states that this algorithm should end here and select existing path over new path in the
+         * best path selection process. Benefits are listed in the RFC: @see http://tools.ietf.org/html/rfc500
+         * - This algorithm SHOULD NOT be applied when either path is from a BGP Confederation peer.
+         *  - not applicable, we don't deal with confederation peers
+         * - The algorithm SHOULD NOT be applied when both paths are from peers with an identical BGP identifier
+         *   (i.e., there exist parallel BGP sessions between two BGP speakers).
+         *  - not applicable, BUG-2631 prevents parallel sessions to be created.
+         */
+        return true;
+    }
+}
diff --git a/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathState.java b/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/BestPathState.java
new file mode 100644 (file)
index 0000000..eeecacc
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.bgp.rib.impl;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev100924.AsNumber;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.path.attributes.AsPath;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.path.attributes.LocalPref;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.path.attributes.MultiExitDisc;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.path.attributes.Origin;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.message.rev130919.path.attributes.as.path.Segments;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.types.rev130919.BgpOrigin;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.types.rev130919.as.path.segment.c.segment.AListCase;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.types.rev130919.as.path.segment.c.segment.ASetCase;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
+
+@NotThreadSafe
+final class BestPathState {
+    private static final Collection<PathArgument> AS_PATH = ImmutableList.<PathArgument>of(new NodeIdentifier(AsPath.QNAME), new NodeIdentifier(Segments.QNAME));
+    private static final Collection<PathArgument> LOCAL_PREF = ImmutableList.<PathArgument>of(new NodeIdentifier(LocalPref.QNAME), new NodeIdentifier(QName.create(LocalPref.QNAME, "pref")));
+    private static final Collection<PathArgument> MED = ImmutableList.<PathArgument>of(new NodeIdentifier(MultiExitDisc.QNAME), new NodeIdentifier(QName.create(MultiExitDisc.QNAME, "med")));
+    private static final Collection<PathArgument> ORIGIN = ImmutableList.<PathArgument>of(new NodeIdentifier(Origin.QNAME), new NodeIdentifier(QName.create(Origin.QNAME, "value")));
+
+    private final ContainerNode attributes;
+    private Long localPref;
+    private Long multiExitDisc;
+    private BgpOrigin origin;
+    private final Long peerAs = 0L;
+    private final int asPathLength = 0;
+    private boolean resolved;
+
+    BestPathState(final ContainerNode attributes) {
+        this.attributes = Preconditions.checkNotNull(attributes);
+    }
+
+    private void resolveValues() {
+        if (resolved) {
+            return;
+        }
+
+        final Optional<NormalizedNode<?, ?>> maybeLocalPref = NormalizedNodes.findNode(attributes, LOCAL_PREF);
+        if (maybeLocalPref.isPresent()) {
+            localPref = (Long) ((LeafNode<?>)maybeLocalPref.get()).getValue();
+        } else {
+            localPref = null;
+        }
+
+        final Optional<NormalizedNode<?, ?>> maybeMultiExitDisc = NormalizedNodes.findNode(attributes, MED);
+        if (maybeMultiExitDisc.isPresent()) {
+            multiExitDisc = (Long) ((LeafNode<?>)maybeMultiExitDisc.get()).getValue();
+        } else {
+            multiExitDisc = null;
+        }
+
+        final Optional<NormalizedNode<?, ?>> maybeOrigin = NormalizedNodes.findNode(attributes, ORIGIN);
+        if (maybeOrigin.isPresent()) {
+            final String originStr = (String) ((LeafNode<?>)maybeOrigin.get()).getValue();
+            switch (originStr) {
+            case "igp":
+                origin = BgpOrigin.Igp;
+                break;
+            case "egp":
+                origin = BgpOrigin.Egp;
+                break;
+            case "incomplete":
+                origin = BgpOrigin.Incomplete;
+                break;
+            default:
+                throw new IllegalArgumentException("Unhandleed origin value " + originStr);
+            }
+        } else {
+            origin = null;
+        }
+
+        final Optional<NormalizedNode<?, ?>> maybeSegments = NormalizedNodes.findNode(attributes, AS_PATH);
+        if (maybeSegments.isPresent()) {
+            final UnkeyedListNode segments = (UnkeyedListNode) maybeSegments.get();
+
+            if (segments.getSize() != 0) {
+                // FIXME: peer AS number
+
+                // FIXME: asPathLength = countAsPath(this.bestState.getAsPath().getSegments());
+                boolean haveSegment;
+                for (UnkeyedListEntryNode s : segments.getValue()) {
+
+                }
+            }
+        }
+
+        resolved = true;
+    }
+
+    Long getLocalPref() {
+        resolveValues();
+        return localPref;
+    }
+
+    Long getMultiExitDisc() {
+        resolveValues();
+        return multiExitDisc;
+    }
+
+    BgpOrigin getOrigin() {
+        resolveValues();
+        return origin;
+    }
+
+    Long getPeerAs() {
+        resolveValues();
+        return peerAs;
+    }
+
+    int getAsPathLength() {
+        resolveValues();
+        return asPathLength;
+    }
+
+    private static int countAsPath(final List<Segments> segments) {
+        // an AS_SET counts as 1, no matter how many ASs are in the set.
+        int count = 0;
+        boolean setPresent = false;
+        for (final Segments s : segments) {
+            if (s.getCSegment() instanceof ASetCase) {
+                setPresent = true;
+            } else {
+                final AListCase list = (AListCase) s.getCSegment();
+                count += list.getAList().getAsSequence().size();
+            }
+        }
+        return (setPresent) ? count + 1 : count;
+    }
+
+    private static AsNumber getPeerAs(final List<Segments> segments) {
+        if (segments.isEmpty()) {
+            return null;
+        }
+
+        final AListCase first = (AListCase) segments.get(0).getCSegment();
+        return first.getAList().getAsSequence().get(0).getAs();
+    }
+
+}
diff --git a/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/RouterIds.java b/bgp/rib-impl/src/main/java/org/opendaylight/protocol/bgp/rib/impl/RouterIds.java
new file mode 100644 (file)
index 0000000..ddaeb43
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.bgp.rib.impl;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.net.InetAddresses;
+import com.google.common.primitives.UnsignedInteger;
+import javax.annotation.Nonnull;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev130925.PeerId;
+
+final class RouterIds {
+    private static final LoadingCache<String, UnsignedInteger> ROUTER_IDS = CacheBuilder.newBuilder().weakValues().build(new CacheLoader<String, UnsignedInteger>() {
+        @Override
+        public UnsignedInteger load(final String key) {
+            return UnsignedInteger.fromIntBits(InetAddresses.coerceToInteger(InetAddresses.forString(key)));
+        }
+    });
+    private static final LoadingCache<PeerId, UnsignedInteger> BGP_ROUTER_IDS = CacheBuilder.newBuilder().weakValues().build(new CacheLoader<PeerId, UnsignedInteger>() {
+        @Override
+        public UnsignedInteger load(final PeerId key) {
+            return routerIdForAddress(key.getValue().substring(BGP_PREFIX.length()));
+        }
+    });
+    private static final String BGP_PREFIX = "bgp://";
+
+    private RouterIds() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Get a router ID in unsigned integer format from an Ipv4Address. This implementation uses an internal
+     * cache, so the objects can be expected to perform quickly when compared with equals and similar.
+     *
+     * @param address Router ID as a dotted-quad
+     * @return Router ID as an {@link UnsignedInteger}
+     */
+    public static UnsignedInteger routerIdForAddress(final @Nonnull String address) {
+        return ROUTER_IDS.getUnchecked(address);
+    }
+
+    public static UnsignedInteger routerIdForPeerId(final @Nonnull PeerId peerId) {
+        Preconditions.checkArgument(peerId.getValue().startsWith(BGP_PREFIX), "Unhandled peer ID %s", peerId);
+        return BGP_ROUTER_IDS.getUnchecked(peerId);
+    }
+}