Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java b/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
index 001518e..0c0f375 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.net;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -71,9 +72,33 @@
         return new DefaultAnnotations(merged);
     }
 
+    /**
+     * Convert Annotations to DefaultAnnotations if needed and merges.
+     *
+     * @see #merge(DefaultAnnotations, SparseAnnotations)
+     *
+     * @param annotations       base annotations
+     * @param sparseAnnotations additional sparse annotations
+     * @return combined annotations or the original base annotations if there
+     * are not additional annotations
+     */
+    public static DefaultAnnotations merge(Annotations annotations,
+                                    SparseAnnotations sparseAnnotations) {
+        if (annotations instanceof DefaultAnnotations) {
+            return merge((DefaultAnnotations) annotations, sparseAnnotations);
+        }
+
+        DefaultAnnotations.Builder builder = DefaultAnnotations.builder();
+        for (String key : annotations.keys()) {
+            builder.set(key, annotations.value(key));
+        }
+        return merge(builder.build(), sparseAnnotations);
+    }
+
     @Override
     public Set<String> keys() {
-        return map.keySet();
+        // TODO: unmodifiable to be removed after switching to ImmutableMap;
+        return Collections.unmodifiableSet(map.keySet());
     }
 
     @Override
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
index fd04e5c..788d23a 100644
--- 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
@@ -45,6 +45,18 @@
         this.serialNumber = serialNumber;
     }
 
