SdnIpFib.java 12.3 KB
/*
 * Copyright 2015-present Open Networking Laboratory
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.onosproject.sdnip;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IpAddress;
import org.onlab.packet.IpPrefix;
import org.onlab.packet.MacAddress;
import org.onlab.packet.VlanId;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.incubator.net.intf.Interface;
import org.onosproject.incubator.net.intf.InterfaceEvent;
import org.onosproject.incubator.net.intf.InterfaceListener;
import org.onosproject.incubator.net.intf.InterfaceService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.flow.DefaultTrafficSelector;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.intent.Constraint;
import org.onosproject.net.intent.Key;
import org.onosproject.net.intent.MultiPointToSinglePointIntent;
import org.onosproject.net.intent.constraint.PartialFailureConstraint;
import org.onosproject.routing.FibListener;
import org.onosproject.routing.FibUpdate;
import org.onosproject.routing.IntentSynchronizationService;
import org.onosproject.routing.RoutingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * FIB component of SDN-IP.
 */
@Component(immediate = true, enabled = false)
public class SdnIpFib {
    private Logger log = LoggerFactory.getLogger(getClass());

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected InterfaceService interfaceService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected IntentSynchronizationService intentSynchronizer;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected CoreService coreService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected RoutingService routingService;

    private final InternalFibListener fibListener = new InternalFibListener();
    private final InternalInterfaceListener interfaceListener = new InternalInterfaceListener();

    private static final int PRIORITY_OFFSET = 100;
    private static final int PRIORITY_MULTIPLIER = 5;
    protected static final ImmutableList<Constraint> CONSTRAINTS
            = ImmutableList.of(new PartialFailureConstraint());

    private final Map<IpPrefix, MultiPointToSinglePointIntent> routeIntents
            = new ConcurrentHashMap<>();

    private ApplicationId appId;

    @Activate
    public void activate() {
        appId = coreService.getAppId(SdnIp.SDN_IP_APP);

        interfaceService.addListener(interfaceListener);

        routingService.addFibListener(fibListener);
        routingService.start();
    }

    @Deactivate
    public void deactivate() {
        interfaceService.removeListener(interfaceListener);
        // TODO remove listener
        routingService.stop();
    }

    private void update(Collection<FibUpdate> updates, Collection<FibUpdate> withdraws) {
        int submitCount = 0, withdrawCount = 0;
        //
        // NOTE: Semantically, we MUST withdraw existing intents before
        // submitting new intents.
        //
        synchronized (this) {
            MultiPointToSinglePointIntent intent;

            //
            // Prepare the Intent batch operations for the intents to withdraw
            //
            for (FibUpdate withdraw : withdraws) {
                checkArgument(withdraw.type() == FibUpdate.Type.DELETE,
                        "FibUpdate with wrong type in withdraws list");

                IpPrefix prefix = withdraw.entry().prefix();
                intent = routeIntents.remove(prefix);
                if (intent == null) {
                    log.trace("SDN-IP No intent in routeIntents to delete " +
                            "for prefix: {}", prefix);
                    continue;
                }
                intentSynchronizer.withdraw(intent);
                withdrawCount++;
            }

            //
            // Prepare the Intent batch operations for the intents to submit
            //
            for (FibUpdate update : updates) {
                checkArgument(update.type() == FibUpdate.Type.UPDATE,
                        "FibUpdate with wrong type in updates list");

                IpPrefix prefix = update.entry().prefix();
                intent = generateRouteIntent(prefix, update.entry().nextHopIp(),
                        update.entry().nextHopMac());

                if (intent == null) {
                    // This preserves the old semantics - if an intent can't be
                    // generated, we don't do anything with that prefix. But
                    // perhaps we should withdraw the old intent anyway?
                    continue;
                }

                routeIntents.put(prefix, intent);
                intentSynchronizer.submit(intent);
                submitCount++;
            }

            log.debug("SDN-IP submitted {}/{}, withdrew = {}/{}", submitCount,
                    updates.size(), withdrawCount, withdraws.size());
        }
    }

