org.opendaylight.vbd.impl.VbdBridgeDomain.java Source code

Java tutorial

Introduction

Here is the source code for org.opendaylight.vbd.impl.VbdBridgeDomain.java

Source

/*
 * Copyright (c) 2016 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.vbd.impl;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.opendaylight.controller.md.sal.binding.api.BindingTransactionChain;
import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
import org.opendaylight.controller.md.sal.binding.api.DataBroker;
import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
import org.opendaylight.controller.md.sal.binding.api.DataTreeIdentifier;
import org.opendaylight.controller.md.sal.binding.api.DataTreeModification;
import org.opendaylight.controller.md.sal.binding.api.MountPointService;
import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
import org.opendaylight.vbd.api.VxlanTunnelIdAllocator;
import org.opendaylight.vbd.impl.transaction.VbdNetconfConnectionProbe;
import org.opendaylight.vbd.impl.transaction.VbdNetconfTransaction;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4AddressNoZone;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.interfaces.rev140508.Interfaces;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.interfaces.rev140508.interfaces.Interface;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.interfaces.rev140508.interfaces.InterfaceKey;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.external.reference.rev160129.ExternalReference;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.NodeVbridgeAugment;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.TerminationPointVbridgeAugment;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.TerminationPointVbridgeAugmentBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.TopologyVbridgeAugment;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.network.topology.topology.node.BridgeMember;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.network.topology.topology.node.BridgeMemberBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.topology.rev160129.network.topology.topology.node.termination.point._interface.type.TunnelInterfaceBuilder;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.tunnel.vlan.rev160429.NodeVbridgeVlanAugment;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.tunnel.vlan.rev160429.TunnelTypeVlan;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.tunnel.vlan.rev160429.network.topology.topology.tunnel.parameters.VlanNetworkParameters;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vbridge.tunnel.vxlan.rev160429.TunnelTypeVxlan;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vpp.vlan.rev161214.SubinterfaceAugmentation;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vpp.vlan.rev161214.interfaces._interface.SubInterfaces;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vpp.vlan.rev161214.interfaces._interface.sub.interfaces.SubInterface;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.vpp.vlan.rev161214.interfaces._interface.sub.interfaces.SubInterfaceKey;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.LinkId;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TpId;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.link.attributes.Destination;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.link.attributes.Source;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.Topology;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Link;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.LinkKey;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeKey;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.node.TerminationPoint;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.node.TerminationPointBuilder;
import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.node.attributes.SupportingNode;
import org.opendaylight.yangtools.concepts.ListenerRegistration;
import org.opendaylight.yangtools.yang.binding.DataObject;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.opendaylight.yangtools.yang.binding.KeyedInstanceIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of a single Virtual Bridge Domain. It is bound to a particular network topology instance, manages
 * bridge members and projects state into the operational data store.
 */
public final class VbdBridgeDomain implements ClusteredDataTreeChangeListener<Topology> {
    private static final Logger LOG = LoggerFactory.getLogger(VbdBridgeDomain.class);

    static final short VLAN_TAG_INDEX_ZERO = 0;
    static final int MAXLEN = 8;
    private static final int SOURCE_VPP_INDEX = 0;
    private static final int DESTINATION_VPP_INDEX = 1;
    private final DataBroker dataBroker;
    private final KeyedInstanceIdentifier<Topology, TopologyKey> topology;
    @GuardedBy("this")

    private final BindingTransactionChain chain;
    private final ListenerRegistration<?> reg;
    private final MountPointService mountService;
    private final VppModifier vppModifier;
    private final VxlanTunnelIdAllocator tunnelIdAllocator;
    private final String bridgeDomainName;
    private final String iiBridgeDomainOnVPPRest;
    private TopologyVbridgeAugment config;
    @SuppressWarnings("FieldCanBeLocal")
    private Multimap<NodeId, KeyedInstanceIdentifier<Node, NodeKey>> nodesToVpps = ArrayListMultimap.create();

    private VbdBridgeDomain(final DataBroker dataBroker, final MountPointService mountService,
            final KeyedInstanceIdentifier<Topology, TopologyKey> topology, final BindingTransactionChain chain,
            VxlanTunnelIdAllocator tunnelIdAllocator) throws Exception {
        this.dataBroker = Preconditions.checkNotNull(dataBroker);
        this.bridgeDomainName = topology.getKey().getTopologyId().getValue();
        this.vppModifier = new VppModifier(dataBroker, mountService, bridgeDomainName, this);

        this.topology = Preconditions.checkNotNull(topology);
        this.chain = Preconditions.checkNotNull(chain);
        this.mountService = mountService;
        this.tunnelIdAllocator = tunnelIdAllocator;

        this.iiBridgeDomainOnVPPRest = VbdUtil.provideIidBridgeDomainOnVPPRest(bridgeDomainName);

        wipeOperationalState(topology, chain);
        createFreshOperationalState(topology, chain);
        readParams(topology, chain);

        reg = dataBroker.registerDataTreeChangeListener(
                new DataTreeIdentifier<>(LogicalDatastoreType.CONFIGURATION, topology), this);
    }

