Moved net to core
diff --git a/core/api/pom.xml b/core/api/pom.xml
new file mode 100644
index 0000000..3fd8a01
--- /dev/null
+++ b/core/api/pom.xml
@@ -0,0 +1,30 @@
+<?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-api</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS network control API</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava-testlib</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-misc</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/core/api/src/main/java/org/onlab/onos/GreetService.java b/core/api/src/main/java/org/onlab/onos/GreetService.java
new file mode 100644
index 0000000..c196147
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/GreetService.java
@@ -0,0 +1,23 @@
+package org.onlab.onos;
+
+/**
+ * Example of a simple service that provides greetings and it
+ * remembers the names which were greeted.
+ */
+public interface GreetService {
+
+    /**
+     * Returns a greeting tailored to the specified name.
+     *
+     * @param name some name
+     * @return greeting
+     */
+    String yo(String name);
+
+    /**
+     * Returns an iterable of names encountered thus far.
+     *
+     * @return iterable of names
+     */
+    Iterable<String> names();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java b/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java
new file mode 100644
index 0000000..93dca8e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/AbstractEvent.java
@@ -0,0 +1,51 @@
+package org.onlab.onos.event;
+
+/**
+ * Base event implementation.
+ */
+public class AbstractEvent<T extends Enum, S extends Object> implements Event<T, S> {
+
+    private final long time;
+    private final T type;
+    private S subject;
+
+    /**
+     * Creates an event of a given type and for the specified subject and the
+     * current time.
+     *
+     * @param type    event type
+     * @param subject event subject
+     */
+    protected AbstractEvent(T type, S subject) {
+        this(type, subject, System.currentTimeMillis());
+    }
+
+    /**
+     * Creates an event of a given type and for the specified subject and time.
+     *
+     * @param type    event type
+     * @param subject event subject
+     * @param time    occurrence time
+     */
+    protected AbstractEvent(T type, S subject, long time) {
+        this.type = type;
+        this.subject = subject;
+        this.time = time;
+    }
+
+    @Override
+    public long time() {
+        return time;
+    }
+
+    @Override
+    public T type() {
+        return type;
+    }
+
+    @Override
+    public S subject() {
+        return subject;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/AbstractListenerRegistry.java b/core/api/src/main/java/org/onlab/onos/event/AbstractListenerRegistry.java
new file mode 100644
index 0000000..9710306
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/AbstractListenerRegistry.java
@@ -0,0 +1,64 @@
+package org.onlab.onos.event;
+
+import org.slf4j.Logger;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Base implementation of an event sink and a registry capable of tracking
+ * listeners and dispatching events to them as part of event sink processing.
+ */
+public class AbstractListenerRegistry<E extends Event, L extends EventListener<E>>
+        implements EventSink<E> {
+
+    private final Logger log = getLogger(getClass());
+
+    private final Set<L> listeners = new CopyOnWriteArraySet<>();
+
+    /**
+     * Adds the specified listener.
+     *
+     * @param listener listener to be added
+     */
+    public void addListener(L listener) {
+        checkNotNull(listener, "Listener cannot be null");
+        listeners.add(listener);
+    }
+
+    /**
+     * Removes the specified listener.
+     *
+     * @param listener listener to be removed
+     */
+    public void removeListener(L listener) {
+        checkNotNull(listener, "Listener cannot be null");
+        checkArgument(listeners.remove(listener), "Listener not registered");
+    }
+
+    @Override
+    public void process(E event) {
+        for (L listener : listeners) {
+            try {
+                listener.event(event);
+            } catch (Exception error) {
+                reportProblem(event, error);
+            }
+        }
+    }
+
+    /**
+     * Reports a problem encountered while processing an event.
+     *
+     * @param event event being processed
+     * @param error error encountered while processing
+     */
+    protected void reportProblem(E event, Throwable error) {
+        log.warn("Exception encountered while processing event " + event, error);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/DefaultEventSinkRegistry.java b/core/api/src/main/java/org/onlab/onos/event/DefaultEventSinkRegistry.java
new file mode 100644
index 0000000..1c7fb13
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/DefaultEventSinkRegistry.java
@@ -0,0 +1,47 @@
+package org.onlab.onos.event;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Base implementation of event sink registry.
+ */
+public class DefaultEventSinkRegistry implements EventSinkRegistry {
+
+    private final Map<Class<? extends Event>, EventSink<? extends Event>>
+            sinks = new ConcurrentHashMap<>();
+
+    @Override
+    public <E extends Event> void addSink(Class<E> eventClass, EventSink<E> sink) {
+        checkNotNull(eventClass, "Event class cannot be null");
+        checkNotNull(sink, "Event sink cannot be null");
+        checkArgument(!sinks.containsKey(eventClass),
+                      "Event sink already registered for %s", eventClass.getName());
+        sinks.put(eventClass, sink);
+    }
+
+    @Override
+    public <E extends Event> void removeSink(Class<E> eventClass) {
+        checkNotNull(eventClass, "Event class cannot be null");
+        checkArgument(sinks.remove(eventClass) != null,
+                      "Event sink not registered for %s", eventClass.getName());
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <E extends Event> EventSink<E> getSink(Class<E> eventClass) {
+        // TODO: add implicit registration of descendant classes
+        return (EventSink<E>) sinks.get(eventClass);
+    }
+
+    @Override
+    public Set<Class<? extends Event>> getSinks() {
+        return ImmutableSet.copyOf(sinks.keySet());
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/Event.java b/core/api/src/main/java/org/onlab/onos/event/Event.java
new file mode 100644
index 0000000..baefa67
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/Event.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.event;
+
+/**
+ * Abstraction of an of a time-stamped event pertaining to an arbitrary subject.
+ */
+public interface Event<T extends Enum, S extends Object> {
+
+    /**
+     * Returns the timestamp of when the event occurred, given in milliseconds
+     * since the start of epoch.
+     *
+     * @return timestamp in milliseconds
+     */
+    long time();
+
+    /**
+     * Returns the type of the event.
+     *
+     * @return event type
+     */
+    T type();
+
+    /**
+     * Returns the subject of the event.
+     *
+     * @return subject to which this event pertains
+     */
+    S subject();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/EventDeliveryService.java b/core/api/src/main/java/org/onlab/onos/event/EventDeliveryService.java
new file mode 100644
index 0000000..4b7a52d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/EventDeliveryService.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.event;
+
+/**
+ * Abstraction of an entity capable of accepting events to be posted and
+ * then dispatching them to the appropriate event sink.
+ */
+public interface EventDeliveryService extends EventDispatcher, EventSinkRegistry {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/EventDispatcher.java b/core/api/src/main/java/org/onlab/onos/event/EventDispatcher.java
new file mode 100644
index 0000000..847df88
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/EventDispatcher.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.event;
+
+/**
+ * Abstraction of a mechanism capable of accepting and dispatching events to
+ * appropriate event sinks. Where the event sinks are obtained is unspecified.
+ * Similarly, whether the events are accepted and dispatched synchronously
+ * or asynchronously is unspecified as well.
+ */
+public interface EventDispatcher {
+
+    /**
+     * Posts the specified event for dispatching.
+     *
+     * @param event event to be posted
+     */
+    void post(Event event);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/EventListener.java b/core/api/src/main/java/org/onlab/onos/event/EventListener.java
new file mode 100644
index 0000000..1e0d7e6
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/EventListener.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.event;
+
+/**
+ * Entity capable of receiving events.
+ */
+public interface EventListener<E extends Event> {
+
+    /**
+     * Reacts to the specified event.
+     *
+     * @param event event to be processed
+     */
+    void event(E event);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/EventSink.java b/core/api/src/main/java/org/onlab/onos/event/EventSink.java
new file mode 100644
index 0000000..45ad408
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/EventSink.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.event;
+
+/**
+ * Abstraction of an event sink capable of processing the specified event types.
+ */
+public interface EventSink<E extends Event> {
+
+    /**
+     * Processes the specified event.
+     *
+     * @param event event to be processed
+     */
+    void process(E event);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/event/EventSinkRegistry.java b/core/api/src/main/java/org/onlab/onos/event/EventSinkRegistry.java
new file mode 100644
index 0000000..7398de7
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/event/EventSinkRegistry.java
@@ -0,0 +1,45 @@
+package org.onlab.onos.event;
+
+import java.util.Set;
+
+/**
+ * Abstraction of an event sink registry capable of tracking sinks based on
+ * their event class.
+ */
+public interface EventSinkRegistry {
+
+    /**
+     * Adds the specified sink for the given event class.
+     *
+     * @param eventClass event class
+     * @param sink       event sink
+     * @param <E>        type of event
+     */
+    <E extends Event> void addSink(Class<E> eventClass, EventSink<E> sink);
+
+    /**
+     * Removes the sink associated with the given event class.
+     *
+     * @param eventClass event class
+     * @param <E>        type of event
+     */
+    <E extends Event> void removeSink(Class<E> eventClass);
+
+    /**
+     * Returns the event sink associated with the specified event class.
+     *
+     * @param eventClass event class
+     * @param <E>        type of event
+     * @return event sink or null if none found
+     */
+    <E extends Event> EventSink<E> getSink(Class<E> eventClass);
+
+    /**
+     * Returns the set of all event classes for which sinks are presently
+     * registered.
+     *
+     * @return set of event classes
+     */
+    Set<Class<? extends Event>> getSinks();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/AbstractElement.java b/core/api/src/main/java/org/onlab/onos/net/AbstractElement.java
new file mode 100644
index 0000000..01341a5
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/AbstractElement.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+/**
+ * Base implementation of network elements, i.e. devices or hosts.
+ */
+public class AbstractElement extends AbstractModel implements Element {
+
+    protected final ElementId id;
+
+    /**
+     * Creates a network element attributed to the specified provider.
+     *
+     * @param providerId identity of the provider
+     * @param id         element identifier
+     */
+    protected AbstractElement(ProviderId providerId, ElementId id) {
+        super(providerId);
+        this.id = id;
+    }
+
+    @Override
+    public ElementId id() {
+        return id;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/AbstractModel.java b/core/api/src/main/java/org/onlab/onos/net/AbstractModel.java
new file mode 100644
index 0000000..e195fde
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/AbstractModel.java
@@ -0,0 +1,26 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+/**
+ * Base implementation of a network model entity.
+ */
+public class AbstractModel implements Provided {
+
+    private final ProviderId providerId;
+
+    /**
+     * Creates a model entity attributed to the specified provider.
+     *
+     * @param providerId identity of the provider
+     */
+    protected AbstractModel(ProviderId providerId) {
+        this.providerId = providerId;
+    }
+
+    @Override
+    public ProviderId providerId() {
+        return providerId;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java b/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java
new file mode 100644
index 0000000..8f332e9
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/ConnectPoint.java
@@ -0,0 +1,85 @@
+package org.onlab.onos.net;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Abstraction of a network connection point expressed as a pair of the
+ * network element identifier and port number.
+ */
+public class ConnectPoint {
+
+    private final ElementId elementId;
+    private final PortNumber portNumber;
+
+    /**
+     * Creates a new connection point.
+     *
+     * @param elementId  network element identifier
+     * @param portNumber port number
+     */
+    public ConnectPoint(ElementId elementId, PortNumber portNumber) {
+        this.elementId = elementId;
+        this.portNumber = portNumber;
+    }
+
+    /**
+     * Returns the network element identifier.
+     *
+     * @return element identifier
+     */
+    public ElementId elementId() {
+        return elementId;
+    }
+
+    /**
+     * Returns the identifier of the infrastructure device if the connection
+     * point belongs to a network element which is indeed an infrastructure
+     * device.
+     *
+     * @return network element identifier as a device identifier
+     * @throws java.lang.IllegalStateException if connection point is not
+     *                                         associated with a device
+     */
+    public DeviceId deviceId() {
+        if (elementId instanceof DeviceId) {
+            return (DeviceId) elementId;
+        }
+        throw new IllegalStateException("Connection point not associated " +
+                "with an infrastructure device");
+    }
+
+    /**
+     * Returns the connection port number.
+     *
+     * @return port number
+     */
+    public PortNumber port() {
+        return portNumber;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(elementId, portNumber);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ConnectPoint) {
+            final ConnectPoint other = (ConnectPoint) obj;
+            return Objects.equals(this.elementId, other.elementId) &&
+                    Objects.equals(this.portNumber, other.portNumber);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("elementId", elementId)
+                .add("portNumber", portNumber)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
new file mode 100644
index 0000000..69c10b7
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
@@ -0,0 +1,103 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Default infrastructure device model implementation.
+ */
+public class DefaultDevice extends AbstractElement implements Device {
+
+    private final Type type;
+    private final String manufacturer;
+    private final String serialNumber;
+    private final String hwVersion;
+    private final String swVersion;
+
+    /**
+     * Creates a network element attributed to the specified provider.
+     *
+     * @param providerId   identity of the provider
+     * @param id           device identifier
+     * @param type         device type
+     * @param manufacturer device manufacturer
+     * @param hwVersion    device HW version
+     * @param swVersion    device SW version
+     * @param serialNumber device serial number
+     */
+    public DefaultDevice(ProviderId providerId, DeviceId id, Type type,
+                         String manufacturer, String hwVersion, String swVersion,
+                         String serialNumber) {
+        super(providerId, id);
+        this.type = type;
+        this.manufacturer = manufacturer;
+        this.hwVersion = hwVersion;
+        this.swVersion = swVersion;
+        this.serialNumber = serialNumber;
+    }
+
+    @Override
+    public DeviceId id() {
+        return (DeviceId) super.id();
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public String manufacturer() {
+        return manufacturer;
+    }
+
+    @Override
+    public String hwVersion() {
+        return hwVersion;
+    }
+
+    @Override
+    public String swVersion() {
+        return swVersion;
+    }
+
+    @Override
+    public String serialNumber() {
+        return serialNumber;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, type, manufacturer, hwVersion, swVersion, serialNumber);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof DefaultDevice) {
+            final DefaultDevice other = (DefaultDevice) obj;
+            return Objects.equals(this.id, other.id) &&
+                    Objects.equals(this.type, other.type) &&
+                    Objects.equals(this.manufacturer, other.manufacturer) &&
+                    Objects.equals(this.hwVersion, other.hwVersion) &&
+                    Objects.equals(this.swVersion, other.swVersion) &&
+                    Objects.equals(this.serialNumber, other.serialNumber);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("id", id)
+                .add("type", type)
+                .add("manufacturer", manufacturer)
+                .add("hwVersion", hwVersion)
+                .add("swVersion", swVersion)
+                .add("serialNumber", serialNumber)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultEdgeLink.java b/core/api/src/main/java/org/onlab/onos/net/DefaultEdgeLink.java
new file mode 100644
index 0000000..41cd045
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultEdgeLink.java
@@ -0,0 +1,43 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Default edge link model implementation.
+ */
+public class DefaultEdgeLink extends DefaultLink implements EdgeLink {
+
+    private final HostId hostId;
+    private final HostLocation hostLocation;
+
+    /**
+     * Creates an edge link using the supplied information.
+     *
+     * @param providerId   provider identity
+     * @param hostPoint    host-side connection point
+     * @param hostLocation location where host attaches to the network
+     * @param isIngress    true to indicated host-to-network direction; false
+     *                     for network-to-host direction
+     */
+    public DefaultEdgeLink(ProviderId providerId, ConnectPoint hostPoint,
+                           HostLocation hostLocation, boolean isIngress) {
+        super(providerId, isIngress ? hostLocation : hostPoint,
+              isIngress ? hostPoint : hostLocation, Type.EDGE);
+        checkArgument(hostPoint.elementId() instanceof HostId,
+                      "Host point does not refer to a host ID");
+        this.hostId = (HostId) hostPoint.elementId();
+        this.hostLocation = hostLocation;
+    }
+
+    @Override
+    public HostId hostId() {
+        return hostId;
+    }
+
+    @Override
+    public HostLocation hostLocation() {
+        return hostLocation;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java b/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java
new file mode 100644
index 0000000..1f7783c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultLink.java
@@ -0,0 +1,74 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Default infrastructure link model implementation.
+ */
+public class DefaultLink extends AbstractModel implements Link {
+
+    private final ConnectPoint src;
+    private final ConnectPoint dst;
+    private final Type type;
+
+    /**
+     * Creates a link description using the supplied information.
+     *
+     * @param providerId provider identity
+     * @param src        link source
+     * @param dst        link destination
+     * @param type       link type
+     */
+    public DefaultLink(ProviderId providerId, ConnectPoint src, ConnectPoint dst,
+                       Type type) {
+        super(providerId);
+        this.src = src;
+        this.dst = dst;
+        this.type = type;
+    }
+
+    @Override
+    public ConnectPoint src() {
+        return src;
+    }
+
+    @Override
+    public ConnectPoint dst() {
+        return dst;
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(src, dst, type);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof DefaultLink) {
+            final DefaultLink other = (DefaultLink) obj;
+            return Objects.equals(this.src, other.src) &&
+                    Objects.equals(this.dst, other.dst) &&
+                    Objects.equals(this.type, other.type);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("src", src)
+                .add("dst", dst)
+                .add("type", type)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
new file mode 100644
index 0000000..378cc37
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
@@ -0,0 +1,70 @@
+package org.onlab.onos.net;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Default port implementation.
+ */
+public class DefaultPort implements Port {
+
+    private final Element element;
+    private final PortNumber number;
+    private final boolean isEnabled;
+
+    /**
+     * Creates a network element attributed to the specified provider.
+     *
+     * @param element   parent network element
+     * @param number    port number
+     * @param isEnabled indicator whether the port is up and active
+     */
+    public DefaultPort(Element element, PortNumber number,
+                       boolean isEnabled) {
+        this.element = element;
+        this.number = number;
+        this.isEnabled = isEnabled;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(number, isEnabled);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof DefaultPort) {
+            final DefaultPort other = (DefaultPort) obj;
+            return Objects.equals(this.element.id(), other.element.id()) &&
+                    Objects.equals(this.number, other.number) &&
+                    Objects.equals(this.isEnabled, other.isEnabled);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("element", element.id())
+                .add("number", number)
+                .add("isEnabled", isEnabled)
+                .toString();
+    }
+
+    @Override
+    public PortNumber number() {
+        return number;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return isEnabled;
+    }
+
+    @Override
+    public Element element() {
+        return element;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Description.java b/core/api/src/main/java/org/onlab/onos/net/Description.java
new file mode 100644
index 0000000..38338c1
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Description.java
@@ -0,0 +1,7 @@
+package org.onlab.onos.net;
+
+/**
+ * Base abstraction of a piece of information about network elements.
+ */
+public interface Description {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Device.java b/core/api/src/main/java/org/onlab/onos/net/Device.java
new file mode 100644
index 0000000..9e6018e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Device.java
@@ -0,0 +1,63 @@
+package org.onlab.onos.net;
+
+/**
+ * Representation of a network infrastructure device.
+ */
+public interface Device extends Element {
+
+    /**
+     * Coarse classification of the type of the infrastructure device.
+     */
+    public enum Type {
+        SWITCH, ROUTER, FIREWALL, BALANCER, IPS, IDS, CONTROLLER, OTHER
+    }
+
+    /**
+     * Returns the device identifier.
+     *
+     * @return device id
+     */
+    DeviceId id();
+
+    /**
+     * Returns the type of the infrastructure device.
+     *
+     * @return type of the device
+     */
+    Type type();
+
+    /**
+     * Returns the device manufacturer name.
+     *
+     * @return manufacturer name
+     */
+    String manufacturer();
+
+    /**
+     * Returns the device hardware version.
+     *
+     * @return hardware version
+     */
+    String hwVersion();
+
+    /**
+     * Returns the device software version.
+     *
+     * @return software version
+     */
+    String swVersion();
+
+    /**
+     * Returns the device serial number.
+     *
+     * @return serial number
+     */
+    String serialNumber();
+
+    // Device realizedBy(); ?
+
+    // ports are not provided directly, but rather via DeviceService.getPorts(Device device);
+
+    // Set<Behavior> behaviours(); // set of supported behaviours
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/DeviceId.java b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
new file mode 100644
index 0000000..ef8c5ab
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
@@ -0,0 +1,33 @@
+package org.onlab.onos.net;
+
+import java.net.URI;
+
+/**
+ * Immutable representation of a device identity.
+ */
+public final class DeviceId extends ElementId {
+
+    // Public construction is prohibited
+    private DeviceId(URI uri) {
+        super(uri);
+    }
+
+    /**
+     * Creates a device id using the supplied URI.
+     *
+     * @param uri device URI
+     */
+    public static DeviceId deviceId(URI uri) {
+        return new DeviceId(uri);
+    }
+
+    /**
+     * Creates a device id using the supplied URI string.
+     *
+     * @param string device URI string
+     */
+    public static DeviceId deviceId(String string) {
+        return deviceId(URI.create(string));
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/EdgeLink.java b/core/api/src/main/java/org/onlab/onos/net/EdgeLink.java
new file mode 100644
index 0000000..b1d5f7f
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/EdgeLink.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.net;
+
+/**
+ * Abstraction of a link between an end-station host and the network
+ * infrastructure.
+ */
+public interface EdgeLink extends Link {
+
+    /**
+     * Returns the host identification.
+     *
+     * @return host identifier
+     */
+    HostId hostId();
+
+    /**
+     * Returns the connection point where the host attaches to the
+     * network infrastructure.
+     *
+     * @return host location point
+     */
+    HostLocation hostLocation();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Element.java b/core/api/src/main/java/org/onlab/onos/net/Element.java
new file mode 100644
index 0000000..5d3969e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Element.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.net;
+
+/**
+ * Base abstraction of a network element, i.e. an infrastructure device or an end-station host.
+ */
+public interface Element extends Provided {
+
+    /**
+     * Returns the network element identifier.
+     *
+     * @return element identifier
+     */
+    ElementId id();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/ElementId.java b/core/api/src/main/java/org/onlab/onos/net/ElementId.java
new file mode 100644
index 0000000..e205bb6
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/ElementId.java
@@ -0,0 +1,51 @@
+package org.onlab.onos.net;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * Immutable representation of a network element identity.
+ */
+public abstract class ElementId {
+
+    private final URI uri;
+
+    /**
+     * Creates an element identifier using the supplied URI.
+     *
+     * @param uri backing URI
+     */
+    protected ElementId(URI uri) {
+        this.uri = uri;
+    }
+
+    /**
+     * Returns the backing URI.
+     *
+     * @return backing URI
+     */
+    public URI uri() {
+        return uri;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(uri);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ElementId) {
+            final ElementId that = (ElementId) obj;
+            return this.getClass() == that.getClass() &&
+                    Objects.equals(this.uri, that.uri);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return uri.toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Host.java b/core/api/src/main/java/org/onlab/onos/net/Host.java
new file mode 100644
index 0000000..17eaf22
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Host.java
@@ -0,0 +1,21 @@
+package org.onlab.onos.net;
+
+/**
+ * Abstraction of an end-station host on the network, essentially a NIC.
+ */
+public interface Host extends Element {
+
+    // MAC, IP(s), optional VLAN ID
+
+
+    /**
+     * Returns the most recent host location where the host attaches to the
+     * network edge.
+     *
+     * @return host location
+     */
+    HostLocation location();
+
+    // list of recent locations?
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/HostId.java b/core/api/src/main/java/org/onlab/onos/net/HostId.java
new file mode 100644
index 0000000..3e274b3
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/HostId.java
@@ -0,0 +1,33 @@
+package org.onlab.onos.net;
+
+import java.net.URI;
+
+/**
+ * Immutable representation of a host identity.
+ */
+public final class HostId extends ElementId {
+
+    // Public construction is prohibited
+    private HostId(URI uri) {
+        super(uri);
+    }
+
+    /**
+     * Creates a device id using the supplied URI.
+     *
+     * @param uri device URI
+     */
+    public static HostId hostId(URI uri) {
+        return new HostId(uri);
+    }
+
+    /**
+     * Creates a device id using the supplied URI string.
+     *
+     * @param string device URI string
+     */
+    public static HostId hostId(String string) {
+        return hostId(URI.create(string));
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/HostLocation.java b/core/api/src/main/java/org/onlab/onos/net/HostLocation.java
new file mode 100644
index 0000000..22673a6
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/HostLocation.java
@@ -0,0 +1,42 @@
+package org.onlab.onos.net;
+
+import java.util.Objects;
+
+/**
+ * Representation of a network edge location where an end-station host is
+ * connected.
+ */
+public class HostLocation extends ConnectPoint {
+
+    private final long time;
+
+    public HostLocation(DeviceId deviceId, PortNumber portNumber, long time) {
+        super(deviceId, portNumber);
+        this.time = time;
+    }
+
+    /**
+     * Returns the timestamp when the location was established, given in
+     * milliseconds since start of epoch.
+     *
+     * @return timestamp in milliseconds since start of epoch
+     */
+    public long time() {
+        return time;
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode() + Objects.hash(time);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof HostLocation) {
+            final HostLocation other = (HostLocation) obj;
+            return super.equals(obj) && Objects.equals(this.time, other.time);
+        }
+        return false;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Link.java b/core/api/src/main/java/org/onlab/onos/net/Link.java
new file mode 100644
index 0000000..0b60b9c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Link.java
@@ -0,0 +1,54 @@
+package org.onlab.onos.net;
+
+/**
+ * Abstraction of a network infrastructure link.
+ */
+public interface Link extends Provided {
+
+    /**
+     * Coarse representation of the link type.
+     */
+    public enum Type {
+        /**
+         * Signifies that this is a direct single-segment link.
+         */
+        DIRECT,
+
+        /**
+         * Signifies that this link is potentially comprised from multiple
+         * underlying segments or hops, and as such should be used to tag
+         * links traversing optical paths, tunnels or intervening 'dark'
+         * switches.
+         */
+        INDIRECT,
+
+        /**
+         * Signifies that this link is an edge, i.e. host link.
+         */
+        EDGE
+    }
+
+    /**
+     * Returns the link source connection point.
+     *
+     * @return link source connection point
+     */
+    ConnectPoint src();
+
+    /**
+     * Returns the link destination connection point.
+     *
+     * @return link destination connection point
+     */
+    ConnectPoint dst();
+
+    /**
+     * Returns the link type.
+     *
+     * @return link type
+     */
+    Type type();
+
+    // LinkInfo info(); // Additional link information / decorations
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/MastershipRole.java b/core/api/src/main/java/org/onlab/onos/net/MastershipRole.java
new file mode 100644
index 0000000..10d53ad
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/MastershipRole.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net;
+
+/**
+ * Representation of a relationship role of a controller instance to a device
+ * or a region of network environment.
+ */
+public enum MastershipRole {
+
+    /**
+     * Represents a relationship where the controller instance is the master
+     * to a device or a region of network environment.
+     */
+    MASTER,
+
+    /**
+     * Represents a relationship where the controller instance is the standby,
+     * i.e. potential master to a device or a region of network environment.
+     */
+    STANDBY,
+
+    /**
+     * Represents that the controller instance is not eligible to be the master
+     * to a device or a region of network environment.
+     */
+    NONE
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Path.java b/core/api/src/main/java/org/onlab/onos/net/Path.java
new file mode 100644
index 0000000..22a363a
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Path.java
@@ -0,0 +1,20 @@
+package org.onlab.onos.net;
+
+import java.util.List;
+
+/**
+ * Representation of a contiguous directed path in a network. Path comprises
+ * of a sequence of links, where adjacent links must share the same device,
+ * meaning that destination of the source of one link must coincide with the
+ * destination of the previous link.
+ */
+public interface Path extends Link {
+
+    /**
+     * Returns sequence of links comprising the path.
+     *
+     * @return list of links
+     */
+    List<Link> links();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Port.java b/core/api/src/main/java/org/onlab/onos/net/Port.java
new file mode 100644
index 0000000..e98278b
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Port.java
@@ -0,0 +1,31 @@
+package org.onlab.onos.net;
+
+/**
+ * Abstraction of a network port.
+ */
+public interface Port {
+
+    /**
+     * Returns the port number.
+     *
+     * @return port number
+     */
+    PortNumber number();
+
+    /**
+     * Indicates whether or not the port is currently up and active.
+     *
+     * @return true if the port is operational
+     */
+    boolean isEnabled();
+
+    /**
+     * Returns the parent network element to which this port belongs.
+     *
+     * @return parent network element
+     */
+    Element element();
+
+    // set of port attributes
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/PortNumber.java b/core/api/src/main/java/org/onlab/onos/net/PortNumber.java
new file mode 100644
index 0000000..d5fc5f2
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/PortNumber.java
@@ -0,0 +1,68 @@
+package org.onlab.onos.net;
+
+import java.util.Objects;
+
+import com.google.common.primitives.UnsignedLongs;
+
+/**
+ * Representation of a port number.
+ */
+public final class PortNumber {
+
+    private static final long MAX_NUMBER = (2L * Integer.MAX_VALUE) + 1;
+
+    private final long number;
+
+    // Public creation is prohibited
+    private PortNumber(long number) {
+        this.number = number;
+    }
+
+    /**
+     * Returns the port number representing the specified long value.
+     *
+     * @param number port number as long value
+     * @return port number
+     */
+    public static PortNumber portNumber(long number) {
+        return new PortNumber(number);
+    }
+
+    /**
+     * Returns the port number representing the specified string value.
+     *
+     * @param string port number as string value
+     * @return port number
+     */
+    public static PortNumber portNumber(String string) {
+        return new PortNumber(UnsignedLongs.decode(string));
+    }
+
+    /**
+     * Returns the backing long value.
+     *
+     * @return port number as long
+     */
+    public long toLong() {
+        return number;
+    }
+
+    @Override
+    public String toString() {
+        return UnsignedLongs.toString(number);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(number);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof PortNumber) {
+            final PortNumber other = (PortNumber) obj;
+            return this.number == other.number;
+        }
+        return false;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/Provided.java b/core/api/src/main/java/org/onlab/onos/net/Provided.java
new file mode 100644
index 0000000..92bfe95
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/Provided.java
@@ -0,0 +1,17 @@
+package org.onlab.onos.net;
+
+import org.onlab.onos.net.provider.ProviderId;
+
+/**
+ * Abstraction of an entity supplied by a provider.
+ */
+public interface Provided {
+
+    /**
+     * Returns the identifier of the provider which supplied the entity.
+     *
+     * @return provider identification
+     */
+    ProviderId providerId();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
new file mode 100644
index 0000000..833625d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
@@ -0,0 +1,80 @@
+package org.onlab.onos.net.device;
+
+import java.net.URI;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.onos.net.Device.Type;
+
+/**
+ * Default implementation of immutable device description entity.
+ */
+public class DefaultDeviceDescription implements DeviceDescription {
+    private final URI uri;
+    private final Type type;
+    private final String manufacturer;
+    private final String hwVersion;
+    private final String swVersion;
+    private final String serialNumber;
+
+    /**
+     * Creates a device description using the supplied information.
+     *
+     * @param uri          device URI
+     * @param type         device type
+     * @param manufacturer device manufacturer
+     * @param hwVersion    device HW version
+     * @param swVersion    device SW version
+     * @param serialNumber device serial number
+     */
+    public DefaultDeviceDescription(URI uri, Type type, String manufacturer,
+                                    String hwVersion, String swVersion,
+                                    String serialNumber) {
+        this.uri = checkNotNull(uri, "Device URI cannot be null");
+        this.type = checkNotNull(type, "Device type cannot be null");
+        this.manufacturer = manufacturer;
+        this.hwVersion = hwVersion;
+        this.swVersion = swVersion;
+        this.serialNumber = serialNumber;
+    }
+
+    @Override
+    public URI deviceURI() {
+        return uri;
+    }
+
+    @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public String manufacturer() {
+        return manufacturer;
+    }
+
+    @Override
+    public String hwVersion() {
+        return hwVersion;
+    }
+
+    @Override
+    public String swVersion() {
+        return swVersion;
+    }
+
+    @Override
+    public String serialNumber() {
+        return serialNumber;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("uri", uri).add("type", type).add("mfr", manufacturer)
+                .add("hw", hwVersion).add("sw", swVersion)
+                .add("serial", serialNumber)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java
new file mode 100644
index 0000000..1d52ac9
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.PortNumber;
+
+/**
+ * Default implementation of immutable port description.
+ */
+public class DefaultPortDescription implements PortDescription {
+
+    private final PortNumber number;
+    private final boolean isEnabled;
+
+    public DefaultPortDescription(PortNumber number, boolean isEnabled) {
+        this.number = number;
+        this.isEnabled = isEnabled;
+    }
+
+    @Override
+    public PortNumber portNumber() {
+        return number;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return isEnabled;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceAdminService.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceAdminService.java
new file mode 100644
index 0000000..8aec28a
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceAdminService.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
+
+/**
+ * Service for administering the inventory of infrastructure devices.
+ */
+public interface DeviceAdminService {
+
+    /**
+     * Applies the current mastership role for the specified device.
+     *
+     * @param deviceId device identifier
+     * @param role     requested role
+     */
+    void setRole(DeviceId deviceId, MastershipRole role);
+
+    /**
+     * Removes the device with the specified identifier.
+     *
+     * @param deviceId device identifier
+     */
+    void removeDevice(DeviceId deviceId);
+
+    // TODO: add ability to administratively suspend/resume device
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceDescription.java
new file mode 100644
index 0000000..e32c19d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceDescription.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.Description;
+import org.onlab.onos.net.Device;
+
+import java.net.URI;
+
+/**
+ * Carrier of immutable information about a device.
+ */
+public interface DeviceDescription extends Description {
+
+    /**
+     * Protocol/provider specific URI that can be used to encode the identity
+     * information required to communicate with the device externally, e.g.
+     * datapath ID.
+     *
+     * @return provider specific URI for the device
+     */
+    URI deviceURI();
+
+    /**
+     * Returns the type of the infrastructure device.
+     *
+     * @return type of the device
+     */
+    Device.Type type();
+
+    /**
+     * Returns the device manufacturer name.
+     *
+     * @return manufacturer name
+     */
+    String manufacturer();
+
+    /**
+     * Returns the device hardware version.
+     *
+     * @return hardware version
+     */
+    String hwVersion();
+
+    /**
+     * Returns the device software version.
+     *
+     * @return software version
+     */
+    String swVersion();
+
+    /**
+     * Returns the device serial number.
+     *
+     * @return serial number
+     */
+    String serialNumber();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java
new file mode 100644
index 0000000..0a33777
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceEvent.java
@@ -0,0 +1,112 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.event.AbstractEvent;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.Port;
+
+/**
+ * Describes infrastructure device event.
+ */
+public class DeviceEvent extends AbstractEvent<DeviceEvent.Type, Device> {
+
+    private final Port port;
+
+    /**
+     * Type of device events.
+     */
+    public enum Type {
+        /**
+         * Signifies that a new device has been detected.
+         */
+        DEVICE_ADDED,
+
+        /**
+         * Signifies that some device attributes have changed; excludes
+         * availability changes.
+         */
+        DEVICE_UPDATED,
+
+        /**
+         * Signifies that a device has been removed.
+         */
+        DEVICE_REMOVED,
+
+        /**
+         * Signifies that a device has been administratively suspended.
+         */
+        DEVICE_SUSPENDED,
+
+        /**
+         * Signifies that a device has come online or has gone offline.
+         */
+        DEVICE_AVAILABILITY_CHANGED,
+
+        /**
+         * Signifies that the current controller instance relationship has
+         * changed with respect to a device.
+         */
+        DEVICE_MASTERSHIP_CHANGED,
+
+        /**
+         * Signifies that a port has been added.
+         */
+        PORT_ADDED,
+
+        /**
+         * Signifies that a port has been updated.
+         */
+        PORT_UPDATED,
+
+        /**
+         * Signifies that a port has been removed.
+         */
+        PORT_REMOVED
+    }
+
+    /**
+     * Creates an event of a given type and for the specified device and the
+     * current time.
+     *
+     * @param type   device event type
+     * @param device event device subject
+     */
+    public DeviceEvent(Type type, Device device) {
+        this(type, device, null);
+    }
+
+    /**
+     * Creates an event of a given type and for the specified device, port
+     * and the current time.
+     *
+     * @param type   device event type
+     * @param device event device subject
+     * @param port   optional port subject
+     */
+    public DeviceEvent(Type type, Device device, Port port) {
+        super(type, device);
+        this.port = port;
+    }
+
+    /**
+     * Creates an event of a given type and for the specified device and time.
+     *
+     * @param type   device event type
+     * @param device event device subject
+     * @param port   optional port subject
+     * @param time   occurrence time
+     */
+    public DeviceEvent(Type type, Device device, Port port, long time) {
+        super(type, device, time);
+        this.port = port;
+    }
+
+    /**
+     * Returns the port subject.
+     *
+     * @return port subject or null if the event is not port specific.
+     */
+    public Port port() {
+        return port;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceListener.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceListener.java
new file mode 100644
index 0000000..b11d617
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceListener.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving infrastructure device related events.
+ */
+public interface DeviceListener extends EventListener<DeviceEvent> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java
new file mode 100644
index 0000000..9934b8d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProvider.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.provider.Provider;
+
+/**
+ * Abstraction of a device information provider.
+ */
+public interface DeviceProvider extends Provider {
+
+    // TODO: consider how dirty the triggerProbe gets; if it costs too much, let's drop it
+
+    /**
+     * Triggers an asynchronous probe of the specified device, intended to
+     * determine whether the host is present or not. An indirect result of this
+     * should be invocation of
+     * {@link org.onlab.onos.net.device.DeviceProviderService#deviceConnected} )} or
+     * {@link org.onlab.onos.net.device.DeviceProviderService#deviceDisconnected}
+     * at some later point in time.
+     *
+     * @param device device to be probed
+     */
+    void triggerProbe(Device device);
+
+    /**
+     * Notifies the provider of a mastership role change for the specified
+     * device as decided by the core.
+     *
+     * @param device  affected device
+     * @param newRole newly determined mastership role
+     */
+    void roleChanged(Device device, MastershipRole newRole);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderRegistry.java
new file mode 100644
index 0000000..cff252e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderRegistry.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.provider.ProviderRegistry;
+
+/**
+ * Abstraction of a device provider registry.
+ */
+public interface DeviceProviderRegistry
+        extends ProviderRegistry<DeviceProvider, DeviceProviderService> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java
new file mode 100644
index 0000000..5401646
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.provider.ProviderService;
+
+import java.util.List;
+
+/**
+ * Service through which device providers can inject device information into
+ * the core.
+ */
+public interface DeviceProviderService extends ProviderService<DeviceProvider> {
+
+    // TODO: define suspend and remove actions on the mezzanine administrative API
+
+    /**
+     * Signals the core that a device has connected or has been detected somehow.
+     *
+     * @param deviceDescription information about network device
+     */
+    void deviceConnected(DeviceId deviceId, DeviceDescription deviceDescription);
+
+    /**
+     * Signals the core that a device has disconnected or is no longer reachable.
+     *
+     * @param deviceId identity of the device to be removed
+     */
+    void deviceDisconnected(DeviceId deviceId);
+
+    /**
+     * Sends information about all ports of a device. It is up to the core to
+     * determine what has changed.
+     * <p/>
+     *
+     * @param deviceId         identity of the device
+     * @param portDescriptions list of device ports
+     */
+    void updatePorts(DeviceId deviceId, List<PortDescription> portDescriptions);
+
+    /**
+     * Used to notify the core about port status change of a single port.
+     *
+     * @param deviceId        identity of the device
+     * @param portDescription description of the port that changed
+     */
+    void portStatusChanged(DeviceId deviceId, PortDescription portDescription);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceService.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceService.java
new file mode 100644
index 0000000..8364935
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceService.java
@@ -0,0 +1,87 @@
+package org.onlab.onos.net.device;
+
+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 java.util.List;
+
+/**
+ * Service for interacting with the inventory of infrastructure devices.
+ */
+public interface DeviceService {
+
+    /**
+     * Returns the number of infrastructure devices known to the system.
+     *
+     * @return number of infrastructure devices
+     */
+    int getDeviceCount();
+
+    /**
+     * Returns a collection of the currently known infrastructure
+     * devices.
+     *
+     * @return collection of devices
+     */
+    Iterable<Device> getDevices();
+
+    /**
+     * Returns the device with the specified identifier.
+     *
+     * @param deviceId device identifier
+     * @return device or null if one with the given identifier is not known
+     */
+    Device getDevice(DeviceId deviceId);
+
+    /**
+     * Returns the current mastership role for the specified device.
+     *
+     * @param deviceId device identifier
+     * @return designated mastership role
+     */
+    MastershipRole getRole(DeviceId deviceId);
+
+
+    /**
+     * Returns the list of ports associated with the device.
+     *
+     * @param deviceId device identifier
+     * @return list of ports
+     */
+    List<Port> getPorts(DeviceId deviceId);
+
+    /**
+     * Returns the port with the specified number and hosted by the given device.
+     *
+     * @param deviceId   device identifier
+     * @param portNumber port number
+     * @return device port
+     */
+    Port getPort(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Indicates whether or not the device is presently online and available.
+     *
+     * @param deviceId device identifier
+     * @return true if the device is available
+     */
+    boolean isAvailable(DeviceId deviceId);
+
+    /**
+     * Adds the specified device listener.
+     *
+     * @param listener device listener
+     */
+    void addListener(DeviceListener listener);
+
+    /**
+     * Removes the specified device listener.
+     *
+     * @param listener device listener
+     */
+    void removeListener(DeviceListener listener);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java
new file mode 100644
index 0000000..f0dc8ee
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java
@@ -0,0 +1,26 @@
+package org.onlab.onos.net.device;
+
+import org.onlab.onos.net.PortNumber;
+
+/**
+ * Information about a port.
+ */
+public interface PortDescription {
+
+    // TODO: possibly relocate this to a common ground so that this can also used by host tracking if required
+
+    /**
+     * Returns the port number.
+     *
+     * @return port number
+     */
+    PortNumber portNumber();
+
+    /**
+     * Indicates whether or not the port is up and active.
+     *
+     * @return true if the port is active and has carrier signal
+     */
+    boolean isEnabled();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowDescription.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowDescription.java
new file mode 100644
index 0000000..2a64815
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowDescription.java
@@ -0,0 +1,12 @@
+package org.onlab.onos.net.flow;
+
+import org.onlab.onos.net.Description;
+
+/**
+ * Information about a flow rule.
+ */
+public interface FlowDescription extends Description {
+
+    // Match and action, possibly reason for flow rule, unless reason is too OF-specific.
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
new file mode 100644
index 0000000..894e245
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
@@ -0,0 +1,12 @@
+package org.onlab.onos.net.flow;
+
+import org.onlab.onos.net.provider.Provider;
+
+/**
+ * Abstraction of a flow rule provider.
+ */
+public interface FlowRuleProvider extends Provider {
+
+    // TODO: pushFlowRule
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java
new file mode 100644
index 0000000..099d9f4
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderRegistry.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.flow;
+
+import org.onlab.onos.net.provider.ProviderRegistry;
+
+/**
+ * Abstraction for a flow rule provider registry.
+ */
+public interface FlowRuleProviderRegistry
+        extends ProviderRegistry<FlowRuleProvider, FlowRuleProviderService> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
new file mode 100644
index 0000000..a483d92
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.net.flow;
+
+import org.onlab.onos.net.provider.ProviderService;
+
+/**
+ * Service through which flowrule providers can inject flowrule information into
+ * the core.
+ */
+public interface FlowRuleProviderService extends ProviderService<FlowRuleProvider> {
+
+    /**
+     * Signals that a flow that was previously installed has been removed.
+     *
+     * @param flowDescription information about the removed flow
+     */
+    void flowRemoved(FlowDescription flowDescription);
+
+    /**
+     * Signals that a flowrule is missing for some network traffic.
+     *
+     * @param flowDescription information about traffic in need of flow rule(s)
+     */
+    void flowMissing(FlowDescription flowDescription);
+
+    /**
+     * Signals that a flowrule has been added.
+     *
+     * TODO  think about if this really makes sense, e.g. if stats collection or
+     * something can leverage it.
+     *
+     * @param flowDescription the rule that was added
+     */
+    void flowAdded(FlowDescription flowDescription);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java b/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java
new file mode 100644
index 0000000..e3c67b9
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostDescription.java
@@ -0,0 +1,11 @@
+package org.onlab.onos.net.host;
+
+/**
+ * Information describing host and its location.
+ */
+public interface HostDescription {
+
+    // IP, MAC, VLAN-ID, HostLocation -> (ConnectionPoint + timestamp)
+
+}
+
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostEvent.java b/core/api/src/main/java/org/onlab/onos/net/host/HostEvent.java
new file mode 100644
index 0000000..b06e8b8
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostEvent.java
@@ -0,0 +1,53 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.event.AbstractEvent;
+import org.onlab.onos.net.Host;
+
+/**
+ * Describes end-station host event.
+ */
+public class HostEvent extends AbstractEvent<HostEvent.Type, Host> {
+
+    /**
+     * Type of host events.
+     */
+    public enum Type {
+        /**
+         * Signifies that a new host has been detected.
+         */
+        HOST_ADDED,
+
+        /**
+         * Signifies that a host has been removed.
+         */
+        HOST_REMOVED,
+
+        /**
+         * Signifies that a host location has changed.
+         */
+        HOST_MOVED
+    }
+
+    /**
+     * Creates an event of a given type and for the specified host and the
+     * current time.
+     *
+     * @param type host event type
+     * @param host event host subject
+     */
+    public HostEvent(Type type, Host host) {
+        super(type, host);
+    }
+
+    /**
+     * Creates an event of a given type and for the specified host and time.
+     *
+     * @param type host event type
+     * @param host event host subject
+     * @param time occurrence time
+     */
+    public HostEvent(Type type, Host host, long time) {
+        super(type, host, time);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostListener.java b/core/api/src/main/java/org/onlab/onos/net/host/HostListener.java
new file mode 100644
index 0000000..0e3e0e1
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostListener.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving end-station host related events.
+ */
+public interface HostListener extends EventListener<HostEvent> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostProvider.java b/core/api/src/main/java/org/onlab/onos/net/host/HostProvider.java
new file mode 100644
index 0000000..8b92883
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostProvider.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.net.Host;
+import org.onlab.onos.net.provider.Provider;
+
+/**
+ * Provider of information about hosts and their location on the network.
+ */
+public interface HostProvider extends Provider {
+
+    // TODO: consider how dirty the triggerProbe gets; if it costs too much, let's drop it
+
+    /**
+     * Triggers an asynchronous probe of the specified host, intended to
+     * determine whether the host is present or not. An indirect result of this
+     * should be invocation of {@link org.onlab.onos.net.host.HostProviderService#hostDetected(HostDescription)} or
+     * {@link org.onlab.onos.net.host.HostProviderService#hostVanished(HostDescription)}
+     * at some later point in time.
+     *
+     * @param host host to probe
+     */
+    void triggerProbe(Host host);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/host/HostProviderRegistry.java
new file mode 100644
index 0000000..76f281e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostProviderRegistry.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.net.provider.ProviderRegistry;
+
+/**
+ * Abstraction of a host provider registry.
+ */
+public interface HostProviderRegistry
+        extends ProviderRegistry<HostProvider, HostProviderService> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostProviderService.java b/core/api/src/main/java/org/onlab/onos/net/host/HostProviderService.java
new file mode 100644
index 0000000..76c2aad
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostProviderService.java
@@ -0,0 +1,25 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.net.provider.ProviderService;
+
+/**
+ * Means of conveying host information to the core.
+ */
+public interface HostProviderService extends ProviderService<HostProvider> {
+
+    /**
+     * Notifies the core when a host has been detected on a network along with
+     * information that identifies the hoot location.
+     *
+     * @param hostDescription description of host and its location
+     */
+    void hostDetected(HostDescription hostDescription);
+
+    /**
+     * Notifies the core when a host is no longer detected on a network.
+     *
+     * @param hostDescription description of host
+     */
+    void hostVanished(HostDescription hostDescription);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostService.java b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
new file mode 100644
index 0000000..ed94522
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
@@ -0,0 +1,67 @@
+package org.onlab.onos.net.host;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.Host;
+
+import java.util.Set;
+
+/**
+ * Service for interacting with the inventory of end-station hosts.
+ */
+public interface HostService {
+
+    /**
+     * Returns a collection of all end-station hosts.
+     *
+     * @return collection of hosts
+     */
+    Iterable<Host> getHosts();
+
+    /**
+     * Returns the host with the specified identifier.
+     *
+     * @param hostId host identifier
+     * @return host or null if one with the given identifier is not known
+     */
+    Host getHost(ElementId hostId); // TODO: change to HostId
+
+    // TODO: determine which ones make sense or which we care to support
+    // Set<Host> getHostsByVlan(VlanId vlan);
+    // Set<Host> getHostsByMac(MacAddress mac);
+    // Set<Host> getHostsByIp(IpAddress ip);
+
+    /**
+     * Returns the set of hosts whose most recent location is the specified
+     * connection point.
+     *
+     * @param connectPoint connection point
+     * @return set of hosts connected to the connection point
+     */
+    Set<Host> getConnectedHosts(ConnectPoint connectPoint);
+
+    /**
+     * Returns the set of hosts whose most recent location is the specified
+     * infrastructure device.
+     *
+     * @param deviceId device identifier
+     * @return set of hosts connected to the device
+     */
+    Set<Host> getConnectedHosts(DeviceId deviceId);
+
+    /**
+     * Adds the specified host listener.
+     *
+     * @param listener host listener
+     */
+    void addListener(HostListener listener);
+
+    /**
+     * Removes the specified host listener.
+     *
+     * @param listener host listener
+     */
+    void removeListener(HostListener listener);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/DefaultLinkDescription.java b/core/api/src/main/java/org/onlab/onos/net/link/DefaultLinkDescription.java
new file mode 100644
index 0000000..ffdc2fa
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/DefaultLinkDescription.java
@@ -0,0 +1,43 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Link;
+
+/**
+ * Default implementation of immutable link description entity.
+ */
+public class DefaultLinkDescription implements LinkDescription {
+
+    private final ConnectPoint src;
+    private final ConnectPoint dst;
+    private final Link.Type type;
+
+    /**
+     * Creates a link description using the supplied information.
+     *
+     * @param src  link source
+     * @param dst  link destination
+     * @param type link type
+     */
+    public DefaultLinkDescription(ConnectPoint src, ConnectPoint dst, Link.Type type) {
+        this.src = src;
+        this.dst = dst;
+        this.type = type;
+    }
+
+    @Override
+    public ConnectPoint src() {
+        return src;
+    }
+
+    @Override
+    public ConnectPoint dst() {
+        return dst;
+    }
+
+    @Override
+    public Link.Type type() {
+        return type;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkAdminService.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkAdminService.java
new file mode 100644
index 0000000..228b3c9
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkAdminService.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+
+/**
+ * Service for administering the inventory of infrastructure links.
+ */
+public interface LinkAdminService {
+
+    /**
+     * Removes all infrastructure links leading to and from the
+     * specified connection point.
+     *
+     * @param connectPoint connection point
+     */
+    void removeLinks(ConnectPoint connectPoint);
+
+    /**
+     * Removes all infrastructure links leading to and from the
+     * specified device.
+     *
+     * @param deviceId device identifier
+     */
+    void removeLinks(DeviceId deviceId);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkDescription.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkDescription.java
new file mode 100644
index 0000000..b1be82c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkDescription.java
@@ -0,0 +1,34 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Link;
+
+/**
+ * Describes an infrastructure link.
+ */
+public interface LinkDescription {
+
+    /**
+     * Returns the link source.
+     *
+     * @return links source
+     */
+    ConnectPoint src();
+
+    /**
+     * Returns the link destination.
+     *
+     * @return links destination
+     */
+    ConnectPoint dst();
+
+    /**
+     * Returns the link type.
+     *
+     * @return link type
+     */
+    Link.Type type();
+
+
+    // Add further link attributes
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkEvent.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkEvent.java
new file mode 100644
index 0000000..a85965a
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkEvent.java
@@ -0,0 +1,53 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.event.AbstractEvent;
+import org.onlab.onos.net.Link;
+
+/**
+ * Describes infrastructure link event.
+ */
+public class LinkEvent extends AbstractEvent<LinkEvent.Type, Link> {
+
+    /**
+     * Type of link events.
+     */
+    public enum Type {
+        /**
+         * Signifies that a new link has been detected.
+         */
+        LINK_ADDED,
+
+        /**
+         * Signifies that a link has been updated.
+         */
+        LINK_UPDATED,
+
+        /**
+         * Signifies that a link has been removed.
+         */
+        LINK_REMOVED
+    }
+
+    /**
+     * Creates an event of a given type and for the specified link and the
+     * current time.
+     *
+     * @param type link event type
+     * @param link event link subject
+     */
+    public LinkEvent(Type type, Link link) {
+        super(type, link);
+    }
+
+    /**
+     * Creates an event of a given type and for the specified link and time.
+     *
+     * @param type link event type
+     * @param link event link subject
+     * @param time occurrence time
+     */
+    public LinkEvent(Type type, Link link, long time) {
+        super(type, link, time);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkListener.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkListener.java
new file mode 100644
index 0000000..03039db
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkListener.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving infrastructure link related events.
+ */
+public interface LinkListener extends EventListener<LinkEvent> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkProvider.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkProvider.java
new file mode 100644
index 0000000..a8ade18
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkProvider.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.provider.Provider;
+
+/**
+ * Abstraction of an entity providing information about infrastructure links
+ * to the core.
+ */
+public interface LinkProvider extends Provider {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderRegistry.java
new file mode 100644
index 0000000..2aa54e7
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderRegistry.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.provider.ProviderRegistry;
+
+/**
+ * Abstraction of an infrastructure link provider registry.
+ */
+public interface LinkProviderRegistry
+        extends ProviderRegistry<LinkProvider, LinkProviderService> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderService.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderService.java
new file mode 100644
index 0000000..7019660
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkProviderService.java
@@ -0,0 +1,42 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.provider.ProviderService;
+
+/**
+ * Means for injecting link information into the core.
+ */
+public interface LinkProviderService extends ProviderService<LinkProvider> {
+
+    /**
+     * Signals that an infrastructure link has been detected.
+     *
+     * @param linkDescription link information
+     */
+    void linkDetected(LinkDescription linkDescription);
+
+    /**
+     * Signals that an infrastructure link has disappeared.
+     *
+     * @param linkDescription link information
+     */
+    void linkVanished(LinkDescription linkDescription);
+
+    /**
+     * Signals that infrastructure links associated with the specified
+     * connect point have vanished.
+     *
+     * @param connectPoint connect point
+     */
+    void linksVanished(ConnectPoint connectPoint);
+
+    /**
+     * Signals that infrastructure links associated with the specified
+     * device have vanished.
+     *
+     * @param deviceId device identifier
+     */
+    void linksVanished(DeviceId deviceId);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/link/LinkService.java b/core/api/src/main/java/org/onlab/onos/net/link/LinkService.java
new file mode 100644
index 0000000..1257d97
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/link/LinkService.java
@@ -0,0 +1,104 @@
+package org.onlab.onos.net.link;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+
+import java.util.Set;
+
+/**
+ * Service for interacting with the inventory of infrastructure links.
+ */
+public interface LinkService {
+
+    /**
+     * Returns the count of all known infrastructure links.
+     *
+     * @return number of infrastructure links
+     */
+    int getLinkCount();
+
+    /**
+     * Returns a collection of all known infrastructure links.
+     *
+     * @return all infrastructure links
+     */
+    Iterable<Link> getLinks();
+
+    /**
+     * Returns set of all infrastructure links leading to and from the
+     * specified device.
+     *
+     * @param deviceId device identifier
+     * @return set of device links
+     */
+    Set<Link> getDeviceLinks(DeviceId deviceId);
+
+    /**
+     * Returns set of all infrastructure links leading from the specified device.
+     *
+     * @param deviceId device identifier
+     * @return set of device egress links
+     */
+    Set<Link> getDeviceEgressLinks(DeviceId deviceId);
+
+    /**
+     * Returns set of all infrastructure links leading to the specified device.
+     *
+     * @param deviceId device identifier
+     * @return set of device ingress links
+     */
+    Set<Link> getDeviceIngressLinks(DeviceId deviceId);
+
+    /**
+     * Returns set of all infrastructure links leading to and from the
+     * specified connection point.
+     *
+     * @param connectPoint connection point
+     * @return set of links
+     */
+    Set<Link> getLinks(ConnectPoint connectPoint);
+
+    /**
+     * Returns set of all infrastructure links leading from the specified
+     * connection point.
+     *
+     * @param connectPoint connection point
+     * @return set of device egress links
+     */
+    Set<Link> getEgressLinks(ConnectPoint connectPoint);
+
+    /**
+     * Returns set of all infrastructure links leading to the specified
+     * connection point.
+     *
+     * @param connectPoint connection point
+     * @return set of device ingress links
+     */
+    Set<Link> getIngressLinks(ConnectPoint connectPoint);
+
+    /**
+     * Returns the infrastructure links between the specified source
+     * and destination connection points.
+     *
+     * @param src source connection point
+     * @param dst destination connection point
+     * @return link from source to destination; null if none found
+     */
+    Link getLink(ConnectPoint src, ConnectPoint dst);
+
+    /**
+     * Adds the specified link listener.
+     *
+     * @param listener link listener
+     */
+    void addListener(LinkListener listener);
+
+    /**
+     * Removes the specified link listener.
+     *
+     * @param listener link listener
+     */
+    void removeListener(LinkListener listener);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProvider.java b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProvider.java
new file mode 100644
index 0000000..d1c992f
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProvider.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.net.provider;
+
+/**
+ * Base provider implementation.
+ */
+public abstract class AbstractProvider implements Provider {
+
+    private final ProviderId providerId;
+
+    /**
+     * Creates a provider with the supplier identifier.
+     *
+     * @param id provider id
+     */
+    protected AbstractProvider(ProviderId id) {
+        this.providerId = id;
+    }
+
+    @Override
+    public ProviderId id() {
+        return providerId;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderRegistry.java
new file mode 100644
index 0000000..f8cdb6b
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderRegistry.java
@@ -0,0 +1,68 @@
+package org.onlab.onos.net.provider;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Base implementation of provider registry.
+ *
+ * @param <P> type of the information provider
+ * @param <S> type of the provider service
+ */
+public abstract class AbstractProviderRegistry<P extends Provider, S extends ProviderService<P>>
+        implements ProviderRegistry<P, S> {
+
+    private final Map<ProviderId, P> providers = new HashMap<>();
+    private final Map<ProviderId, S> services = new HashMap<>();
+
+    /**
+     * Creates a new provider service bound to the specified provider.
+     *
+     * @param provider provider
+     * @return provider service
+     */
+    protected abstract S createProviderService(P provider);
+
+    @Override
+    public synchronized S register(P provider) {
+        checkNotNull(provider, "Provider cannot be null");
+        checkState(!services.containsKey(provider.id()), "Provider %s already registered", provider.id());
+        S service = createProviderService(provider);
+        services.put(provider.id(), service);
+        providers.put(provider.id(), provider);
+        return service;
+    }
+
+    @Override
+    public synchronized void unregister(P provider) {
+        checkNotNull(provider, "Provider cannot be null");
+        S service = services.get(provider.id());
+        if (service != null && service instanceof AbstractProviderService) {
+            ((AbstractProviderService) service).invalidate();
+            services.remove(provider.id());
+            providers.remove(provider.id());
+        }
+    }
+
+    @Override
+    public synchronized Set<ProviderId> getProviders() {
+        return ImmutableSet.copyOf(services.keySet());
+    }
+
+    /**
+     * Returns the provider registered with the specified provider ID.
+     *
+     * @param providerId provider identifier
+     * @return provider
+     */
+    protected synchronized P getProvider(ProviderId providerId) {
+        return providers.get(providerId);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderService.java b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderService.java
new file mode 100644
index 0000000..5497842
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/AbstractProviderService.java
@@ -0,0 +1,46 @@
+package org.onlab.onos.net.provider;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Base implementation of a provider service, which tracks the provider to
+ * which it is issued and can be invalidated.
+ *
+ * @param <P> type of the information provider
+ */
+public abstract class AbstractProviderService<P extends Provider> implements ProviderService<P> {
+
+    private boolean isValid = true;
+    private final P provider;
+
+    /**
+     * Creates a provider service on behalf of the specified provider.
+     *
+     * @param provider provider to which this service is being issued
+     */
+    protected AbstractProviderService(P provider) {
+        this.provider = provider;
+    }
+
+    /**
+     * Invalidates this provider service.
+     */
+    public void invalidate() {
+        isValid = false;
+    }
+
+    /**
+     * Checks the validity of this provider service.
+     *
+     * @throws java.lang.IllegalStateException if the service is no longer valid
+     */
+    public void checkValidity() {
+        checkState(isValid, "Provider service is no longer valid");
+    }
+
+    @Override
+    public P provider() {
+        return provider;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/Provider.java b/core/api/src/main/java/org/onlab/onos/net/provider/Provider.java
new file mode 100644
index 0000000..3771611
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/Provider.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.net.provider;
+
+/**
+ * Abstraction of a provider of information about network environment.
+ */
+public interface Provider {
+
+    /**
+     * Returns the provider identifier.
+     *
+     * @return provider identification
+     */
+    ProviderId id();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java
new file mode 100644
index 0000000..a945354
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderId.java
@@ -0,0 +1,47 @@
+package org.onlab.onos.net.provider;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Notion of provider identity.
+ */
+public class ProviderId {
+
+    private final String id;
+
+    /**
+     * Creates a new provider identifier from the specified string.
+     * The providers are expected to follow the reverse DNS convention, e.g.
+     * {@code org.onlab.onos.provider.of.device}
+     *
+     * @param id string identifier
+     */
+    public ProviderId(String id) {
+        this.id = id;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final ProviderId other = (ProviderId) obj;
+        return Objects.equals(this.id, other.id);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this).add("id", id).toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/ProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderRegistry.java
new file mode 100644
index 0000000..08975e2
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderRegistry.java
@@ -0,0 +1,41 @@
+package org.onlab.onos.net.provider;
+
+import java.util.Set;
+
+/**
+ * Registry for tracking information providers with the core.
+ *
+ * @param <P> type of the information provider
+ * @param <S> type of the provider service
+ */
+public interface ProviderRegistry<P extends Provider, S extends ProviderService<P>> {
+
+    /**
+     * Registers the supplied provider with the core.
+     *
+     * @param provider provider to be registered
+     * @return provider service for injecting information into core
+     * @throws java.lang.IllegalArgumentException if the provider is registered already
+     */
+    S register(P provider);
+
+    /**
+     * Unregisters the supplied provider. As a result the previously issued
+     * provider service will be invalidated and any subsequent invocations
+     * of its methods may throw {@link java.lang.IllegalStateException}.
+     * <p/>
+     * Unregistering a provider that has not been previously registered results
+     * in a no-op.
+     *
+     * @param provider provider to be unregistered
+     */
+    void unregister(P provider);
+
+    /**
+     * Returns a set of currently registered provider identities.
+     *
+     * @return set of provider identifiers
+     */
+    Set<ProviderId> getProviders();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/provider/ProviderService.java b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderService.java
new file mode 100644
index 0000000..ae21a61
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/provider/ProviderService.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.net.provider;
+
+/**
+ * Abstraction of a service through which providers can inject information
+ * about the network environment into the core.
+ *
+ * @param <P> type of the information provider
+ */
+public interface ProviderService<P extends Provider> {
+
+    /**
+     * Returns the provider to which this service has been issued.
+     *
+     * @return provider to which this service has been assigned
+     */
+    P provider();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java b/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java
new file mode 100644
index 0000000..af8f122
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/ClusterId.java
@@ -0,0 +1,49 @@
+package org.onlab.onos.net.topology;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Representation of the topology cluster identity.
+ */
+public final class ClusterId {
+
+    private final int id;
+
+    // Public construction is prohibit
+    private ClusterId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * Returns the cluster identifier, represented by the specified integer
+     * serial number.
+     *
+     * @param id integer serial number
+     * @return cluster identifier
+     */
+    public static ClusterId clusterId(int id) {
+        return new ClusterId(id);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ClusterId) {
+            final ClusterId other = (ClusterId) obj;
+            return Objects.equals(this.id, other.id);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this).add("id", id).toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java
new file mode 100644
index 0000000..f33dcf7
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/DefaultTopologyCluster.java
@@ -0,0 +1,81 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.net.DeviceId;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Default implementation of a network topology cluster.
+ */
+public class DefaultTopologyCluster implements TopologyCluster {
+
+    private final ClusterId id;
+    private final int deviceCount;
+    private final int linkCount;
+    private final DeviceId root;
+
+    /**
+     * Creates a new topology cluster descriptor with the specified attributes.
+     *
+     * @param id          cluster id
+     * @param deviceCount number of devices in the cluster
+     * @param linkCount   number of links in the cluster
+     * @param root        cluster root node
+     */
+    public DefaultTopologyCluster(ClusterId id, int deviceCount, int linkCount,
+                                  DeviceId root) {
+        this.id = id;
+        this.deviceCount = deviceCount;
+        this.linkCount = linkCount;
+        this.root = root;
+    }
+
+    @Override
+    public ClusterId id() {
+        return id;
+    }
+
+    @Override
+    public int deviceCount() {
+        return deviceCount;
+    }
+
+    @Override
+    public int linkCount() {
+        return linkCount;
+    }
+
+    @Override
+    public DeviceId root() {
+        return root;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, deviceCount, linkCount, root);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof DefaultTopologyCluster) {
+            final DefaultTopologyCluster other = (DefaultTopologyCluster) obj;
+            return Objects.equals(this.id, other.id) &&
+                    Objects.equals(this.deviceCount, other.deviceCount) &&
+                    Objects.equals(this.linkCount, other.linkCount) &&
+                    Objects.equals(this.root, other.root);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("id", id)
+                .add("deviceCount", deviceCount)
+                .add("linkCount", linkCount)
+                .add("root", root)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/LinkWeight.java b/core/api/src/main/java/org/onlab/onos/net/topology/LinkWeight.java
new file mode 100644
index 0000000..89ef577
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/LinkWeight.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.graph.EdgeWeight;
+
+/**
+ * Entity capable of determining cost or weight of a specified topology
+ * graph edge.
+ */
+public interface LinkWeight extends EdgeWeight<TopoVertex, TopoEdge> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopoEdge.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopoEdge.java
new file mode 100644
index 0000000..ef94eca
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopoEdge.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.graph.Edge;
+import org.onlab.onos.net.Link;
+
+/**
+ * Represents an edge in the topology graph.
+ */
+public interface TopoEdge extends Edge<TopoVertex> {
+
+    /**
+     * Returns the associated infrastructure link.
+     *
+     * @return backing infrastructure link
+     */
+    Link link();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopoVertex.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopoVertex.java
new file mode 100644
index 0000000..d5a8b95
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopoVertex.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.graph.Vertex;
+import org.onlab.onos.net.DeviceId;
+
+/**
+ * Represents a vertex in the topology graph.
+ */
+public interface TopoVertex extends Vertex {
+
+    /**
+     * Returns the associated infrastructure device identification.
+     *
+     * @return device identifier
+     */
+    DeviceId deviceId();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/Topology.java b/core/api/src/main/java/org/onlab/onos/net/topology/Topology.java
new file mode 100644
index 0000000..f71a5ec
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/Topology.java
@@ -0,0 +1,50 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.net.Provided;
+
+/**
+ * Represents a network topology computation snapshot.
+ */
+public interface Topology extends Provided {
+
+    /**
+     * Returns the time, specified in milliseconds since start of epoch,
+     * when the topology became active and made available.
+     *
+     * @return time in milliseconds since start of epoch
+     */
+    long time();
+
+    /**
+     * Returns the number of SCCs (strongly connected components) in the
+     * topology.
+     *
+     * @return number of clusters
+     */
+    int clusterCount();
+
+    /**
+     * Returns the number of infrastructure devices in the topology.
+     *
+     * @return number of devices
+     */
+    int deviceCount();
+
+
+    /**
+     * Returns the number of infrastructure links in the topology.
+     *
+     * @return number of links
+     */
+    int linkCount();
+
+    /**
+     * Returns the number of infrastructure paths computed between devices
+     * in the topology. This means the number of all the shortest paths
+     * (hop-count) between all device pairs.
+     *
+     * @return number of paths
+     */
+    int pathCount();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyCluster.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyCluster.java
new file mode 100644
index 0000000..46c9872
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyCluster.java
@@ -0,0 +1,38 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.net.DeviceId;
+
+/**
+ * Representation of an SCC (strongly-connected component) in a network topology.
+ */
+public interface TopologyCluster {
+
+    /**
+     * Returns the cluster id.
+     *
+     * @return cluster identifier
+     */
+    ClusterId id();
+
+    /**
+     * Returns the number of devices in the cluster.
+     *
+     * @return number of cluster devices
+     */
+    int deviceCount();
+
+    /**
+     * Returns the number of infrastructure links in the cluster.
+     *
+     * @return number of cluster links
+     */
+    int linkCount();
+
+    /**
+     * Returns the device identifier of the cluster root device.
+     *
+     * @return cluster root device identifier
+     */
+    DeviceId root();
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyDescription.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyDescription.java
new file mode 100644
index 0000000..dd0102d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyDescription.java
@@ -0,0 +1,71 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.graph.Graph;
+import org.onlab.graph.GraphPathSearch;
+import org.onlab.onos.net.Description;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+
+import java.util.Set;
+
+/**
+ * Describes attribute(s) of a network topology.
+ */
+public interface TopologyDescription extends Description {
+
+    /**
+     * Returns the creation timestamp of the topology description. This is
+     * expressed in system nanos to allow proper sequencing.
+     *
+     * @return topology description creation timestamp
+     */
+    long timestamp();
+
+    /**
+     * Returns the topology graph.
+     *
+     * @return network graph
+     */
+    Graph<TopoVertex, TopoEdge> graph();
+
+    /**
+     * Returns the results of the path search through the network graph. This
+     * is assumed to contain results of seach fro the given device to all
+     * other devices.
+     *
+     * @param srcDeviceId source device identifier
+     * @return path search result for the given source node
+     */
+    GraphPathSearch.Result pathResults(DeviceId srcDeviceId);
+
+    /**
+     * Returns the set of topology SCC clusters.
+     *
+     * @return set of SCC clusters
+     */
+    Set<TopologyCluster> clusters();
+
+    /**
+     * Returns the set of devices contained by the specified topology cluster.
+     *
+     * @return set of devices that belong to the specified cluster
+     */
+    Set<DeviceId> clusterDevices(TopologyCluster cluster);
+
+    /**
+     * Returns the set of infrastructure links contained by the specified cluster.
+     *
+     * @return set of links that form the given cluster
+     */
+    Set<Link> clusterLinks(TopologyCluster cluster);
+
+    /**
+     * Returns the topology SCC cluster which contains the given device.
+     *
+     * @param deviceId device identifier
+     * @return topology cluster that contains the specified device
+     */
+    TopologyCluster clusterFor(DeviceId deviceId);
+
+}
+
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyEvent.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyEvent.java
new file mode 100644
index 0000000..0be5323
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyEvent.java
@@ -0,0 +1,42 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.event.AbstractEvent;
+
+/**
+ * Describes network topology event.
+ */
+public class TopologyEvent extends AbstractEvent<TopologyEvent.Type, Topology> {
+
+    /**
+     * Type of topology events.
+     */
+    public enum Type {
+        /**
+         * Signifies that topology has changed.
+         */
+        TOPOLOGY_CHANGED
+    }
+
+    /**
+     * Creates an event of a given type and for the specified topology and the
+     * current time.
+     *
+     * @param type     topology event type
+     * @param topology event topology subject
+     */
+    public TopologyEvent(Type type, Topology topology) {
+        super(type, topology);
+    }
+
+    /**
+     * Creates an event of a given type and for the specified topology and time.
+     *
+     * @param type     link event type
+     * @param topology event topology subject
+     * @param time     occurrence time
+     */
+    public TopologyEvent(Type type, Topology topology, long time) {
+        super(type, topology, time);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyListener.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyListener.java
new file mode 100644
index 0000000..bcc2cf3
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyListener.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.event.EventListener;
+
+/**
+ * Entity capable of receiving network topology related events.
+ */
+public interface TopologyListener extends EventListener<TopologyEvent> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java
new file mode 100644
index 0000000..7ad8cc0
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProvider.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.net.provider.Provider;
+
+/**
+ * Means for injecting topology information into the core.
+ */
+public interface TopologyProvider extends Provider {
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderRegistry.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderRegistry.java
new file mode 100644
index 0000000..40bfa66
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderRegistry.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.net.provider.ProviderRegistry;
+
+/**
+ * Abstraction of a network topology provider registry.
+ */
+public interface TopologyProviderRegistry extends
+        ProviderRegistry<TopologyProvider, TopologyProviderService> {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderService.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderService.java
new file mode 100644
index 0000000..0e03767
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyProviderService.java
@@ -0,0 +1,25 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.onos.event.Event;
+import org.onlab.onos.net.provider.ProviderService;
+
+import java.util.List;
+
+/**
+ * Means for injecting topology information into the core.
+ */
+public interface TopologyProviderService extends ProviderService<TopologyProvider> {
+
+    // What can be conveyed in a topology that isn't by individual
+    // providers?
+
+    /**
+     * Signals the core that some aspect of the topology has changed.
+     *
+     * @param topoDescription information about topology
+     * @param reasons         events that triggered topology change
+     */
+    void topologyChanged(TopologyDescription topoDescription,
+                         List<Event> reasons);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java
new file mode 100644
index 0000000..36ee666
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java
@@ -0,0 +1,96 @@
+package org.onlab.onos.net.topology;
+
+import org.onlab.graph.Graph;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Path;
+
+import java.util.Set;
+
+/**
+ * Service for providing network topology information.
+ */
+public interface TopologyService {
+
+    /**
+     * Returns the current topology descriptor.
+     *
+     * @return current topology
+     */
+    Topology currentTopology();
+
+    /**
+     * Returns the set of clusters in the specified topology.
+     *
+     * @param topology topology descriptor
+     * @return set of topology clusters
+     */
+    Set<TopologyCluster> getClusters(Topology topology);
+
+    /**
+     * Returns the graph view of the specified topology.
+     *
+     * @param topology topology descriptor
+     * @return topology graph view
+     */
+    Graph<TopoVertex, TopoEdge> getGraph(Topology topology);
+
+    /**
+     * Returns the set of all shortest paths, precomputed in terms of hop-count,
+     * between the specified source and destination devices.
+     *
+     * @param topology topology descriptor
+     * @param src      source device
+     * @param dst      destination device
+     * @return set of all shortest paths between the two devices
+     */
+    Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst);
+
+    /**
+     * Returns the set of all shortest paths, computed using the supplied
+     * edge-weight entity, between the specified source and destination devices.
+     *
+     * @param topology topology descriptor
+     * @param src      source device
+     * @param dst      destination device
+     * @return set of all shortest paths between the two devices
+     */
+    Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst,
+                       LinkWeight weight);
+
+    /**
+     * Indicates whether the specified connection point is part of the network
+     * infrastructure or part of network edge.
+     *
+     * @param topology     topology descriptor
+     * @param connectPoint connection point
+     * @return true of connection point is in infrastructure; false if edge
+     */
+    boolean isInfrastructure(Topology topology, ConnectPoint connectPoint);
+
+
+    /**
+     * Indicates whether the specified connection point belong to the
+     * broadcast tree.
+     *
+     * @param topology     topology descriptor
+     * @param connectPoint connection point
+     * @return true if broadcast is permissible
+     */
+    boolean isInBroadcastTree(Topology topology, ConnectPoint connectPoint);
+
+    /**
+     * Adds the specified topology listener.
+     *
+     * @param listener topology listener
+     */
+    void addListener(TopologyListener listener);
+
+    /**
+     * Removes the specified topology listener.
+     *
+     * @param listener topology listener
+     */
+    void removeListener(TopologyListener listener);
+
+}
diff --git a/core/api/src/main/javadoc/org/onlab/onos/event/package.html b/core/api/src/main/javadoc/org/onlab/onos/event/package.html
new file mode 100644
index 0000000..1591f3d
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/event/package.html
@@ -0,0 +1,3 @@
+<body>
+Local event delivery subsystem interfaces &amp; supporting abstractions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/device/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/device/package.html
new file mode 100644
index 0000000..521a30a
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/device/package.html
@@ -0,0 +1,3 @@
+<body>
+Infrastructure device model &amp; related services API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/flow/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/flow/package.html
new file mode 100644
index 0000000..46028d4
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/flow/package.html
@@ -0,0 +1,3 @@
+<body>
+Flow rule model &amp; related services API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/host/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/host/package.html
new file mode 100644
index 0000000..e076f65
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/host/package.html
@@ -0,0 +1,3 @@
+<body>
+End-station host model &amp; related services API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/link/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/link/package.html
new file mode 100644
index 0000000..50972e2
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/link/package.html
@@ -0,0 +1,3 @@
+<body>
+Infrastructure link model &amp; related services API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/package.html
new file mode 100644
index 0000000..7d2cd0a
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/package.html
@@ -0,0 +1,3 @@
+<body>
+Network model entities &amp; service API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/provider/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/provider/package.html
new file mode 100644
index 0000000..75c9ed4
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/provider/package.html
@@ -0,0 +1,3 @@
+<body>
+Base abstractions related to network entity providers and their brokers.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/net/topology/package.html b/core/api/src/main/javadoc/org/onlab/onos/net/topology/package.html
new file mode 100644
index 0000000..7601432
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/net/topology/package.html
@@ -0,0 +1,3 @@
+<body>
+Network topology model &amp; related services API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/main/javadoc/org/onlab/onos/package.html b/core/api/src/main/javadoc/org/onlab/onos/package.html
new file mode 100644
index 0000000..cda72f5
--- /dev/null
+++ b/core/api/src/main/javadoc/org/onlab/onos/package.html
@@ -0,0 +1,3 @@
+<body>
+ONOS Core API definitions.
+</body>
\ No newline at end of file
diff --git a/core/api/src/test/java/org/onlab/onos/event/AbstractEventTest.java b/core/api/src/test/java/org/onlab/onos/event/AbstractEventTest.java
new file mode 100644
index 0000000..b79b836
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/AbstractEventTest.java
@@ -0,0 +1,64 @@
+package org.onlab.onos.event;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.onos.event.TestEvent.Type.FOO;
+
+/**
+ * Tests of the base event abstraction.
+ */
+public class AbstractEventTest {
+
+    /**
+     * Validates the base attributes of an event.
+     *
+     * @param event   event to validate
+     * @param type    event type
+     * @param subject event subject
+     * @param time    event time
+     * @param <T>     type of event
+     * @param <S>     type of subject
+     */
+    protected static <T extends Enum, S>
+    void validateEvent(Event<T, S> event, T type, S subject, long time) {
+        assertEquals("incorrect type", type, event.type());
+        assertEquals("incorrect subject", subject, event.subject());
+        assertEquals("incorrect time", time, event.time());
+    }
+
+    /**
+     * Validates the base attributes of an event.
+     *
+     * @param event   event to validate
+     * @param type    event type
+     * @param subject event subject
+     * @param minTime minimum event time inclusive
+     * @param maxTime maximum event time inclusive
+     * @param <T>     type of event
+     * @param <S>     type of subject
+     */
+    protected static <T extends Enum, S>
+    void validateEvent(Event<T, S> event, T type, S subject,
+                       long minTime, long maxTime) {
+        assertEquals("incorrect type", type, event.type());
+        assertEquals("incorrect subject", subject, event.subject());
+        assertTrue("incorrect time", minTime <= event.time() && event.time() <= maxTime);
+    }
+
+    @Test
+    public void withTime() {
+        TestEvent event = new TestEvent(FOO, "foo", 123L);
+        validateEvent(event, FOO, "foo", 123L);
+    }
+
+    @Test
+    public void withoutTime() {
+        long before = System.currentTimeMillis();
+        TestEvent event = new TestEvent(FOO, "foo");
+        long after = System.currentTimeMillis();
+        validateEvent(event, FOO, "foo", before, after);
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/event/AbstractListenerRegistryTest.java b/core/api/src/test/java/org/onlab/onos/event/AbstractListenerRegistryTest.java
new file mode 100644
index 0000000..e41eb27
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/AbstractListenerRegistryTest.java
@@ -0,0 +1,49 @@
+package org.onlab.onos.event;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests of the base listener manager.
+ */
+public class AbstractListenerRegistryTest {
+
+    @Test
+    public void basics() {
+        TestListener listener = new TestListener();
+        TestListener secondListener = new TestListener();
+        TestListenerRegistry manager = new TestListenerRegistry();
+        manager.addListener(listener);
+        manager.addListener(secondListener);
+
+        TestEvent event = new TestEvent(TestEvent.Type.BAR, "bar");
+        manager.process(event);
+        assertTrue("event not processed", listener.events.contains(event));
+        assertTrue("event not processed", secondListener.events.contains(event));
+
+        manager.removeListener(listener);
+
+        TestEvent another = new TestEvent(TestEvent.Type.FOO, "foo");
+        manager.process(another);
+        assertFalse("event processed", listener.events.contains(another));
+        assertTrue("event not processed", secondListener.events.contains(event));
+    }
+
+    @Test
+    public void badListener() {
+        TestListener listener = new BrokenListener();
+        TestListener secondListener = new TestListener();
+        TestListenerRegistry manager = new TestListenerRegistry();
+        manager.addListener(listener);
+        manager.addListener(secondListener);
+
+        TestEvent event = new TestEvent(TestEvent.Type.BAR, "bar");
+        manager.process(event);
+        assertFalse("event processed", listener.events.contains(event));
+        assertFalse("error not reported", manager.errors.isEmpty());
+        assertTrue("event not processed", secondListener.events.contains(event));
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/event/BrokenListener.java b/core/api/src/test/java/org/onlab/onos/event/BrokenListener.java
new file mode 100644
index 0000000..dc83419
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/BrokenListener.java
@@ -0,0 +1,13 @@
+package org.onlab.onos.event;
+
+/**
+ * Test event listener fixture.
+ */
+public class BrokenListener extends TestListener {
+
+    public void event(TestEvent event) {
+        throw new IllegalStateException("boom");
+    }
+
+}
+
diff --git a/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java b/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java
new file mode 100644
index 0000000..ff10b50
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java
@@ -0,0 +1,52 @@
+package org.onlab.onos.event;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests of the default event sink registry.
+ */
+public class DefaultEventSinkRegistryTest {
+
+    private DefaultEventSinkRegistry registry;
+
+    private static class FooEvent extends TestEvent {
+        public FooEvent(String subject) { super(Type.FOO, subject); }
+    }
+
+    private static class BarEvent extends TestEvent {
+        public BarEvent(String subject) { super(Type.BAR, subject); }
+    }
+
+    private static class FooSink implements EventSink<FooEvent> {
+        @Override public void process(FooEvent event) {}
+    }
+
+    private static class BarSink implements EventSink<BarEvent> {
+        @Override public void process(BarEvent event) {}
+    }
+
+    @Before
+    public void setUp() {
+        registry = new DefaultEventSinkRegistry();
+    }
+
+    @Test
+    public void basics() {
+        FooSink fooSink = new FooSink();
+        BarSink barSink = new BarSink();
+        registry.addSink(FooEvent.class, fooSink);
+        registry.addSink(BarEvent.class, barSink);
+
+        assertEquals("incorrect sink count", 2, registry.getSinks().size());
+        assertEquals("incorrect sink", fooSink, registry.getSink(FooEvent.class));
+        assertEquals("incorrect sink", barSink, registry.getSink(BarEvent.class));
+
+        registry.removeSink(FooEvent.class);
+        assertNull("incorrect sink", registry.getSink(FooEvent.class));
+        assertEquals("incorrect sink", barSink, registry.getSink(BarEvent.class));
+
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/event/TestEvent.java b/core/api/src/test/java/org/onlab/onos/event/TestEvent.java
new file mode 100644
index 0000000..25c8f46
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/TestEvent.java
@@ -0,0 +1,19 @@
+package org.onlab.onos.event;
+
+/**
+ * Test event fixture.
+ */
+public class TestEvent extends AbstractEvent<TestEvent.Type, String> {
+
+    public enum Type { FOO, BAR };
+
+    public TestEvent(Type type, String subject) {
+        super(type, subject);
+    }
+
+    public TestEvent(Type type, String subject, long timestamp) {
+        super(type, subject, timestamp);
+    }
+
+}
+
diff --git a/core/api/src/test/java/org/onlab/onos/event/TestListener.java b/core/api/src/test/java/org/onlab/onos/event/TestListener.java
new file mode 100644
index 0000000..ec2185a
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/TestListener.java
@@ -0,0 +1,19 @@
+package org.onlab.onos.event;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test event listener fixture.
+ */
+public class TestListener implements EventListener<TestEvent> {
+
+    public final List<TestEvent> events = new ArrayList<>();
+
+    @Override
+    public void event(TestEvent event) {
+        events.add(event);
+    }
+
+}
+
diff --git a/core/api/src/test/java/org/onlab/onos/event/TestListenerRegistry.java b/core/api/src/test/java/org/onlab/onos/event/TestListenerRegistry.java
new file mode 100644
index 0000000..e676030
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/event/TestListenerRegistry.java
@@ -0,0 +1,21 @@
+package org.onlab.onos.event;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test event listener manager fixture.
+ */
+public class TestListenerRegistry
+        extends AbstractListenerRegistry<TestEvent, TestListener> {
+
+    public final List<Throwable> errors = new ArrayList<>();
+
+    @Override
+    protected void reportProblem(TestEvent event, Throwable error) {
+        super.reportProblem(event, error);
+        errors.add(error);
+    }
+
+}
+
diff --git a/core/api/src/test/java/org/onlab/onos/net/DefaultDeviceTest.java b/core/api/src/test/java/org/onlab/onos/net/DefaultDeviceTest.java
new file mode 100644
index 0000000..c37a15c
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/DefaultDeviceTest.java
@@ -0,0 +1,53 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.Device.Type.SWITCH;
+import static org.onlab.onos.net.DeviceId.deviceId;
+
+/**
+ * Test of the default device model entity.
+ */
+public class DefaultDeviceTest {
+
+    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 SW = "3.9.1";
+    private static final String SN1 = "43311-12345";
+    private static final String SN2 = "42346-43512";
+
+
+    @Test
+    public void testEquality() {
+        Device d1 = new DefaultDevice(PID, DID1, SWITCH, MFR, HW, SW, SN1);
+        Device d2 = new DefaultDevice(PID, DID1, SWITCH, MFR, HW, SW, SN1);
+        Device d3 = new DefaultDevice(PID, DID2, SWITCH, MFR, HW, SW, SN2);
+        Device d4 = new DefaultDevice(PID, DID2, SWITCH, MFR, HW, SW, SN2);
+        Device d5 = new DefaultDevice(PID, DID2, SWITCH, MFR, HW, SW, SN1);
+
+        new EqualsTester().addEqualityGroup(d1, d2)
+                .addEqualityGroup(d3, d4)
+                .addEqualityGroup(d5)
+                .testEquals();
+    }
+
+    @Test
+    public void basics() {
+        Device device = new DefaultDevice(PID, DID1, SWITCH, MFR, HW, SW, SN1);
+        assertEquals("incorrect provider", PID, device.providerId());
+        assertEquals("incorrect id", DID1, device.id());
+        assertEquals("incorrect type", SWITCH, device.type());
+        assertEquals("incorrect manufacturer", MFR, device.manufacturer());
+        assertEquals("incorrect hw", HW, device.hwVersion());
+        assertEquals("incorrect sw", SW, device.swVersion());
+        assertEquals("incorrect serial", SN1, device.serialNumber());
+        assertEquals("incorrect serial", SN1, device.serialNumber());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/DefaultEdgeLinkTest.java b/core/api/src/test/java/org/onlab/onos/net/DefaultEdgeLinkTest.java
new file mode 100644
index 0000000..368ff16
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/DefaultEdgeLinkTest.java
@@ -0,0 +1,58 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.DefaultLinkTest.cp;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.HostId.hostId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Test of the default edge link model entity.
+ */
+public class DefaultEdgeLinkTest {
+
+    private static final ProviderId PID = new ProviderId("foo");
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final HostId HID1 = hostId("nic:foobar");
+    private static final HostId HID2 = hostId("nic:barfoo");
+    private static final PortNumber P0 = portNumber(0);
+    private static final PortNumber P1 = portNumber(1);
+
+    @Test
+    public void testEquality() {
+        EdgeLink l1 = new DefaultEdgeLink(PID, cp(HID1, P0),
+                                          new HostLocation(DID1, P1, 123L), true);
+        EdgeLink l2 = new DefaultEdgeLink(PID, cp(HID1, P0),
+                                          new HostLocation(DID1, P1, 123L), true);
+
+        EdgeLink l3 = new DefaultEdgeLink(PID, cp(HID2, P0),
+                                          new HostLocation(DID1, P1, 123L), false);
+        EdgeLink l4 = new DefaultEdgeLink(PID, cp(HID2, P0),
+                                          new HostLocation(DID1, P1, 123L), false);
+
+        EdgeLink l5 = new DefaultEdgeLink(PID, cp(HID1, P0),
+                                          new HostLocation(DID1, P1, 123L), false);
+
+        new EqualsTester().addEqualityGroup(l1, l2)
+                .addEqualityGroup(l3, l4)
+                .addEqualityGroup(l5)
+                .testEquals();
+    }
+
+    @Test
+    public void basics() {
+        HostLocation hostLocation = new HostLocation(DID1, P1, 123L);
+        EdgeLink link = new DefaultEdgeLink(PID, cp(HID1, P0), hostLocation, false);
+        assertEquals("incorrect src", cp(HID1, P0), link.src());
+        assertEquals("incorrect dst", hostLocation, link.dst());
+        assertEquals("incorrect type", Link.Type.EDGE, link.type());
+        assertEquals("incorrect hostId", HID1, link.hostId());
+        assertEquals("incorrect connect point", hostLocation, link.hostLocation());
+        assertEquals("incorrect time", 123L, link.hostLocation().time());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/DefaultLinkTest.java b/core/api/src/test/java/org/onlab/onos/net/DefaultLinkTest.java
new file mode 100644
index 0000000..ec8511e
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/DefaultLinkTest.java
@@ -0,0 +1,50 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+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.PortNumber.portNumber;
+
+/**
+ * Test of the default link model entity.
+ */
+public class DefaultLinkTest {
+
+    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 PortNumber P1 = portNumber(1);
+    private static final PortNumber P2 = portNumber(2);
+
+    public static ConnectPoint cp(ElementId id, PortNumber pn) {
+        return new ConnectPoint(id, pn);
+    }
+
+    @Test
+    public void testEquality() {
+        Link l1 = new DefaultLink(PID, cp(DID1, P1), cp(DID2, P2), DIRECT);
+        Link l2 = new DefaultLink(PID, cp(DID1, P1), cp(DID2, P2), DIRECT);
+        Link l3 = new DefaultLink(PID, cp(DID1, P2), cp(DID2, P2), DIRECT);
+        Link l4 = new DefaultLink(PID, cp(DID1, P2), cp(DID2, P2), DIRECT);
+        Link l5 = new DefaultLink(PID, cp(DID1, P2), cp(DID2, P2), INDIRECT);
+
+        new EqualsTester().addEqualityGroup(l1, l2)
+                .addEqualityGroup(l3, l4)
+                .addEqualityGroup(l5)
+                .testEquals();
+    }
+
+    @Test
+    public void basics() {
+        Link link = new DefaultLink(PID, cp(DID1, P1), cp(DID2, P2), DIRECT);
+        assertEquals("incorrect src", cp(DID1, P1), link.src());
+        assertEquals("incorrect dst", cp(DID2, P2), link.dst());
+        assertEquals("incorrect type", DIRECT, link.type());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java b/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java
new file mode 100644
index 0000000..e9d3da6
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java
@@ -0,0 +1,47 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.Device.Type.SWITCH;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Test of the default port model entity.
+ */
+public class DefaultPortTest {
+
+    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 PortNumber P1 = portNumber(1);
+    private static final PortNumber P2 = portNumber(2);
+
+    @Test
+    public void testEquality() {
+        Device device = new DefaultDevice(PID, DID1, SWITCH, "m", "h", "s", "n");
+        Port p1 = new DefaultPort(device, portNumber(1), true);
+        Port p2 = new DefaultPort(device, portNumber(1), true);
+        Port p3 = new DefaultPort(device, portNumber(2), true);
+        Port p4 = new DefaultPort(device, portNumber(2), true);
+        Port p5 = new DefaultPort(device, portNumber(1), false);
+
+        new EqualsTester().addEqualityGroup(p1, p2)
+                .addEqualityGroup(p3, p4)
+                .addEqualityGroup(p5)
+                .testEquals();
+    }
+
+    @Test
+    public void basics() {
+        Device device = new DefaultDevice(PID, DID1, SWITCH, "m", "h", "s", "n");
+        Port port = new DefaultPort(device, portNumber(1), true);
+        assertEquals("incorrect element", device, port.element());
+        assertEquals("incorrect number", portNumber(1), port.number());
+        assertEquals("incorrect state", true, port.isEnabled());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/DeviceIdTest.java b/core/api/src/test/java/org/onlab/onos/net/DeviceIdTest.java
new file mode 100644
index 0000000..eaee54c
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/DeviceIdTest.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import static org.onlab.onos.net.DeviceId.deviceId;
+
+/**
+ * Test of the device identifier.
+ */
+public class DeviceIdTest extends ElementIdTest {
+
+    @Test
+    public void basics() {
+        new EqualsTester()
+                .addEqualityGroup(deviceId("of:foo"),
+                                  deviceId("of:foo"))
+                .addEqualityGroup(deviceId("of:bar"))
+                .testEquals();
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/ElementIdTest.java b/core/api/src/test/java/org/onlab/onos/net/ElementIdTest.java
new file mode 100644
index 0000000..cf209b3
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/ElementIdTest.java
@@ -0,0 +1,36 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the network element identifier.
+ */
+public class ElementIdTest {
+
+    private static class FooId extends ElementId {
+        public FooId(URI uri) {
+            super(uri);
+        }
+    }
+
+    public static URI uri(String str) {
+        return URI.create(str);
+    }
+
+    @Test
+    public void basics() {
+        new EqualsTester()
+                .addEqualityGroup(new FooId(uri("of:foo")),
+                                  new FooId(uri("of:foo")))
+                .addEqualityGroup(new FooId(uri("of:bar")))
+                .testEquals();
+        assertEquals("wrong uri", uri("ofcfg:foo"),
+                     new FooId(uri("ofcfg:foo")).uri());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/HostIdTest.java b/core/api/src/test/java/org/onlab/onos/net/HostIdTest.java
new file mode 100644
index 0000000..3adcabc
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/HostIdTest.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.net;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import static org.onlab.onos.net.HostId.hostId;
+
+/**
+ * Test of the host identifier.
+ */
+public class HostIdTest extends ElementIdTest {
+
+    @Test
+    public void basics() {
+        new EqualsTester()
+                .addEqualityGroup(hostId("nic:foo"),
+                                  hostId("nic:foo"))
+                .addEqualityGroup(hostId("nic:bar"))
+                .testEquals();
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/PortNumberTest.java b/core/api/src/test/java/org/onlab/onos/net/PortNumberTest.java
new file mode 100644
index 0000000..528ad09
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/PortNumberTest.java
@@ -0,0 +1,31 @@
+package org.onlab.onos.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+import org.junit.Test;
+
+import com.google.common.testing.EqualsTester;
+
+/**
+ * Test of the port number.
+ */
+public class PortNumberTest extends ElementIdTest {
+
+    @Override
+    @Test
+    public void basics() {
+        new EqualsTester()
+        .addEqualityGroup(portNumber(123),
+                portNumber("123"))
+                .addEqualityGroup(portNumber(321))
+                .testEquals();
+    }
+
+    @Test
+    public void number() {
+        assertEquals("incorrect long value", 12345, portNumber(12345).toLong());
+    }
+
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/device/DefaultDeviceDescriptionTest.java b/core/api/src/test/java/org/onlab/onos/net/device/DefaultDeviceDescriptionTest.java
new file mode 100644
index 0000000..9d06edf
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/device/DefaultDeviceDescriptionTest.java
@@ -0,0 +1,36 @@
+package org.onlab.onos.net.device;
+
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.onos.net.Device.Type.SWITCH;
+
+/**
+ * Test of the default device description.
+ */
+public class DefaultDeviceDescriptionTest {
+
+    private static final URI DURI = URI.create("of:foo");
+    private static final String MFR = "whitebox";
+    private static final String HW = "1.1.x";
+    private static final String SW = "3.9.1";
+    private static final String SN = "43311-12345";
+
+
+    @Test
+    public void basics() {
+        DeviceDescription device =
+                new DefaultDeviceDescription(DURI, SWITCH, MFR, HW, SW, SN);
+        assertEquals("incorrect uri", DURI, device.deviceURI());
+        assertEquals("incorrect type", SWITCH, device.type());
+        assertEquals("incorrect manufacturer", MFR, device.manufacturer());
+        assertEquals("incorrect hw", HW, device.hwVersion());
+        assertEquals("incorrect sw", SW, device.swVersion());
+        assertEquals("incorrect serial", SN, device.serialNumber());
+        assertTrue("incorrect toString", device.toString().contains("uri=of:foo"));
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/device/DeviceEventTest.java b/core/api/src/test/java/org/onlab/onos/net/device/DeviceEventTest.java
new file mode 100644
index 0000000..6f1219a
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/device/DeviceEventTest.java
@@ -0,0 +1,45 @@
+package org.onlab.onos.net.device;
+
+import org.junit.Test;
+import org.onlab.onos.event.AbstractEventTest;
+import org.onlab.onos.net.DefaultDevice;
+import org.onlab.onos.net.DefaultPort;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.DeviceId.deviceId;
+
+/**
+ * Tests of the device event.
+ */
+public class DeviceEventTest extends AbstractEventTest {
+
+    private Device createDevice() {
+        return new DefaultDevice(new ProviderId("foo"), deviceId("of:foo"),
+                                 Device.Type.SWITCH, "box", "hw", "sw", "sn");
+    }
+
+    @Test
+    public void withTime() {
+        Device device = createDevice();
+        Port port = new DefaultPort(device, PortNumber.portNumber(123L), true);
+        DeviceEvent event = new DeviceEvent(DeviceEvent.Type.DEVICE_ADDED,
+                                            device, port, 123L);
+        validateEvent(event, DeviceEvent.Type.DEVICE_ADDED, device, 123L);
+        assertEquals("incorrect port", port, event.port());
+    }
+
+    @Test
+    public void withoutTime() {
+        Device device = createDevice();
+        Port port = new DefaultPort(device, PortNumber.portNumber(123L), true);
+        long before = System.currentTimeMillis();
+        DeviceEvent event = new DeviceEvent(DeviceEvent.Type.DEVICE_ADDED, device);
+        long after = System.currentTimeMillis();
+        validateEvent(event, DeviceEvent.Type.DEVICE_ADDED, device, before, after);
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/link/DefaultLinkDescriptionTest.java b/core/api/src/test/java/org/onlab/onos/net/link/DefaultLinkDescriptionTest.java
new file mode 100644
index 0000000..2c69625
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/link/DefaultLinkDescriptionTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.link;
+
+import org.junit.Test;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.PortNumber;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.DefaultLinkTest.cp;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Link.Type.DIRECT;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Test of the default link description.
+ */
+public class DefaultLinkDescriptionTest {
+
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final DeviceId DID2 = deviceId("of:bar");
+    private static final PortNumber P1 = portNumber(1);
+
+    @Test
+    public void basics() {
+        LinkDescription desc = new DefaultLinkDescription(cp(DID1, P1), cp(DID2, P1), DIRECT);
+        assertEquals("incorrect src", cp(DID1, P1), desc.src());
+        assertEquals("incorrect dst", cp(DID2, P1), desc.dst());
+        assertEquals("incorrect type", DIRECT, desc.type());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/link/LinkEventTest.java b/core/api/src/test/java/org/onlab/onos/net/link/LinkEventTest.java
new file mode 100644
index 0000000..dae9f85
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/link/LinkEventTest.java
@@ -0,0 +1,41 @@
+package org.onlab.onos.net.link;
+
+import org.junit.Test;
+import org.onlab.onos.event.AbstractEventTest;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultLink;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.provider.ProviderId;
+
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+
+/**
+ * Tests of the device event.
+ */
+public class LinkEventTest extends AbstractEventTest {
+
+    private Link createLink() {
+        return new DefaultLink(new ProviderId("foo"),
+                               new ConnectPoint(deviceId("of:foo"), portNumber(1)),
+                               new ConnectPoint(deviceId("of:bar"), portNumber(2)),
+                               Link.Type.INDIRECT);
+    }
+
+    @Test
+    public void withTime() {
+        Link link = createLink();
+        LinkEvent event = new LinkEvent(LinkEvent.Type.LINK_ADDED, link, 123L);
+        validateEvent(event, LinkEvent.Type.LINK_ADDED, link, 123L);
+    }
+
+    @Test
+    public void withoutTime() {
+        Link link = createLink();
+        long before = System.currentTimeMillis();
+        LinkEvent event = new LinkEvent(LinkEvent.Type.LINK_ADDED, link);
+        long after = System.currentTimeMillis();
+        validateEvent(event, LinkEvent.Type.LINK_ADDED, link, before, after);
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderRegistryTest.java b/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderRegistryTest.java
new file mode 100644
index 0000000..abbe5be
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderRegistryTest.java
@@ -0,0 +1,74 @@
+package org.onlab.onos.net.provider;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the base provider registry.
+ */
+public class AbstractProviderRegistryTest {
+
+    private class TestProviderService extends AbstractProviderService<TestProvider> {
+        protected TestProviderService(TestProvider provider) {
+            super(provider);
+        }
+    }
+
+    private class TestProviderRegistry extends AbstractProviderRegistry<TestProvider, TestProviderService> {
+        @Override
+        protected TestProviderService createProviderService(TestProvider provider) {
+            return new TestProviderService(provider);
+        }
+    }
+
+    @Test
+    public void basics() {
+        TestProviderRegistry registry = new TestProviderRegistry();
+        assertEquals("incorrect provider count", 0, registry.getProviders().size());
+
+        ProviderId fooId = new ProviderId("foo");
+        TestProvider pFoo = new TestProvider(fooId);
+        TestProviderService psFoo = registry.register(pFoo);
+        assertEquals("incorrect provider count", 1, registry.getProviders().size());
+        assertThat("provider not found", registry.getProviders().contains(fooId));
+        assertEquals("incorrect provider", psFoo.provider(), pFoo);
+
+        ProviderId barId = new ProviderId("bar");
+        TestProvider pBar = new TestProvider(barId);
+        TestProviderService psBar = registry.register(pBar);
+        assertEquals("incorrect provider count", 2, registry.getProviders().size());
+        assertThat("provider not found", registry.getProviders().contains(barId));
+        assertEquals("incorrect provider", psBar.provider(), pBar);
+
+        psFoo.checkValidity();
+        registry.unregister(pFoo);
+        psBar.checkValidity();
+        assertEquals("incorrect provider count", 1, registry.getProviders().size());
+        assertThat("provider not found", registry.getProviders().contains(barId));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void duplicateRegistration() {
+        TestProviderRegistry registry = new TestProviderRegistry();
+        TestProvider pFoo = new TestProvider(new ProviderId("foo"));
+        registry.register(pFoo);
+        registry.register(pFoo);
+    }
+
+    @Test
+    public void voidUnregistration() {
+        TestProviderRegistry registry = new TestProviderRegistry();
+        registry.unregister(new TestProvider(new ProviderId("foo")));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void unregistration() {
+        TestProviderRegistry registry = new TestProviderRegistry();
+        TestProvider pFoo = new TestProvider(new ProviderId("foo"));
+        TestProviderService psFoo = registry.register(pFoo);
+        registry.unregister(pFoo);
+        psFoo.checkValidity();
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderTest.java b/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderTest.java
new file mode 100644
index 0000000..745aebc
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/provider/AbstractProviderTest.java
@@ -0,0 +1,18 @@
+package org.onlab.onos.net.provider;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the base provider implementation.
+ */
+public class AbstractProviderTest {
+
+    @Test
+    public void basics() {
+        ProviderId id = new ProviderId("foo.bar");
+        TestProvider provider = new TestProvider(id);
+        assertEquals("incorrect id", id, provider.id());
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/provider/ProviderIdTest.java b/core/api/src/test/java/org/onlab/onos/net/provider/ProviderIdTest.java
new file mode 100644
index 0000000..1c05507
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/provider/ProviderIdTest.java
@@ -0,0 +1,19 @@
+package org.onlab.onos.net.provider;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+/**
+ * Test of the provider identifier.
+ */
+public class ProviderIdTest {
+
+    @Test
+    public void basics() {
+        new EqualsTester()
+                .addEqualityGroup(new ProviderId("foo"), new ProviderId("foo"))
+                .addEqualityGroup(new ProviderId("bar"))
+                .testEquals();
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/provider/TestProvider.java b/core/api/src/test/java/org/onlab/onos/net/provider/TestProvider.java
new file mode 100644
index 0000000..785577a
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/provider/TestProvider.java
@@ -0,0 +1,17 @@
+package org.onlab.onos.net.provider;
+
+/**
+ * Test provider fixture.
+ */
+public class TestProvider extends AbstractProvider {
+
+    /**
+     * Creates a provider with the supplier identifier.
+     *
+     * @param id provider id
+     */
+    protected TestProvider(ProviderId id) {
+        super(id);
+    }
+
+}
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..13deec4
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,45 @@
+<?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</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-core</artifactId>
+    <packaging>pom</packaging>
+
+    <description>ONOS Core root project</description>
+
+    <modules>
+        <module>api</module>
+        <module>trivial</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-misc</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
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);
+    }
+
+}