+    /**
+     * Creates a device description using the supplied information.
+     * @param base DeviceDescription to basic information
+     * @param annotations Annotations to use.
+     */
+    public DefaultDeviceDescription(DeviceDescription base,
+                                    SparseAnnotations... annotations) {
+        this(base.deviceURI(), base.type(), base.manufacturer(),
+             base.hwVersion(), base.swVersion(), base.serialNumber(),
+             annotations);
+    }
+
     @Override
     public URI deviceURI() {
         return uri;
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
index 1d52ac9..eb75ede 100644
--- 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
@@ -1,20 +1,43 @@
 package org.onlab.onos.net.device;
 
+import org.onlab.onos.net.AbstractDescription;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
 
 /**
  * Default implementation of immutable port description.
  */
-public class DefaultPortDescription implements PortDescription {
+public class DefaultPortDescription extends AbstractDescription
+        implements PortDescription {
 
     private final PortNumber number;
     private final boolean isEnabled;
 
-    public DefaultPortDescription(PortNumber number, boolean isEnabled) {
+    /**
+     * Creates a port description using the supplied information.
+     *
+     * @param number       port number
+     * @param isEnabled    port enabled state
+     * @param annotations  optional key/value annotations map
+     */
+    public DefaultPortDescription(PortNumber number, boolean isEnabled,
+                SparseAnnotations... annotations) {
+        super(annotations);
         this.number = number;
         this.isEnabled = isEnabled;
     }
 
+    /**
+     * Creates a port description using the supplied information.
+     *
+     * @param base         PortDescription to get basic information from
+     * @param annotations  optional key/value annotations map
+     */
+    public DefaultPortDescription(PortDescription base,
+            SparseAnnotations annotations) {
+        this(base.portNumber(), base.isEnabled(), annotations);
+    }
+
     @Override
     public PortNumber portNumber() {
         return number;
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
index f0dc8ee..f01b49c 100644
--- 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
@@ -1,11 +1,12 @@
 package org.onlab.onos.net.device;
 
+import org.onlab.onos.net.Description;
 import org.onlab.onos.net.PortNumber;
 
 /**
  * Information about a port.
  */
-public interface PortDescription {
+public interface PortDescription extends Description {
 
     // TODO: possibly relocate this to a common ground so that this can also used by host tracking if required
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java
new file mode 100644
index 0000000..eefe750
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java
@@ -0,0 +1,49 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Base intent implementation.
+ */
+public abstract class AbstractIntent implements Intent {
+
+    private final IntentId id;
+
+    /**
+     * Creates a base intent with the specified identifier.
+     *
+     * @param id intent identifier
+     */
+    protected AbstractIntent(IntentId id) {
+        this.id = id;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected AbstractIntent() {
+        this.id = null;
+    }
+
+    @Override
+    public IntentId getId() {
+        return id;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        AbstractIntent that = (AbstractIntent) o;
+        return id.equals(that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
new file mode 100644
index 0000000..ad34a2c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
@@ -0,0 +1,99 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A list of BatchOperationEntry.
+ *
+ * @param <T> the enum of operators <br>
+ *        This enum must be defined in each sub-classes.
+ *
+ */
+public abstract class BatchOperation<T extends BatchOperationEntry<?, ?>> {
+    private List<T> ops;
+
+    /**
+     * Creates new {@link BatchOperation} object.
+     */
+    public BatchOperation() {
+        ops = new LinkedList<>();
+    }
+
+    /**
+     * Creates {@link BatchOperation} object from a list of batch operation
+     * entries.
+     *
+     * @param batchOperations the list of batch operation entries.
+     */
+    public BatchOperation(List<T> batchOperations) {
+        ops = new LinkedList<>(checkNotNull(batchOperations));
+    }
+
+    /**
+     * Removes all operations maintained in this object.
+     */
+    public void clear() {
+        ops.clear();
+    }
+
+    /**
+     * Returns the number of operations in this object.
+     *
+     * @return the number of operations in this object
+     */
+    public int size() {
+        return ops.size();
+    }
+
+    /**
+     * Returns the operations in this object.
+     *
+     * @return the operations in this object
+     */
+    public List<T> getOperations() {
+        return Collections.unmodifiableList(ops);
+    }
+
+    /**
+     * Adds an operation.
+     *
+     * @param entry the operation to be added
+     * @return this object if succeeded, null otherwise
+     */
+    public BatchOperation<T> addOperation(T entry) {
+        return ops.add(entry) ? this : null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null) {
+            return false;
+        }
+
+        if (getClass() != o.getClass()) {
+            return false;
+        }
+        BatchOperation<?> other = (BatchOperation<?>) o;
+
+        return this.ops.equals(other.ops);
+    }
+
+    @Override
+    public int hashCode() {
+        return ops.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return ops.toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
new file mode 100644
index 0000000..b5dfa88
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
@@ -0,0 +1,82 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A super class for batch operation entry classes.
+ * <p>
+ * This is the interface to classes which are maintained by BatchOperation as
+ * its entries.
+ */
+public class BatchOperationEntry<T extends Enum<?>, U extends BatchOperationTarget> {
+    private final T operator;
+    private final U target;
+
+    /**
+     * Default constructor for serializer.
+     */
+    @Deprecated
+    protected BatchOperationEntry() {
+        this.operator = null;
+        this.target = null;
+    }
+
+    /**
+     * Constructs new instance for the entry of the BatchOperation.
+     *
+     * @param operator the operator of this operation
+     * @param target the target object of this operation
+     */
+    public BatchOperationEntry(T operator, U target) {
+        this.operator = operator;
+        this.target = target;
+    }
+
+    /**
+     * Gets the target object of this operation.
+     *
+     * @return the target object of this operation
+     */
+    public U getTarget() {
+        return target;
+    }
+
+    /**
+     * Gets the operator of this operation.
+     *
+     * @return the operator of this operation
+     */
+    public T getOperator() {
+        return operator;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        BatchOperationEntry<?, ?> other = (BatchOperationEntry<?, ?>) o;
+        return (this.operator == other.operator) &&
+            Objects.equals(this.target, other.target);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(operator, target);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("operator", operator)
+            .add("target", target)
+            .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
new file mode 100644
index 0000000..c678f31
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
@@ -0,0 +1,9 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An interface of the class which is assigned to BatchOperation.
+ */
+public interface BatchOperationTarget {
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
new file mode 100644
index 0000000..629a9d1
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
@@ -0,0 +1,84 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.Objects;
+
+/**
+ * Abstraction of connectivity intent for traffic matching some criteria.
+ */
+public abstract class ConnectivityIntent extends AbstractIntent {
+
+    // TODO: other forms of intents should be considered for this family:
+    //   point-to-point with constraints (waypoints/obstacles)
+    //   multi-to-single point with constraints (waypoints/obstacles)
+    //   single-to-multi point with constraints (waypoints/obstacles)
+    //   concrete path (with alternate)
+    //   ...
+
+    private final TrafficSelector selector;
+    // TODO: should consider which is better for multiple actions,
+    // defining compound action class or using list of actions.
+    private final TrafficTreatment treatment;
+
+    /**
+     * Creates a connectivity intent that matches on the specified intent
+     * and applies the specified action.
+     *
+     * @param id    intent identifier
+     * @param match traffic match
+     * @param action action
+     * @throws NullPointerException if the match or action is null
+     */
+    protected ConnectivityIntent(IntentId id, TrafficSelector match, TrafficTreatment action) {
+        super(id);
+        this.selector = checkNotNull(match);
+        this.treatment = checkNotNull(action);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected ConnectivityIntent() {
+        super();
+        this.selector = null;
+        this.treatment = null;
+    }
+
+    /**
+     * Returns the match specifying the type of traffic.
+     *
+     * @return traffic match
+     */
+    public TrafficSelector getTrafficSelector() {
+        return selector;
+    }
+
+    /**
+     * Returns the action applied to the traffic.
+     *
+     * @return applied action
+     */
+    public TrafficTreatment getTrafficTreatment() {
+        return treatment;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!super.equals(o)) {
+            return false;
+        }
+        ConnectivityIntent that = (ConnectivityIntent) o;
+        return Objects.equal(this.selector, that.selector)
+                && Objects.equal(this.treatment, that.treatment);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), selector, treatment);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IdGenerator.java b/core/api/src/main/java/org/onlab/onos/net/intent/IdGenerator.java
new file mode 100644
index 0000000..0bba622
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IdGenerator.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * A generalized interface for ID generation
+ *
+ * {@link #getNewId()} generates a globally unique ID instance on
+ * each invocation.
+ *
+ * @param <T> the type of ID
+ */
+// TODO: do we need to define a base marker interface for ID,
+// then changed the type parameter to <T extends BaseId> something
+// like that?
+public interface IdGenerator<T> {
+    /**
+     * Returns a globally unique ID instance.
+     *
+     * @return globally unique ID instance
+     */
+    T getNewId();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java
new file mode 100644
index 0000000..66bc759
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of an intent that can be installed into
+ * the underlying system without additional compilation.
+ */
+public interface InstallableIntent extends Intent {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
new file mode 100644
index 0000000..b239ede
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of an application level intent.
+ *
+ * Make sure that an Intent should be immutable when a new type is defined.
+ */
+public interface Intent extends BatchOperationTarget {
+    /**
+     * Returns the intent identifier.
+     *
+     * @return intent identifier
+     */
+    IntentId getId();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
new file mode 100644
index 0000000..b450d71
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
@@ -0,0 +1,39 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * A list of intent operations.
+ */
+public class IntentBatchOperation extends
+        BatchOperation<BatchOperationEntry<IntentBatchOperation.Operator, ?>> {
+    /**
+     * The intent operators.
+     */
+    public enum Operator {
+        ADD,
+        REMOVE,
+    }
+
+    /**
+     * Adds an add-intent operation.
+     *
+     * @param intent the intent to be added
+     * @return the IntentBatchOperation object if succeeded, null otherwise
+     */
+    public IntentBatchOperation addAddIntentOperation(Intent intent) {
+        return (null == super.addOperation(
+                new BatchOperationEntry<Operator, Intent>(Operator.ADD, intent)))
+                ? null : this;
+    }
+
+    /**
+     * Adds a remove-intent operation.
+     *
+     * @param id the ID of intent to be removed
+     * @return the IntentBatchOperation object if succeeded, null otherwise
+     */
+    public IntentBatchOperation addRemoveIntentOperation(IntentId id) {
+        return (null == super.addOperation(
+                new BatchOperationEntry<Operator, IntentId>(Operator.REMOVE, id)))
+                ? null : this;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
new file mode 100644
index 0000000..dbc3cc4
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
@@ -0,0 +1,20 @@
+package org.onlab.onos.net.intent;
+
+import java.util.List;
+
+/**
+ * Abstraction of a compiler which is capable of taking an intent
+ * and translating it to other, potentially installable, intents.
+ *
+ * @param <T> the type of intent
+ */
+public interface IntentCompiler<T extends Intent> {
+    /**
+     * Compiles the specified intent into other intents.
+     *
+     * @param intent intent to be compiled
+     * @return list of resulting intents
+     * @throws IntentException if issues are encountered while compiling the intent
+     */
+    List<Intent> compile(T intent);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
new file mode 100644
index 0000000..27ae834
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
@@ -0,0 +1,113 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A class to represent an intent related event.
+ */
+public class IntentEvent {
+
+    // TODO: determine a suitable parent class; if one does not exist, consider introducing one
+
+    private final long time;
+    private final Intent intent;
+    private final IntentState state;
+    private final IntentState previous;
+
+    /**
+     * Creates an event describing a state change of an intent.
+     *
+     * @param intent subject intent
+     * @param state new intent state
+     * @param previous previous intent state
+     * @param time time the event created in milliseconds since start of epoch
+     * @throws NullPointerException if the intent or state is null
+     */
+    public IntentEvent(Intent intent, IntentState state, IntentState previous, long time) {
+        this.intent = checkNotNull(intent);
+        this.state = checkNotNull(state);
+        this.previous = previous;
+        this.time = time;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected IntentEvent() {
+        this.intent = null;
+        this.state = null;
+        this.previous = null;
+        this.time = 0;
+    }
+
+    /**
+     * Returns the state of the intent which caused the event.
+     *
+     * @return the state of the intent
+     */
+    public IntentState getState() {
+        return state;
+    }
+
+    /**
+     * Returns the previous state of the intent which caused the event.
+     *
+     * @return the previous state of the intent
+     */
+    public IntentState getPreviousState() {
+        return previous;
+    }
+
+    /**
+     * Returns the intent associated with the event.
+     *
+     * @return the intent
+     */
+    public Intent getIntent() {
+        return intent;
+    }
+
+    /**
+     * Returns the time at which the event was created.
+     *
+     * @return the time in milliseconds since start of epoch
+     */
+    public long getTime() {
+        return time;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        IntentEvent that = (IntentEvent) o;
+        return Objects.equals(this.intent, that.intent)
+                && Objects.equals(this.state, that.state)
+                && Objects.equals(this.previous, that.previous)
+                && Objects.equals(this.time, that.time);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(intent, state, previous, time);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("intent", intent)
+                .add("state", state)
+                .add("previous", previous)
+                .add("time", time)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java
new file mode 100644
index 0000000..f59ecfc
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java
@@ -0,0 +1,13 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Listener for {@link IntentEvent intent events}.
+ */
+public interface IntentEventListener {
+    /**
+     * Processes the specified intent event.
+     *
+     * @param event the event to process
+     */
+    void event(IntentEvent event);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
new file mode 100644
index 0000000..4148dea
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Represents an intent related error.
+ */
+public class IntentException extends RuntimeException {
+
+    private static final long serialVersionUID = 1907263634145241319L;
+
+    /**
+     * Constructs an exception with no message and no underlying cause.
+     */
+    public IntentException() {
+    }
+
+    /**
+     * Constructs an exception with the specified message.
+     *
+     * @param message the message describing the specific nature of the error
+     */
+    public IntentException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs an exception with the specified message and the underlying cause.
+     *
+     * @param message the message describing the specific nature of the error
+     * @param cause the underlying cause of this error
+     */
+    public IntentException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
new file mode 100644
index 0000000..c6338a7f
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Map;
+
+/**
+ * Service for extending the capability of intent framework by
+ * adding additional compilers or/and installers.
+ */
+public interface IntentExtensionService {
+    /**
+     * Registers the specified compiler for the given intent class.
+     *
+     * @param cls intent class
+     * @param compiler intent compiler
+     * @param <T> the type of intent
+     */
+    <T extends Intent> void registerCompiler(Class<T> cls, IntentCompiler<T> compiler);
+
+    /**
+     * Unregisters the compiler for the specified intent class.
+     *
+     * @param cls intent class
+     * @param <T> the type of intent
+     */
+    <T extends Intent> void unregisterCompiler(Class<T> cls);
+
+    /**
+     * Returns immutable set of bindings of currently registered intent compilers.
+     *
+     * @return the set of compiler bindings
+     */
+    Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> getCompilers();
+
+    /**
+     * Registers the specified installer for the given installable intent class.
+     *
+     * @param cls installable intent class
+     * @param installer intent installer
+     * @param <T> the type of installable intent
+     */
+    <T extends InstallableIntent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer);
+
+    /**
+     * Unregisters the installer for the given installable intent class.
+     *
+     * @param cls installable intent class
+     * @param <T> the type of installable intent
+     */
+    <T extends InstallableIntent> void unregisterInstaller(Class<T> cls);
+
+    /**
+     * Returns immutable set of bindings of currently registered intent installers.
+     *
+     * @return the set of installer bindings
+     */
+    Map<Class<? extends InstallableIntent>, IntentInstaller<? extends InstallableIntent>> getInstallers();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
new file mode 100644
index 0000000..798e00c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
@@ -0,0 +1,68 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Intent identifier suitable as an external key.
+ *
+ * This class is immutable.
+ */
+public final class IntentId implements BatchOperationTarget {
+
+    private static final int DEC = 10;
+    private static final int HEX = 16;
+
+    private final long id;
+
+    /**
+     * Creates an intent identifier from the specified string representation.
+     *
+     * @param value long value
+     * @return intent identifier
+     */
+    public static IntentId valueOf(String value) {
+        long id = value.toLowerCase().startsWith("0x")
+                ? Long.parseLong(value.substring(2), HEX)
+                : Long.parseLong(value, DEC);
+        return new IntentId(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected IntentId() {
+        this.id = 0;
+    }
+
+    /**
+     * Constructs the ID corresponding to a given long value.
+     *
+     * @param id the underlying value of this ID
+     */
+    public IntentId(long id) {
+        this.id = id;
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (id ^ (id >>> 32));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof IntentId)) {
+            return false;
+        }
+
+        IntentId that = (IntentId) obj;
+        return this.id == that.id;
+    }
+
+    @Override
+    public String toString() {
+        return "0x" + Long.toHexString(id);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
new file mode 100644
index 0000000..738be04
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of entity capable of installing intents to the environment.
+ */
+public interface IntentInstaller<T extends InstallableIntent> {
+    /**
+     * Installs the specified intent to the environment.
+     *
+     * @param intent intent to be installed
+     * @throws IntentException if issues are encountered while installing the intent
+     */
+    void install(T intent);
+
+    /**
+     * Uninstalls the specified intent from the environment.
+     *
+     * @param intent intent to be uninstalled
+     * @throws IntentException if issues are encountered while uninstalling the intent
+     */
+    void uninstall(T intent);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
new file mode 100644
index 0000000..470d98b
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of a batch of intent submit/withdraw operations.
+ */
+public interface IntentOperations {
+
+    // TODO: elaborate once the revised BatchOperation scheme is in place
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
new file mode 100644
index 0000000..2b4fb59
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
@@ -0,0 +1,76 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Set;
+
+/**
+ * Service for application submitting or withdrawing their intents.
+ */
+public interface IntentService {
+    /**
+     * Submits an intent into the system.
+     *
+     * This is an asynchronous request meaning that any compiling
+     * or installation activities may be done at later time.
+     *
+     * @param intent intent to be submitted
+     */
+    void submit(Intent intent);
+
+    /**
+     * Withdraws an intent from the system.
+     *
+     * This is an asynchronous request meaning that the environment
+     * may be affected at later time.
+     *
+     * @param intent intent to be withdrawn
+     */
+    void withdraw(Intent intent);
+
+    /**
+     * Submits a batch of submit &amp; withdraw operations. Such a batch is
+     * assumed to be processed together.
+     *
+     * This is an asynchronous request meaning that the environment
+     * may be affected at later time.
+     *
+     * @param operations batch of intent operations
+     */
+    void execute(IntentOperations operations);
+
+    /**
+     * Returns immutable set of intents currently in the system.
+     *
+     * @return set of intents
+     */
+    Set<Intent> getIntents();
+
+    /**
+     * Retrieves the intent specified by its identifier.
+     *
+     * @param id intent identifier
+     * @return the intent or null if one with the given identifier is not found
+     */
+    Intent getIntent(IntentId id);
+
+    /**
+     * Retrieves the state of an intent by its identifier.
+     *
+     * @param id intent identifier
+     * @return the intent state or null if one with the given identifier is not found
+     */
+    IntentState getIntentState(IntentId id);
+
+    /**
+     * Adds the specified listener for intent events.
+     *
+     * @param listener listener to be added
+     */
+    void addListener(IntentEventListener listener);
+
+    /**
+     * Removes the specified listener for intent events.
+     *
+     * @param listener listener to be removed
+     */
+    void removeListener(IntentEventListener listener);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
new file mode 100644
index 0000000..20476e5
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
@@ -0,0 +1,55 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * This class represents the states of an intent.
+ *
+ * <p>
+ * Note: The state is expressed as enum, but there is possibility
+ * in the future that we define specific class instead of enum to improve
+ * the extensibility of state definition.
+ * </p>
+ */
+public enum IntentState {
+    // FIXME: requires discussion on State vs. EventType and a solid state-transition diagram
+    // TODO: consider the impact of conflict detection
+    // TODO: consider the impact that external events affect an installed intent
+    /**
+     * The beginning state.
+     *
+     * All intent in the runtime take this state first.
+     */
+    SUBMITTED,
+
+    /**
+     * The intent compilation has been completed.
+     *
+     * An intent translation graph (tree) is completely created.
+     * Leaves of the graph are installable intent type.
+     */
+    COMPILED,
+
+    /**
+     * The intent has been successfully installed.
+     */
+    INSTALLED,
+
+    /**
+     * The intent is being withdrawn.
+     *
+     * When {@link IntentService#withdraw(Intent)} is called,
+     * the intent takes this state first.
+     */
+    WITHDRAWING,
+
+    /**
+     * The intent has been successfully withdrawn.
+     */
+    WITHDRAWN,
+
+    /**
+     * The intent has failed to be compiled, installed, or withdrawn.
+     *
+     * When the intent failed to be withdrawn, it is still, at least partially installed.
+     */
+    FAILED,
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
new file mode 100644
index 0000000..1e421ab
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
@@ -0,0 +1,110 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+
+/**
+ * Abstraction of multiple source to single destination connectivity intent.
+ */
+public class MultiPointToSinglePointIntent extends ConnectivityIntent {
+
+    private final Set<ConnectPoint> ingressPorts;
+    private final ConnectPoint egressPort;
+
+    /**
+     * Creates a new multi-to-single point connectivity intent for the specified
+     * traffic match and action.
+     *
+     * @param id           intent identifier
+     * @param match        traffic match
+     * @param action       action
+     * @param ingressPorts set of ports from which ingress traffic originates
+     * @param egressPort   port to which traffic will egress
+     * @throws NullPointerException if {@code ingressPorts} or
+     * {@code egressPort} is null.
+     * @throws IllegalArgumentException if the size of {@code ingressPorts} is
+     * not more than 1
+     */
+    public MultiPointToSinglePointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+            Set<ConnectPoint> ingressPorts, ConnectPoint egressPort) {
+        super(id, match, action);
+
+        checkNotNull(ingressPorts);
+        checkArgument(!ingressPorts.isEmpty(),
+                "there should be at least one ingress port");
+
+        this.ingressPorts = Sets.newHashSet(ingressPorts);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected MultiPointToSinglePointIntent() {
+        super();
+        this.ingressPorts = null;
+        this.egressPort = null;
+    }
+
+    /**
+     * Returns the set of ports on which ingress traffic should be connected to
+     * the egress port.
+     *
+     * @return set of ingress ports
+     */
+    public Set<ConnectPoint> getIngressPorts() {
+        return ingressPorts;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public ConnectPoint getEgressPort() {
+        return egressPort;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        MultiPointToSinglePointIntent that = (MultiPointToSinglePointIntent) o;
+        return Objects.equals(this.ingressPorts, that.ingressPorts)
+                && Objects.equals(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPorts, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPorts", getIngressPorts())
+                .add("egressPort", getEgressPort())
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java
new file mode 100644
index 0000000..d11dc7c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java
@@ -0,0 +1,58 @@
+package org.onlab.onos.net.intent;
+
+import org.onlab.onos.net.ConnectPoint;
+
+// TODO: consider if this intent should be sub-class of ConnectivityIntent
+/**
+ * An optical layer Intent for a connectivity from a transponder port to another
+ * transponder port.
+ * <p>
+ * This class doesn't accepts lambda specifier. This class computes path between
+ * ports and assign lambda automatically. The lambda can be specified using
+ * OpticalPathFlow class.
+ */
+public class OpticalConnectivityIntent extends AbstractIntent {
+    protected ConnectPoint srcConnectPoint;
+    protected ConnectPoint dstConnectPoint;
+
+    /**
+     * Constructor.
+     *
+     * @param id ID for this new Intent object.
+     * @param srcConnectPoint The source transponder port.
+     * @param dstConnectPoint The destination transponder port.
+     */
+    public OpticalConnectivityIntent(IntentId id,
+            ConnectPoint srcConnectPoint, ConnectPoint dstConnectPoint) {
+        super(id);
+        this.srcConnectPoint = srcConnectPoint;
+        this.dstConnectPoint = dstConnectPoint;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected OpticalConnectivityIntent() {
+        super();
+        this.srcConnectPoint = null;
+        this.dstConnectPoint = null;
+    }
+
+    /**
+     * Gets source transponder port.
+     *
+     * @return The source transponder port.
+     */
+    public ConnectPoint getSrcConnectPoint() {
+        return srcConnectPoint;
+    }
+
+    /**
+     * Gets destination transponder port.
+     *
+     * @return The source transponder port.
+     */
+    public ConnectPoint getDstConnectPoint() {
+        return dstConnectPoint;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java
new file mode 100644
index 0000000..893d70e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java
@@ -0,0 +1,177 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+
+// TODO: consider if this intent should be sub-class of Connectivity intent
+/**
+ * A packet layer Intent for a connectivity from a set of ports to a set of
+ * ports.
+ * <p>
+ * TODO: Design methods to support the ReactiveForwarding and the SDN-IP. <br>
+ * NOTE: Should this class support modifier methods? Should this object a
+ * read-only object?
+ */
+public class PacketConnectivityIntent extends AbstractIntent {
+    protected Set<ConnectPoint> srcConnectPoints;
+    protected TrafficSelector selector;
+    protected Set<ConnectPoint> dstConnectPoints;
+    protected boolean canSetupOpticalFlow;
+    protected int idleTimeoutValue;
+    protected int hardTimeoutValue;
+
+    /**
+     * Creates a connectivity intent for the packet layer.
+     * <p>
+     * When the "canSetupOpticalFlow" option is true, this intent will compute
+     * the packet/optical converged path, decompose it to the OpticalPathFlow
+     * and the PacketPathFlow objects, and execute the operations to add them
+     * considering the dependency between the packet and optical layers.
+     *
+     * @param id ID for this new Intent object.
+     * @param srcConnectPoints The set of source switch ports.
+     * @param match Traffic specifier for this object.
+     * @param dstConnectPoints The set of destination switch ports.
+     * @param canSetupOpticalFlow The flag whether this intent can create
+     *        optical flows if needed.
+     */
+    public PacketConnectivityIntent(IntentId id,
+            Collection<ConnectPoint> srcConnectPoints, TrafficSelector match,
+            Collection<ConnectPoint> dstConnectPoints, boolean canSetupOpticalFlow) {
+        super(id);
+        this.srcConnectPoints = new HashSet<ConnectPoint>(srcConnectPoints);
+        this.selector = match;
+        this.dstConnectPoints = new HashSet<ConnectPoint>(dstConnectPoints);
+        this.canSetupOpticalFlow = canSetupOpticalFlow;
+        this.idleTimeoutValue = 0;
+        this.hardTimeoutValue = 0;
+
+        // TODO: check consistency between these parameters.
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected PacketConnectivityIntent() {
+        super();
+        this.srcConnectPoints = null;
+        this.selector = null;
+        this.dstConnectPoints = null;
+        this.canSetupOpticalFlow = false;
+        this.idleTimeoutValue = 0;
+        this.hardTimeoutValue = 0;
+    }
+
+    /**
+     * Gets the set of source switch ports.
+     *
+     * @return the set of source switch ports.
+     */
+    public Collection<ConnectPoint> getSrcConnectPoints() {
+        return Collections.unmodifiableCollection(srcConnectPoints);
+    }
+
+    /**
+     * Gets the traffic specifier.
+     *
+     * @return The traffic specifier.
+     */
+    public TrafficSelector getMatch() {
+        return selector;
+    }
+
+    /**
+     * Gets the set of destination switch ports.
+     *
+     * @return the set of destination switch ports.
+     */
+    public Collection<ConnectPoint> getDstConnectPoints() {
+        return Collections.unmodifiableCollection(dstConnectPoints);
+    }
+
+    /**
+     * Adds the specified port to the set of source ports.
+     *
+     * @param port ConnectPoint object to be added
+     */
+    public void addSrcConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Adds the specified port to the set of destination ports.
+     *
+     * @param port ConnectPoint object to be added
+     */
+    public void addDstConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Removes the specified port from the set of source ports.
+     *
+     * @param port ConnectPoint object to be removed
+     */
+    public void removeSrcConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Removes the specified port from the set of destination ports.
+     *
+     * @param port ConnectPoint object to be removed
+     */
+    public void removeDstConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Sets idle-timeout value.
+     *
+     * @param timeout Idle-timeout value (seconds)
+     */
+    public void setIdleTimeout(int timeout) {
+        idleTimeoutValue = timeout;
+    }
+
+    /**
+     * Sets hard-timeout value.
+     *
+     * @param timeout Hard-timeout value (seconds)
+     */
+    public void setHardTimeout(int timeout) {
+        hardTimeoutValue = timeout;
+    }
+
+    /**
+     * Gets idle-timeout value.
+     *
+     * @return Idle-timeout value (seconds)
+     */
+    public int getIdleTimeout() {
+        return idleTimeoutValue;
+    }
+
+    /**
+     * Gets hard-timeout value.
+     *
+     * @return Hard-timeout value (seconds)
+     */
+    public int getHardTimeout() {
+        return hardTimeoutValue;
+    }
+
+    /**
+     * Returns whether this intent can create optical flows if needed.
+     *
+     * @return whether this intent can create optical flows.
+     */
+    public boolean canSetupOpticalFlow() {
+        return canSetupOpticalFlow;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
new file mode 100644
index 0000000..39ad011
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
@@ -0,0 +1,89 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Objects;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Abstraction of explicitly path specified connectivity intent.
+ */
+public class PathIntent extends PointToPointIntent {
+
+    private final Path path;
+
+    /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports and using the specified explicit path.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort ingress port
+     * @param egressPort  egress port
+     * @param path        traversed links
+     * @throws NullPointerException {@code path} is null
+     */
+    public PathIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                      ConnectPoint ingressPort, ConnectPoint egressPort,
+                      Path path) {
+        super(id, match, action, ingressPort, egressPort);
+        this.path = path;
+    }
+
+    protected PathIntent() {
+        super();
+        this.path = null;
+    }
+
+    /**
+     * Returns the links which the traffic goes along.
+     *
+     * @return traversed links
+     */
+    public Path getPath() {
+        return path;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        PathIntent that = (PathIntent) o;
+
+        if (!path.equals(that.path)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), path);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", getIngressPort())
+                .add("egressPort", getEgressPort())
+                .add("path", path)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
new file mode 100644
index 0000000..b1d18ee
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
@@ -0,0 +1,100 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Abstraction of point-to-point connectivity.
+ */
+public class PointToPointIntent extends ConnectivityIntent {
+
+    private final ConnectPoint ingressPort;
+    private final ConnectPoint egressPort;
+
+    /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort ingress port
+     * @param egressPort  egress port
+     * @throws NullPointerException if {@code ingressPort} or {@code egressPort} is null.
+     */
+    public PointToPointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                              ConnectPoint ingressPort, ConnectPoint egressPort) {
+        super(id, match, action);
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected PointToPointIntent() {
+        super();
+        this.ingressPort = null;
+        this.egressPort = null;
+    }
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to
+     * the egress.
+     *
+     * @return ingress port
+     */
+    public ConnectPoint getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public ConnectPoint getEgressPort() {
+        return egressPort;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        PointToPointIntent that = (PointToPointIntent) o;
+        return Objects.equals(this.ingressPort, that.ingressPort)
+                && Objects.equals(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPort, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPort)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
new file mode 100644
index 0000000..e69a740
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
@@ -0,0 +1,110 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+
+/**
+ * Abstraction of single source, multiple destination connectivity intent.
+ */
+public class SinglePointToMultiPointIntent extends ConnectivityIntent {
+
+    private final ConnectPoint ingressPort;
+    private final Set<ConnectPoint> egressPorts;
+
+    /**
+     * Creates a new single-to-multi point connectivity intent.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort port on which traffic will ingress
+     * @param egressPorts set of ports on which traffic will egress
+     * @throws NullPointerException if {@code ingressPort} or
+     * {@code egressPorts} is null
+     * @throws IllegalArgumentException if the size of {@code egressPorts} is
+     * not more than 1
+     */
+    public SinglePointToMultiPointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                                         ConnectPoint ingressPort,
+                                         Set<ConnectPoint> egressPorts) {
+        super(id, match, action);
+
+        checkNotNull(egressPorts);
+        checkArgument(!egressPorts.isEmpty(),
+                "there should be at least one egress port");
+
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPorts = Sets.newHashSet(egressPorts);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected SinglePointToMultiPointIntent() {
+        super();
+        this.ingressPort = null;
+        this.egressPorts = null;
+    }
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to the egress.
+     *
+     * @return ingress port
+     */
+    public ConnectPoint getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the set of ports on which the traffic should egress.
+     *
+     * @return set of egress ports
+     */
+    public Set<ConnectPoint> getEgressPorts() {
+        return egressPorts;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        SinglePointToMultiPointIntent that = (SinglePointToMultiPointIntent) o;
+        return Objects.equals(this.ingressPort, that.ingressPort)
+                && Objects.equals(this.egressPorts, that.egressPorts);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPort, egressPorts);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPorts)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
new file mode 100644
index 0000000..e1e6782
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Intent Package. TODO
+ */
+
+package org.onlab.onos.net.intent;
\ No newline at end of file
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java
new file mode 100644
index 0000000..fb1efee
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/ConnectivityIntentTest.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.flow.DefaultTrafficSelector;
+import org.onlab.onos.net.flow.DefaultTrafficTreatment;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+/**
+ * Base facilities to test various connectivity tests.
+ */
+public abstract class ConnectivityIntentTest extends IntentTest {
+
+    public static final IntentId IID = new IntentId(123);
+    public static final TrafficSelector MATCH = (new DefaultTrafficSelector.Builder()).build();
+    public static final TrafficTreatment NOP = (new DefaultTrafficTreatment.Builder()).build();
+
+    public static final ConnectPoint P1 = new ConnectPoint(DeviceId.deviceId("111"), PortNumber.portNumber(0x1));
+    public static final ConnectPoint P2 = new ConnectPoint(DeviceId.deviceId("222"), PortNumber.portNumber(0x2));
+    public static final ConnectPoint P3 = new ConnectPoint(DeviceId.deviceId("333"), PortNumber.portNumber(0x3));
+
+    public static final Set<ConnectPoint> PS1 = itemSet(new ConnectPoint[]{P1, P3});
+    public static final Set<ConnectPoint> PS2 = itemSet(new ConnectPoint[]{P2, P3});
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
new file mode 100644
index 0000000..df46ec5
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/FakeIntentManager.java
@@ -0,0 +1,268 @@
+package org.onlab.onos.net.intent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Fake implementation of the intent service to assist in developing tests
+ * of the interface contract.
+ */
+public class FakeIntentManager implements TestableIntentService {
+
+    private final Map<IntentId, Intent> intents = new HashMap<>();
+    private final Map<IntentId, IntentState> intentStates = new HashMap<>();
+    private final Map<IntentId, List<InstallableIntent>> installables = new HashMap<>();
+    private final Set<IntentEventListener> listeners = new HashSet<>();
+
+    private final Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> compilers = new HashMap<>();
+    private final Map<Class<? extends InstallableIntent>,
+            IntentInstaller<? extends InstallableIntent>> installers = new HashMap<>();
+
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+    private final List<IntentException> exceptions = new ArrayList<>();
+
+    @Override
+    public List<IntentException> getExceptions() {
+        return exceptions;
+    }
+
+    // Provides an out-of-thread simulation of intent submit life-cycle
+    private void executeSubmit(final Intent intent) {
+        registerSubclassCompilerIfNeeded(intent);
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    List<InstallableIntent> installable = compileIntent(intent);
+                    installIntents(intent, installable);
+                } catch (IntentException e) {
+                    exceptions.add(e);
+                }
+            }
+        });
+    }
+
+    // Provides an out-of-thread simulation of intent withdraw life-cycle
+    private void executeWithdraw(final Intent intent) {
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    List<InstallableIntent> installable = getInstallable(intent.getId());
+                    uninstallIntents(intent, installable);
+                } catch (IntentException e) {
+                    exceptions.add(e);
+                }
+
+            }
+        });
+    }
+
+    private <T extends Intent> IntentCompiler<T> getCompiler(T intent) {
+        @SuppressWarnings("unchecked")
+        IntentCompiler<T> compiler = (IntentCompiler<T>) compilers.get(intent.getClass());
+        if (compiler == null) {
+            throw new IntentException("no compiler for class " + intent.getClass());
+        }
+        return compiler;
+    }
+
+    private <T extends InstallableIntent> IntentInstaller<T> getInstaller(T intent) {
+        @SuppressWarnings("unchecked")
+        IntentInstaller<T> installer = (IntentInstaller<T>) installers.get(intent.getClass());
+        if (installer == null) {
+            throw new IntentException("no installer for class " + intent.getClass());
+        }
+        return installer;
+    }
+
+    private <T extends Intent> List<InstallableIntent> compileIntent(T intent) {
+        try {
+            // For the fake, we compile using a single level pass
+            List<InstallableIntent> installable = new ArrayList<>();
+            for (Intent compiled : getCompiler(intent).compile(intent)) {
+                installable.add((InstallableIntent) compiled);
+            }
+            setState(intent, IntentState.COMPILED);
+            return installable;
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+    private void installIntents(Intent intent, List<InstallableIntent> installable) {
+        try {
+            for (InstallableIntent ii : installable) {
+                registerSubclassInstallerIfNeeded(ii);
+                getInstaller(ii).install(ii);
+            }
+            setState(intent, IntentState.INSTALLED);
+            putInstallable(intent.getId(), installable);
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+    private void uninstallIntents(Intent intent, List<InstallableIntent> installable) {
+        try {
+            for (InstallableIntent ii : installable) {
+                getInstaller(ii).uninstall(ii);
+            }
+            setState(intent, IntentState.WITHDRAWN);
+            removeInstallable(intent.getId());
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+
+    // Sets the internal state for the given intent and dispatches an event
+    private void setState(Intent intent, IntentState state) {
+        IntentState previous = intentStates.get(intent.getId());
+        intentStates.put(intent.getId(), state);
+        dispatch(new IntentEvent(intent, state, previous, System.currentTimeMillis()));
+    }
+
+    private void putInstallable(IntentId id, List<InstallableIntent> installable) {
+        installables.put(id, installable);
+    }
+
+    private void removeInstallable(IntentId id) {
+        installables.remove(id);
+    }
+
+    private List<InstallableIntent> getInstallable(IntentId id) {
+        List<InstallableIntent> installable = installables.get(id);
+        if (installable != null) {
+            return installable;
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public void submit(Intent intent) {
+        intents.put(intent.getId(), intent);
+        setState(intent, IntentState.SUBMITTED);
+        executeSubmit(intent);
+    }
+
+    @Override
+    public void withdraw(Intent intent) {
+        intents.remove(intent.getId());
+        setState(intent, IntentState.WITHDRAWING);
+        executeWithdraw(intent);
+    }
+
+    @Override
+    public void execute(IntentOperations operations) {
+        // TODO: implement later
+    }
+
+    @Override
+    public Set<Intent> getIntents() {
+        return Collections.unmodifiableSet(new HashSet<>(intents.values()));
+    }
+
+    @Override
+    public Intent getIntent(IntentId id) {
+        return intents.get(id);
+    }
+
+    @Override
+    public IntentState getIntentState(IntentId id) {
+        return intentStates.get(id);
+    }
+
+    @Override
+    public void addListener(IntentEventListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(IntentEventListener listener) {
+        listeners.remove(listener);
+    }
+
+    private void dispatch(IntentEvent event) {
+        for (IntentEventListener listener : listeners) {
+            listener.event(event);
+        }
+    }
+
+    @Override
+    public <T extends Intent> void registerCompiler(Class<T> cls, IntentCompiler<T> compiler) {
+        compilers.put(cls, compiler);
+    }
+
+    @Override
+    public <T extends Intent> void unregisterCompiler(Class<T> cls) {
+        compilers.remove(cls);
+    }
+
+    @Override
+    public Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> getCompilers() {
+        return Collections.unmodifiableMap(compilers);
+    }
+
+    @Override
+    public <T extends InstallableIntent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer) {
+        installers.put(cls, installer);
+    }
+
+    @Override
+    public <T extends InstallableIntent> void unregisterInstaller(Class<T> cls) {
+        installers.remove(cls);
+    }
+
+    @Override
+    public Map<Class<? extends InstallableIntent>,
+            IntentInstaller<? extends InstallableIntent>> getInstallers() {
+        return Collections.unmodifiableMap(installers);
+    }
+
+    private void registerSubclassCompilerIfNeeded(Intent intent) {
+        if (!compilers.containsKey(intent.getClass())) {
+            Class<?> cls = intent.getClass();
+            while (cls != Object.class) {
+                // As long as we're within the Intent class descendants
+                if (Intent.class.isAssignableFrom(cls)) {
+                    IntentCompiler<?> compiler = compilers.get(cls);
+                    if (compiler != null) {
+                        compilers.put(intent.getClass(), compiler);
+                        return;
+                    }
+                }
+                cls = cls.getSuperclass();
+            }
+        }
+    }
+
+    private void registerSubclassInstallerIfNeeded(InstallableIntent intent) {
+        if (!installers.containsKey(intent.getClass())) {
+            Class<?> cls = intent.getClass();
+            while (cls != Object.class) {
+                // As long as we're within the InstallableIntent class descendants
+                if (InstallableIntent.class.isAssignableFrom(cls)) {
+                    IntentInstaller<?> installer = installers.get(cls);
+                    if (installer != null) {
+                        installers.put(intent.getClass(), installer);
+                        return;
+                    }
+                }
+                cls = cls.getSuperclass();
+            }
+        }
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java b/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
new file mode 100644
index 0000000..0e63af9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
@@ -0,0 +1,125 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+import org.hamcrest.Description;
+import org.hamcrest.StringDescription;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Hamcrest style class for verifying that a class follows the
+ * accepted rules for immutable classes.
+ *
+ * The rules that are enforced for immutable classes:
+ *    - the class must be declared final
+ *    - all data members of the class must be declared private and final
+ *    - the class must not define any setter methods
+ */
+
+public class ImmutableClassChecker {
+
+    private String failureReason = "";
+
+    /**
+     * Method to determine if a given class is a properly specified
+     * immutable class.
+     *
+     * @param clazz the class to check
+     * @return true if the given class is a properly specified immutable class.
+     */
+    private boolean isImmutableClass(Class<?> clazz) {
+        // class must be declared final
+        if (!Modifier.isFinal(clazz.getModifiers())) {
+            failureReason = "a class that is not final";
+            return false;
+        }
+
+        // class must have only final and private data members
+        for (final Field field : clazz.getDeclaredFields()) {
+            if (field.getName().startsWith("__cobertura")) {
+                //  cobertura sticks these fields into classes - ignore them
+                continue;
+            }
+            if (!Modifier.isFinal(field.getModifiers())) {
+                failureReason = "a field named '" + field.getName() +
+                                "' that is not final";
+                return false;
+            }
+            if (!Modifier.isPrivate(field.getModifiers())) {
+                //
+                // NOTE: We relax the recommended rules for defining immutable
+                // objects and allow "static final" fields that are not
+                // private. The "final" check was already done above so we
+                // don't repeat it here.
+                //
+                if (!Modifier.isStatic(field.getModifiers())) {
+                    failureReason = "a field named '" + field.getName() +
+                                "' that is not private and is not static";
+                    return false;
+                }
+            }
+        }
+
+        //  class must not define any setters
+        for (final Method method : clazz.getMethods()) {
+            if (method.getDeclaringClass().equals(clazz)) {
+                if (method.getName().startsWith("set")) {
+                    failureReason = "a class with a setter named '" + method.getName() + "'";
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Describe why an error was reported.  Uses Hamcrest style Description
+     * interfaces.
+     *
+     * @param description the Description object to use for reporting the
+     *                    mismatch
+     */
+    public void describeMismatch(Description description) {
+        description.appendText(failureReason);
+    }
+
+    /**
+     * Describe the source object that caused an error, using a Hamcrest
+     * Matcher style interface.  In this case, it always returns
+     * that we are looking for a properly defined utility class.
+     *
+     * @param description the Description object to use to report the "to"
+     *                    object
+     */
+    public void describeTo(Description description) {
+        description.appendText("a properly defined immutable class");
+    }
+
+    /**
+     * Assert that the given class adheres to the utility class rules.
+     *
+     * @param clazz the class to check
+     *
+     * @throws java.lang.AssertionError if the class is not a valid
+     *         utility class
+     */
+    public static void assertThatClassIsImmutable(Class<?> clazz) {
+        final ImmutableClassChecker checker = new ImmutableClassChecker();
+        if (!checker.isImmutableClass(clazz)) {
+            final Description toDescription = new StringDescription();
+            final Description mismatchDescription = new StringDescription();
+
+            checker.describeTo(toDescription);
+            checker.describeMismatch(mismatchDescription);
+            final String reason =
+                    "\n" +
+                    "Expected: is \"" + toDescription.toString() + "\"\n" +
+                    "    but : was \"" + mismatchDescription.toString() + "\"";
+
+            throw new AssertionError(reason);
+        }
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java
new file mode 100644
index 0000000..02564e6
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentExceptionTest.java
@@ -0,0 +1,33 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the intent exception.
+ */
+public class IntentExceptionTest {
+
+    @Test
+    public void basics() {
+        validate(new IntentException(), null, null);
+        validate(new IntentException("foo"), "foo", null);
+
+        Throwable cause = new NullPointerException("bar");
+        validate(new IntentException("foo", cause), "foo", cause);
+    }
+
+    /**
+     * Validates that the specified exception has the correct message and cause.
+     *
+     * @param e       exception to test
+     * @param message expected message
+     * @param cause   expected cause
+     */
+    protected void validate(RuntimeException e, String message, Throwable cause) {
+        assertEquals("incorrect message", message, e.getMessage());
+        assertEquals("incorrect cause", cause, e.getCause());
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java
new file mode 100644
index 0000000..0ca669b
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdGenerator.java
@@ -0,0 +1,14 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * This interface is for generator of IntentId. It is defined only for
+ * testing purpose to keep type safety on mock creation.
+ *
+ * <p>
+ * {@link #getNewId()} generates a globally unique {@link IntentId} instance
+ * on each invocation. Application developers should not generate IntentId
+ * by themselves. Instead use an implementation of this interface.
+ * </p>
+ */
+public interface IntentIdGenerator extends IdGenerator<IntentId> {
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
new file mode 100644
index 0000000..2a0824c
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * This class tests the immutability, equality, and non-equality of
+ * {@link IntentId}.
+ */
+public class IntentIdTest {
+    /**
+     * Tests the immutability of {@link IntentId}.
+     */
+    @Test
+    public void intentIdFollowsGuidelineForImmutableObject() {
+        ImmutableClassChecker.assertThatClassIsImmutable(IntentId.class);
+    }
+
+    /**
+     * Tests equality of {@link IntentId}.
+     */
+    @Test
+    public void testEquality() {
+        IntentId id1 = new IntentId(1L);
+        IntentId id2 = new IntentId(1L);
+
+        assertThat(id1, is(id2));
+    }
+
+    /**
+     * Tests non-equality of {@link IntentId}.
+     */
+    @Test
+    public void testNonEquality() {
+        IntentId id1 = new IntentId(1L);
+        IntentId id2 = new IntentId(2L);
+
+        assertThat(id1, is(not(id2)));
+    }
+
+    @Test
+    public void valueOf() {
+        IntentId id = new IntentId(12345);
+        assertEquals("incorrect valueOf", id, IntentId.valueOf("12345"));
+    }
+
+    @Test
+    public void valueOfHex() {
+        IntentId id = new IntentId(0xdeadbeefL);
+        assertEquals("incorrect valueOf", id, IntentId.valueOf(id.toString()));
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java
new file mode 100644
index 0000000..c7682b1
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentServiceTest.java
@@ -0,0 +1,310 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.onlab.onos.net.intent.IntentState.*;
+import static org.junit.Assert.*;
+
+// TODO: consider make it categorized as integration test when it become
+//   slow test or fragile test
+/**
+ * Suite of tests for the intent service contract.
+ */
+public class IntentServiceTest {
+
+    public static final IntentId IID = new IntentId(123);
+    public static final IntentId INSTALLABLE_IID = new IntentId(234);
+
+    protected static final int GRACE_MS = 500; // millis
+
+    protected TestableIntentService service;
+    protected TestListener listener = new TestListener();
+
+    @Before
+    public void setUp() {
+        service = createIntentService();
+        service.addListener(listener);
+    }
+
+    @After
+    public void tearDown() {
+        service.removeListener(listener);
+    }
+
+    /**
+     * Creates a service instance appropriately instrumented for testing.
+     *
+     * @return testable intent service
+     */
+    protected TestableIntentService createIntentService() {
+        return new FakeIntentManager();
+    }
+
+    @Test
+    public void basics() {
+        // Make sure there are no intents
+        assertEquals("incorrect intent count", 0, service.getIntents().size());
+
+        // Register a compiler and an installer both setup for success.
+        service.registerCompiler(TestIntent.class, new TestCompiler(new TestInstallableIntent(INSTALLABLE_IID)));
+        service.registerInstaller(TestInstallableIntent.class, new TestInstaller(false));
+
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", INSTALLED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, COMPILED, INSTALLED);
+
+        // Make sure there is just one intent (and is ours)
+        assertEquals("incorrect intent count", 1, service.getIntents().size());
+        assertEquals("incorrect intent", intent, service.getIntent(intent.getId()));
+
+        // Reset the listener events
+        listener.events.clear();
+
+        // Now withdraw the intent
+        service.withdraw(intent);
+
+        // Allow a small window of time until the event is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", WITHDRAWN,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, WITHDRAWING, WITHDRAWN);
+
+        // TODO: discuss what is the fate of intents after they have been withdrawn
+        // Make sure that the intent is no longer in the system
+//        assertEquals("incorrect intent count", 0, service.getIntents().size());
+//        assertNull("intent should not be found", service.getIntent(intent.getId()));
+//        assertNull("intent state should not be found", service.getIntentState(intent.getId()));
+    }
+
+    @Test
+    public void failedCompilation() {
+        // Register a compiler programmed for success
+        service.registerCompiler(TestIntent.class, new TestCompiler(true));
+
+        // Submit an intent
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", FAILED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, FAILED);
+    }
+
+    @Test
+    public void failedInstallation() {
+        // Register a compiler programmed for success and installer for failure
+        service.registerCompiler(TestIntent.class, new TestCompiler(new TestInstallableIntent(INSTALLABLE_IID)));
+        service.registerInstaller(TestInstallableIntent.class, new TestInstaller(true));
+
+        // Submit an intent
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", FAILED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, COMPILED, FAILED);
+    }
+
+    /**
+     * Validates that the test event listener has received the following events
+     * for the specified intent. Events received for other intents will not be
+     * considered.
+     *
+     * @param intent intent subject
+     * @param states list of states for which events are expected
+     */
+    protected void validateEvents(Intent intent, IntentState... states) {
+        Iterator<IntentEvent> events = listener.events.iterator();
+        for (IntentState state : states) {
+            IntentEvent event = events.hasNext() ? events.next() : null;
+            if (event == null) {
+                fail("expected event not found: " + state);
+            } else if (intent.equals(event.getIntent())) {
+                assertEquals("incorrect state", state, event.getState());
+            }
+        }
+
+        // Remainder of events should not apply to this intent; make sure.
+        while (events.hasNext()) {
+            assertFalse("unexpected event for intent",
+                        intent.equals(events.next().getIntent()));
+        }
+    }
+
+    @Test
+    public void compilerBasics() {
+        // Make sure there are no compilers
+        assertEquals("incorrect compiler count", 0, service.getCompilers().size());
+
+        // Add a compiler and make sure that it appears in the map
+        IntentCompiler<TestIntent> compiler = new TestCompiler(false);
+        service.registerCompiler(TestIntent.class, compiler);
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestIntent.class));
+
+        // Remove the same and make sure that it no longer appears in the map
+        service.unregisterCompiler(TestIntent.class);
+        assertNull("compiler should not be registered",
+                   service.getCompilers().get(TestIntent.class));
+    }
+
+    @Test
+    public void installerBasics() {
+        // Make sure there are no installers
+        assertEquals("incorrect installer count", 0, service.getInstallers().size());
+
+        // Add an installer and make sure that it appears in the map
+        IntentInstaller<TestInstallableIntent> installer = new TestInstaller(false);
+        service.registerInstaller(TestInstallableIntent.class, installer);
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestInstallableIntent.class));
+
+        // Remove the same and make sure that it no longer appears in the map
+        service.unregisterInstaller(TestInstallableIntent.class);
+        assertNull("installer should not be registered",
+                   service.getInstallers().get(TestInstallableIntent.class));
+    }
+
+    @Test
+    public void implicitRegistration() {
+        // Add a compiler and make sure that it appears in the map
+        IntentCompiler<TestIntent> compiler = new TestCompiler(new TestSubclassInstallableIntent(INSTALLABLE_IID));
+        service.registerCompiler(TestIntent.class, compiler);
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestIntent.class));
+
+        // Add a installer and make sure that it appears in the map
+        IntentInstaller<TestInstallableIntent> installer = new TestInstaller(false);
+        service.registerInstaller(TestInstallableIntent.class, installer);
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestInstallableIntent.class));
+
+
+        // Submit an intent which is a subclass of the one we registered
+        final Intent intent = new TestSubclassIntent(IID);
+        service.submit(intent);
+
+        // Allow some time for the intent to be compiled and installed
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", INSTALLED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that now we have an implicit registration of the compiler
+        // under the intent subclass
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestSubclassIntent.class));
+
+        // Make sure that now we have an implicit registration of the installer
+        // under the intent subclass
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestSubclassInstallableIntent.class));
+
+        // TODO: discuss whether or if implicit registration should require implicit unregistration
+        // perhaps unregister by compiler or installer itself, rather than by class would be better
+    }
+
+
+    // Fixture to track emitted intent events
+    protected class TestListener implements IntentEventListener {
+        final List<IntentEvent> events = new ArrayList<>();
+
+        @Override
+        public void event(IntentEvent event) {
+            events.add(event);
+        }
+    }
+
+    // Controllable compiler
+    private class TestCompiler implements IntentCompiler<TestIntent> {
+        private final boolean fail;
+        private final List<Intent> result;
+
+        TestCompiler(boolean fail) {
+            this.fail = fail;
+            this.result = Collections.emptyList();
+        }
+
+        TestCompiler(Intent... result) {
+            this.fail = false;
+            this.result = Arrays.asList(result);
+        }
+
+        @Override
+        public List<Intent> compile(TestIntent intent) {
+            if (fail) {
+                throw new IntentException("compile failed by design");
+            }
+            List<Intent> compiled = new ArrayList<>(result);
+            return compiled;
+        }
+    }
+
+    // Controllable installer
+    private class TestInstaller implements IntentInstaller<TestInstallableIntent> {
+        private final boolean fail;
+
+        TestInstaller(boolean fail) {
+            this.fail = fail;
+        }
+
+        @Override
+        public void install(TestInstallableIntent intent) {
+            if (fail) {
+                throw new IntentException("install failed by design");
+            }
+        }
+
+        @Override
+        public void uninstall(TestInstallableIntent intent) {
+            if (fail) {
+                throw new IntentException("remove failed by design");
+            }
+        }
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java
new file mode 100644
index 0000000..a6cedf9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentTest.java
@@ -0,0 +1,65 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+/**
+ * Base facilities to test various intent tests.
+ */
+public abstract class IntentTest {
+    /**
+     * Produces a set of items from the supplied items.
+     *
+     * @param items items to be placed in set
+     * @param <T>   item type
+     * @return set of items
+     */
+    protected static <T> Set<T> itemSet(T[] items) {
+        return new HashSet<>(Arrays.asList(items));
+    }
+
+    @Test
+    public void equalsAndHashCode() {
+        Intent one = createOne();
+        Intent like = createOne();
+        Intent another = createAnother();
+
+        assertTrue("should be equal", one.equals(like));
+        assertEquals("incorrect hashCode", one.hashCode(), like.hashCode());
+
+        assertFalse("should not be equal", one.equals(another));
+
+        assertFalse("should not be equal", one.equals(null));
+        assertFalse("should not be equal", one.equals("foo"));
+    }
+
+    @Test
+    public void testToString() {
+        Intent one = createOne();
+        Intent like = createOne();
+        assertEquals("incorrect toString", one.toString(), like.toString());
+    }
+
+    /**
+     * Creates a new intent, but always a like intent, i.e. all instances will
+     * be equal, but should not be the same.
+     *
+     * @return intent
+     */
+    protected abstract Intent createOne();
+
+    /**
+     * Creates another intent, not equals to the one created by
+     * {@link #createOne()} and with a different hash code.
+     *
+     * @return another intent
+     */
+    protected abstract Intent createAnother();
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java
new file mode 100644
index 0000000..d971ba2
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the multi-to-single point intent descriptor.
+ */
+public class MultiPointToSinglePointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        MultiPointToSinglePointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+        assertEquals("incorrect ingress", PS1, intent.getIngressPorts());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+    }
+
+    @Override
+    protected MultiPointToSinglePointIntent createOne() {
+        return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS1, P2);
+    }
+
+    @Override
+    protected MultiPointToSinglePointIntent createAnother() {
+        return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS2, P1);
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
new file mode 100644
index 0000000..bd8dc08
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
@@ -0,0 +1,36 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.onlab.onos.net.NetTestTools;
+import org.onlab.onos.net.Path;
+
+public class PathIntentTest extends ConnectivityIntentTest {
+    // 111:11 --> 222:22
+    private static final Path PATH1 = NetTestTools.createPath("111", "222");
+
+    // 111:11 --> 333:33
+    private static final Path PATH2 = NetTestTools.createPath("222", "333");
+
+    @Test
+    public void basics() {
+        PathIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+        assertEquals("incorrect action", NOP, intent.getTrafficTreatment());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+        assertEquals("incorrect path", PATH1, intent.getPath());
+    }
+
+    @Override
+    protected PathIntent createOne() {
+        return new PathIntent(IID, MATCH, NOP, P1, P2, PATH1);
+    }
+
+    @Override
+    protected PathIntent createAnother() {
+        return new PathIntent(IID, MATCH, NOP, P1, P3, PATH2);
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java
new file mode 100644
index 0000000..426a3d9
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/PointToPointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the point-to-point intent descriptor.
+ */
+public class PointToPointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        PointToPointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+    }
+
+    @Override
+    protected PointToPointIntent createOne() {
+        return new PointToPointIntent(IID, MATCH, NOP, P1, P2);
+    }
+
+    @Override
+    protected PointToPointIntent createAnother() {
+        return new PointToPointIntent(IID, MATCH, NOP, P2, P1);
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java
new file mode 100644
index 0000000..0561a87
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntentTest.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.net.intent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the single-to-multi point intent descriptor.
+ */
+public class SinglePointToMultiPointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        SinglePointToMultiPointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getTrafficSelector());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", PS2, intent.getEgressPorts());
+    }
+
+    @Override
+    protected SinglePointToMultiPointIntent createOne() {
+        return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P1, PS2);
+    }
+
+    @Override
+    protected SinglePointToMultiPointIntent createAnother() {
+        return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P2, PS1);
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java
new file mode 100644
index 0000000..a6ce52e
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestInstallableIntent.java
@@ -0,0 +1,28 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An installable intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestInstallableIntent extends AbstractIntent implements InstallableIntent {
+    /**
+     * Constructs an instance with the specified intent ID.
+     *
+     * @param id intent ID
+     */
+    public TestInstallableIntent(IntentId id) {
+        super(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected TestInstallableIntent() {
+        super();
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java
new file mode 100644
index 0000000..2f30727
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestIntent extends AbstractIntent {
+    /**
+     * Constructs an instance with the specified intent ID.
+     *
+     * @param id intent ID
+     */
+    public TestIntent(IntentId id) {
+        super(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected TestIntent() {
+        super();
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java
new file mode 100644
index 0000000..40765c2
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassInstallableIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestSubclassInstallableIntent extends TestInstallableIntent implements InstallableIntent {
+    /**
+     * Constructs an instance with the specified intent ID.
+     *
+     * @param id intent ID
+     */
+    public TestSubclassInstallableIntent(IntentId id) {
+        super(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected TestSubclassInstallableIntent() {
+        super();
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java
new file mode 100644
index 0000000..43bb0dd
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestSubclassIntent.java
@@ -0,0 +1,27 @@
+package org.onlab.onos.net.intent;
+//TODO is this the right package?
+
+/**
+ * An intent used in the unit test.
+ *
+ * FIXME: we don't want to expose this class publicly, but the current Kryo
+ * serialization mechanism does not allow this class to be private and placed
+ * on testing directory.
+ */
+public class TestSubclassIntent extends TestIntent {
+    /**
+     * Constructs an instance with the specified intent ID.
+     *
+     * @param id intent ID
+     */
+    public TestSubclassIntent(IntentId id) {
+        super(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected TestSubclassIntent() {
+        super();
+    }
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java
new file mode 100644
index 0000000..f22585e
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestTools.java
@@ -0,0 +1,126 @@
+package org.onlab.onos.net.intent;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Set of test tools.
+ */
+public final class TestTools {
+
+    // Disallow construction
+    private TestTools() {
+    }
+
+    /**
+     * Utility method to pause the current thread for the specified number of
+     * milliseconds.
+     *
+     * @param ms number of milliseconds to pause
+     */
+    public static void delay(int ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            fail("unexpected interrupt");
+        }
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     *
+     * @param start start time, in millis since start of epoch from which the
+     *        duration will be measured
+     * @param delay initial delay (in milliseconds) before the first assertion
+     *        attempt
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the given start time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(long start, int delay, int step,
+                                   int duration, Runnable assertions) {
+        delay(delay);
+        while (true) {
+            try {
+                assertions.run();
+                break;
+            } catch (AssertionError e) {
+                if (System.currentTimeMillis() - start > duration) {
+                    throw e;
+                }
+            }
+            delay(step);
+        }
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time.
+     *
+     * @param delay initial delay (in milliseconds) before the first assertion
+     *        attempt
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the current time time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int delay, int step, int duration,
+                                   Runnable assertions) {
+        assertAfter(System.currentTimeMillis(), delay, step, duration,
+                    assertions);
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time and the first assertion
+     * attempt is delayed by the value of {@code step} parameter.
+     *
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the current time time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int step, int duration,
+                                   Runnable assertions) {
+        assertAfter(step, step, duration, assertions);
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time and each successive
+     * assertion attempt is delayed by at least 10 milliseconds unless the
+     * {@code duration} is less than that, in which case the one and only
+     * assertion is made after that delay.
+     *
+     * @param duration number of milliseconds beyond the current time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int duration, Runnable assertions) {
+        int step = Math.min(duration, Math.max(10, duration / 10));
+        assertAfter(step, duration, assertions);
+    }
+
+}
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java b/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java
new file mode 100644
index 0000000..95502d3
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/TestableIntentService.java
@@ -0,0 +1,12 @@
+package org.onlab.onos.net.intent;
+
+import java.util.List;
+
+/**
+ * Abstraction of an extensible intent service enabled for unit tests.
+ */
+public interface TestableIntentService extends IntentService, IntentExtensionService {
+
+    List<IntentException> getExceptions();
+
+}
diff --git a/core/store/dist/pom.xml b/core/store/dist/pom.xml
index 2451955..1faab74 100644
--- a/core/store/dist/pom.xml
+++ b/core/store/dist/pom.xml
@@ -33,6 +33,12 @@
             <artifactId>onlab-nio</artifactId>
             <version>${project.version}</version>
         </dependency>
+        
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-netty</artifactId>
+            <version>${project.version}</version>
+        </dependency>
 
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
@@ -51,15 +57,6 @@
           <groupId>de.javakaffee</groupId>
           <artifactId>kryo-serializers</artifactId>
         </dependency>
-        <dependency>
-          <groupId>io.netty</groupId>
-          <artifactId>netty-all</artifactId>
-        </dependency>
-        <dependency>
-          <groupId>commons-pool</groupId>
-          <artifactId>commons-pool</artifactId>
-          <version>1.6</version>
-        </dependency>
     </dependencies>
 
     <build>
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/OnosClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/OnosClusterCommunicationManager.java
index 9bd25b4..e6e4a4d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/OnosClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/OnosClusterCommunicationManager.java
@@ -23,10 +23,10 @@
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
 import org.onlab.onos.store.cluster.messaging.MessageSubject;
-import org.onlab.onos.store.messaging.Endpoint;
-import org.onlab.onos.store.messaging.Message;
-import org.onlab.onos.store.messaging.MessageHandler;
-import org.onlab.onos.store.messaging.MessagingService;
+import org.onlab.netty.Endpoint;
+import org.onlab.netty.Message;
+import org.onlab.netty.MessageHandler;
+import org.onlab.netty.MessagingService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Endpoint.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Endpoint.java
deleted file mode 100644
index bd6d45f..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Endpoint.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.onlab.onos.store.messaging;
-
-/**
- * Representation of a TCP/UDP communication end point.
- */
-public class Endpoint {
-
-    private final int port;
-    private final String host;
-
-    public Endpoint(String host, int port) {
-        this.host = host;
-        this.port = port;
-    }
-
-    public String host() {
-        return host;
-    }
-
-    public int port() {
-        return port;
-    }
-
-    @Override
-    public String toString() {
-        return "Endpoint [port=" + port + ", host=" + host + "]";
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + ((host == null) ? 0 : host.hashCode());
-        result = prime * result + port;
-        return result;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        Endpoint other = (Endpoint) obj;
-        if (host == null) {
-            if (other.host != null) {
-                return false;
-            }
-        } else if (!host.equals(other.host)) {
-            return false;
-        }
-        if (port != other.port) {
-            return false;
-        }
-        return true;
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Message.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Message.java
deleted file mode 100644
index d814927..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Message.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.onlab.onos.store.messaging;
-
-import java.io.IOException;
-
-/**
- * A unit of communication.
- * Has a payload. Also supports a feature to respond back to the sender.
- */
-public interface Message {
-
-    /**
-     * Returns the payload of this message.
-     * @return message payload.
-     */
-    public Object payload();
-
-    /**
-     * Sends a reply back to the sender of this messge.
-     * @param data payload of the response.
-     * @throws IOException if there is a communication error.
-     */
-    public void respond(Object data) throws IOException;
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessageHandler.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessageHandler.java
deleted file mode 100644
index 8eaef1e..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessageHandler.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.onlab.onos.store.messaging;
-
-import java.io.IOException;
-
-/**
- * Handler for a message.
- */
-public interface MessageHandler {
-
-    /**
-     * Handles the message.
-     * @param message message.
-     * @throws IOException.
-     */
-    public void handle(Message message) throws IOException;
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessagingService.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessagingService.java
deleted file mode 100644
index 4aa32cb..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/MessagingService.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.onlab.onos.store.messaging;
-
-import java.io.IOException;
-
-/**
- * Interface for low level messaging primitives.
- */
-public interface MessagingService {
-    /**
-     * Sends a message asynchronously to the specified communication end point.
-     * The message is specified using the type and payload.
-     * @param ep end point to send the message to.
-     * @param type type of message.
-     * @param payload message payload.
-     * @throws IOException
-     */
-    public void sendAsync(Endpoint ep, String type, Object payload) throws IOException;
-
-    /**
-     * Sends a message synchronously and waits for a response.
-     * @param ep end point to send the message to.
-     * @param type type of message.
-     * @param payload message payload.
-     * @return a response future
-     * @throws IOException
-     */
-    public <T> Response<T> sendAndReceive(Endpoint ep, String type, Object payload) throws IOException;
-
-    /**
-     * Registers a new message handler for message type.
-     * @param type message type.
-     * @param handler message handler
-     */
-    public void registerHandler(String type, MessageHandler handler);
-
-    /**
-     * Unregister current handler, if one exists for message type.
-     * @param type message type
-     */
-    public void unregisterHandler(String type);
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Response.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Response.java
deleted file mode 100644
index ff0d84f..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/Response.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.onlab.onos.store.messaging;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Response object returned when making synchronous requests.
- * Can you used to check is a response is ready and/or wait for a response
- * to become available.
- *
- * @param <T> type of response.
- */
-public interface Response<T> {
-
-    /**
-     * Gets the response waiting for a designated timeout period.
-     * @param timeout timeout period (since request was sent out)
-     * @param tu unit of time.
-     * @return response
-     * @throws TimeoutException if the timeout expires before the response arrives.
-     */
-    public T get(long timeout, TimeUnit tu) throws TimeoutException;
-
-    /**
-     * Gets the response waiting for indefinite timeout period.
-     * @return response
-     * @throws InterruptedException if the thread is interrupted before the response arrives.
-     */
-    public T get() throws InterruptedException;
-
-    /**
-     * Checks if the response is ready without blocking.
-     * @return true if response is ready, false otherwise.
-     */
-    public boolean isReady();
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/AsyncResponse.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/AsyncResponse.java
deleted file mode 100644
index ac2337d..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/AsyncResponse.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.onlab.onos.store.messaging.Response;
-
-/**
- * An asynchronous response.
- * This class provides a base implementation of Response, with methods to retrieve the
- * result and query to see if the result is ready. The result can only be retrieved when
- * it is ready and the get methods will block if the result is not ready yet.
- * @param <T> type of response.
- */
-public class AsyncResponse<T> implements Response<T> {
-
-    private T value;
-    private boolean done = false;
-    private final long start = System.nanoTime();
-
-    @Override
-    public T get(long timeout, TimeUnit tu) throws TimeoutException {
-        timeout = tu.toNanos(timeout);
-        boolean interrupted = false;
-        try {
-            synchronized (this) {
-                while (!done) {
-                    try {
-                        long timeRemaining = timeout - (System.nanoTime() - start);
-                        if (timeRemaining <= 0) {
-                            throw new TimeoutException("Operation timed out.");
-                        }
-                        TimeUnit.NANOSECONDS.timedWait(this, timeRemaining);
-                    } catch (InterruptedException e) {
-                        interrupted = true;
-                    }
-                }
-            }
-        } finally {
-            if (interrupted) {
-                Thread.currentThread().interrupt();
-            }
-        }
-        return value;
-    }
-
-    @Override
-    public T get() throws InterruptedException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean isReady() {
-        return done;
-    }
-
-    /**
-     * Sets response value and unblocks any thread blocking on the response to become
-     * available.
-     * @param data response data.
-     */
-    @SuppressWarnings("unchecked")
-    public synchronized void setResponse(Object data) {
-        if (!done) {
-            done = true;
-            value = (T) data;
-            this.notifyAll();
-        }
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/EchoHandler.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/EchoHandler.java
deleted file mode 100644
index 7891c5c..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/EchoHandler.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.io.IOException;
-
-import org.onlab.onos.store.messaging.Message;
-import org.onlab.onos.store.messaging.MessageHandler;
-
-/**
- * Message handler that echos the message back to the sender.
- */
-public class EchoHandler implements MessageHandler {
-
-    @Override
-    public void handle(Message message) throws IOException {
-        System.out.println("Received: " + message.payload() + ". Echoing it back to the sender.");
-        message.respond(message.payload());
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/InternalMessage.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/InternalMessage.java
deleted file mode 100644
index 8a87a3e..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/InternalMessage.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.io.IOException;
-
-import org.onlab.onos.store.messaging.Endpoint;
-import org.onlab.onos.store.messaging.Message;
-
-/**
- * Internal message representation with additional attributes
- * for supporting, synchronous request/reply behavior.
- */
-public final class InternalMessage implements Message {
-
-    private long id;
-    private Endpoint sender;
-    private String type;
-    private Object payload;
-    private transient NettyMessagingService messagingService;
-    public static final String REPLY_MESSAGE_TYPE = "NETTY_MESSAGIG_REQUEST_REPLY";
-
-    // Must be created using the Builder.
-    private InternalMessage() {}
-
-    public long id() {
-        return id;
-    }
-
-    public String type() {
-        return type;
-    }
-
-    public Endpoint sender() {
-        return sender;
-    }
-
-    @Override
-    public Object payload() {
-        return payload;
-    }
-
-    @Override
-    public void respond(Object data) throws IOException {
-        Builder builder = new Builder(messagingService);
-        InternalMessage message = builder.withId(this.id)
-             // FIXME: Sender should be messagingService.localEp.
-            .withSender(this.sender)
-            .withPayload(data)
-            .withType(REPLY_MESSAGE_TYPE)
-            .build();
-        messagingService.sendAsync(sender, message);
-    }
-
-
-    /**
-     * Builder for InternalMessages.
-     */
-    public static class Builder {
-        private InternalMessage message;
-
-        public Builder(NettyMessagingService messagingService) {
-            message = new InternalMessage();
-            message.messagingService = messagingService;
-        }
-
-        public Builder withId(long id) {
-            message.id = id;
-            return this;
-        }
-
-        public Builder withType(String type) {
-            message.type = type;
-            return this;
-        }
-
-        public Builder withSender(Endpoint sender) {
-            message.sender = sender;
-            return this;
-        }
-        public Builder withPayload(Object payload) {
-            message.payload = payload;
-            return this;
-        }
-
-        public InternalMessage build() {
-            return message;
-        }
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/LoggingHandler.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/LoggingHandler.java
deleted file mode 100644
index bf871f8..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/LoggingHandler.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import org.onlab.onos.store.messaging.Message;
-import org.onlab.onos.store.messaging.MessageHandler;
-
-/**
- * A MessageHandler that simply logs the information.
- */
-public class LoggingHandler implements MessageHandler {
-
-    @Override
-    public void handle(Message message) {
-        System.out.println("Received: " + message.payload());
-    }
-}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java
deleted file mode 100644
index 7f94015..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.util.Arrays;
-import java.util.List;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import org.onlab.onos.store.cluster.messaging.SerializationService;
-import org.onlab.onos.store.messaging.Endpoint;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.ByteToMessageDecoder;
-
-/**
- * Decode bytes into a InrenalMessage.
- */
-public class MessageDecoder extends ByteToMessageDecoder {
-
-    private final NettyMessagingService messagingService;
-    private final SerializationService serializationService;
-
-    public MessageDecoder(NettyMessagingService messagingService, SerializationService serializationService) {
-        this.messagingService = messagingService;
-        this.serializationService = serializationService;
-    }
-
-    @Override
-    protected void decode(ChannelHandlerContext context, ByteBuf in,
-            List<Object> messages) throws Exception {
-
-        byte[] preamble = in.readBytes(MessageEncoder.PREAMBLE.length).array();
-        checkState(Arrays.equals(MessageEncoder.PREAMBLE, preamble), "Message has wrong preamble");
-
-        // read message Id.
-        long id = in.readLong();
-
-        // read message type; first read size and then bytes.
-        String type = new String(in.readBytes(in.readInt()).array());
-
-        // read sender host name; first read size and then bytes.
-        String host = new String(in.readBytes(in.readInt()).array());
-
-        // read sender port.
-        int port = in.readInt();
-
-        Endpoint sender = new Endpoint(host, port);
-
-        // read message payload; first read size and then bytes.
-        Object payload = serializationService.decode(in.readBytes(in.readInt()).array());
-
-        InternalMessage message = new InternalMessage.Builder(messagingService)
-                .withId(id)
-                .withSender(sender)
-                .withType(type)
-                .withPayload(payload)
-                .build();
-
-        messages.add(message);
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java
deleted file mode 100644
index b1c660c..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import org.onlab.onos.store.cluster.messaging.SerializationService;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.MessageToByteEncoder;
-
-/**
- * Encode InternalMessage out into a byte buffer.
- */
-public class MessageEncoder extends MessageToByteEncoder<InternalMessage> {
-
-    // onosiscool in ascii
-    public static final byte[] PREAMBLE = "onosiscool".getBytes();
-
-    private final SerializationService serializationService;
-
-    public MessageEncoder(SerializationService serializationService) {
-        this.serializationService = serializationService;
-    }
-
-    @Override
-    protected void encode(ChannelHandlerContext context, InternalMessage message,
-            ByteBuf out) throws Exception {
-
-        // write preamble
-        out.writeBytes(PREAMBLE);
-
-        // write id
-        out.writeLong(message.id());
-
-        // write type length
-        out.writeInt(message.type().length());
-
-        // write type
-        out.writeBytes(message.type().getBytes());
-
-        // write sender host name size
-        out.writeInt(message.sender().host().length());
-
-        // write sender host name.
-        out.writeBytes(message.sender().host().getBytes());
-
-        // write port
-        out.writeInt(message.sender().port());
-
-        try {
-            serializationService.encode(message.payload());
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-
-        byte[] payload = serializationService.encode(message.payload());
-
-        // write payload length.
-        out.writeInt(payload.length);
-
-        // write payload bytes
-        out.writeBytes(payload);
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java
deleted file mode 100644
index b6b3857..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java
+++ /dev/null
@@ -1,260 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.io.IOException;
-import java.net.UnknownHostException;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
-
-import io.netty.bootstrap.Bootstrap;
-import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.PooledByteBufAllocator;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInitializer;
-import io.netty.channel.ChannelOption;
-import io.netty.channel.EventLoopGroup;
-import io.netty.channel.SimpleChannelInboundHandler;
-import io.netty.channel.nio.NioEventLoopGroup;
-import io.netty.channel.socket.SocketChannel;
-import io.netty.channel.socket.nio.NioServerSocketChannel;
-import io.netty.channel.socket.nio.NioSocketChannel;
-
-import org.apache.commons.lang.math.RandomUtils;
-import org.apache.commons.pool.KeyedObjectPool;
-import org.apache.commons.pool.KeyedPoolableObjectFactory;
-import org.apache.commons.pool.impl.GenericKeyedObjectPool;
-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.store.cluster.messaging.SerializationService;
-import org.onlab.onos.store.messaging.Endpoint;
-import org.onlab.onos.store.messaging.MessageHandler;
-import org.onlab.onos.store.messaging.MessagingService;
-import org.onlab.onos.store.messaging.Response;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-
-/**
- * A Netty based implementation of MessagingService.
- */
-@Component(immediate = true)
-@Service
-public class NettyMessagingService implements MessagingService {
-
-    private final Logger log = LoggerFactory.getLogger(getClass());
-
-    private KeyedObjectPool<Endpoint, Channel> channels =
-            new GenericKeyedObjectPool<Endpoint, Channel>(new OnosCommunicationChannelFactory());
-    private final int port;
-    private final EventLoopGroup bossGroup = new NioEventLoopGroup();
-    private final EventLoopGroup workerGroup = new NioEventLoopGroup();
-    private final ConcurrentMap<String, MessageHandler> handlers = new ConcurrentHashMap<>();
-    private Cache<Long, AsyncResponse<?>> responseFutures;
-    private final Endpoint localEp;
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected SerializationService serializationService;
-
-    public NettyMessagingService() {
-        // TODO: Default port should be configurable.
-        this(8080);
-    }
-
-    // FIXME: Constructor should not throw exceptions.
-    public NettyMessagingService(int port) {
-        this.port = port;
-        try {
-            localEp = new Endpoint(java.net.InetAddress.getLocalHost().getHostName(), port);
-        } catch (UnknownHostException e) {
-            // bailing out.
-            throw new RuntimeException(e);
-        }
-    }
-
-    @Activate
-    public void activate() throws Exception {
-        responseFutures = CacheBuilder.newBuilder()
-                .maximumSize(100000)
-                .weakValues()
-                // TODO: Once the entry expires, notify blocking threads (if any).
-                .expireAfterWrite(10, TimeUnit.MINUTES)
-                .build();
-        startAcceptingConnections();
-    }
-
-    @Deactivate
-    public void deactivate() throws Exception {
-        channels.close();
-        bossGroup.shutdownGracefully();
-        workerGroup.shutdownGracefully();
-    }
-
-    @Override
-    public void sendAsync(Endpoint ep, String type, Object payload) throws IOException {
-        InternalMessage message = new InternalMessage.Builder(this)
-            .withId(RandomUtils.nextLong())
-            .withSender(localEp)
-            .withType(type)
-            .withPayload(payload)
-            .build();
-        sendAsync(ep, message);
-    }
-
-    protected void sendAsync(Endpoint ep, InternalMessage message) throws IOException {
-        Channel channel = null;
-        try {
-            channel = channels.borrowObject(ep);
-            channel.eventLoop().execute(new WriteTask(channel, message));
-        } catch (Exception e) {
-            throw new IOException(e);
-        } finally {
-            try {
-                channels.returnObject(ep, channel);
-            } catch (Exception e) {
-                log.warn("Error returning object back to the pool", e);
-                // ignored.
-            }
-        }
-    }
-
-    @Override
-    public <T> Response<T> sendAndReceive(Endpoint ep, String type, Object payload)
-            throws IOException {
-        AsyncResponse<T> futureResponse = new AsyncResponse<T>();
-        Long messageId = RandomUtils.nextLong();
-        responseFutures.put(messageId, futureResponse);
-        InternalMessage message = new InternalMessage.Builder(this)
-            .withId(messageId)
-            .withSender(localEp)
-            .withType(type)
-            .withPayload(payload)
-            .build();
-        sendAsync(ep, message);
-        return futureResponse;
-    }
-
-    @Override
-    public void registerHandler(String type, MessageHandler handler) {
-        // TODO: Is this the right semantics for handler registration?
-        handlers.putIfAbsent(type, handler);
-    }
-
-    public void unregisterHandler(String type) {
-        handlers.remove(type);
-    }
-
-    private MessageHandler getMessageHandler(String type) {
-        return handlers.get(type);
-    }
-
-    private void startAcceptingConnections() throws InterruptedException {
-        ServerBootstrap b = new ServerBootstrap();
-        b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
-        b.group(bossGroup, workerGroup)
-            .channel(NioServerSocketChannel.class)
-            .childHandler(new OnosCommunicationChannelInitializer())
-            .option(ChannelOption.SO_BACKLOG, 128)
-            .childOption(ChannelOption.SO_KEEPALIVE, true);
-
-        // Bind and start to accept incoming connections.
-        b.bind(port).sync();
-    }
-
-    private class OnosCommunicationChannelFactory
-        implements KeyedPoolableObjectFactory<Endpoint, Channel> {
-
-        @Override
-        public void activateObject(Endpoint endpoint, Channel channel)
-                throws Exception {
-        }
-
-        @Override
-        public void destroyObject(Endpoint ep, Channel channel) throws Exception {
-            channel.close();
-        }
-
-        @Override
-        public Channel makeObject(Endpoint ep) throws Exception {
-            Bootstrap b = new Bootstrap();
-            b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
-            b.group(workerGroup);
-            // TODO: Make this faster:
-            // http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html#37.0
-            b.channel(NioSocketChannel.class);
-            b.option(ChannelOption.SO_KEEPALIVE, true);
-            b.handler(new OnosCommunicationChannelInitializer());
-
-            // Start the client.
-            ChannelFuture f = b.connect(ep.host(), ep.port()).sync();
-            return f.channel();
-        }
-
-        @Override
-        public void passivateObject(Endpoint ep, Channel channel)
-                throws Exception {
-        }
-
-        @Override
-        public boolean validateObject(Endpoint ep, Channel channel) {
-            return channel.isOpen();
-        }
-    }
-
-    private class OnosCommunicationChannelInitializer extends ChannelInitializer<SocketChannel> {
-
-        @Override
-        protected void initChannel(SocketChannel channel) throws Exception {
-            channel.pipeline()
-                .addLast(new MessageEncoder(serializationService))
-                .addLast(new MessageDecoder(NettyMessagingService.this, serializationService))
-                .addLast(new NettyMessagingService.InboundMessageDispatcher());
-        }
-    }
-
-    private class WriteTask implements Runnable {
-
-        private final Object message;
-        private final Channel channel;
-
-        public WriteTask(Channel channel, Object message) {
-            this.message = message;
-            this.channel = channel;
-        }
-
-        @Override
-        public void run() {
-            channel.writeAndFlush(message);
-        }
-    }
-
-    private class InboundMessageDispatcher extends SimpleChannelInboundHandler<InternalMessage> {
-
-        @Override
-        protected void channelRead0(ChannelHandlerContext ctx, InternalMessage message) throws Exception {
-            String type = message.type();
-            if (type.equals(InternalMessage.REPLY_MESSAGE_TYPE)) {
-                try {
-                    AsyncResponse<?> futureResponse =
-                        NettyMessagingService.this.responseFutures.getIfPresent(message.id());
-                    if (futureResponse != null) {
-                        futureResponse.setResponse(message.payload());
-                    }
-                    log.warn("Received a reply. But was unable to locate the request handle");
-                } finally {
-                    NettyMessagingService.this.responseFutures.invalidate(message.id());
-                }
-                return;
-            }
-            MessageHandler handler = NettyMessagingService.this.getMessageHandler(type);
-            handler.handle(message);
-        }
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java
deleted file mode 100644
index 95753e7..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import java.util.concurrent.TimeUnit;
-
-import org.onlab.onos.store.cluster.impl.MessageSerializer;
-import org.onlab.onos.store.messaging.Endpoint;
-import org.onlab.onos.store.messaging.Response;
-
-public final class SimpleClient {
-    private SimpleClient() {}
-
-    public static void main(String... args) throws Exception {
-        NettyMessagingService messaging = new TestNettyMessagingService(9081);
-        messaging.activate();
-
-        messaging.sendAsync(new Endpoint("localhost", 8080), "simple", "Hello World");
-        Response<String> response = messaging.sendAndReceive(new Endpoint("localhost", 8080), "echo", "Hello World");
-        System.out.println("Got back:" + response.get(2, TimeUnit.SECONDS));
-    }
-
-    public static class TestNettyMessagingService extends NettyMessagingService {
-        public TestNettyMessagingService(int port) throws Exception {
-            super(port);
-            MessageSerializer mgr = new MessageSerializer();
-            mgr.activate();
-            this.serializationService = mgr;
-        }
-    }
-}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java
deleted file mode 100644
index 1b331ba..0000000
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.onlab.onos.store.messaging.impl;
-
-import org.onlab.onos.store.cluster.impl.MessageSerializer;
-
-public final class SimpleServer {
-    private SimpleServer() {}
-
-    public static void main(String... args) throws Exception {
-        NettyMessagingService server = new TestNettyMessagingService();
-        server.activate();
-        server.registerHandler("simple", new LoggingHandler());
-        server.registerHandler("echo", new EchoHandler());
-    }
-
-    public static class TestNettyMessagingService extends NettyMessagingService {
-        protected TestNettyMessagingService() {
-            MessageSerializer mgr = new MessageSerializer();
-            mgr.activate();
-            this.serializationService = mgr;
-        }
-    }
-}
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/cluster/impl/ClusterCommunicationManagerTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/cluster/impl/ClusterCommunicationManagerTest.java
index 3d87fb1..44e5421 100644
--- a/core/store/dist/src/test/java/org/onlab/onos/store/cluster/impl/ClusterCommunicationManagerTest.java
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/cluster/impl/ClusterCommunicationManagerTest.java
@@ -7,7 +7,7 @@
 import org.onlab.onos.cluster.DefaultControllerNode;
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.store.cluster.messaging.impl.OnosClusterCommunicationManager;
-import org.onlab.onos.store.messaging.impl.NettyMessagingService;
+import org.onlab.netty.NettyMessagingService;
 import org.onlab.packet.IpPrefix;
 
 import java.util.concurrent.CountDownLatch;
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
index bc0a055..0b0ae37 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
@@ -9,6 +9,8 @@
 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.net.Annotations;
+import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.DefaultDevice;
 import org.onlab.onos.net.DefaultPort;
 import org.onlab.onos.net.Device;
@@ -16,6 +18,9 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
+import org.onlab.onos.net.device.DefaultDeviceDescription;
+import org.onlab.onos.net.device.DefaultPortDescription;
 import org.onlab.onos.net.device.DeviceDescription;
 import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceStore;
@@ -45,6 +50,7 @@
 import static org.onlab.onos.net.device.DeviceEvent.Type.*;
 import static org.slf4j.LoggerFactory.getLogger;
 import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
+import static org.onlab.onos.net.DefaultAnnotations.merge;
 
 // TODO: synchronization should be done in more fine-grained manner.
 /**
@@ -112,8 +118,8 @@
             = createIfAbsentUnchecked(providerDescs, providerId,
                     new InitDeviceDescs(deviceDescription));
 
+        // update description
         descs.putDeviceDesc(deviceDescription);
-
         Device newDevice = composeDevice(deviceId, providerDescs);
 
         if (oldDevice == null) {
@@ -144,7 +150,8 @@
 
         // We allow only certain attributes to trigger update
         if (!Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
-            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion())) {
+            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion()) ||
+            !isAnnotationsEqual(oldDevice.annotations(), newDevice.annotations())) {
 
             synchronized (this) {
                 devices.replace(newDevice.id(), oldDevice, newDevice);
@@ -203,7 +210,7 @@
                 PortNumber number = portDescription.portNumber();
                 Port oldPort = ports.get(number);
                 // update description
-                descs.putPortDesc(number, portDescription);
+                descs.putPortDesc(portDescription);
                 Port newPort = composePort(device, number, descsMap);
 
                 events.add(oldPort == null ?
@@ -225,12 +232,14 @@
         return new DeviceEvent(PORT_ADDED, device, newPort);
     }
 
-    // CHecks if the specified port requires update and if so, it replaces the
+    // 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 oldPort,
                                    Port newPort,
                                    ConcurrentMap<PortNumber, Port> ports) {
-        if (oldPort.isEnabled() != newPort.isEnabled()) {
+        if (oldPort.isEnabled() != newPort.isEnabled() ||
+            !isAnnotationsEqual(oldPort.annotations(), newPort.annotations())) {
+
             ports.put(oldPort.number(), newPort);
             return new DeviceEvent(PORT_UPDATED, device, newPort);
         }
@@ -272,17 +281,17 @@
         checkArgument(descsMap != null, DEVICE_NOT_FOUND, deviceId);
 
         DeviceDescriptions descs = descsMap.get(providerId);
+        // assuming all providers must to give DeviceDescription
         checkArgument(descs != null,
                 "Device description for Device ID %s from Provider %s was not found",
                 deviceId, providerId);
 
-        // TODO: implement multi-provider
         synchronized (this) {
             ConcurrentMap<PortNumber, Port> ports = getPortMap(deviceId);
             final PortNumber number = portDescription.portNumber();
             Port oldPort = ports.get(number);
             // update description
-            descs.putPortDesc(number, portDescription);
+            descs.putPortDesc(portDescription);
             Port newPort = composePort(device, number, descsMap);
             if (oldPort == null) {
                 return createPort(device, newPort, ports);
@@ -321,6 +330,26 @@
         }
     }
 
+    private static boolean isAnnotationsEqual(Annotations lhs, Annotations rhs) {
+        if (lhs == rhs) {
+            return true;
+        }
+        if (lhs == null || rhs == null) {
+            return false;
+        }
+
+        if (!lhs.keys().equals(rhs.keys())) {
+            return false;
+        }
+
+        for (String key : lhs.keys()) {
+            if (!lhs.value(key).equals(rhs.value(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns a Device, merging description given from multiple Providers.
      *
@@ -336,46 +365,67 @@
         ProviderId primary = pickPrimaryPID(providerDescs);
 
         DeviceDescriptions desc = providerDescs.get(primary);
+
+        // base
         Type type = desc.getDeviceDesc().type();
         String manufacturer = desc.getDeviceDesc().manufacturer();
         String hwVersion = desc.getDeviceDesc().hwVersion();
         String swVersion = desc.getDeviceDesc().swVersion();
         String serialNumber = desc.getDeviceDesc().serialNumber();
+        DefaultAnnotations annotations = DefaultAnnotations.builder().build();
+        annotations = merge(annotations, desc.getDeviceDesc().annotations());
 
         for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
             if (e.getKey().equals(primary)) {
                 continue;
             }
-            // FIXME: implement attribute merging once we have K-V attributes
+            // TODO: should keep track of Description timestamp
+            // and only merge conflicting keys when timestamp is newer
+            // Currently assuming there will never be a key conflict between
+            // providers
+
+            // annotation merging. not so efficient, should revisit later
+            annotations = merge(annotations, e.getValue().getDeviceDesc().annotations());
         }
 
-        return new DefaultDevice(primary, deviceId , type, manufacturer, hwVersion, swVersion, serialNumber);
+        return new DefaultDevice(primary, deviceId , type, manufacturer,
+                            hwVersion, swVersion, serialNumber, annotations);
     }
 
-    // probably want composePorts
+    // probably want composePort"s" also
     private Port composePort(Device device, PortNumber number,
                 ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs) {
 
         ProviderId primary = pickPrimaryPID(providerDescs);
         DeviceDescriptions primDescs = providerDescs.get(primary);
+        // if no primary, assume not enabled
+        // TODO: revisit this default port enabled/disabled behavior
+        boolean isEnabled = false;
+        DefaultAnnotations annotations = DefaultAnnotations.builder().build();
+
         final PortDescription portDesc = primDescs.getPortDesc(number);
-        boolean isEnabled;
         if (portDesc != null) {
             isEnabled = portDesc.isEnabled();
-        } else {
-            // if no primary, assume not enabled
-            // TODO: revisit this port enabled/disabled behavior
-            isEnabled = false;
+            annotations = merge(annotations, portDesc.annotations());
         }
 
         for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
             if (e.getKey().equals(primary)) {
                 continue;
             }
-            // FIXME: implement attribute merging once we have K-V attributes
+            // TODO: should keep track of Description timestamp
+            // and only merge conflicting keys when timestamp is newer
+            // Currently assuming there will never be a key conflict between
+            // providers
+
+            // annotation merging. not so efficient, should revisit later
+            final PortDescription otherPortDesc = e.getValue().getPortDesc(number);
+            if (otherPortDesc != null) {
+                annotations = merge(annotations, otherPortDesc.annotations());
+            }
         }
 
-        return new DefaultPort(device, number, isEnabled);
+        return new DefaultPort(device, number, isEnabled, annotations);
     }
 
     /**
@@ -428,7 +478,7 @@
         private final ConcurrentMap<PortNumber, PortDescription> portDescs;
 
         public DeviceDescriptions(DeviceDescription desc) {
-            this.deviceDesc = new AtomicReference<>(desc);
+            this.deviceDesc = new AtomicReference<>(checkNotNull(desc));
             this.portDescs = new ConcurrentHashMap<>();
         }
 
@@ -444,12 +494,38 @@
             return Collections.unmodifiableCollection(portDescs.values());
         }
 
-        public DeviceDescription putDeviceDesc(DeviceDescription newDesc) {
-            return deviceDesc.getAndSet(newDesc);
+        /**
+         * Puts DeviceDescription, merging annotations as necessary.
+         *
+         * @param newDesc new DeviceDescription
+         * @return previous DeviceDescription
+         */
+        public synchronized DeviceDescription putDeviceDesc(DeviceDescription newDesc) {
+            DeviceDescription oldOne = deviceDesc.get();
+            DeviceDescription newOne = newDesc;
+            if (oldOne != null) {
+                SparseAnnotations merged = merge(oldOne.annotations(),
+                                                 newDesc.annotations());
+                newOne = new DefaultDeviceDescription(newOne, merged);
+            }
+            return deviceDesc.getAndSet(newOne);
         }
 
-        public PortDescription putPortDesc(PortNumber number, PortDescription newDesc) {
-            return portDescs.put(number, newDesc);
+        /**
+         * Puts PortDescription, merging annotations as necessary.
+         *
+         * @param newDesc new PortDescription
+         * @return previous PortDescription
+         */
+        public synchronized PortDescription putPortDesc(PortDescription newDesc) {
+            PortDescription oldOne = portDescs.get(newDesc.portNumber());
+            PortDescription newOne = newDesc;
+            if (oldOne != null) {
+                SparseAnnotations merged = merge(oldOne.annotations(),
+                                                 newDesc.annotations());
+                newOne = new DefaultPortDescription(newOne, merged);
+            }
+            return portDescs.put(newOne.portNumber(), newOne);
         }
     }
 }
diff --git a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
index 431fba3..a0d6e1c 100644
--- a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
+++ b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
@@ -22,10 +22,13 @@
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.onlab.onos.net.Annotations;
+import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
 import org.onlab.onos.net.device.DefaultDeviceDescription;
 import org.onlab.onos.net.device.DefaultPortDescription;
 import org.onlab.onos.net.device.DeviceDescription;
@@ -57,6 +60,23 @@
     private static final PortNumber P2 = PortNumber.portNumber(2);
     private static final PortNumber P3 = PortNumber.portNumber(3);
 
+    private static final SparseAnnotations A1 = DefaultAnnotations.builder()
+            .set("A1", "a1")
+            .set("B1", "b1")
+            .build();
+    private static final SparseAnnotations A1_2 = DefaultAnnotations.builder()
+            .remove("A1")
+            .set("B3", "b3")
+            .build();
+    private static final SparseAnnotations A2 = DefaultAnnotations.builder()
+            .set("A2", "a2")
+            .set("B2", "b2")
+            .build();
+    private static final SparseAnnotations A2_2 = DefaultAnnotations.builder()
+            .remove("A2")
+            .set("B4", "b4")
+            .build();
+
     private SimpleDeviceStore simpleDeviceStore;
     private DeviceStore deviceStore;
 
@@ -106,6 +126,24 @@
         assertEquals(SN, device.serialNumber());
     }
 
+    /**
+     * Verifies that Annotations created by merging {@code annotations} is
+     * equal to actual Annotations.
+     *
+     * @param actual Annotations to check
+     * @param annotations
+     */
+    private static void assertAnnotationsEquals(Annotations actual, SparseAnnotations... annotations) {
+        DefaultAnnotations expected = DefaultAnnotations.builder().build();
+        for (SparseAnnotations a : annotations) {
+            expected = DefaultAnnotations.merge(expected, a);
+        }
+        assertEquals(expected.keys(), actual.keys());
+        for (String key : expected.keys()) {
+            assertEquals(expected.value(key), actual.value(key));
+        }
+    }
+
     @Test
     public final void testGetDeviceCount() {
         assertEquals("initialy empty", 0, deviceStore.getDeviceCount());
@@ -171,26 +209,41 @@
     public final void testCreateOrUpdateDeviceAncillary() {
         DeviceDescription description =
                 new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW1, SN);
+                        HW, SW1, SN, A2);
         DeviceEvent event = deviceStore.createOrUpdateDevice(PIDA, DID1, description);
         assertEquals(DEVICE_ADDED, event.type());
         assertDevice(DID1, SW1, event.subject());
         assertEquals(PIDA, event.subject().providerId());
+        assertAnnotationsEquals(event.subject().annotations(), A2);
         assertFalse("Ancillary will not bring device up", deviceStore.isAvailable(DID1));
 
         DeviceDescription description2 =
                 new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW2, SN);
+                        HW, SW2, SN, A1);
         DeviceEvent event2 = deviceStore.createOrUpdateDevice(PID, DID1, description2);
         assertEquals(DEVICE_UPDATED, event2.type());
         assertDevice(DID1, SW2, event2.subject());
         assertEquals(PID, event2.subject().providerId());
+        assertAnnotationsEquals(event2.subject().annotations(), A1, A2);
         assertTrue(deviceStore.isAvailable(DID1));
 
         assertNull("No change expected", deviceStore.createOrUpdateDevice(PID, DID1, description2));
 
         // For now, Ancillary is ignored once primary appears
         assertNull("No change expected", deviceStore.createOrUpdateDevice(PIDA, DID1, description));
+
+        // But, Ancillary annotations will be in effect
+        DeviceDescription description3 =
+                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
+                        HW, SW1, SN, A2_2);
+        DeviceEvent event3 = deviceStore.createOrUpdateDevice(PIDA, DID1, description3);
+        assertEquals(DEVICE_UPDATED, event3.type());
+        // basic information will be the one from Primary
+        assertDevice(DID1, SW2, event3.subject());
+        assertEquals(PID, event3.subject().providerId());
+        // but annotation from Ancillary will be merged
+        assertAnnotationsEquals(event3.subject().annotations(), A1, A2, A2_2);
+        assertTrue(deviceStore.isAvailable(DID1));
     }
 
 
@@ -299,27 +352,40 @@
         putDeviceAncillary(DID1, SW1);
         putDevice(DID1, SW1);
         List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true)
+                new DefaultPortDescription(P1, true, A1)
                 );
         deviceStore.updatePorts(PID, DID1, pds);
 
         DeviceEvent event = deviceStore.updatePortStatus(PID, DID1,
-                new DefaultPortDescription(P1, false));
+                new DefaultPortDescription(P1, false, A1_2));
         assertEquals(PORT_UPDATED, event.type());
         assertDevice(DID1, SW1, event.subject());
         assertEquals(P1, event.port().number());
+        assertAnnotationsEquals(event.port().annotations(), A1, A1_2);
         assertFalse("Port is disabled", event.port().isEnabled());
 
         DeviceEvent event2 = deviceStore.updatePortStatus(PIDA, DID1,
                 new DefaultPortDescription(P1, true));
         assertNull("Ancillary is ignored if primary exists", event2);
 
+        // but, Ancillary annotation update will be notified
         DeviceEvent event3 = deviceStore.updatePortStatus(PIDA, DID1,
-                new DefaultPortDescription(P2, true));
-        assertEquals(PORT_ADDED, event3.type());
+                new DefaultPortDescription(P1, true, A2));
+        assertEquals(PORT_UPDATED, event3.type());
         assertDevice(DID1, SW1, event3.subject());
-        assertEquals(P2, event3.port().number());
-        assertFalse("Port is disabled if not given from provider", event3.port().isEnabled());
+        assertEquals(P1, event3.port().number());
+        assertAnnotationsEquals(event3.port().annotations(), A1, A1_2, A2);
+        assertFalse("Port is disabled", event3.port().isEnabled());
+
+        // port only reported from Ancillary will be notified as down
+        DeviceEvent event4 = deviceStore.updatePortStatus(PIDA, DID1,
+                new DefaultPortDescription(P2, true));
+        assertEquals(PORT_ADDED, event4.type());
+        assertDevice(DID1, SW1, event4.subject());
+        assertEquals(P2, event4.port().number());
+        assertAnnotationsEquals(event4.port().annotations());
+        assertFalse("Port is disabled if not given from primary provider",
+                        event4.port().isEnabled());
     }
 
     @Test
diff --git a/pom.xml b/pom.xml
index 3124467..56bbd74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,19 @@
             </dependency>
 
             <dependency>
+                <groupId>org.hamcrest</groupId>
+                <artifactId>hamcrest-core</artifactId>
+                <version>1.3</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.hamcrest</groupId>
+                <artifactId>hamcrest-library</artifactId>
+                <version>1.3</version>
+                <scope>test</scope>
+            </dependency>
+
+            <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-api</artifactId>
                 <version>1.7.6</version>
@@ -98,16 +111,16 @@
                 <version>3.3.2</version>
             </dependency>
 
-	    <dependency>
-	      <groupId>org.codehaus.jackson</groupId>
-	      <artifactId>jackson-core-asl</artifactId>
-	      <version>1.9.13</version>
-	    </dependency>
-	    <dependency>
-	      <groupId>org.codehaus.jackson</groupId>
-	      <artifactId>jackson-mapper-asl</artifactId>
-	      <version>1.9.13</version>
-	    </dependency>
+            <dependency>
+                <groupId>org.codehaus.jackson</groupId>
+                <artifactId>jackson-core-asl</artifactId>
+                <version>1.9.13</version>
+            </dependency>
+            <dependency>
+                <groupId>org.codehaus.jackson</groupId>
+                <artifactId>jackson-mapper-asl</artifactId>
+                <version>1.9.13</version>
+            </dependency>
 
 
             <!-- Web related -->
@@ -244,6 +257,14 @@
             <artifactId>junit</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-library</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-jdk14</artifactId>
         </dependency>
@@ -320,6 +341,35 @@
                 </plugin>
 
                 <!-- TODO: add findbugs plugin for static code analysis; for explicit invocation only -->
+                <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+                <plugin>
+                        <groupId>org.eclipse.m2e</groupId>
+                        <artifactId>lifecycle-mapping</artifactId>
+                        <version>1.0.0</version>
+                        <configuration>
+                                <lifecycleMappingMetadata>
+                                        <pluginExecutions>
+                                                <pluginExecution>
+                                                        <pluginExecutionFilter>
+                                                                <groupId>org.jacoco</groupId>
+                                                                <artifactId>
+                                                                        jacoco-maven-plugin
+                                                                </artifactId>
+                                                                <versionRange>
+                                                                        [0.7.1.201405082137,)
+                                                                </versionRange>
+                                                                <goals>
+                                                                        <goal>prepare-agent</goal>
+                                                                </goals>
+                                                        </pluginExecutionFilter>
+                                                        <action>
+                                                                <ignore></ignore>
+                                                        </action>
+                                                </pluginExecution>
+                                        </pluginExecutions>
+                                </lifecycleMappingMetadata>
+                        </configuration>
+                </plugin>
             </plugins>
         </pluginManagement>
 
diff --git a/tools/build/conf/pom.xml b/tools/build/conf/pom.xml
index c2ad09c..4865705 100644
--- a/tools/build/conf/pom.xml
+++ b/tools/build/conf/pom.xml
@@ -6,5 +6,10 @@
   <groupId>org.onlab.tools</groupId>
   <artifactId>onos-build-conf</artifactId>
   <version>1.0</version>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
 </project>
 
diff --git a/tools/test/bin/onos-fetch-vms b/tools/test/bin/onos-fetch-vms
new file mode 100755
index 0000000..2fd4602
--- /dev/null
+++ b/tools/test/bin/onos-fetch-vms
@@ -0,0 +1,11 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Remotely fetches the ONOS test VMs from a local share into ~/Downloads.
+#-------------------------------------------------------------------------------
+
+[ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
+. $ONOS_ROOT/tools/build/envDefaults
+
+mkdir -p /tmp/onos
+mount -t smbfs smb://guest:@10.254.1.15/onos /tmp/onos
+cp /tmp/onos/*.ova ~/Downloads
diff --git a/utils/netty/pom.xml b/utils/netty/pom.xml
new file mode 100644
index 0000000..d335117
--- /dev/null
+++ b/utils/netty/pom.xml
@@ -0,0 +1,49 @@
+<?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>onlab-utils</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onlab-netty</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>Network I/O using Netty framework</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava-testlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-misc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onlab.onos</groupId>
+            <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>de.javakaffee</groupId>
+            <artifactId>kryo-serializers</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-pool</groupId>
+            <artifactId>commons-pool</artifactId>
+            <version>1.6</version>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/utils/pom.xml b/utils/pom.xml
index 2beeba8..feb60e9 100644
--- a/utils/pom.xml
+++ b/utils/pom.xml
@@ -19,6 +19,7 @@
     <modules>
         <module>junit</module>
         <module>misc</module>
+        <module>netty</module>
         <module>nio</module>
         <module>osgi</module>
         <module>rest</module>