    /**
     * Topology listener registered for every created bridge domain.
     * @param changes topology modification events
     */
    @Override
    public synchronized void onDataTreeChanged(@Nonnull final Collection<DataTreeModification<Topology>> changes) {
        for (DataTreeModification<Topology> change : changes) {
            LOG.debug("Bridge domain {} for {} processing change {}", this.bridgeDomainName,
                    PPrint.topology(topology), change.getClass());
            final DataObjectModification<Topology> modification = change.getRootNode();
            ListenableFuture<Void> modificationTask;
            switch (modification.getModificationType()) {
            case WRITE:
                modificationTask = handleNewTopology(modification);
                break;
            case SUBTREE_MODIFIED:
                modificationTask = handleModifiedTopology(modification);
                break;
            case DELETE:
                LOG.debug("Topology {} deleted, expecting shutdown", PPrint.topology(topology));
                modificationTask = deleteBridgeDomain();
                break;
            default:
                LOG.warn("Unhandled topology modification {}", modification);
                modificationTask = Futures.immediateFuture(null);
                break;
            }
            Futures.addCallback(modificationTask, new FutureCallback<Void>() {
                @Override
                public void onSuccess(@Nullable Void aVoid) {
                    LOG.info("Topology change {} for bridge domain {} completed",
                            modification.getModificationType(), PPrint.topology(topology));
                }

                @Override
                public void onFailure(@Nonnull Throwable throwable) {
                    LOG.warn("Topology change for {} bridge domain {} failed", modification.getModificationType(),
                            PPrint.topology(topology));
                }
            });
        }
    }

    private ListenableFuture<Void> handleNewTopology(final DataObjectModification<Topology> modification) {
        Preconditions.checkNotNull(modification.getDataAfter());
        final Topology data = modification.getDataAfter();
        // Handle VBridge augmentation
        final TopologyVbridgeAugment vbdConfiguration = data.getAugmentation(TopologyVbridgeAugment.class);
        if (vbdConfiguration != null) {
            // Spread configuration
            setConfiguration(vbdConfiguration);
            vppModifier.setConfig(vbdConfiguration);
        } else {
            LOG.error("Topology {} has no configuration", PPrint.topology(topology));
        }
        // Handle new nodes
        final Collection<DataObjectModification<? extends DataObject>> modifiedChildren = modification
                .getModifiedChildren();
        final List<ListenableFuture<Void>> newCumulativeTopologyResult = new ArrayList<>();
        for (final DataObjectModification<? extends DataObject> childNode : modifiedChildren) {
            LOG.debug("Processing created child {} from topology {}", childNode, PPrint.topology(topology));
            if (Node.class.isAssignableFrom(childNode.getDataType())) {
                newCumulativeTopologyResult.add(handleModifiedNode(childNode));
            }
        }
        final ListenableFuture<List<Void>> newTopologyResult = Futures.allAsList(newCumulativeTopologyResult);
        return transform(newTopologyResult);
    }

    private ListenableFuture<Void> handleModifiedTopology(final DataObjectModification<Topology> modification) {
        Preconditions.checkNotNull(modification.getDataAfter());
        final DataObjectModification<TopologyVbridgeAugment> topologyModification = modification
                .getModifiedAugmentation(TopologyVbridgeAugment.class);
        // Handle VBridge augmentation
        if (topologyModification != null && !DataObjectModification.ModificationType.DELETE
                .equals(topologyModification.getModificationType())) {
            // Update configuration
            updateConfiguration(topologyModification);
        }
        // Handle new/modified nodes
        final Collection<DataObjectModification<? extends DataObject>> modifiedChildren = modification
                .getModifiedChildren();
        final List<ListenableFuture<Void>> updatedCumulativeTopologyTask = new ArrayList<>();
        for (final DataObjectModification<? extends DataObject> childNode : modifiedChildren) {
            LOG.debug("Processing modified child {} from topology {}", childNode, PPrint.topology(topology));
            if (Node.class.isAssignableFrom(childNode.getDataType())) {
                updatedCumulativeTopologyTask.add(handleModifiedNode(childNode));
            }
        }
        final ListenableFuture<List<Void>> updatedTopologyResult = Futures.allAsList(updatedCumulativeTopologyTask);
        return transform(updatedTopologyResult);
    }

    private ListenableFuture<Void> deleteBridgeDomain() {
        LOG.debug("Deleting entire bridge domain {}", bridgeDomainName);
        final Collection<KeyedInstanceIdentifier<Node, NodeKey>> vppNodes = nodesToVpps.values();
        final List<ListenableFuture<Void>> deleteBdTask = new ArrayList<>();
        vppNodes.forEach((vppNode) -> deleteBdTask.add(vppModifier.deleteBridgeDomainFromVppNode(vppNode)));
        nodesToVpps.clear();
        final ListenableFuture<List<Void>> cumulativeDeleteBdTask = Futures.allAsList(deleteBdTask);
        return transform(cumulativeDeleteBdTask);
    }