    /**
     * Generates a route intent for a prefix, the next hop IP address, and
     * the next hop MAC address.
     * <p/>
     * This method will find the egress interface for the intent.
     * Intent will match dst IP prefix and rewrite dst MAC address at all other
     * border switches, then forward packets according to dst MAC address.
     *
     * @param prefix            IP prefix of the route to add
     * @param nextHopIpAddress  IP address of the next hop
     * @param nextHopMacAddress MAC address of the next hop
     * @return the generated intent, or null if no intent should be submitted
     */
    private MultiPointToSinglePointIntent generateRouteIntent(
            IpPrefix prefix,
            IpAddress nextHopIpAddress,
            MacAddress nextHopMacAddress) {

        // Find the attachment point (egress interface) of the next hop
        Interface egressInterface =
                interfaceService.getMatchingInterface(nextHopIpAddress);
        if (egressInterface == null) {
            log.warn("No outgoing interface found for {}",
                    nextHopIpAddress);
            return null;
        }

        // Generate the intent itself
        Set<ConnectPoint> ingressPorts = new HashSet<>();
        ConnectPoint egressPort = egressInterface.connectPoint();
        log.debug("Generating intent for prefix {}, next hop mac {}",
                prefix, nextHopMacAddress);

        for (Interface intf : interfaceService.getInterfaces()) {
            // TODO this should be only peering interfaces
            if (!intf.connectPoint().equals(egressInterface.connectPoint())) {
                ConnectPoint srcPort = intf.connectPoint();
                ingressPorts.add(srcPort);
            }
        }

        // Match the destination IP prefix at the first hop
        TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
        if (prefix.isIp4()) {
            selector.matchEthType(Ethernet.TYPE_IPV4);
            // if it is default route, then we do not need match destination
            // IP address
            if (prefix.prefixLength() != 0) {
                selector.matchIPDst(prefix);
            }
        } else {
            selector.matchEthType(Ethernet.TYPE_IPV6);
            // if it is default route, then we do not need match destination
            // IP address
            if (prefix.prefixLength() != 0) {
                selector.matchIPv6Dst(prefix);
            }

        }

        // Rewrite the destination MAC address
        TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder()
                .setEthDst(nextHopMacAddress);
        if (!egressInterface.vlan().equals(VlanId.NONE)) {
            treatment.setVlanId(egressInterface.vlan());
            // If we set VLAN ID, we have to make sure a VLAN tag exists.
            // TODO support no VLAN -> VLAN routing
            selector.matchVlanId(VlanId.ANY);
        }

        int priority =
                prefix.prefixLength() * PRIORITY_MULTIPLIER + PRIORITY_OFFSET;
        Key key = Key.of(prefix.toString(), appId);
        return MultiPointToSinglePointIntent.builder()
                .appId(appId)
                .key(key)
                .selector(selector.build())
                .treatment(treatment.build())
                .ingressPoints(ingressPorts)
                .egressPoint(egressPort)
                .priority(priority)
                .constraints(CONSTRAINTS)
                .build();
    }

    private void updateInterface(Interface intf) {
        synchronized (this) {
            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry : routeIntents.entrySet()) {
                MultiPointToSinglePointIntent intent = entry.getValue();
                Set<ConnectPoint> ingress = Sets.newHashSet(intent.ingressPoints());
                ingress.add(intf.connectPoint());

                MultiPointToSinglePointIntent newIntent =
                        MultiPointToSinglePointIntent.builder(intent)
                                .ingressPoints(ingress)
                                .build();

                routeIntents.put(entry.getKey(), newIntent);
                intentSynchronizer.submit(newIntent);
            }
        }
    }

    private void removeInterface(Interface intf) {
        synchronized (this) {
            for (Map.Entry<IpPrefix, MultiPointToSinglePointIntent> entry : routeIntents.entrySet()) {
                MultiPointToSinglePointIntent intent = entry.getValue();
                if (intent.egressPoint().equals(intf.connectPoint())) {
                    // This intent just lost its head. Remove it and let
                    // higher layer routing reroute.
                    intentSynchronizer.withdraw(routeIntents.remove(entry.getKey()));
                } else {
                    if (intent.ingressPoints().contains(intf.connectPoint())) {

                        Set<ConnectPoint> ingress = Sets.newHashSet(intent.ingressPoints());
                        ingress.remove(intf.connectPoint());

                        MultiPointToSinglePointIntent newIntent =
                                MultiPointToSinglePointIntent.builder(intent)
                                .ingressPoints(ingress)
                                .build();

                        routeIntents.put(entry.getKey(), newIntent);
                        intentSynchronizer.submit(newIntent);
                    }
                }
            }
        }
    }

    private class InternalFibListener implements FibListener {
        @Override
        public void update(Collection<FibUpdate> updates, Collection<FibUpdate> withdraws) {
            SdnIpFib.this.update(updates, withdraws);
        }
    }

    private class InternalInterfaceListener implements InterfaceListener {

        @Override
        public void event(InterfaceEvent event) {
            switch (event.type()) {
            case INTERFACE_ADDED:
                updateInterface(event.subject());
                break;
            case INTERFACE_UPDATED:
                break;
            case INTERFACE_REMOVED:
                removeInterface(event.subject());
                break;
            default:
                break;
            }
        }
    }

}