Moved net to core
diff --git a/core/trivial/pom.xml b/core/trivial/pom.xml
new file mode 100644
index 0000000..1806ba4
--- /dev/null
+++ b/core/trivial/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onlab.onos</groupId>
+        <artifactId>onos-core</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-core-trivial</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS network control trivial implementations of core subsystems</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onos-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/core/trivial/src/main/java/org/onlab/onos/event/impl/SimpleEventDispatcher.java b/core/trivial/src/main/java/org/onlab/onos/event/impl/SimpleEventDispatcher.java
new file mode 100644
index 0000000..3834676
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/event/impl/SimpleEventDispatcher.java
@@ -0,0 +1,93 @@
+package org.onlab.onos.event.impl;
+
+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.Service;
+import org.onlab.onos.event.AbstractEvent;
+import org.onlab.onos.event.DefaultEventSinkRegistry;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.event.EventSink;
+import org.slf4j.Logger;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.namedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Simple implementation of an event dispatching service.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleEventDispatcher extends DefaultEventSinkRegistry
+        implements EventDeliveryService {
+
+    private final Logger log = getLogger(getClass());
+
+    private final ExecutorService executor =
+            newSingleThreadExecutor(namedThreads("event-dispatch-%d"));
+
+    @SuppressWarnings("unchecked")
+    private static final Event KILL_PILL = new AbstractEvent(null, 0) {
+    };
+
+    private final BlockingQueue<Event> events = new LinkedBlockingQueue<>();
+
+    private volatile boolean stopped = false;
+
+    @Override
+    public void post(Event event) {
+        events.add(event);
+    }
+
+    @Activate
+    public void activate() {
+        stopped = false;
+        executor.execute(new DispatchLoop());
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        stopped = true;
+        post(KILL_PILL);
+        log.info("Stopped");
+    }
+
+    // Auxiliary event dispatching loop that feeds off the events queue.
+    private class DispatchLoop implements Runnable {
+        @Override
+        @SuppressWarnings("unchecked")
+        public void run() {
+            log.info("Dispatch loop initiated");
+            while (!stopped) {
+                try {
+                    // Fetch the next event and if it is the kill-pill, bail
+                    Event event = events.take();
+                    if (event == KILL_PILL) {
+                        break;
+                    }
+
+                    // Locate the sink for the event class and use it to
+                    // process the event
+                    EventSink sink = getSink(event.getClass());
+                    if (sink != null) {
+                        sink.process(event);
+                    } else {
+                        log.warn("No sink registered for event class {}",
+                                 event.getClass());
+                    }
+                } catch (Exception e) {
+                    log.warn("Error encountered while dispatching event:", e);
+                }
+            }
+            log.info("Dispatch loop terminated");
+        }
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/impl/GreetManager.java b/core/trivial/src/main/java/org/onlab/onos/impl/GreetManager.java
new file mode 100644
index 0000000..e5baf0e
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/impl/GreetManager.java
@@ -0,0 +1,52 @@
+package org.onlab.onos.impl;
+
+import com.google.common.collect.ImmutableSet;
+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.Service;
+import org.onlab.onos.GreetService;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Trivial implementation of the seed service to demonstrate component and
+ * service annotations.
+ */
+@Component(immediate = true)
+@Service
+public class GreetManager implements GreetService {
+
+    private final Logger log = getLogger(getClass());
+
+    private final Set<String> names = new HashSet<>();
+
+    @Override
+    public synchronized String yo(String name) {
+        checkNotNull(name, "Name cannot be null");
+        names.add(name);
+        log.info("Greeted '{}'", name);
+        return "Whazup " + name + "?";
+    }
+
+    @Override
+    public synchronized Iterable<String> names() {
+        return ImmutableSet.copyOf(names);
+    }
+
+    @Activate
+    public void activate() {
+        log.info("SeedManager started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("SeedManager stopped");
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/impl/SomeOtherComponent.java b/core/trivial/src/main/java/org/onlab/onos/impl/SomeOtherComponent.java
new file mode 100644
index 0000000..627573f
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/impl/SomeOtherComponent.java
@@ -0,0 +1,37 @@
+package org.onlab.onos.impl;
+
+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.onos.GreetService;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Example of a component that does not provide any service, but consumes one.
+ */
+@Component(immediate = true)
+public class SomeOtherComponent {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected GreetService service;
+    // protected to allow injection for testing;
+    // alternative is to write bindSeedService and unbindSeedService, which is more code
+
+    @Activate
+    public void activate() {
+        log.info("SomeOtherComponent started");
+        service.yo("neighbour");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("SomeOtherComponent stopped");
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/DefaultTopologyDescription.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/DefaultTopologyDescription.java
new file mode 100644
index 0000000..773762a
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/DefaultTopologyDescription.java
@@ -0,0 +1,80 @@
+package org.onlab.onos.net.trivial.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import org.onlab.graph.Graph;
+import org.onlab.graph.GraphPathSearch;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.topology.ClusterId;
+import org.onlab.onos.net.topology.TopoEdge;
+import org.onlab.onos.net.topology.TopoVertex;
+import org.onlab.onos.net.topology.TopologyCluster;
+import org.onlab.onos.net.topology.TopologyDescription;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Default implementation of an immutable topology data carrier.
+ */
+public class DefaultTopologyDescription implements TopologyDescription {
+
+    private final long nanos;
+    private final Graph<TopoVertex, TopoEdge> graph;
+    private final Map<DeviceId, GraphPathSearch.Result<TopoVertex, TopoEdge>> results;
+    private final Map<ClusterId, TopologyCluster> clusters;
+    private final Multimap<ClusterId, DeviceId> clusterDevices;
+    private final Multimap<ClusterId, Link> clusterLinks;
+    private final Map<DeviceId, TopologyCluster> deviceClusters;
+
+    public DefaultTopologyDescription(long nanos, Graph<TopoVertex, TopoEdge> graph,
+                                      Map<DeviceId, GraphPathSearch.Result<TopoVertex, TopoEdge>> results,
+                                      Map<ClusterId, TopologyCluster> clusters,
+                                      Multimap<ClusterId, DeviceId> clusterDevices,
+                                      Multimap<ClusterId, Link> clusterLinks,
+                                      Map<DeviceId, TopologyCluster> deviceClusters) {
+        this.nanos = nanos;
+        this.graph = graph;
+        this.results = results;
+        this.clusters = clusters;
+        this.clusterDevices = clusterDevices;
+        this.clusterLinks = clusterLinks;
+        this.deviceClusters = deviceClusters;
+    }
+
+    @Override
+    public long timestamp() {
+        return nanos;
+    }
+
+    @Override
+    public Graph<TopoVertex, TopoEdge> graph() {
+        return graph;
+    }
+
+    @Override
+    public GraphPathSearch.Result<TopoVertex, TopoEdge> pathResults(DeviceId srcDeviceId) {
+        return results.get(srcDeviceId);
+    }
+
+    @Override
+    public Set<TopologyCluster> clusters() {
+        return ImmutableSet.copyOf(clusters.values());
+    }
+
+    @Override
+    public Set<DeviceId> clusterDevices(TopologyCluster cluster) {
+        return null; // clusterDevices.get(cluster.id());
+    }
+
+    @Override
+    public Set<Link> clusterLinks(TopologyCluster cluster) {
+        return null; // clusterLinks.get(cluster.id());
+    }
+
+    @Override
+    public TopologyCluster clusterFor(DeviceId deviceId) {
+        return deviceClusters.get(deviceId);
+    }
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManager.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManager.java
new file mode 100644
index 0000000..17d3aa0
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManager.java
@@ -0,0 +1,220 @@
+package org.onlab.onos.net.trivial.impl;
+
+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.apache.felix.scr.annotations.Service;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.device.DeviceAdminService;
+import org.onlab.onos.net.device.DeviceDescription;
+import org.onlab.onos.net.device.DeviceEvent;
+import org.onlab.onos.net.device.DeviceListener;
+import org.onlab.onos.net.device.DeviceProvider;
+import org.onlab.onos.net.device.DeviceProviderRegistry;
+import org.onlab.onos.net.device.DeviceProviderService;
+import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.device.PortDescription;
+import org.onlab.onos.net.provider.AbstractProviderRegistry;
+import org.onlab.onos.net.provider.AbstractProviderService;
+import org.slf4j.Logger;
+
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides basic implementation of the device SB &amp; NB APIs.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleDeviceManager
+        extends AbstractProviderRegistry<DeviceProvider, DeviceProviderService>
+        implements DeviceService, DeviceAdminService, DeviceProviderRegistry {
+
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String PORT_NUMBER_NULL = "Port number cannot be null";
+    private static final String DEVICE_DESCRIPTION_NULL = "Device description cannot be null";
+    private static final String PORT_DESCRIPTION_NULL = "Port description cannot be null";
+    private static final String ROLE_NULL = "Role cannot be null";
+
+    private final Logger log = getLogger(getClass());
+
+    private final AbstractListenerRegistry<DeviceEvent, DeviceListener>
+            listenerRegistry = new AbstractListenerRegistry<>();
+
+        private final SimpleDeviceStore store = new SimpleDeviceStore();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected EventDeliveryService eventDispatcher;
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(DeviceEvent.class, listenerRegistry);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(DeviceEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    public int getDeviceCount() {
+        return store.getDeviceCount();
+    }
+
+    @Override
+    public Iterable<Device> getDevices() {
+        return store.getDevices();
+    }
+
+    @Override
+    public Device getDevice(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getDevice(deviceId);
+    }
+
+    @Override
+    public MastershipRole getRole(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getRole(deviceId);
+    }
+
+    @Override
+    public List<Port> getPorts(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getPorts(deviceId);
+    }
+
+    @Override
+    public Port getPort(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        checkNotNull(portNumber, PORT_NUMBER_NULL);
+        return store.getPort(deviceId, portNumber);
+    }
+
+    @Override
+    public boolean isAvailable(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.isAvailable(deviceId);
+    }
+
+    @Override
+    public void addListener(DeviceListener listener) {
+        listenerRegistry.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(DeviceListener listener) {
+        listenerRegistry.removeListener(listener);
+    }
+
+    @Override
+    protected DeviceProviderService createProviderService(DeviceProvider provider) {
+        return new InternalDeviceProviderService(provider);
+    }
+
+    @Override
+    public void setRole(DeviceId deviceId, MastershipRole newRole) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        checkNotNull(newRole, ROLE_NULL);
+        DeviceEvent event = store.setRole(deviceId, newRole);
+        if (event != null) {
+            Device device = event.subject();
+            DeviceProvider provider = getProvider(device.providerId());
+            if (provider != null) {
+                provider.roleChanged(device, newRole);
+            }
+            post(event);
+        }
+    }
+
+    @Override
+    public void removeDevice(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        DeviceEvent event = store.removeDevice(deviceId);
+        if (event != null) {
+            log.info("Device {} administratively removed", deviceId);
+            post(event);
+        }
+    }
+
+    // Personalized device provider service issued to the supplied provider.
+    private class InternalDeviceProviderService extends AbstractProviderService<DeviceProvider>
+            implements DeviceProviderService {
+
+        InternalDeviceProviderService(DeviceProvider provider) {
+            super(provider);
+        }
+
+        @Override
+        public void deviceConnected(DeviceId deviceId, DeviceDescription deviceDescription) {
+            checkNotNull(deviceId, DEVICE_ID_NULL);
+            checkNotNull(deviceDescription, DEVICE_DESCRIPTION_NULL);
+            checkValidity();
+            DeviceEvent event = store.createOrUpdateDevice(provider().id(),
+                                                           deviceId, deviceDescription);
+
+            // If there was a change of any kind, trigger role selection process.
+            if (event != null) {
+                log.info("Device {} connected", deviceId);
+                Device device = event.subject();
+                provider().roleChanged(device, store.getRole(device.id()));
+                post(event);
+            }
+        }
+
+        @Override
+        public void deviceDisconnected(DeviceId deviceId) {
+            checkNotNull(deviceId, DEVICE_ID_NULL);
+            checkValidity();
+            DeviceEvent event = store.markOffline(deviceId);
+            if (event != null) {
+                log.info("Device {} disconnected", deviceId);
+                post(event);
+            }
+        }
+
+        @Override
+        public void updatePorts(DeviceId deviceId, List<PortDescription> portDescriptions) {
+            checkNotNull(deviceId, DEVICE_ID_NULL);
+            checkNotNull(portDescriptions, "Port descriptions list cannot be null");
+            checkValidity();
+            List<DeviceEvent> events = store.updatePorts(deviceId, portDescriptions);
+            for (DeviceEvent event : events) {
+                post(event);
+            }
+        }
+
+        @Override
+        public void portStatusChanged(DeviceId deviceId, PortDescription portDescription) {
+            checkNotNull(deviceId, DEVICE_ID_NULL);
+            checkNotNull(portDescription, PORT_DESCRIPTION_NULL);
+            checkValidity();
+            DeviceEvent event = store.updatePortStatus(deviceId, portDescription);
+            if (event != null) {
+                log.info("Device {} port {} status changed", deviceId,
+                         event.port().number());
+                post(event);
+            }
+        }
+    }
+
+    // Posts the specified event to the local event dispatcher.
+    private void post(DeviceEvent event) {
+        if (event != null && eventDispatcher != null) {
+            eventDispatcher.post(event);
+        }
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java
new file mode 100644
index 0000000..e219a63
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleDeviceStore.java
@@ -0,0 +1,327 @@
+package org.onlab.onos.net.trivial.impl;
+
+import com.google.common.collect.ImmutableList;
+import org.onlab.onos.net.DefaultDevice;
+import org.onlab.onos.net.DefaultPort;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.device.DeviceDescription;
+import org.onlab.onos.net.device.DeviceEvent;
+import org.onlab.onos.net.device.PortDescription;
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.onlab.onos.net.device.DeviceEvent.Type.*;
+
+/**
+ * Manages inventory of infrastructure devices using trivial in-memory
+ * implementation.
+ */
+class SimpleDeviceStore {
+
+    public static final String DEVICE_NOT_FOUND = "Device with ID %s not found";
+
+    private final Map<DeviceId, DefaultDevice> devices = new ConcurrentHashMap<>();
+    private final Map<DeviceId, MastershipRole> roles = new ConcurrentHashMap<>();
+    private final Set<DeviceId> availableDevices = new HashSet<>();
+    private final Map<DeviceId, Map<PortNumber, Port>> devicePorts = new HashMap<>();
+
+    /**
+     * Returns the number of devices known to the system.
+     *
+     * @return number of devices
+     */
+    int getDeviceCount() {
+        return devices.size();
+    }
+
+    /**
+     * Returns an iterable collection of all devices known to the system.
+     *
+     * @return device collection
+     */
+    Iterable<Device> getDevices() {
+        return Collections.unmodifiableSet(new HashSet<Device>(devices.values()));
+    }
+
+    /**
+     * Returns the device with the specified identifier.
+     *
+     * @param deviceId device identifier
+     * @return device
+     */
+    Device getDevice(DeviceId deviceId) {
+        return devices.get(deviceId);
+    }
+
+    /**
+     * Creates a new infrastructure device, or updates an existing one using
+     * the supplied device description.
+     *
+     * @param providerId        provider identifier
+     * @param deviceId          device identifier
+     * @param deviceDescription device description
+     * @return ready to send event describing what occurred; null if no change
+     */
+    DeviceEvent createOrUpdateDevice(ProviderId providerId, DeviceId deviceId,
+                                     DeviceDescription deviceDescription) {
+        DefaultDevice device = devices.get(deviceId);
+        if (device == null) {
+            return createDevice(providerId, deviceId, deviceDescription);
+        }
+        return updateDevice(providerId, device, deviceDescription);
+    }
+
+    // Creates the device and returns the appropriate event if necessary.
+    private DeviceEvent createDevice(ProviderId providerId, DeviceId deviceId,
+                                     DeviceDescription desc) {
+        DefaultDevice device = new DefaultDevice(providerId, deviceId, desc.type(),
+                                                 desc.manufacturer(),
+                                                 desc.hwVersion(), desc.swVersion(),
+                                                 desc.serialNumber());
+        synchronized (this) {
+            devices.put(deviceId, device);
+            availableDevices.add(deviceId);
+
+            // For now claim the device as a master automatically.
+            roles.put(deviceId, MastershipRole.MASTER);
+        }
+        return new DeviceEvent(DeviceEvent.Type.DEVICE_ADDED, device, null);
+    }
+
+    // Updates the device and returns the appropriate event if necessary.
+    private DeviceEvent updateDevice(ProviderId providerId, DefaultDevice device,
+                                     DeviceDescription desc) {
+        // We allow only certain attributes to trigger update
+        if (!Objects.equals(device.hwVersion(), desc.hwVersion()) ||
+                !Objects.equals(device.swVersion(), desc.swVersion())) {
+            DefaultDevice updated = new DefaultDevice(providerId, device.id(),
+                                                      desc.type(),
+                                                      desc.manufacturer(),
+                                                      desc.hwVersion(),
+                                                      desc.swVersion(),
+                                                      desc.serialNumber());
+            synchronized (this) {
+                devices.put(device.id(), updated);
+                availableDevices.add(device.id());
+            }
+            return new DeviceEvent(DeviceEvent.Type.DEVICE_UPDATED, device, null);
+        }
+
+        // Otherwise merely attempt to change availability
+        synchronized (this) {
+            boolean added = availableDevices.add(device.id());
+            return !added ? null :
+                    new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device, null);
+        }
+    }
+
+    /**
+     * Removes the specified infrastructure device.
+     *
+     * @param deviceId device identifier
+     * @return ready to send event describing what occurred; null if no change
+     */
+    DeviceEvent markOffline(DeviceId deviceId) {
+        synchronized (this) {
+            Device device = devices.get(deviceId);
+            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
+            boolean removed = availableDevices.remove(deviceId);
+            return !removed ? null :
+                    new DeviceEvent(DEVICE_AVAILABILITY_CHANGED, device, null);
+        }
+    }
+
+    /**
+     * Updates the ports of the specified infrastructure device using the given
+     * list of port descriptions. The list is assumed to be comprehensive.
+     *
+     * @param deviceId         device identifier
+     * @param portDescriptions list of port descriptions
+     * @return ready to send events describing what occurred; empty list if no change
+     */
+    List<DeviceEvent> updatePorts(DeviceId deviceId,
+                                  List<PortDescription> portDescriptions) {
+        List<DeviceEvent> events = new ArrayList<>();
+        synchronized (this) {
+            Device device = devices.get(deviceId);
+            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
+            Map<PortNumber, Port> ports = getPortMap(deviceId);
+
+            // Add new ports
+            Set<PortNumber> processed = new HashSet<>();
+            for (PortDescription portDescription : portDescriptions) {
+                Port port = ports.get(portDescription.portNumber());
+                events.add(port == null ?
+                                   createPort(device, portDescription, ports) :
+                                   updatePort(device, port, portDescription, ports));
+                processed.add(portDescription.portNumber());
+            }
+
+            events.addAll(pruneOldPorts(device, ports, processed));
+        }
+        return events;
+    }
+
+    // Creates a new port based on the port description adds it to the map and
+    // Returns corresponding event.
+    private DeviceEvent createPort(Device device, PortDescription portDescription,
+                                   Map<PortNumber, Port> ports) {
+        DefaultPort port = new DefaultPort(device, portDescription.portNumber(),
+                                           portDescription.isEnabled());
+        ports.put(port.number(), port);
+        return new DeviceEvent(PORT_ADDED, device, port);
+    }
+
+    // CHecks if the specified port requires update and if so, it replaces the
+    // existing entry in the map and returns corresponding event.
+    private DeviceEvent updatePort(Device device, Port port,
+                                   PortDescription portDescription,
+                                   Map<PortNumber, Port> ports) {
+        if (port.isEnabled() != portDescription.isEnabled()) {
+            DefaultPort updatedPort =
+                    new DefaultPort(device, portDescription.portNumber(),
+                                    portDescription.isEnabled());
+            ports.put(port.number(), updatedPort);
+            return new DeviceEvent(PORT_UPDATED, device, port);
+        }
+        return null;
+    }
+
+    // Prunes the specified list of ports based on which ports are in the
+    // processed list and returns list of corresponding events.
+    private List<DeviceEvent> pruneOldPorts(Device device,
+                                            Map<PortNumber, Port> ports,
+                                            Set<PortNumber> processed) {
+        List<DeviceEvent> events = new ArrayList<>();
+        Iterator<PortNumber> iterator = ports.keySet().iterator();
+        while (iterator.hasNext()) {
+            PortNumber portNumber = iterator.next();
+            if (!processed.contains(portNumber)) {
+                events.add(new DeviceEvent(PORT_REMOVED, device,
+                                           ports.get(portNumber)));
+                iterator.remove();
+            }
+        }
+        return events;
+    }
+
+    // Gets the map of ports for the specified device; if one does not already
+    // exist, it creates and registers a new one.
+    private Map<PortNumber, Port> getPortMap(DeviceId deviceId) {
+        Map<PortNumber, Port> ports = devicePorts.get(deviceId);
+        if (ports == null) {
+            ports = new HashMap<>();
+            devicePorts.put(deviceId, ports);
+        }
+        return ports;
+    }
+
+    /**
+     * Updates the port status of the specified infrastructure device using the
+     * given port description.
+     *
+     * @param deviceId        device identifier
+     * @param portDescription port description
+     * @return ready to send event describing what occurred; null if no change
+     */
+    DeviceEvent updatePortStatus(DeviceId deviceId,
+                                 PortDescription portDescription) {
+        synchronized (this) {
+            Device device = devices.get(deviceId);
+            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
+            Map<PortNumber, Port> ports = getPortMap(deviceId);
+            Port port = ports.get(portDescription.portNumber());
+            return updatePort(device, port, portDescription, ports);
+        }
+    }
+
+    /**
+     * Returns the list of ports that belong to the specified device.
+     *
+     * @param deviceId device identifier
+     * @return list of device ports
+     */
+    List<Port> getPorts(DeviceId deviceId) {
+        Map<PortNumber, Port> ports = devicePorts.get(deviceId);
+        return ports == null ? new ArrayList<Port>() : ImmutableList.copyOf(ports.values());
+    }
+
+    /**
+     * Returns the specified device port.
+     *
+     * @param deviceId   device identifier
+     * @param portNumber port number
+     * @return device port
+     */
+    Port getPort(DeviceId deviceId, PortNumber portNumber) {
+        Map<PortNumber, Port> ports = devicePorts.get(deviceId);
+        return ports == null ? null : ports.get(portNumber);
+    }
+
+    /**
+     * Indicates whether the specified device is available/online.
+     *
+     * @param deviceId device identifier
+     * @return true if device is available
+     */
+    boolean isAvailable(DeviceId deviceId) {
+        return availableDevices.contains(deviceId);
+    }
+
+    /**
+     * Returns the mastership role determined for this device.
+     *
+     * @param deviceId device identifier
+     * @return mastership role
+     */
+    MastershipRole getRole(DeviceId deviceId) {
+        MastershipRole role = roles.get(deviceId);
+        return role != null ? role : MastershipRole.NONE;
+    }
+
+    /**
+     * Administratively sets the role of the specified device.
+     *
+     * @param deviceId device identifier
+     * @param role     mastership role to apply
+     * @return mastership role change event or null if no change
+     */
+    DeviceEvent setRole(DeviceId deviceId, MastershipRole role) {
+        synchronized (this) {
+            Device device = getDevice(deviceId);
+            checkArgument(device != null, DEVICE_NOT_FOUND, deviceId);
+            MastershipRole oldRole = roles.put(deviceId, role);
+            return oldRole == role ? null :
+                    new DeviceEvent(DEVICE_MASTERSHIP_CHANGED, device, null);
+        }
+    }
+
+    /**
+     * Administratively removes the specified device from the store.
+     *
+     * @param deviceId device to be removed
+     */
+    DeviceEvent removeDevice(DeviceId deviceId) {
+        synchronized (this) {
+            roles.remove(deviceId);
+            Device device = devices.remove(deviceId);
+            return device == null ? null :
+                    new DeviceEvent(DEVICE_REMOVED, device, null);
+        }
+    }
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleHostManager.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleHostManager.java
new file mode 100644
index 0000000..17849b1
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleHostManager.java
@@ -0,0 +1,76 @@
+package org.onlab.onos.net.trivial.impl;
+
+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.apache.felix.scr.annotations.Service;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.net.host.HostDescription;
+import org.onlab.onos.net.host.HostEvent;
+import org.onlab.onos.net.host.HostListener;
+import org.onlab.onos.net.host.HostProvider;
+import org.onlab.onos.net.host.HostProviderRegistry;
+import org.onlab.onos.net.host.HostProviderService;
+import org.onlab.onos.net.provider.AbstractProviderRegistry;
+import org.onlab.onos.net.provider.AbstractProviderService;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides basic implementation of the host SB &amp; NB APIs.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleHostManager
+        extends AbstractProviderRegistry<HostProvider, HostProviderService>
+        implements HostProviderRegistry {
+
+    private final Logger log = getLogger(getClass());
+
+    private final AbstractListenerRegistry<HostEvent, HostListener>
+            listenerRegistry = new AbstractListenerRegistry<>();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private EventDeliveryService eventDispatcher;
+
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(HostEvent.class, listenerRegistry);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(HostEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    protected HostProviderService createProviderService(HostProvider provider) {
+        return new InternalHostProviderService(provider);
+    }
+
+    // Personalized host provider service issued to the supplied provider.
+    private class InternalHostProviderService extends AbstractProviderService<HostProvider>
+            implements HostProviderService {
+
+        InternalHostProviderService(HostProvider provider) {
+            super(provider);
+        }
+
+        @Override
+        public void hostDetected(HostDescription hostDescription) {
+            log.info("Host {} detected", hostDescription);
+        }
+
+        @Override
+        public void hostVanished(HostDescription hostDescription) {
+            log.info("Host {} vanished", hostDescription);
+        }
+    }
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkManager.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkManager.java
new file mode 100644
index 0000000..88d0663
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkManager.java
@@ -0,0 +1,208 @@
+package org.onlab.onos.net.trivial.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.Set;
+
+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.apache.felix.scr.annotations.Service;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.link.LinkAdminService;
+import org.onlab.onos.net.link.LinkDescription;
+import org.onlab.onos.net.link.LinkEvent;
+import org.onlab.onos.net.link.LinkListener;
+import org.onlab.onos.net.link.LinkProvider;
+import org.onlab.onos.net.link.LinkProviderRegistry;
+import org.onlab.onos.net.link.LinkProviderService;
+import org.onlab.onos.net.link.LinkService;
+import org.onlab.onos.net.provider.AbstractProviderRegistry;
+import org.onlab.onos.net.provider.AbstractProviderService;
+import org.slf4j.Logger;
+
+import com.google.common.collect.Sets;
+
+/**
+ * Provides basic implementation of the link SB &amp; NB APIs.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleLinkManager
+extends AbstractProviderRegistry<LinkProvider, LinkProviderService>
+implements LinkService, LinkAdminService, LinkProviderRegistry {
+
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String LINK_DESC_NULL = "Link description cannot be null";
+    private static final String CONNECT_POINT_NULL = "Connection point cannot be null";
+
+    private final Logger log = getLogger(getClass());
+
+    private final AbstractListenerRegistry<LinkEvent, LinkListener>
+    listenerRegistry = new AbstractListenerRegistry<>();
+
+    private final SimpleLinkStore store = new SimpleLinkStore();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected EventDeliveryService eventDispatcher;
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(LinkEvent.class, listenerRegistry);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(LinkEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    protected LinkProviderService createProviderService(LinkProvider provider) {
+        return new InternalLinkProviderService(provider);
+    }
+
+    @Override
+    public int getLinkCount() {
+        return store.getLinkCount();
+    }
+
+    @Override
+    public Iterable<Link> getLinks() {
+        return store.getLinks();
+    }
+
+    @Override
+    public Set<Link> getDeviceLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return Sets.union(store.getDeviceEgressLinks(deviceId),
+                store.getDeviceIngressLinks(deviceId));
+    }
+
+    @Override
+    public Set<Link> getDeviceEgressLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getDeviceEgressLinks(deviceId);
+    }
+
+    @Override
+    public Set<Link> getDeviceIngressLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getDeviceIngressLinks(deviceId);
+    }
+
+    @Override
+    public Set<Link> getLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return Sets.union(store.getEgressLinks(connectPoint),
+                store.getIngressLinks(connectPoint));
+    }
+
+    @Override
+    public Set<Link> getEgressLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return store.getEgressLinks(connectPoint);
+    }
+
+    @Override
+    public Set<Link> getIngressLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return store.getIngressLinks(connectPoint);
+    }
+
+    @Override
+    public Link getLink(ConnectPoint src, ConnectPoint dst) {
+        checkNotNull(src, CONNECT_POINT_NULL);
+        checkNotNull(dst, CONNECT_POINT_NULL);
+        return store.getLink(src, dst);
+    }
+
+    @Override
+    public void removeLinks(ConnectPoint connectPoint) {
+        removeLinks(getLinks(connectPoint));
+    }
+
+    @Override
+    public void removeLinks(DeviceId deviceId) {
+        removeLinks(getDeviceLinks(deviceId));
+    }
+
+    @Override
+    public void addListener(LinkListener listener) {
+        listenerRegistry.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(LinkListener listener) {
+        listenerRegistry.removeListener(listener);
+    }
+
+    // Personalized link provider service issued to the supplied provider.
+    private class InternalLinkProviderService extends AbstractProviderService<LinkProvider>
+    implements LinkProviderService {
+
+        InternalLinkProviderService(LinkProvider provider) {
+            super(provider);
+        }
+
+        @Override
+        public void linkDetected(LinkDescription linkDescription) {
+            checkNotNull(linkDescription, LINK_DESC_NULL);
+            checkValidity();
+            log.debug("Link {} detected", linkDescription);
+            LinkEvent event = store.createOrUpdateLink(provider().id(),
+                    linkDescription);
+            post(event);
+        }
+
+        @Override
+        public void linkVanished(LinkDescription linkDescription) {
+            checkNotNull(linkDescription, LINK_DESC_NULL);
+            checkValidity();
+            log.info("Link {} vanished", linkDescription);
+            LinkEvent event = store.removeLink(linkDescription.src(),
+                    linkDescription.dst());
+            post(event);
+        }
+
+        @Override
+        public void linksVanished(ConnectPoint connectPoint) {
+            checkNotNull(connectPoint, "Connect point cannot be null");
+            checkValidity();
+            log.info("Link for connection point {} vanished", connectPoint);
+            removeLinks(getLinks(connectPoint));
+        }
+
+        @Override
+        public void linksVanished(DeviceId deviceId) {
+            checkNotNull(deviceId, DEVICE_ID_NULL);
+            checkValidity();
+            log.info("Link for device {} vanished", deviceId);
+            removeLinks(getDeviceLinks(deviceId));
+        }
+    }
+
+    // Removes all links in the specified set and emits appropriate events.
+    private void removeLinks(Set<Link> links) {
+        for (Link link : links) {
+            LinkEvent event = store.removeLink(link.src(), link.dst());
+            post(event);
+        }
+    }
+
+    // Posts the specified event to the local event dispatcher.
+    private void post(LinkEvent event) {
+        if (event != null && eventDispatcher != null) {
+            eventDispatcher.post(event);
+        }
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java
new file mode 100644
index 0000000..d10c3a4
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleLinkStore.java
@@ -0,0 +1,218 @@
+package org.onlab.onos.net.trivial.impl;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultLink;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.link.LinkDescription;
+import org.onlab.onos.net.link.LinkEvent;
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.onlab.onos.net.Link.Type.DIRECT;
+import static org.onlab.onos.net.Link.Type.INDIRECT;
+import static org.onlab.onos.net.link.LinkEvent.Type.LINK_ADDED;
+import static org.onlab.onos.net.link.LinkEvent.Type.LINK_REMOVED;
+import static org.onlab.onos.net.link.LinkEvent.Type.LINK_UPDATED;
+
+/**
+ * Manages inventory of infrastructure links using trivial in-memory link
+ * implementation.
+ */
+class SimpleLinkStore {
+
+    // Link inventory
+    private final Map<LinkKey, DefaultLink> links = new ConcurrentHashMap<>();
+
+    // Egress and ingress link sets
+    private final Multimap<DeviceId, Link> srcLinks = HashMultimap.create();
+    private final Multimap<DeviceId, Link> dstLinks = HashMultimap.create();
+
+    private static final Set<Link> EMPTY = ImmutableSet.copyOf(new Link[]{});
+
+    /**
+     * Returns the number of links in the store.
+     *
+     * @return number of links
+     */
+    int getLinkCount() {
+        return links.size();
+    }
+
+    /**
+     * Returns an iterable collection of all links in the inventory.
+     *
+     * @return collection of all links
+     */
+    Iterable<Link> getLinks() {
+        return Collections.unmodifiableSet(new HashSet<Link>(links.values()));
+    }
+
+    /**
+     * Returns all links egressing from the specified device.
+     *
+     * @param deviceId device identifier
+     * @return set of device links
+     */
+    Set<Link> getDeviceEgressLinks(DeviceId deviceId) {
+        return ImmutableSet.copyOf(srcLinks.get(deviceId));
+    }
+
+    /**
+     * Returns all links ingressing from the specified device.
+     *
+     * @param deviceId device identifier
+     * @return set of device links
+     */
+    Set<Link> getDeviceIngressLinks(DeviceId deviceId) {
+        return ImmutableSet.copyOf(dstLinks.get(deviceId));
+    }
+
+    /**
+     * Returns the link between the two end-points.
+     *
+     * @param src source connection point
+     * @param dst destination connection point
+     * @return link or null if one not found between the end-points
+     */
+    Link getLink(ConnectPoint src, ConnectPoint dst) {
+        return links.get(new LinkKey(src, dst));
+    }
+
+    /**
+     * Returns all links egressing from the specified connection point.
+     *
+     * @param src source connection point
+     * @return set of connection point links
+     */
+    Set<Link> getEgressLinks(ConnectPoint src) {
+        Set<Link> egress = new HashSet<>();
+        for (Link link : srcLinks.get(src.deviceId())) {
+            if (link.src().equals(src)) {
+                egress.add(link);
+            }
+        }
+        return egress;
+    }
+
+    /**
+     * Returns all links ingressing to the specified connection point.
+     *
+     * @param dst destination connection point
+     * @return set of connection point links
+     */
+    Set<Link> getIngressLinks(ConnectPoint dst) {
+        Set<Link> ingress = new HashSet<>();
+        for (Link link : dstLinks.get(dst.deviceId())) {
+            if (link.dst().equals(dst)) {
+                ingress.add(link);
+            }
+        }
+        return ingress;
+    }
+
+    /**
+     * Creates a new link, or updates an existing one, based on the given
+     * information.
+     *
+     * @param providerId      provider identity
+     * @param linkDescription link description
+     * @return create or update link event, or null if no change resulted
+     */
+    public LinkEvent createOrUpdateLink(ProviderId providerId,
+                                        LinkDescription linkDescription) {
+        LinkKey key = new LinkKey(linkDescription.src(), linkDescription.dst());
+        DefaultLink link = links.get(key);
+        if (link == null) {
+            return createLink(providerId, key, linkDescription);
+        }
+        return updateLink(providerId, link, key, linkDescription);
+    }
+
+    // Creates and stores the link and returns the appropriate event.
+    private LinkEvent createLink(ProviderId providerId, LinkKey key,
+                                 LinkDescription linkDescription) {
+        DefaultLink link = new DefaultLink(providerId, key.src, key.dst,
+                                           linkDescription.type());
+        synchronized (this) {
+            links.put(key, link);
+            srcLinks.put(link.src().deviceId(), link);
+            dstLinks.put(link.dst().deviceId(), link);
+        }
+        return new LinkEvent(LINK_ADDED, link);
+    }
+
+    // Updates, if necessary the specified link and returns the appropriate event.
+    private LinkEvent updateLink(ProviderId providerId, DefaultLink link,
+                                 LinkKey key, LinkDescription linkDescription) {
+        if (link.type() == INDIRECT && linkDescription.type() == DIRECT) {
+            synchronized (this) {
+                srcLinks.remove(link.src().deviceId(), link);
+                dstLinks.remove(link.dst().deviceId(), link);
+
+                DefaultLink updated =
+                        new DefaultLink(providerId, link.src(), link.dst(),
+                                        linkDescription.type());
+                links.put(key, updated);
+                srcLinks.put(link.src().deviceId(), updated);
+                dstLinks.put(link.dst().deviceId(), updated);
+                return new LinkEvent(LINK_UPDATED, updated);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Removes the link based on the specified information.
+     *
+     * @param src link source
+     * @param dst link destination
+     * @return remove link event, or null if no change resulted
+     */
+    LinkEvent removeLink(ConnectPoint src, ConnectPoint dst) {
+        synchronized (this) {
+            Link link = links.remove(new LinkKey(src, dst));
+            if (link != null) {
+                srcLinks.remove(link.src().deviceId(), link);
+                dstLinks.remove(link.dst().deviceId(), link);
+                return new LinkEvent(LINK_REMOVED, link);
+            }
+            return null;
+        }
+    }
+
+    // Auxiliary key to track links.
+    private class LinkKey {
+        final ConnectPoint src;
+        final ConnectPoint dst;
+
+        LinkKey(ConnectPoint src, ConnectPoint dst) {
+            this.src = src;
+            this.dst = dst;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(src, dst);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof LinkKey) {
+                final LinkKey other = (LinkKey) obj;
+                return Objects.equals(this.src, other.src) &&
+                        Objects.equals(this.dst, other.dst);
+            }
+            return false;
+        }
+    }
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyManager.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyManager.java
new file mode 100644
index 0000000..1e95598
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyManager.java
@@ -0,0 +1,155 @@
+package org.onlab.onos.net.trivial.impl;
+
+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.apache.felix.scr.annotations.Service;
+import org.onlab.graph.Graph;
+import org.onlab.onos.event.AbstractListenerRegistry;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.provider.AbstractProviderRegistry;
+import org.onlab.onos.net.provider.AbstractProviderService;
+import org.onlab.onos.net.topology.LinkWeight;
+import org.onlab.onos.net.topology.TopoEdge;
+import org.onlab.onos.net.topology.TopoVertex;
+import org.onlab.onos.net.topology.Topology;
+import org.onlab.onos.net.topology.TopologyCluster;
+import org.onlab.onos.net.topology.TopologyDescription;
+import org.onlab.onos.net.topology.TopologyEvent;
+import org.onlab.onos.net.topology.TopologyListener;
+import org.onlab.onos.net.topology.TopologyProvider;
+import org.onlab.onos.net.topology.TopologyProviderRegistry;
+import org.onlab.onos.net.topology.TopologyProviderService;
+import org.onlab.onos.net.topology.TopologyService;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides basic implementation of the topology SB &amp; NB APIs.
+ */
+@Component(immediate = true)
+@Service
+public class SimpleTopologyManager
+        extends AbstractProviderRegistry<TopologyProvider, TopologyProviderService>
+        implements TopologyService, TopologyProviderRegistry {
+
+    public static final String TOPOLOGY_NULL = "Topology cannot be null";
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    public static final String CONNECTION_POINT_NULL = "Connection point cannot be null";
+
+    private final Logger log = getLogger(getClass());
+
+    private final AbstractListenerRegistry<TopologyEvent, TopologyListener>
+            listenerRegistry = new AbstractListenerRegistry<>();
+
+    private final SimpleTopologyStore store = new SimpleTopologyStore();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    private EventDeliveryService eventDispatcher;
+
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(TopologyEvent.class, listenerRegistry);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        eventDispatcher.removeSink(TopologyEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    protected TopologyProviderService createProviderService(TopologyProvider provider) {
+        return new InternalTopologyProviderService(provider);
+    }
+
+    @Override
+    public Topology currentTopology() {
+        return null;
+    }
+
+    @Override
+    public Set<TopologyCluster> getClusters(Topology topology) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        return null;
+    }
+
+    @Override
+    public Graph<TopoVertex, TopoEdge> getGraph(Topology topology) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        return null;
+    }
+
+    @Override
+    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        return null;
+    }
+
+    @Override
+    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst, LinkWeight weight) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        checkNotNull(weight, "Link weight cannot be null");
+        return null;
+    }
+
+    @Override
+    public boolean isInfrastructure(Topology topology, ConnectPoint connectPoint) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        checkNotNull(connectPoint, CONNECTION_POINT_NULL);
+        return false;
+    }
+
+    @Override
+    public boolean isInBroadcastTree(Topology topology, ConnectPoint connectPoint) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        checkNotNull(connectPoint, CONNECTION_POINT_NULL);
+        return false;
+    }
+
+    @Override
+    public void addListener(TopologyListener listener) {
+        listenerRegistry.addListener(listener);
+    }
+
+    @Override
+    public void removeListener(TopologyListener listener) {
+        listenerRegistry.removeListener(listener);
+    }
+
+    // Personalized host provider service issued to the supplied provider.
+    private class InternalTopologyProviderService
+            extends AbstractProviderService<TopologyProvider>
+            implements TopologyProviderService {
+
+        InternalTopologyProviderService(TopologyProvider provider) {
+            super(provider);
+        }
+
+        @Override
+        public void topologyChanged(TopologyDescription topoDescription,
+                                    List<Event> reasons) {
+            checkNotNull(topoDescription, "Topology description cannot be null");
+            log.info("Topology changed due to: {}",
+                     reasons == null ? "initial compute" : reasons);
+        }
+    }
+
+}
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyStore.java
new file mode 100644
index 0000000..1a54cad
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/impl/SimpleTopologyStore.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.net.trivial.impl;
+
+/**
+ * Manages inventory of topology snapshots using trivial in-memory
+ * implementation.
+ */
+public class SimpleTopologyStore {
+}
diff --git a/core/trivial/src/main/javadoc/org/onlab/onos/event/impl/package.html b/core/trivial/src/main/javadoc/org/onlab/onos/event/impl/package.html
new file mode 100644
index 0000000..a22d62d
--- /dev/null
+++ b/core/trivial/src/main/javadoc/org/onlab/onos/event/impl/package.html
@@ -0,0 +1,3 @@
+<body>
+ONOS local event dispatching mechanism.
+</body>
\ No newline at end of file
diff --git a/core/trivial/src/main/javadoc/org/onlab/onos/impl/package.html b/core/trivial/src/main/javadoc/org/onlab/onos/impl/package.html
new file mode 100644
index 0000000..087085c
--- /dev/null
+++ b/core/trivial/src/main/javadoc/org/onlab/onos/impl/package.html
@@ -0,0 +1,3 @@
+<body>
+ONOS Core infrastructure implementations.
+</body>
\ No newline at end of file
diff --git a/core/trivial/src/main/javadoc/org/onlab/onos/net/trivial/impl/package.html b/core/trivial/src/main/javadoc/org/onlab/onos/net/trivial/impl/package.html
new file mode 100644
index 0000000..ba285bd
--- /dev/null
+++ b/core/trivial/src/main/javadoc/org/onlab/onos/net/trivial/impl/package.html
@@ -0,0 +1,3 @@
+<body>
+ONOS core implementations.
+</body>
\ No newline at end of file
diff --git a/core/trivial/src/test/java/org/onlab/onos/impl/GreetManagerTest.java b/core/trivial/src/test/java/org/onlab/onos/impl/GreetManagerTest.java
new file mode 100644
index 0000000..c498660
--- /dev/null
+++ b/core/trivial/src/test/java/org/onlab/onos/impl/GreetManagerTest.java
@@ -0,0 +1,31 @@
+package org.onlab.onos.impl;
+
+import org.junit.Test;
+import org.onlab.onos.GreetService;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Example of a component &amp; service implementation unit test.
+ */
+public class GreetManagerTest {
+
+    @Test
+    public void basics() {
+        GreetService service = new GreetManager();
+        assertEquals("incorrect greeting", "Whazup dude?", service.yo("dude"));
+
+        Iterator<String> names = service.names().iterator();
+        assertEquals("incorrect name", "dude", names.next());
+        assertFalse("no more names expected", names.hasNext());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullArg() {
+        new GreetManager().yo(null);
+    }
+
+}
diff --git a/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManagerTest.java b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManagerTest.java
new file mode 100644
index 0000000..bcece2d
--- /dev/null
+++ b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleDeviceManagerTest.java
@@ -0,0 +1,253 @@
+package org.onlab.onos.net.trivial.impl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.device.DefaultDeviceDescription;
+import org.onlab.onos.net.device.DefaultPortDescription;
+import org.onlab.onos.net.device.DeviceAdminService;
+import org.onlab.onos.net.device.DeviceDescription;
+import org.onlab.onos.net.device.DeviceEvent;
+import org.onlab.onos.net.device.DeviceListener;
+import org.onlab.onos.net.device.DeviceProvider;
+import org.onlab.onos.net.device.DeviceProviderRegistry;
+import org.onlab.onos.net.device.DeviceProviderService;
+import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.device.PortDescription;
+import org.onlab.onos.net.provider.AbstractProvider;
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.*;
+import static org.onlab.onos.net.Device.Type.SWITCH;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.device.DeviceEvent.Type.*;
+
+/**
+ * Test codifying the device service & device provider service contracts.
+ */
+public class SimpleDeviceManagerTest {
+
+    private static final ProviderId PID = new ProviderId("foo");
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final DeviceId DID2 = deviceId("of:bar");
+    private static final String MFR = "whitebox";
+    private static final String HW = "1.1.x";
+    private static final String SW1 = "3.8.1";
+    private static final String SW2 = "3.9.5";
+    private static final String SN = "43311-12345";
+
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+
+
+    private SimpleDeviceManager mgr;
+
+    protected DeviceService service;
+    protected DeviceAdminService admin;
+    protected DeviceProviderRegistry registry;
+    protected DeviceProviderService providerService;
+    protected TestProvider provider;
+    protected TestListener listener = new TestListener();
+
+    @Before
+    public void setUp() {
+        mgr = new SimpleDeviceManager();
+        service = mgr;
+        admin = mgr;
+        registry = mgr;
+        mgr.eventDispatcher = new TestEventDispatcher();
+        mgr.activate();
+
+        service.addListener(listener);
+
+        provider = new TestProvider();
+        providerService = registry.register(provider);
+        assertTrue("provider should be registered",
+                   registry.getProviders().contains(provider.id()));
+    }
+
+    @After
+    public void tearDown() {
+        registry.unregister(provider);
+        assertFalse("provider should not be registered",
+                    registry.getProviders().contains(provider.id()));
+        service.removeListener(listener);
+        mgr.deactivate();
+    }
+
+    private void connectDevice(DeviceId deviceId, String swVersion) {
+        DeviceDescription description =
+                new DefaultDeviceDescription(deviceId.uri(), SWITCH, MFR,
+                                             HW, swVersion, SN);
+        providerService.deviceConnected(deviceId, description);
+        assertNotNull("device should be found", service.getDevice(DID1));
+    }
+
+    @Test
+    public void deviceConnected() {
+        assertNull("device should not be found", service.getDevice(DID1));
+        connectDevice(DID1, SW1);
+        validateEvents(DEVICE_ADDED);
+
+        Iterator<Device> it = service.getDevices().iterator();
+        assertNotNull("one device expected", it.next());
+        assertFalse("only one device expected", it.hasNext());
+        assertEquals("incorrect device count", 1, service.getDeviceCount());
+        assertTrue("device should be available", service.isAvailable(DID1));
+    }
+
+    @Test
+    public void deviceDisconnected() {
+        connectDevice(DID1, SW1);
+        connectDevice(DID2, SW1);
+        validateEvents(DEVICE_ADDED, DEVICE_ADDED);
+        assertTrue("device should be available", service.isAvailable(DID1));
+
+        // Disconnect
+        providerService.deviceDisconnected(DID1);
+        assertNotNull("device should not be found", service.getDevice(DID1));
+        assertFalse("device should not be available", service.isAvailable(DID1));
+        validateEvents(DEVICE_AVAILABILITY_CHANGED);
+
+        // Reconnect
+        connectDevice(DID1, SW1);
+        validateEvents(DEVICE_AVAILABILITY_CHANGED);
+
+        assertEquals("incorrect device count", 2, service.getDeviceCount());
+    }
+
+    @Test
+    public void deviceUpdated() {
+        connectDevice(DID1, SW1);
+        validateEvents(DEVICE_ADDED);
+
+        connectDevice(DID1, SW2);
+        validateEvents(DEVICE_UPDATED);
+    }
+
+    @Test
+    public void getRole() {
+        connectDevice(DID1, SW1);
+        assertEquals("incorrect role", MastershipRole.MASTER, service.getRole(DID1));
+    }
+
+    @Test
+    public void setRole() throws InterruptedException {
+        connectDevice(DID1, SW1);
+        admin.setRole(DID1, MastershipRole.STANDBY);
+        validateEvents(DEVICE_ADDED, DEVICE_MASTERSHIP_CHANGED);
+        assertEquals("incorrect role", MastershipRole.STANDBY, service.getRole(DID1));
+        assertEquals("incorrect device", DID1, provider.deviceReceived.id());
+        assertEquals("incorrect role", MastershipRole.STANDBY, provider.roleReceived);
+    }
+
+    @Test
+    public void updatePorts() {
+        connectDevice(DID1, SW1);
+        List<PortDescription> pds = new ArrayList<>();
+        pds.add(new DefaultPortDescription(P1, true));
+        pds.add(new DefaultPortDescription(P2, true));
+        pds.add(new DefaultPortDescription(P3, true));
+        providerService.updatePorts(DID1, pds);
+        validateEvents(DEVICE_ADDED, PORT_ADDED, PORT_ADDED, PORT_ADDED);
+        pds.clear();
+
+        pds.add(new DefaultPortDescription(P1, false));
+        pds.add(new DefaultPortDescription(P3, true));
+        providerService.updatePorts(DID1, pds);
+        validateEvents(PORT_UPDATED, PORT_REMOVED);
+    }
+
+    @Test
+    public void updatePortStatus() {
+        connectDevice(DID1, SW1);
+        List<PortDescription> pds = new ArrayList<>();
+        pds.add(new DefaultPortDescription(P1, true));
+        pds.add(new DefaultPortDescription(P2, true));
+        providerService.updatePorts(DID1, pds);
+        validateEvents(DEVICE_ADDED, PORT_ADDED, PORT_ADDED);
+
+        providerService.portStatusChanged(DID1, new DefaultPortDescription(P1, false));
+        validateEvents(PORT_UPDATED);
+        providerService.portStatusChanged(DID1, new DefaultPortDescription(P1, false));
+        assertTrue("no events expected", listener.events.isEmpty());
+    }
+
+    @Test
+    public void getPorts() {
+        connectDevice(DID1, SW1);
+        List<PortDescription> pds = new ArrayList<>();
+        pds.add(new DefaultPortDescription(P1, true));
+        pds.add(new DefaultPortDescription(P2, true));
+        providerService.updatePorts(DID1, pds);
+        validateEvents(DEVICE_ADDED, PORT_ADDED, PORT_ADDED);
+        assertEquals("wrong port count", 2, service.getPorts(DID1).size());
+
+        Port port = service.getPort(DID1, P1);
+        assertEquals("incorrect port", P1, service.getPort(DID1, P1).number());
+        assertEquals("incorrect state", true, service.getPort(DID1, P1).isEnabled());
+    }
+
+    @Test
+    public void removeDevice() {
+        connectDevice(DID1, SW1);
+        connectDevice(DID2, SW2);
+        assertEquals("incorrect device count", 2, service.getDeviceCount());
+        admin.removeDevice(DID1);
+        assertNull("device should not be found", service.getDevice(DID1));
+        assertNotNull("device should be found", service.getDevice(DID2));
+        assertEquals("incorrect device count", 1, service.getDeviceCount());
+
+    }
+
+    protected void validateEvents(Enum... types) {
+        int i = 0;
+        assertEquals("wrong events received", types.length, listener.events.size());
+        for (Event event : listener.events) {
+            assertEquals("incorrect event type", types[i], event.type());
+            i++;
+        }
+        listener.events.clear();
+    }
+
+
+    private class TestProvider extends AbstractProvider implements DeviceProvider {
+        private Device deviceReceived;
+        private MastershipRole roleReceived;
+
+        public TestProvider() {
+            super(PID);
+        }
+
+        @Override
+        public void triggerProbe(Device device) {
+        }
+
+        @Override
+        public void roleChanged(Device device, MastershipRole newRole) {
+            deviceReceived = device;
+            roleReceived = newRole;
+        }
+    }
+
+    private static class TestListener implements DeviceListener {
+        final List<DeviceEvent> events = new ArrayList<>();
+
+        @Override
+        public void event(DeviceEvent event) {
+            events.add(event);
+        }
+    }
+
+}
diff --git a/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleLinkManagerTest.java b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleLinkManagerTest.java
new file mode 100644
index 0000000..8dd9e1d
--- /dev/null
+++ b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/SimpleLinkManagerTest.java
@@ -0,0 +1,257 @@
+package org.onlab.onos.net.trivial.impl;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.link.DefaultLinkDescription;
+import org.onlab.onos.net.link.LinkAdminService;
+import org.onlab.onos.net.link.LinkEvent;
+import org.onlab.onos.net.link.LinkListener;
+import org.onlab.onos.net.link.LinkProvider;
+import org.onlab.onos.net.link.LinkProviderRegistry;
+import org.onlab.onos.net.link.LinkProviderService;
+import org.onlab.onos.net.link.LinkService;
+import org.onlab.onos.net.provider.AbstractProvider;
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Link.Type.DIRECT;
+import static org.onlab.onos.net.Link.Type.INDIRECT;
+import static org.onlab.onos.net.link.LinkEvent.Type.*;
+
+/**
+ * Test codifying the link service & link provider service contracts.
+ */
+public class SimpleLinkManagerTest {
+
+    private static final ProviderId PID = new ProviderId("foo");
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final DeviceId DID2 = deviceId("of:bar");
+    private static final DeviceId DID3 = deviceId("of:goo");
+
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+
+
+    private SimpleLinkManager mgr;
+
+    protected LinkService service;
+    protected LinkAdminService admin;
+    protected LinkProviderRegistry registry;
+    protected LinkProviderService providerService;
+    protected TestProvider provider;
+    protected TestListener listener = new TestListener();
+
+    @Before
+    public void setUp() {
+        mgr = new SimpleLinkManager();
+        service = mgr;
+        admin = mgr;
+        registry = mgr;
+        mgr.eventDispatcher = new TestEventDispatcher();
+        mgr.activate();
+
+        service.addListener(listener);
+
+        provider = new TestProvider();
+        providerService = registry.register(provider);
+        assertTrue("provider should be registered",
+                   registry.getProviders().contains(provider.id()));
+    }
+
+    @After
+    public void tearDown() {
+        registry.unregister(provider);
+        assertFalse("provider should not be registered",
+                    registry.getProviders().contains(provider.id()));
+        service.removeListener(listener);
+        mgr.deactivate();
+    }
+
+    @Test
+    public void createLink() {
+        addLink(DID1, P1, DID2, P2, DIRECT);
+        addLink(DID2, P2, DID1, P1, DIRECT);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+
+        Iterator<Link> it = service.getLinks().iterator();
+        it.next();
+        it.next();
+        assertFalse("incorrect link count", it.hasNext());
+    }
+
+    @Test
+    public void updateLink() {
+        addLink(DID1, P1, DID2, P2, DIRECT);
+        addLink(DID2, P2, DID1, P1, INDIRECT);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+
+        providerService.linkDetected(new DefaultLinkDescription(cp(DID2, P2), cp(DID1, P1), DIRECT));
+        validateEvents(LINK_UPDATED);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+
+        providerService.linkDetected(new DefaultLinkDescription(cp(DID2, P2), cp(DID1, P1), INDIRECT));
+        providerService.linkDetected(new DefaultLinkDescription(cp(DID2, P2), cp(DID1, P1), DIRECT));
+        assertEquals("no events expected", 0, listener.events.size());
+    }
+
+    @Test
+    public void removeLink() {
+        addLink(DID1, P1, DID2, P2, DIRECT);
+        addLink(DID2, P2, DID1, P1, DIRECT);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+
+        providerService.linkVanished(new DefaultLinkDescription(cp(DID1, P1), cp(DID2, P2), DIRECT));
+        validateEvents(LINK_REMOVED);
+        assertEquals("incorrect link count", 1, service.getLinkCount());
+        assertNull("link should not be found", service.getLink(cp(DID1, P1), cp(DID2, P2)));
+        assertNotNull("link should be found", service.getLink(cp(DID2, P2), cp(DID1, P1)));
+
+        providerService.linkVanished(new DefaultLinkDescription(cp(DID1, P1), cp(DID2, P2), DIRECT));
+        assertEquals("no events expected", 0, listener.events.size());
+    }
+
+    @Test
+    public void removeLinksByConnectionPoint() {
+        Link l1 = addLink(DID1, P1, DID2, P2, DIRECT);
+        Link l2 = addLink(DID2, P2, DID1, P1, DIRECT);
+        addLink(DID3, P3, DID2, P1, DIRECT);
+        addLink(DID2, P1, DID3, P3, DIRECT);
+        assertEquals("incorrect link count", 4, service.getLinkCount());
+
+        providerService.linksVanished(cp(DID1, P1));
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+        assertNull("link should be gone", service.getLink(l1.src(), l1.dst()));
+        assertNull("link should be gone", service.getLink(l2.src(), l2.dst()));
+    }
+
+    @Test
+    public void removeLinksByDevice() {
+        addLink(DID1, P1, DID2, P2, DIRECT);
+        addLink(DID2, P2, DID1, P1, DIRECT);
+        addLink(DID3, P3, DID2, P1, DIRECT);
+        addLink(DID2, P1, DID3, P3, DIRECT);
+        Link l5 = addLink(DID3, P1, DID1, P2, DIRECT);
+        Link l6 = addLink(DID1, P2, DID3, P1, DIRECT);
+        assertEquals("incorrect link count", 6, service.getLinkCount());
+
+        providerService.linksVanished(DID2);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+        assertNotNull("link should not be gone", service.getLink(l5.src(), l5.dst()));
+        assertNotNull("link should not be gone", service.getLink(l6.src(), l6.dst()));
+    }
+
+    @Test
+    public void removeLinksAsAdminByConnectionPoint() {
+        Link l1 = addLink(DID1, P1, DID2, P2, DIRECT);
+        Link l2 = addLink(DID2, P2, DID1, P1, DIRECT);
+        addLink(DID3, P3, DID2, P1, DIRECT);
+        addLink(DID2, P1, DID3, P3, DIRECT);
+        assertEquals("incorrect link count", 4, service.getLinkCount());
+
+        admin.removeLinks(cp(DID1, P1));
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+        assertNull("link should be gone", service.getLink(l1.src(), l1.dst()));
+        assertNull("link should be gone", service.getLink(l2.src(), l2.dst()));
+    }
+
+    @Test
+    public void removeLinksAsAdminByDevice() {
+        addLink(DID1, P1, DID2, P2, DIRECT);
+        addLink(DID2, P2, DID1, P1, DIRECT);
+        addLink(DID3, P3, DID2, P1, DIRECT);
+        addLink(DID2, P1, DID3, P3, DIRECT);
+        Link l5 = addLink(DID3, P1, DID1, P2, DIRECT);
+        Link l6 = addLink(DID1, P2, DID3, P1, DIRECT);
+        assertEquals("incorrect link count", 6, service.getLinkCount());
+
+        admin.removeLinks(DID2);
+        assertEquals("incorrect link count", 2, service.getLinkCount());
+        assertNotNull("link should not be gone", service.getLink(l5.src(), l5.dst()));
+        assertNotNull("link should not be gone", service.getLink(l6.src(), l6.dst()));
+    }
+
+    @Test
+    public void getLinks() {
+        Link l1 = addLink(DID1, P1, DID2, P2, DIRECT);
+        Link l2 = addLink(DID2, P2, DID1, P1, DIRECT);
+        Link l3 = addLink(DID3, P3, DID2, P1, DIRECT);
+        Link l4 = addLink(DID2, P1, DID3, P3, DIRECT);
+        assertEquals("incorrect link count", 4, service.getLinkCount());
+
+        Set<Link> links = service.getLinks(cp(DID1, P1));
+        assertEquals("incorrect links", ImmutableSet.of(l1, l2), links);
+        links = service.getEgressLinks(cp(DID1, P1));
+        assertEquals("incorrect links", ImmutableSet.of(l1), links);
+        links = service.getIngressLinks(cp(DID1, P1));
+        assertEquals("incorrect links", ImmutableSet.of(l2), links);
+
+        links = service.getDeviceLinks(DID2);
+        assertEquals("incorrect links", ImmutableSet.of(l1, l2, l3, l4), links);
+        links = service.getDeviceLinks(DID3);
+        assertEquals("incorrect links", ImmutableSet.of(l3, l4), links);
+
+        links = service.getDeviceEgressLinks(DID2);
+        assertEquals("incorrect links", ImmutableSet.of(l2, l4), links);
+        links = service.getDeviceIngressLinks(DID2);
+        assertEquals("incorrect links", ImmutableSet.of(l1, l3), links);
+    }
+
+
+    private Link addLink(DeviceId sd, PortNumber sp, DeviceId dd, PortNumber dp,
+                         Link.Type type) {
+        providerService.linkDetected(new DefaultLinkDescription(cp(sd, sp), cp(dd, dp), type));
+        Link link = listener.events.get(0).subject();
+        validateEvents(LINK_ADDED);
+        return link;
+    }
+
+    private ConnectPoint cp(DeviceId id, PortNumber portNumber) {
+        return new ConnectPoint(id, portNumber);
+    }
+
+    protected void validateEvents(Enum... types) {
+        int i = 0;
+        assertEquals("wrong events received", types.length, listener.events.size());
+        for (Event event : listener.events) {
+            assertEquals("incorrect event type", types[i], event.type());
+            i++;
+        }
+        listener.events.clear();
+    }
+
+
+    private class TestProvider extends AbstractProvider implements LinkProvider {
+        private Device deviceReceived;
+        private MastershipRole roleReceived;
+
+        public TestProvider() {
+            super(PID);
+        }
+    }
+
+    private static class TestListener implements LinkListener {
+        final List<LinkEvent> events = new ArrayList<>();
+
+        @Override
+        public void event(LinkEvent event) {
+            events.add(event);
+        }
+    }
+
+}
diff --git a/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/TestEventDispatcher.java b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/TestEventDispatcher.java
new file mode 100644
index 0000000..82f8be5
--- /dev/null
+++ b/core/trivial/src/test/java/org/onlab/onos/net/trivial/impl/TestEventDispatcher.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.net.trivial.impl;
+
+import org.onlab.onos.event.DefaultEventSinkRegistry;
+import org.onlab.onos.event.Event;
+import org.onlab.onos.event.EventDeliveryService;
+import org.onlab.onos.event.EventSink;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Implements event delivery system that delivers events synchronously, or
+ * in-line with the post method invocation.
+ */
+public class TestEventDispatcher extends DefaultEventSinkRegistry
+        implements EventDeliveryService {
+
+    @Override
+    public void post(Event event) {
+        EventSink sink = getSink(event.getClass());
+        checkState(sink != null, "No sink for event %s", event);
+        sink.process(event);
+    }
+
+}