    /**
     * In case new topology is created, or existing topology is updated, every new/modified node is resolved.
     *
     * @param nodeModification current node modification
     * @return {@link ListenableFuture} of actual task
     */
    private ListenableFuture<Void> handleModifiedNode(
            final DataObjectModification<? extends DataObject> nodeModification) {
        switch (nodeModification.getModificationType()) {
        case WRITE:
            LOG.debug("Topology {} node {} created", PPrint.topology(topology), nodeModification.getIdentifier());
            final Node newNode = (Node) nodeModification.getDataAfter();
            if (newNode == null) {
                LOG.warn("Provided node is null");
                return Futures.immediateFuture(null);
            }
            return handleNewModifiedNode(newNode);
        case SUBTREE_MODIFIED:
            LOG.debug("Topology {} node {} modified", PPrint.topology(topology), nodeModification.getIdentifier());
            final HashMap<TerminationPoint, Node> modifiedNodes = new HashMap<>();
            for (DataObjectModification<? extends DataObject> nodeChild : nodeModification.getModifiedChildren()) {
                if (TerminationPoint.class.isAssignableFrom(nodeChild.getDataType())) {
                    final TerminationPoint modifedTerminationPoint = (TerminationPoint) nodeChild.getDataAfter();
                    final Node modifiedNode = (Node) nodeChild.getDataAfter();
                    modifiedNodes.put(modifedTerminationPoint, modifiedNode);
                }
            }
            return handleUpdatedModifiedNodes(modifiedNodes);
        case DELETE:
            LOG.debug("Topology {} node {} deleted", PPrint.topology(topology), nodeModification.getIdentifier());
            final Node deletedNode = (Node) nodeModification.getDataBefore();
            return handleDeletedModifiedNode(deletedNode);
        default:
            LOG.warn("Unhandled node modification {} in topology {}", nodeModification, PPrint.topology(topology));
            break;
        }
        return Futures.immediateFuture(null);
    }

    private ListenableFuture<Void> handleNewModifiedNode(@Nonnull final Node newNode) {
        LOG.debug("Topology {} node {} created", PPrint.topology(topology), newNode.getNodeId().getValue());
        final int numberOfVppsBeforeAddition = nodesToVpps.keySet().size();
        final ListenableFuture<Void> createNodeTask = createNode(newNode);
        return Futures.transform(createNodeTask, new Function<Void, Void>() {
            @Nullable
            @Override
            public Void apply(@Nullable Void input) {
                try {
                    // Vxlan tunnel type
                    if (config.getTunnelType().equals(TunnelTypeVxlan.class)) {
                        final int numberOfVppsAfterAddition = nodesToVpps.keySet().size();
                        if ((numberOfVppsBeforeAddition <= numberOfVppsAfterAddition)
                                && (numberOfVppsBeforeAddition >= 1)) {
                            return addVxlanTunnel(newNode.getNodeId()).get();
                        }
                        // Vlan tunnel type
                    } else if (config.getTunnelType().equals(TunnelTypeVlan.class)) {
                        final NodeVbridgeVlanAugment vlanAug = newNode
                                .getAugmentation(NodeVbridgeVlanAugment.class);
                        return addVlanSubInterface(newNode.getNodeId(), vlanAug.getSuperInterface()).get();
                    } else {
                        LOG.warn("Unknown tunnel type {}", config.getTunnelType());
                    }
                } catch (InterruptedException | ExecutionException e) {
                    LOG.warn("Exception while processing tunnel for new node {}", newNode.getNodeId().getValue());
                }
                return null;
            }
        });
    }

    private ListenableFuture<Void> handleUpdatedModifiedNodes(final HashMap<TerminationPoint, Node> modifiedNodes) {
        final List<ListenableFuture<Void>> cumulativeTask = new ArrayList<>();
        modifiedNodes.forEach((terminationPoint, node) -> {
            if (terminationPoint != null
                    && terminationPoint.getAugmentation(TerminationPointVbridgeAugment.class) != null
                    && node.getNodeId() != null) {
                final TerminationPointVbridgeAugment termPointVbridgeAug = terminationPoint
                        .getAugmentation(TerminationPointVbridgeAugment.class);
                final Collection<KeyedInstanceIdentifier<Node, NodeKey>> instanceIdentifiersVPP = nodesToVpps
                        .get(node.getNodeId());
                //TODO: probably iterate via all instance identifiers.
                if (!instanceIdentifiersVPP.isEmpty()) {
                    final DataBroker dataBroker = VbdUtil
                            .resolveDataBrokerForMountPoint(instanceIdentifiersVPP.iterator().next(), mountService);
                    cumulativeTask
                            .add(vppModifier.addInterfaceToBridgeDomainOnVpp(dataBroker, termPointVbridgeAug));
                }
            }
        });
        final ListenableFuture<List<Void>> completedCumulativeTask = Futures.allAsList(cumulativeTask);
        return transform(completedCumulativeTask);
    }

