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>
<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>
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}