    private ListenableFuture<Void> handleDeletedModifiedNode(@Nullable final Node deletedNode) {
        if (deletedNode == null) {
            LOG.warn("Unable to remove bridge domain, provided node is null. Bridge domain {}", bridgeDomainName);
            return Futures.immediateFuture(null);
        }
        final KeyedInstanceIdentifier<Node, NodeKey> vppNodeIid = nodesToVpps.get(deletedNode.getNodeId())
                .iterator().next();
        final KeyedInstanceIdentifier<Node, NodeKey> backingNodeIid = topology.child(Node.class,
                deletedNode.getKey());
        LOG.debug("Removing node from BD. Node: {}. backing node: {}", PPrint.node(vppNodeIid),
                PPrint.node(backingNodeIid));
        final ListenableFuture<Void> removeNodeTask = removeNodeFromBridgeDomain(vppNodeIid, backingNodeIid);
        if (config.getTunnelType().equals(TunnelTypeVxlan.class)) {
            return Futures.transform(removeNodeTask, new Function<Void, Void>() {
                @Nullable
                @Override
                public Void apply(@Nullable Void input) {
                    try {
                        return removeVxlanInterfaces(deletedNode.getNodeId()).get();
                    } catch (InterruptedException | ExecutionException e) {
                        LOG.warn("Remove vxlan interfaces processing failed. Node: {}", deletedNode.getNodeId());
                        return null;
                    }
                }
            });
        } else {
            return removeNodeTask;
        }
    }

    private ListenableFuture<Void> wipeOperationalState(
            final KeyedInstanceIdentifier<Topology, TopologyKey> topology, final BindingTransactionChain chain) {
        LOG.info("Wiping operational state of {}", PPrint.topology(topology));

        final WriteTransaction tx = chain.newWriteOnlyTransaction();
        tx.delete(LogicalDatastoreType.OPERATIONAL, topology);
        return tx.submit();
    }

    private void createFreshOperationalState(final KeyedInstanceIdentifier<Topology, TopologyKey> topology,
            final BindingTransactionChain chain) {
        LOG.info("Creating fresh operational state for {}", PPrint.topology(topology));

        final WriteTransaction tx = chain.newWriteOnlyTransaction();
        tx.put(LogicalDatastoreType.OPERATIONAL, topology, VbdUtil.buildFreshTopology(topology), true);
        tx.submit();
    }

    private void readParams(final KeyedInstanceIdentifier<Topology, TopologyKey> topology,
            final BindingTransactionChain chain) {
        final ReadOnlyTransaction tx = chain.newReadOnlyTransaction();
        Futures.addCallback(tx.read(LogicalDatastoreType.CONFIGURATION, topology),
                new FutureCallback<Optional<Topology>>() {
                    @Override
                    public void onSuccess(@Nullable Optional<Topology> result) {
                        if (result != null) {
                            if (result.isPresent()) {
                                VbdUtil.printVbridgeParams(result.get());
                            }
                        }
                    }

                    @Override
                    public void onFailure(@Nonnull Throwable t) {
                        LOG.warn("Can't read Bridge domain parameters!", t);
                    }
                });
    }

    @Nonnull
    static VbdBridgeDomain create(final DataBroker dataBroker, final MountPointService mountService,
            final KeyedInstanceIdentifier<Topology, TopologyKey> topology, final BindingTransactionChain chain,
            final VxlanTunnelIdAllocator tunnelIdAllocator) throws Exception {
        return new VbdBridgeDomain(dataBroker, mountService, topology, chain, tunnelIdAllocator);
    }

    synchronized void forceStop() {
        // TODO better be to return future
        LOG.debug("Bridge domain {} for {} going down", this, PPrint.topology(topology));
        reg.close();
        chain.close();
        LOG.info("Bridge domain {} for {} is down", this, PPrint.topology(topology));
    }

    synchronized void stop() {
        LOG.debug("Bridge domain {} for {} shutting down", this, PPrint.topology(topology));

        wipeOperationalState(topology, chain);
        chain.close();
    }

    private ListenableFuture<List<InstanceIdentifier<Link>>> findPrunableLinks(
            final KeyedInstanceIdentifier<Node, NodeKey> vbdNode) {
        LOG.debug("Finding prunable links for node {}", PPrint.node(vbdNode));

        final NodeId deletedNodeId = vbdNode.getKey().getNodeId();

        // read the topology to find the links
        final ReadOnlyTransaction rTx = chain.newReadOnlyTransaction();
        return Futures.transform(rTx.read(LogicalDatastoreType.OPERATIONAL, topology),
                new AsyncFunction<Optional<Topology>, List<InstanceIdentifier<Link>>>() {
                    @Override
                    public ListenableFuture<List<InstanceIdentifier<Link>>> apply(
                            @Nonnull Optional<Topology> result) throws Exception {
                        final List<InstanceIdentifier<Link>> prunableLinks = new ArrayList<>();

                        if (result.isPresent()) {
                            final List<Link> links = result.get().getLink();

                            for (final Link link : links) {
                                // check if this link's source or destination matches the deleted node
                                final Source src = link.getSource();
                                final Destination dst = link.getDestination();
                                if (src.getSourceNode().equals(deletedNodeId)) {
                                    LOG.debug("Link {} src matches deleted node id {}, adding to prunable list",
                                            link.getLinkId(), deletedNodeId);
                                    final InstanceIdentifier<Link> linkIID = topology.child(Link.class,
                                            link.getKey());
                                    prunableLinks.add(linkIID);
                                } else if (dst.getDestNode().equals(deletedNodeId)) {
                                    LOG.debug("Link {} dst matches deleted node id {}, adding to prunable list",
                                            link.getLinkId(), deletedNodeId);
                                    final InstanceIdentifier<Link> linkIID = topology.child(Link.class,
                                            link.getKey());
                                    prunableLinks.add(linkIID);
                                }
                            }
                        } else {
                            // result is null or not present
                            LOG.warn("Tried to read virtual bridge topology {}, but got null or absent optional!",
                                    PPrint.topology(topology));
                        }

                        return Futures.immediateFuture(prunableLinks);
                    }
                });
    }

    private void pruneLinks(final KeyedInstanceIdentifier<Node, NodeKey> vbdNode) {
        LOG.debug("Pruning links to/from node {}", PPrint.node(vbdNode));
        final ListenableFuture<List<InstanceIdentifier<Link>>> prunableLinksFuture = findPrunableLinks(vbdNode);

        Futures.addCallback(prunableLinksFuture, new FutureCallback<List<InstanceIdentifier<Link>>>() {
            @Override
            public void onSuccess(@Nullable List<InstanceIdentifier<Link>> result) {
                if (result == null) {
                    LOG.warn("Got null result when finding prunable links for node {} on topology {}",
                            PPrint.node(vbdNode), PPrint.topology(topology));
                    return;
                }

                for (final InstanceIdentifier<Link> linkIID : result) {
                    final WriteTransaction wTx = chain.newWriteOnlyTransaction();
                    wTx.delete(LogicalDatastoreType.OPERATIONAL, linkIID);
                    Futures.addCallback(wTx.submit(), new FutureCallback<Void>() {
                        @Override
                        public void onSuccess(@Nullable Void result) {
                            LOG.debug("Successfully deleted prunable link {} for node {} on vbd topology {}",
                                    linkIID, PPrint.node(vbdNode), PPrint.topology(topology));
                        }

                        @Override
                        public void onFailure(@Nullable Throwable t) {
                            LOG.warn("Failed to delete prunable link {} for node {} on vbd topology {}", linkIID,
                                    PPrint.node(vbdNode), PPrint.topology(topology), t);
                        }
                    });
                }
            }

            @Override
            public void onFailure(@Nullable Throwable t) {
                LOG.warn("Failed to get prunable links for vbd topology {}", PPrint.topology(topology), t);
            }
        });
    }

    private ListenableFuture<Void> removeNodeFromBridgeDomain(final KeyedInstanceIdentifier<Node, NodeKey> vppNode,
            final KeyedInstanceIdentifier<Node, NodeKey> backingNode) {
        LOG.debug("Removing node {} from bridge domain {}", PPrint.node(vppNode), bridgeDomainName);
        final ListenableFuture<Void> deleteNodeTask = vppModifier.deleteBridgeDomainFromVppNode(vppNode);
        pruneLinks(backingNode);

        // remove this node from vbd operational topology
        final WriteTransaction wTx3 = chain.newWriteOnlyTransaction();
        wTx3.delete(LogicalDatastoreType.OPERATIONAL, backingNode);

        Futures.addCallback(wTx3.submit(), new FutureCallback<Void>() {
            @Override
            public void onSuccess(@Nullable Void result) {
                LOG.debug("Removed backing node {} from virtual bridge operational topology {}",
                        backingNode.toString(), bridgeDomainName);
            }

            @Override
            public void onFailure(@Nonnull Throwable t) {
                LOG.warn("Failed to delete node {} from virtual bridge operational topology {}",
                        backingNode.toString(), bridgeDomainName, t);
            }
        });

        nodesToVpps.removeAll(vppNode);
        return deleteNodeTask;
    }

    private ListenableFuture<Void> addVlanSubInterface(final NodeId nodeId, final String supIntfKey) {
        // create sub interface from node's defined super interface
        // set subinterface vlan parameters (pop 1)
        // add subinterface to bridge domain
        final VlanNetworkParameters params = (VlanNetworkParameters) config.getTunnelParameters();
        final KeyedInstanceIdentifier<Node, NodeKey> nodeIID = nodesToVpps.get(nodeId).iterator().next();
        final SubInterface subIntf = VbdUtil.createSubInterface(params.getVlanId(), params.getVlanType(),
                bridgeDomainName);

        final KeyedInstanceIdentifier<Interface, InterfaceKey> iiToSupIntf = InstanceIdentifier
                .create(Interfaces.class).child(Interface.class, new InterfaceKey(supIntfKey));

        final KeyedInstanceIdentifier<SubInterface, SubInterfaceKey> iiToVlanSubIntf = iiToSupIntf
                .augmentation(SubinterfaceAugmentation.class).child(SubInterfaces.class)
                .child(SubInterface.class, new SubInterfaceKey(subIntf.getKey()));

        final DataBroker vppDataBroker = VbdUtil.resolveDataBrokerForMountPoint(nodeIID, mountService);
        if (vppDataBroker == null) {
            LOG.warn("Cannot get data broker to write interface to node {}", PPrint.node(nodeIID));
            return Futures.immediateFuture(null);
        }
        final boolean transactionState = VbdNetconfTransaction.write(vppDataBroker, iiToVlanSubIntf, subIntf,
                VbdNetconfTransaction.RETRY_COUNT);
        if (transactionState) {
            LOG.debug("Successfully wrote subinterface {} to node {}", subIntf.getKey().getIdentifier(),
                    nodeId.getValue());
        } else {
            LOG.warn("Failed to write subinterface {} to node {}", subIntf.getKey().getIdentifier(),
                    nodeId.getValue());
        }
        return Futures.immediateFuture(null);
    }

    /**
     * Read IP addresses from interfaces on src and dst nodes to use as vxlan tunnel endpoints.
     * @param iiToSrcVpp source node
     * @param iiToDstVpp destination node
     * @return list of IP addresses configured on interfaces on each node. The returned list should have a size of
     * exactly two, one element for each node. The first element (index 0) is the IP address for the source node endpoint,
     * and the second element (index 1) is the IP address for the destination node endpoint.
     */
    private List<Ipv4AddressNoZone> getTunnelEndpoints(final KeyedInstanceIdentifier<Node, NodeKey> iiToSrcVpp,
            final KeyedInstanceIdentifier<Node, NodeKey> iiToDstVpp) {
        try {
            return vppModifier.readIpAddressesFromVpps(iiToSrcVpp, iiToDstVpp).stream().filter(Objects::nonNull)
                    .filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
        } catch (final InterruptedException | ExecutionException ex) {
            LOG.warn("Got exception while reading IP addresses from nodes {} and {}", PPrint.node(iiToSrcVpp),
                    PPrint.node(iiToDstVpp), ex);
        }

        return Collections.emptyList();
    }

    /**
     * Get all nodes in the topology which are peers of the target node. A peer node in this instance is a node which
     * should have a link to the given node in a full-mesh topology.
     *
     * @return list of peer nodes
     */
    private List<NodeId> getNodePeers(final NodeId srcNode) {
        return nodesToVpps.keySet().stream().filter(key -> !key.equals(srcNode)).collect(Collectors.toList());
    }

    List<KeyedInstanceIdentifier<Node, NodeKey>> getNodePeersByIID(
            final KeyedInstanceIdentifier<Node, NodeKey> iiToVpp) {
        return nodesToVpps.values().stream().filter(iid -> !iid.equals(iiToVpp)).collect(Collectors.toList());
    }

    private ListenableFuture<Void> addVxlanTunnel(final NodeId sourceNode) {
        final KeyedInstanceIdentifier<Node, NodeKey> iiToSrcVpp = nodesToVpps.get(sourceNode).iterator().next();
        List<ListenableFuture<Void>> cumulativeTask = new ArrayList<>();

        LOG.debug("adding tunnel to vpp node {} (vbd node is {})", PPrint.node(iiToSrcVpp), sourceNode.getValue());
        for (final NodeId dstNode : getNodePeers(sourceNode)) {
            List<ListenableFuture<Void>> perPeerTask = new ArrayList<>();
            final KeyedInstanceIdentifier<Node, NodeKey> iiToDstVpp = nodesToVpps.get(dstNode).iterator().next();
            final Integer srcVxlanTunnelId = tunnelIdAllocator.nextIdFor(iiToSrcVpp);
            final Integer dstVxlanTunnelId = tunnelIdAllocator.nextIdFor(iiToDstVpp);
            final List<Ipv4AddressNoZone> endpoints = getTunnelEndpoints(iiToSrcVpp, iiToDstVpp);

            Preconditions.checkState(endpoints.size() == 2,
                    "Got IP address list with wrong size (should be 2, actual size is " + endpoints.size() + ")");

            final Ipv4AddressNoZone ipAddressSrcVpp = endpoints.get(SOURCE_VPP_INDEX);
            final Ipv4AddressNoZone ipAddressDstVpp = endpoints.get(DESTINATION_VPP_INDEX);
            LOG.debug(
                    "All required IP addresses for creating tunnel were obtained. (src: {} (node {}), dst: {} (node {}))",
                    ipAddressSrcVpp.getValue(), sourceNode.getValue(), ipAddressDstVpp.getValue(),
                    dstNode.getValue());

            String distinguisher = VbdUtil.deriveDistinguisher(config);
            // Termination Points
            LOG.debug("Adding term point to dst node {}", dstNode.getValue());
            perPeerTask
                    .add(addTerminationPoint(topology.child(Node.class, new NodeKey(dstNode)), dstVxlanTunnelId));
            LOG.debug("Adding term point to src node {}", sourceNode.getValue());
            perPeerTask.add(
                    addTerminationPoint(topology.child(Node.class, new NodeKey(sourceNode)), srcVxlanTunnelId));

            // Links between termination points
            perPeerTask.add(addLinkBetweenTerminationPoints(sourceNode, dstNode, srcVxlanTunnelId, dstVxlanTunnelId,
                    distinguisher));
            perPeerTask.add(addLinkBetweenTerminationPoints(dstNode, sourceNode, srcVxlanTunnelId, dstVxlanTunnelId,
                    distinguisher));

            // Virtual interfaces
            perPeerTask.add(vppModifier.createVirtualInterfaceOnVpp(ipAddressSrcVpp, ipAddressDstVpp, iiToSrcVpp,
                    srcVxlanTunnelId));
            perPeerTask.add(vppModifier.createVirtualInterfaceOnVpp(ipAddressDstVpp, ipAddressSrcVpp, iiToDstVpp,
                    dstVxlanTunnelId));

            final ListenableFuture<List<Void>> processedPerPeerTask = Futures.allAsList(perPeerTask);
            cumulativeTask.add(transform(processedPerPeerTask));
        }
        final ListenableFuture<List<Void>> processedCumulativeTask = Futures.allAsList(cumulativeTask);
        return transform(processedCumulativeTask);
    }

    private ListenableFuture<Void> removeVxlanInterfaces(final NodeId sourceNode) {
        final KeyedInstanceIdentifier<Node, NodeKey> iiToSrcVpp = nodesToVpps.get(sourceNode).iterator().next();
        final List<ListenableFuture<Void>> deleteVxlanTaskList = new ArrayList<>();
        for (final NodeId dstNode : getNodePeers(sourceNode)) {
            final KeyedInstanceIdentifier<Node, NodeKey> iiToDstVpp = nodesToVpps.get(dstNode).iterator().next();
            final List<Ipv4AddressNoZone> endpoints = getTunnelEndpoints(iiToSrcVpp, iiToDstVpp);

            Preconditions.checkState(endpoints.size() == 2,
                    "Got IP address list with wrong size (should be 2, actual size is {})", endpoints.size());

            final Ipv4AddressNoZone ipAddressSrcVpp = endpoints.get(SOURCE_VPP_INDEX);
            final Ipv4AddressNoZone ipAddressDstVpp = endpoints.get(DESTINATION_VPP_INDEX);

            // remove bridge domains from vpp
            LOG.debug("Removing bridge domain from vxlan tunnel on node {}", sourceNode);
            deleteVxlanTaskList.add(vppModifier.deleteVxlanInterface(ipAddressSrcVpp, ipAddressDstVpp, iiToSrcVpp));
            LOG.debug("Removing bridge domain from vxlan tunnel on node {}", dstNode);
            deleteVxlanTaskList.add(vppModifier.deleteVxlanInterface(ipAddressDstVpp, ipAddressSrcVpp, iiToDstVpp));
        }
        final ListenableFuture<List<Void>> cumulativeDeleteVxlanTask = Futures.allAsList(deleteVxlanTaskList);
        return transform(cumulativeDeleteVxlanTask);
    }

    private ListenableFuture<Void> addLinkBetweenTerminationPoints(final NodeId newVpp, final NodeId odlVpp,
            final int srcVxlanTunnelId, final int dstVxlanTunnelId, final String distinguisher) {
        final String linkIdStr = newVpp.getValue() + "-" + distinguisher + "-" + odlVpp.getValue();
        final LinkId linkId = new LinkId(linkIdStr);
        final KeyedInstanceIdentifier<Link, LinkKey> iiToLink = topology.child(Link.class, new LinkKey(linkId));
        final WriteTransaction wTx = chain.newWriteOnlyTransaction();
        wTx.put(LogicalDatastoreType.OPERATIONAL, iiToLink,
                VbdUtil.prepareLinkData(newVpp, odlVpp, linkId, srcVxlanTunnelId, dstVxlanTunnelId), true);
        LOG.debug("Adding link between termination points {} and {}", srcVxlanTunnelId, dstVxlanTunnelId);
        return wTx.submit();
    }

    private ListenableFuture<Void> createNode(final Node node) {
        List<ListenableFuture<Void>> createdNodesFuture = new ArrayList<>();
        for (SupportingNode supportingNode : node.getSupportingNode()) {
            final NodeId nodeMount = supportingNode.getNodeRef();
            final VbdNetconfConnectionProbe probe = new VbdNetconfConnectionProbe(supportingNode.getNodeRef(),
                    dataBroker);
            try {
                // Verify netconf connection
                boolean connectionReady = probe.startProbing();
                if (connectionReady) {
                    LOG.debug("Node {} is connected, creating ...", supportingNode.getNodeRef());
                    final TopologyId topologyMount = supportingNode.getTopologyRef();
                    final KeyedInstanceIdentifier<Node, NodeKey> iiToVpp = InstanceIdentifier
                            .create(NetworkTopology.class).child(Topology.class, new TopologyKey(topologyMount))
                            .child(Node.class, new NodeKey(nodeMount));
                    nodesToVpps.put(node.getNodeId(), iiToVpp);
                    ListenableFuture<Void> addVppToBridgeDomainFuture = vppModifier.addVppToBridgeDomain(iiToVpp);
                    createdNodesFuture.add(addSupportingBridgeDomain(addVppToBridgeDomainFuture, node));
                } else {
                    LOG.debug("Failed while connecting to node {}", supportingNode.getNodeRef());
                }
            } catch (InterruptedException | ExecutionException e) {
                LOG.warn("Exception while processing node {} ... ", supportingNode.getNodeRef(), e);
            } catch (TimeoutException e) {
                LOG.warn(
                        "Node {} was not connected within {} seconds. Check node configuration and connectivity to proceed",
                        supportingNode.getNodeRef(), VbdNetconfConnectionProbe.NODE_CONNECTION_TIMER);
            }
        }
        // configure all or nothing
        return Futures.transform(Futures.allAsList(createdNodesFuture), new Function<List<Void>, Void>() {

            @Override
            public Void apply(List<Void> input) {
                return null;
            }

        });
    }

    private ListenableFuture<Void> addSupportingBridgeDomain(
            final ListenableFuture<Void> addVppToBridgeDomainFuture, final Node node) {
        return Futures.transform(addVppToBridgeDomainFuture, new AsyncFunction<Void, Void>() {

            @Override
            public ListenableFuture<Void> apply(@Nonnull Void input) throws Exception {
                LOG.debug("Storing bridge member to operational DS....");
                final BridgeMemberBuilder bridgeMemberBuilder = new BridgeMemberBuilder();
                bridgeMemberBuilder.setSupportingBridgeDomain(new ExternalReference(iiBridgeDomainOnVPPRest));
                final InstanceIdentifier<BridgeMember> iiToBridgeMember = topology.child(Node.class, node.getKey())
                        .augmentation(NodeVbridgeAugment.class).child(BridgeMember.class);
                final WriteTransaction wTx = chain.newWriteOnlyTransaction();
                wTx.put(LogicalDatastoreType.OPERATIONAL, iiToBridgeMember, bridgeMemberBuilder.build(), true);
                return wTx.submit();
            }
        });
    }

    private ListenableFuture<Void> addTerminationPoint(final KeyedInstanceIdentifier<Node, NodeKey> nodeIID,
            final int vxlanTunnelId) {
        // build data
        final ExternalReference ref = new ExternalReference(VbdUtil.provideVxlanId(vxlanTunnelId));
        final TunnelInterfaceBuilder iFaceBuilder = new TunnelInterfaceBuilder();
        iFaceBuilder.setTunnelInterface(ref);

        final TerminationPointVbridgeAugmentBuilder tpAugmentBuilder = new TerminationPointVbridgeAugmentBuilder();
        tpAugmentBuilder.setInterfaceType(iFaceBuilder.build());

        final TerminationPointBuilder tpBuilder = new TerminationPointBuilder();
        tpBuilder.addAugmentation(TerminationPointVbridgeAugment.class, tpAugmentBuilder.build());
        tpBuilder.setTpId(new TpId(VbdUtil.provideVxlanId(vxlanTunnelId)));
        final TerminationPoint tp = tpBuilder.build();

        // process data
        final WriteTransaction wTx = chain.newWriteOnlyTransaction();
        wTx.put(LogicalDatastoreType.OPERATIONAL, nodeIID.child(TerminationPoint.class, tp.getKey()), tp, true);
        LOG.debug("Adding termination point to node {}", nodeIID.getKey());
        return wTx.submit();
    }

    private void setConfiguration(final TopologyVbridgeAugment config) {
        LOG.debug("Topology {} configuration set to {}", PPrint.topology(topology), config);

        this.config = config;
    }

    @GuardedBy("this")
    private void updateConfiguration(final DataObjectModification<TopologyVbridgeAugment> mod) {
        LOG.debug("Topology {} configuration changed", PPrint.topology(topology));

        // FIXME: do something smarter
        setConfiguration(mod.getDataAfter());
    }

    // Transform future util method
    private ListenableFuture<Void> transform(final ListenableFuture<List<Void>> input) {
        return Futures.transform(input, new Function<List<Void>, Void>() {
            @Nullable
            @Override
            public Void apply(@Nullable List<Void> input) {
                // NOOP
                return null;
            }
        });
    }
}