Merge "Prototype bandwidth reservation"
diff --git a/apps/optical/src/main/resources/demo-3-roadm-2-ps.json b/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
index 05c4124..125a307 100644
--- a/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
+++ b/apps/optical/src/main/resources/demo-3-roadm-2-ps.json
@@ -76,7 +76,7 @@
                 "nodeName1": "ROUTER1",
                 "nodeName2": "ROADM1",
                 "bandWidth": 100000,
-                "port1": 1,
+                "port1": 2,
                 "port2": 10
             },
             "type": "pktOptLink"
@@ -90,7 +90,7 @@
                 "nodeName1": "ROUTER2",
                 "nodeName2": "ROADM2",
                 "bandWidth": 100000,
-                "port1": 1,
+                "port1": 2,
                 "port2": 11
             },
             "type": "pktOptLink"
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
index b585285..1d32aa5 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/DevicePortsListCommand.java
@@ -40,7 +40,7 @@
          description = "Lists all ports or all ports of a device")
 public class DevicePortsListCommand extends DevicesListCommand {
 
-    private static final String FMT = "  port=%s, state=%s%s";
+    private static final String FMT = "  port=%s, state=%s, type=%s, speed=%s%s";
 
     @Option(name = "-e", aliases = "--enabled", description = "Show only enabled ports",
             required = false, multiValued = false)
@@ -110,10 +110,14 @@
                 ports.add(mapper.createObjectNode()
                                   .put("port", port.number().toString())
                                   .put("isEnabled", port.isEnabled())
+                                  .put("type", port.type().toString().toLowerCase())
+                                  .put("portSpeed", port.portSpeed())
                                   .set("annotations", annotations(mapper, port.annotations())));
             }
         }
-        return result.put("device", device.id().toString()).set("ports", ports);
+        result.set("device", json(service, mapper, device));
+        result.set("ports", ports);
+        return result;
     }
 
     // Determines if a port should be included in output.
@@ -130,6 +134,7 @@
         for (Port port : ports) {
             if (isIncluded(port)) {
                 print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled",
+                      port.type().toString().toLowerCase(), port.portSpeed(),
                       annotations(port.annotations()));
             }
         }
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
index 9d4f8a8..d9cd764 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultPort.java
@@ -24,9 +24,14 @@
  */
 public class DefaultPort extends AbstractAnnotated implements Port {
 
+    /** Default port speed in Mbps. */
+    public static final long DEFAULT_SPEED = 1_000;
+
     private final Element element;
     private final PortNumber number;
     private final boolean isEnabled;
+    private final Type type;
+    private final long portSpeed;
 
     /**
      * Creates a network element attributed to the specified provider.
@@ -36,40 +41,35 @@
      * @param isEnabled   indicator whether the port is up and active
      * @param annotations optional key/value annotations
      */
-    public DefaultPort(Element element, PortNumber number,
-                       boolean isEnabled, Annotations... annotations) {
+    public DefaultPort(Element element, PortNumber number, boolean isEnabled,
+                       Annotations... annotations) {
+        this(element, number, isEnabled, Type.COPPER, DEFAULT_SPEED, annotations);
+    }
+
+    /**
+     * Creates a network element attributed to the specified provider.
+     *
+     * @param element     parent network element
+     * @param number      port number
+     * @param isEnabled   indicator whether the port is up and active
+     * @param type        port type
+     * @param portSpeed   port speed in Mbs
+     * @param annotations optional key/value annotations
+     */
+    public DefaultPort(Element element, PortNumber number, boolean isEnabled,
+                       Type type, long portSpeed, Annotations... annotations) {
         super(annotations);
         this.element = element;
         this.number = number;
         this.isEnabled = isEnabled;
+        this.type = type;
+        this.portSpeed = portSpeed;
+
     }
 
     @Override
-    public int hashCode() {
-        return Objects.hash(number, isEnabled);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj instanceof DefaultPort) {
-            final DefaultPort other = (DefaultPort) obj;
-            return Objects.equals(this.element.id(), other.element.id()) &&
-                    Objects.equals(this.number, other.number) &&
-                    Objects.equals(this.isEnabled, other.isEnabled);
-        }
-        return false;
-    }
-
-    @Override
-    public String toString() {
-        return toStringHelper(this)
-                .add("element", element.id())
-                .add("number", number)
-                .add("isEnabled", isEnabled)
-                .toString();
+    public Element element() {
+        return element;
     }
 
     @Override
@@ -83,8 +83,45 @@
     }
 
     @Override
-    public Element element() {
-        return element;
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public long portSpeed() {
+        return portSpeed;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(number, isEnabled, type, portSpeed);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DefaultPort) {
+            final DefaultPort other = (DefaultPort) obj;
+            return Objects.equals(this.element.id(), other.element.id()) &&
+                    Objects.equals(this.number, other.number) &&
+                    Objects.equals(this.isEnabled, other.isEnabled) &&
+                    Objects.equals(this.type, other.type) &&
+                    Objects.equals(this.portSpeed, other.portSpeed);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("element", element.id())
+                .add("number", number)
+                .add("isEnabled", isEnabled)
+                .add("type", type)
+                .add("portSpeed", portSpeed)
+                .toString();
     }
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/Port.java b/core/api/src/main/java/org/onlab/onos/net/Port.java
index 7bf6af3..2153593 100644
--- a/core/api/src/main/java/org/onlab/onos/net/Port.java
+++ b/core/api/src/main/java/org/onlab/onos/net/Port.java
@@ -21,6 +21,26 @@
  */
 public interface Port extends Annotated {
 
+    /** Represents coarse port type classification. */
+    public enum Type {
+        /**
+         * Signifies copper-based connectivity.
+         */
+        COPPER,
+
+        /**
+         * Signifies optical fiber-based connectivity.
+         */
+        FIBER
+    }
+
+    /**
+     * Returns the parent network element to which this port belongs.
+     *
+     * @return parent network element
+     */
+    Element element();
+
     /**
      * Returns the port number.
      *
@@ -36,12 +56,18 @@
     boolean isEnabled();
 
     /**
-     * Returns the parent network element to which this port belongs.
+     * Returns the port type.
      *
-     * @return parent network element
+     * @return port type
      */
-    Element element();
+    Type type();
 
-    // set of port attributes
+    /**
+     * Returns the current port speed in Mbps.
+     *
+     * @return current port speed
+     */
+    long portSpeed();
 
+    // TODO: more attributes?
 }
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 47e3280..9688827 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
@@ -15,11 +15,12 @@
  */
 package org.onlab.onos.net.device;
 
+import com.google.common.base.MoreObjects;
 import org.onlab.onos.net.AbstractDescription;
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.SparseAnnotations;
 
-import com.google.common.base.MoreObjects;
+import static org.onlab.onos.net.Port.Type;
 
 /**
  * Default implementation of immutable port description.
@@ -27,32 +28,62 @@
 public class DefaultPortDescription extends AbstractDescription
         implements PortDescription {
 
+    private static final long DEFAULT_SPEED = 1_000;
+
     private final PortNumber number;
     private final boolean isEnabled;
+    private final Type type;
+    private final long portSpeed;
 
     /**
      * Creates a port description using the supplied information.
      *
-     * @param number       port number
-     * @param isEnabled    port enabled state
-     * @param annotations  optional key/value annotations map
+     * @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;
+                                  SparseAnnotations... annotations) {
+        this(number, isEnabled, Type.COPPER, DEFAULT_SPEED, annotations);
     }
 
     /**
      * Creates a port description using the supplied information.
      *
-     * @param base         PortDescription to get basic information from
-     * @param annotations  optional key/value annotations map
+     * @param number      port number
+     * @param isEnabled   port enabled state
+     * @param type        port type
+     * @param portSpeed   port speed in Mbps
+     * @param annotations optional key/value annotations map
+     */
+    public DefaultPortDescription(PortNumber number, boolean isEnabled,
+                                  Type type, long portSpeed,
+                                  SparseAnnotations...annotations) {
+        super(annotations);
+        this.number = number;
+        this.isEnabled = isEnabled;
+        this.type = type;
+        this.portSpeed = portSpeed;
+    }
+
+    // Default constructor for serialization
+    private DefaultPortDescription() {
+        this.number = null;
+        this.isEnabled = false;
+        this.portSpeed = DEFAULT_SPEED;
+        this.type = Type.COPPER;
+    }
+
+    /**
+     * 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);
+                                  SparseAnnotations annotations) {
+        this(base.portNumber(), base.isEnabled(), base.type(), base.portSpeed(),
+             annotations);
     }
 
     @Override
@@ -66,17 +97,24 @@
     }
 
     @Override
+    public Type type() {
+        return type;
+    }
+
+    @Override
+    public long portSpeed() {
+        return portSpeed;
+    }
+
+    @Override
     public String toString() {
         return MoreObjects.toStringHelper(getClass())
                 .add("number", number)
                 .add("isEnabled", isEnabled)
+                .add("type", type)
+                .add("portSpeed", portSpeed)
                 .add("annotations", annotations())
                 .toString();
     }
 
-    // default constructor for serialization
-    private DefaultPortDescription() {
-        this.number = null;
-        this.isEnabled = false;
-    }
 }
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 cdee005..b134d83 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
@@ -18,13 +18,13 @@
 import org.onlab.onos.net.Description;
 import org.onlab.onos.net.PortNumber;
 
+import static org.onlab.onos.net.Port.Type;
+
 /**
  * Information about a port.
  */
 public interface PortDescription extends Description {
 
-    // TODO: possibly relocate this to a common ground so that this can also used by host tracking if required
-
     /**
      * Returns the port number.
      *
@@ -39,4 +39,18 @@
      */
     boolean isEnabled();
 
+    /**
+     * Returns the port type.
+     *
+     * @return port type
+     */
+    Type type();
+
+    /**
+     * Returns the current port speed in Mbps.
+     *
+     * @return current port speed
+     */
+    long portSpeed();
+
 }
diff --git a/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java b/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java
index 3d3d3d3..ebc3107 100644
--- a/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/DefaultPortTest.java
@@ -23,6 +23,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.onlab.onos.net.Device.Type.SWITCH;
 import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Port.Type.COPPER;
+import static org.onlab.onos.net.Port.Type.FIBER;
 import static org.onlab.onos.net.PortNumber.portNumber;
 
 /**
@@ -35,15 +37,16 @@
     private static final DeviceId DID2 = deviceId("of:bar");
     private static final PortNumber P1 = portNumber(1);
     private static final PortNumber P2 = portNumber(2);
+    private static final long SP1 = 1_000_000;
 
     @Test
     public void testEquality() {
         Device device = new DefaultDevice(PID, DID1, SWITCH, "m", "h", "s", "n",
                                           new ChassisId());
-        Port p1 = new DefaultPort(device, portNumber(1), true);
-        Port p2 = new DefaultPort(device, portNumber(1), true);
-        Port p3 = new DefaultPort(device, portNumber(2), true);
-        Port p4 = new DefaultPort(device, portNumber(2), true);
+        Port p1 = new DefaultPort(device, portNumber(1), true, COPPER, SP1);
+        Port p2 = new DefaultPort(device, portNumber(1), true, COPPER, SP1);
+        Port p3 = new DefaultPort(device, portNumber(2), true, FIBER, SP1);
+        Port p4 = new DefaultPort(device, portNumber(2), true, FIBER, SP1);
         Port p5 = new DefaultPort(device, portNumber(1), false);
 
         new EqualsTester().addEqualityGroup(p1, p2)
@@ -56,10 +59,12 @@
     public void basics() {
         Device device = new DefaultDevice(PID, DID1, SWITCH, "m", "h", "s", "n",
                                           new ChassisId());
-        Port port = new DefaultPort(device, portNumber(1), true);
+        Port port = new DefaultPort(device, portNumber(1), true, FIBER, SP1);
         assertEquals("incorrect element", device, port.element());
         assertEquals("incorrect number", portNumber(1), port.number());
         assertEquals("incorrect state", true, port.isEnabled());
+        assertEquals("incorrect speed", SP1, port.portSpeed());
+        assertEquals("incorrect type", FIBER, port.type());
     }
 
 }
diff --git a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
index 6180ada..0989867 100644
--- a/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
+++ b/core/net/src/main/java/org/onlab/onos/cluster/impl/MastershipManager.java
@@ -269,10 +269,6 @@
 
         @Override
         public void notify(MastershipEvent event) {
-            if (clusterService.getLocalNode().id().equals(event.roleInfo().master())) {
-                log.info("ignoring locally-generated event {}", event);
-               // return;
-            }
             log.info("dispatching mastership event {}", event);
             eventDispatcher.post(event);
         }
diff --git a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
index 2441e88..7759550 100644
--- a/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/device/impl/DeviceManager.java
@@ -67,8 +67,8 @@
 @Component(immediate = true)
 @Service
 public class DeviceManager
-    extends AbstractProviderRegistry<DeviceProvider, DeviceProviderService>
-    implements DeviceService, DeviceAdminService, DeviceProviderRegistry {
+        extends AbstractProviderRegistry<DeviceProvider, DeviceProviderService>
+        implements DeviceService, DeviceAdminService, DeviceProviderRegistry {
 
     private static final String DEVICE_ID_NULL = "Device ID cannot be null";
     private static final String PORT_NUMBER_NULL = "Port number cannot be null";
@@ -227,8 +227,8 @@
 
     // Personalized device provider service issued to the supplied provider.
     private class InternalDeviceProviderService
-    extends AbstractProviderService<DeviceProvider>
-    implements DeviceProviderService {
+            extends AbstractProviderService<DeviceProvider>
+            implements DeviceProviderService {
 
         InternalDeviceProviderService(DeviceProvider provider) {
             super(provider);
@@ -236,7 +236,7 @@
 
         @Override
         public void deviceConnected(DeviceId deviceId,
-                DeviceDescription deviceDescription) {
+                                    DeviceDescription deviceDescription) {
             checkNotNull(deviceId, DEVICE_ID_NULL);
             checkNotNull(deviceDescription, DEVICE_DESCRIPTION_NULL);
             checkValidity();
@@ -267,7 +267,7 @@
             deviceClockProviderService.setMastershipTerm(deviceId, term);
 
             DeviceEvent event = store.createOrUpdateDevice(provider().id(),
-                    deviceId, deviceDescription);
+                                                           deviceId, deviceDescription);
 
             // If there was a change of any kind, tell the provider
             // that this instance is the master.
@@ -337,14 +337,14 @@
 
         @Override
         public void updatePorts(DeviceId deviceId,
-                List<PortDescription> portDescriptions) {
+                                List<PortDescription> portDescriptions) {
             checkNotNull(deviceId, DEVICE_ID_NULL);
             checkNotNull(portDescriptions,
-                    "Port descriptions list cannot be null");
+                         "Port descriptions list cannot be null");
             checkValidity();
 
             List<DeviceEvent> events = store.updatePorts(this.provider().id(),
-                    deviceId, portDescriptions);
+                                                         deviceId, portDescriptions);
             for (DeviceEvent event : events) {
                 post(event);
             }
@@ -352,13 +352,13 @@
 
         @Override
         public void portStatusChanged(DeviceId deviceId,
-                PortDescription portDescription) {
+                                      PortDescription portDescription) {
             checkNotNull(deviceId, DEVICE_ID_NULL);
             checkNotNull(portDescription, PORT_DESCRIPTION_NULL);
             checkValidity();
 
             final DeviceEvent event = store.updatePortStatus(this.provider().id(),
-                        deviceId, portDescription);
+                                                             deviceId, portDescription);
             if (event != null) {
                 log.info("Device {} port {} status changed", deviceId, event
                         .port().number());
@@ -370,7 +370,7 @@
         public void unableToAssertRole(DeviceId deviceId, MastershipRole role) {
             // FIXME: implement response to this notification
             log.warn("Failed to assert role [{}] onto Device {}", role,
-                    deviceId);
+                     deviceId);
             if (role == MastershipRole.MASTER) {
                 mastershipService.relinquishMastership(deviceId);
                 // TODO: Shouldn't we be triggering event?
@@ -393,7 +393,7 @@
         // random cache size
         private final int cacheSize = 5;
         // temporarily stores term number + events to check for duplicates. A hack.
-        private HashMultimap<Integer, RoleInfo>  eventCache =
+        private HashMultimap<Integer, RoleInfo> eventCache =
                 HashMultimap.create();
 
         @Override
@@ -414,7 +414,7 @@
                 if (!myNodeId.equals(term.master())) {
                     // something went wrong in consistency, let go
                     log.warn("Mastership has changed after this event."
-                            + "Term Service suggests {} for {}", term, did);
+                                     + "Term Service suggests {} for {}", term, did);
                     // FIXME: Is it possible to let go of MASTER role
                     //        but remain on STANDBY list?
                     mastershipService.relinquishMastership(did);
@@ -432,15 +432,15 @@
                     if (!isReachable(device)) {
                         log.warn("Device {} has disconnected after this event", did);
                         mastershipService.relinquishMastership(did);
-                        applyRole(did, MastershipRole.STANDBY);
                         return;
                     }
                     //flag the device as online. Is there a better way to do this?
-                    DeviceEvent devEvent = store.createOrUpdateDevice(device.providerId(), did,
-                            new DefaultDeviceDescription(
-                                    did.uri(), device.type(), device.manufacturer(),
-                                    device.hwVersion(), device.swVersion(),
-                                    device.serialNumber(), device.chassisId()));
+                    DeviceEvent devEvent =
+                            store.createOrUpdateDevice(device.providerId(), did,
+                                                       new DefaultDeviceDescription(
+                                                               did.uri(), device.type(), device.manufacturer(),
+                                                               device.hwVersion(), device.swVersion(),
+                                                               device.serialNumber(), device.chassisId()));
                     post(devEvent);
                 }
                 applyRole(did, MastershipRole.MASTER);
@@ -448,6 +448,7 @@
                 if (!isReachable(getDevice(did))) {
                     log.warn("Device {} has disconnected after this event", did);
                     mastershipService.relinquishMastership(did);
+                    return;
                 }
                 applyRole(did, MastershipRole.STANDBY);
             }
@@ -455,26 +456,28 @@
 
         // checks for duplicate event, returning true if one is found.
         private boolean checkDuplicate(RoleInfo roleInfo, int term) {
-            synchronized (eventCache) {
-                if (eventCache.get(term).contains(roleInfo)) {
-                    log.info("duplicate event detected; ignoring");
-                    return true;
-                } else {
-                    eventCache.put(term, roleInfo);
-                    // purge by-term oldest entries to keep the cache size under limit
-                    if (eventCache.size() > cacheSize) {
-                        eventCache.removeAll(term - cacheSize);
-                    }
-                    return false;
-                }
-            }
+            // turning off duplicate check
+            return false;
+//            synchronized (eventCache) {
+//                if (eventCache.get(term).contains(roleInfo)) {
+//                    log.info("duplicate event detected; ignoring");
+//                    return true;
+//                } else {
+//                    eventCache.put(term, roleInfo);
+//                    // purge by-term oldest entries to keep the cache size under limit
+//                    if (eventCache.size() > cacheSize) {
+//                        eventCache.removeAll(term - cacheSize);
+//                    }
+//                    return false;
+//                }
+//            }
         }
 
     }
 
     // Store delegate to re-post events emitted from the store.
     private class InternalStoreDelegate
-    implements DeviceStoreDelegate {
+            implements DeviceStoreDelegate {
         @Override
         public void notify(DeviceEvent event) {
             post(event);
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
index f3ae7a4..5449277 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
@@ -543,8 +543,9 @@
                                    Port newPort,
                                    Map<PortNumber, Port> ports) {
         if (oldPort.isEnabled() != newPort.isEnabled() ||
-            !AnnotationsUtil.isEqual(oldPort.annotations(), newPort.annotations())) {
-
+                oldPort.type() != newPort.type() ||
+                oldPort.portSpeed() != newPort.portSpeed() ||
+                !AnnotationsUtil.isEqual(oldPort.annotations(), newPort.annotations())) {
             ports.put(oldPort.number(), newPort);
             return new DeviceEvent(PORT_UPDATED, device, newPort);
         }
@@ -867,7 +868,10 @@
             }
         }
 
-        return new DefaultPort(device, number, isEnabled, annotations);
+        return portDesc == null ?
+                new DefaultPort(device, number, false, annotations) :
+                new DefaultPort(device, number, isEnabled, portDesc.value().type(),
+                                portDesc.value().portSpeed(), annotations);
     }
 
     /**
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
index d1917d6..07120fd 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
@@ -358,6 +358,7 @@
 
         @Override
         public void entryAdded(EntryEvent<DeviceId, RoleValue> event) {
+            entryUpdated(event);
         }
 
         @Override
@@ -366,7 +367,6 @@
 
         @Override
         public void entryUpdated(EntryEvent<DeviceId, RoleValue> event) {
-            // this subsumes entryAdded event
             notifyDelegate(new MastershipEvent(
                     MASTER_CHANGED, event.getKey(), event.getValue().roleInfo()));
         }
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
index 4fc88b6..9341cba 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
@@ -22,11 +22,11 @@
 import java.util.HashSet;
 import java.util.LinkedList;
 
-import org.onlab.onos.core.DefaultApplicationId;
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.DefaultControllerNode;
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.cluster.RoleInfo;
+import org.onlab.onos.core.DefaultApplicationId;
 import org.onlab.onos.mastership.MastershipTerm;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultAnnotations;
@@ -59,6 +59,9 @@
 import org.onlab.onos.net.flow.criteria.Criteria;
 import org.onlab.onos.net.flow.criteria.Criterion;
 import org.onlab.onos.net.flow.instructions.Instructions;
+import org.onlab.onos.net.flow.instructions.L0ModificationInstruction;
+import org.onlab.onos.net.flow.instructions.L2ModificationInstruction;
+import org.onlab.onos.net.flow.instructions.L3ModificationInstruction;
 import org.onlab.onos.net.host.DefaultHostDescription;
 import org.onlab.onos.net.host.HostDescription;
 import org.onlab.onos.net.intent.ConnectivityIntent;
@@ -66,6 +69,7 @@
 import org.onlab.onos.net.intent.Intent;
 import org.onlab.onos.net.intent.IntentId;
 import org.onlab.onos.net.intent.IntentState;
+import org.onlab.onos.net.intent.LinkCollectionIntent;
 import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
 import org.onlab.onos.net.intent.PathIntent;
 import org.onlab.onos.net.intent.PointToPointIntent;
@@ -115,6 +119,7 @@
                     //
                     ControllerNode.State.class,
                     Device.Type.class,
+                    Port.Type.class,
                     ChassisId.class,
                     DefaultAnnotations.class,
                     DefaultControllerNode.class,
@@ -149,6 +154,17 @@
                     DefaultTrafficTreatment.class,
                     Instructions.DropInstruction.class,
                     Instructions.OutputInstruction.class,
+                    L0ModificationInstruction.class,
+                    L0ModificationInstruction.L0SubType.class,
+                    L0ModificationInstruction.ModLambdaInstruction.class,
+                    L2ModificationInstruction.class,
+                    L2ModificationInstruction.L2SubType.class,
+                    L2ModificationInstruction.ModEtherInstruction.class,
+                    L2ModificationInstruction.ModVlanIdInstruction.class,
+                    L2ModificationInstruction.ModVlanPcpInstruction.class,
+                    L3ModificationInstruction.class,
+                    L3ModificationInstruction.L3SubType.class,
+                    L3ModificationInstruction.ModIPInstruction.class,
                     RoleInfo.class,
                     FlowRuleBatchOperation.class,
                     CompletedBatchOperation.class,
@@ -163,7 +179,8 @@
                     DefaultEdgeLink.class,
                     HostToHostIntent.class,
                     PointToPointIntent.class,
-                    MultiPointToSinglePointIntent.class
+                    MultiPointToSinglePointIntent.class,
+                    LinkCollectionIntent.class
                     )
             .register(DefaultApplicationId.class, new DefaultApplicationIdSerializer())
             .register(URI.class, new URISerializer())
diff --git a/core/store/serializers/src/test/java/org/onlab/onos/store/serializers/KryoSerializerTest.java b/core/store/serializers/src/test/java/org/onlab/onos/store/serializers/KryoSerializerTest.java
index 4e7a8e0..a4f098a 100644
--- a/core/store/serializers/src/test/java/org/onlab/onos/store/serializers/KryoSerializerTest.java
+++ b/core/store/serializers/src/test/java/org/onlab/onos/store/serializers/KryoSerializerTest.java
@@ -15,13 +15,10 @@
  */
 package org.onlab.onos.store.serializers;
 
-import static org.junit.Assert.assertEquals;
-import static org.onlab.onos.net.DeviceId.deviceId;
-import static org.onlab.onos.net.PortNumber.portNumber;
-import static java.util.Arrays.asList;
-
-import java.nio.ByteBuffer;
-
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.EqualsTester;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -50,10 +47,12 @@
 import org.onlab.packet.MacAddress;
 import org.onlab.util.KryoNamespace;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.testing.EqualsTester;
+import java.nio.ByteBuffer;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
 
 public class KryoSerializerTest {
 
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 a4a47e2..0c7fb0c 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
@@ -291,8 +291,9 @@
                                    Port newPort,
                                    Map<PortNumber, Port> ports) {
         if (oldPort.isEnabled() != newPort.isEnabled() ||
+                oldPort.type() != newPort.type() ||
+                oldPort.portSpeed() != newPort.portSpeed() ||
                 !AnnotationsUtil.isEqual(oldPort.annotations(), newPort.annotations())) {
-
             ports.put(oldPort.number(), newPort);
             return new DeviceEvent(PORT_UPDATED, device, newPort);
         }
@@ -510,7 +511,10 @@
             }
         }
 
-        return new DefaultPort(device, number, isEnabled, annotations);
+        return portDesc == null ?
+                new DefaultPort(device, number, false, annotations) :
+                new DefaultPort(device, number, isEnabled, portDesc.type(),
+                                portDesc.portSpeed(), annotations);
     }
 
     /**
diff --git a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LLDPLinkProvider.java b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LLDPLinkProvider.java
index 50dceb3..24336aa 100644
--- a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LLDPLinkProvider.java
+++ b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LLDPLinkProvider.java
@@ -145,7 +145,7 @@
                     break;
                 case PORT_ADDED:
                 case PORT_UPDATED:
-                    if (event.port().isEnabled()) {
+                    if (port.isEnabled()) {
                         ld = discoverers.get(device.id());
                         if (ld == null) {
                             return;
@@ -155,6 +155,7 @@
                             ld.addPort(port);
                         }
                     } else {
+                        log.debug("Port down {}", port);
                         ConnectPoint point = new ConnectPoint(device.id(),
                                                               port.number());
                         providerService.linksVanished(point);
diff --git a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
index dd72420..ce6d4fb 100644
--- a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
+++ b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
@@ -15,6 +15,22 @@
  */
 package org.onlab.onos.provider.lldp.impl;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.onlab.onos.net.MastershipRole.MASTER;
+import static org.onlab.onos.net.PortNumber.portNumber;
+import static org.onlab.onos.net.flow.DefaultTrafficTreatment.builder;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
 import org.jboss.netty.util.Timeout;
 import org.jboss.netty.util.TimerTask;
 import org.onlab.onos.mastership.MastershipService;
@@ -36,22 +52,6 @@
 import org.onlab.util.Timer;
 import org.slf4j.Logger;
 
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static org.onlab.onos.net.MastershipRole.MASTER;
-import static org.onlab.onos.net.PortNumber.portNumber;
-import static org.onlab.onos.net.flow.DefaultTrafficTreatment.builder;
-import static org.slf4j.LoggerFactory.getLogger;
-
 /**
  * Run discovery process from a physical switch. Ports are initially labeled as
  * slow ports. When an LLDP is successfully received, label the remote port as
@@ -68,6 +68,7 @@
     // send 1 probe every probeRate milliseconds
     private final long probeRate;
     private final Set<Long> slowPorts;
+    // ports, known to have incoming links
     private final Set<Long> fastPorts;
     // number of unacknowledged probes per port
     private final Map<Long, AtomicInteger> portProbeCount;
@@ -125,6 +126,7 @@
             log.info("Using BDDP to discover network");
         }
 
+        this.isStopped = true;
         start();
         this.log.debug("Started discovery manager for switch {}",
                        device.id());
@@ -140,7 +142,10 @@
     public void addPort(final Port port) {
         this.log.debug("Sending init probe to port {}@{}",
                        port.number().toLong(), device.id());
-        sendProbes(port.number().toLong());
+        boolean isMaster = mastershipService.getLocalRole(device.id()) == MASTER;
+        if (isMaster) {
+            sendProbes(port.number().toLong());
+        }
         synchronized (this) {
             this.slowPorts.add(port.number().toLong());
         }
@@ -233,6 +238,13 @@
      */
     @Override
     public void run(final Timeout t) {
+        boolean isMaster = mastershipService.getLocalRole(device.id()) == MASTER;
+        if (!isMaster) {
+            // reschedule timer
+            timeout = Timer.getTimer().newTimeout(this, this.probeRate, MILLISECONDS);
+            return;
+        }
+
         this.log.trace("Sending probes from {}", device.id());
         synchronized (this) {
             final Iterator<Long> fastIterator = this.fastPorts.iterator();
@@ -245,6 +257,7 @@
                     sendProbes(portNumber);
 
                 } else {
+                    // Link down, demote to slowPorts
                     // Update fast and slow ports
                     fastIterator.remove();
                     this.slowPorts.add(portNumber);
@@ -274,8 +287,12 @@
     }
 
     public void start() {
-        timeout = Timer.getTimer().newTimeout(this, 0, MILLISECONDS);
-        isStopped = false;
+        if (isStopped) {
+            timeout = Timer.getTimer().newTimeout(this, 0, MILLISECONDS);
+            isStopped = false;
+        } else {
+            log.warn("LinkDiscovery started multiple times?");
+        }
     }
 
     /**
@@ -317,9 +334,9 @@
     }
 
     private void sendProbes(Long portNumber) {
-        boolean isMaster = mastershipService.getLocalRole(device.id()) == MASTER;
-        if (isMaster && device.type() != Device.Type.ROADM) {
-            log.debug("Sending probes out to {}@{}", portNumber, device.id());
+        // TODO: should have suppression port configuration, not by type
+        if (device.type() != Device.Type.ROADM) {
+            log.trace("Sending probes out to {}@{}", portNumber, device.id());
             OutboundPacket pkt = this.createOutBoundLLDP(portNumber);
             pktService.emit(pkt);
             if (useBDDP) {
diff --git a/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java b/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
index 6ba0fe3..8aa3d77 100644
--- a/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
+++ b/providers/openflow/device/src/main/java/org/onlab/onos/provider/of/device/impl/OpenFlowDeviceProvider.java
@@ -23,6 +23,7 @@
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DefaultDeviceDescription;
 import org.onlab.onos.net.device.DefaultPortDescription;
@@ -43,14 +44,19 @@
 import org.projectfloodlight.openflow.protocol.OFFactory;
 import org.projectfloodlight.openflow.protocol.OFPortConfig;
 import org.projectfloodlight.openflow.protocol.OFPortDesc;
+import org.projectfloodlight.openflow.protocol.OFPortFeatures;
 import org.projectfloodlight.openflow.protocol.OFPortState;
 import org.projectfloodlight.openflow.protocol.OFPortStatus;
+import org.projectfloodlight.openflow.protocol.OFVersion;
+import org.projectfloodlight.openflow.types.PortSpeed;
 import org.slf4j.Logger;
 
 import java.util.ArrayList;
 import java.util.List;
 
 import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Port.Type.COPPER;
+import static org.onlab.onos.net.Port.Type.FIBER;
 import static org.onlab.onos.openflow.controller.Dpid.dpid;
 import static org.onlab.onos.openflow.controller.Dpid.uri;
 import static org.slf4j.LoggerFactory.getLogger;
@@ -63,6 +69,7 @@
 public class OpenFlowDeviceProvider extends AbstractProvider implements DeviceProvider {
 
     private static final Logger LOG = getLogger(OpenFlowDeviceProvider.class);
+    private static final long MBPS = 1_000 * 1_000;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected DeviceProviderRegistry providerRegistry;
@@ -122,9 +129,9 @@
 
         OpenFlowSwitch sw = controller.getSwitch(dpid(device.id().uri()));
         //if (!checkChannel(device, sw)) {
-          //  LOG.error("Failed to probe device {} on sw={}", device, sw);
+        //  LOG.error("Failed to probe device {} on sw={}", device, sw);
         //  providerService.deviceDisconnected(device.id());
-            //return;
+        //return;
         //}
 
         // Prompt an update of port information. We can use any XID for this.
@@ -143,13 +150,13 @@
 
     // Checks if the OF channel is connected.
     //private boolean checkChannel(Device device, OpenFlowSwitch sw) {
-        // FIXME if possible, we might want this to be part of
-        // OpenFlowSwitch interface so the driver interface isn't misused.
+    // FIXME if possible, we might want this to be part of
+    // OpenFlowSwitch interface so the driver interface isn't misused.
     //    if (sw == null || !((OpenFlowSwitchDriver) sw).isConnected()) {
-      //      return false;
-  //      }
+    //      return false;
+    //      }
     //    return true;
-   // }
+    // }
 
     @Override
     public void roleChanged(Device device, MastershipRole newRole) {
@@ -188,7 +195,7 @@
                                                  sw.hardwareDescription(),
                                                  sw.softwareDescription(),
                                                  sw.serialNumber(),
-                                                cId);
+                                                 cId);
             providerService.deviceConnected(did, description);
             providerService.updatePorts(did, buildPortDescriptions(sw.getPorts()));
         }
@@ -244,8 +251,7 @@
          * @param ports the list of ports
          * @return list of portdescriptions
          */
-        private List<PortDescription> buildPortDescriptions(
-                List<OFPortDesc> ports) {
+        private List<PortDescription> buildPortDescriptions(List<OFPortDesc> ports) {
             final List<PortDescription> portDescs = new ArrayList<>(ports.size());
             for (OFPortDesc port : ports) {
                 portDescs.add(buildPortDescription(port));
@@ -260,12 +266,25 @@
          * @return portDescription for the port.
          */
         private PortDescription buildPortDescription(OFPortDesc port) {
-            final PortNumber portNo = PortNumber.portNumber(port.getPortNo().getPortNumber());
-            final boolean enabled = !port.getState().contains(OFPortState.LINK_DOWN) &&
-                    !port.getConfig().contains(OFPortConfig.PORT_DOWN);
-            return new DefaultPortDescription(portNo, enabled);
+            PortNumber portNo = PortNumber.portNumber(port.getPortNo().getPortNumber());
+            boolean enabled =
+                    !port.getState().contains(OFPortState.LINK_DOWN) &&
+                            !port.getConfig().contains(OFPortConfig.PORT_DOWN);
+            Port.Type type = port.getCurr().contains(OFPortFeatures.PF_FIBER) ? FIBER : COPPER;
+            return new DefaultPortDescription(portNo, enabled, type, portSpeed(port));
         }
 
+        private long portSpeed(OFPortDesc port) {
+            if (port.getVersion() == OFVersion.OF_13) {
+                return port.getCurrSpeed() / MBPS;
+            }
+
+            PortSpeed portSpeed = PortSpeed.SPEED_NONE;
+            for (OFPortFeatures feat : port.getCurr()) {
+                portSpeed = PortSpeed.max(portSpeed, feat.getPortSpeed());
+            }
+            return portSpeed.getSpeedBps() / MBPS;
+        }
     }
 
 }
diff --git a/tools/build/onos-build b/tools/build/onos-build
index 2f31eeb..6257340 100755
--- a/tools/build/onos-build
+++ b/tools/build/onos-build
@@ -6,4 +6,4 @@
 [ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
 . $ONOS_ROOT/tools/build/envDefaults
 
-cd $ONOS_ROOT && mvn clean install && cd docs && mvn javadoc:aggregate
+cd $ONOS_ROOT && mvn clean install "$@" && cd docs && mvn javadoc:aggregate
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index 8d4a784..2e4c564 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -34,6 +34,7 @@
 
 # Short-hand for ONOS build, package and test.
 alias ob='onos-build'
+alias obi='onos-build -Dmaven.test.failure.ignore=true'
 alias obs='onos-build-selective'
 alias op='onos-package'
 alias ot='onos-test'
diff --git a/tools/test/topos/oe-linear-3.json b/tools/test/topos/oe-linear-3.json
index b3cd61b..f361f10 100644
--- a/tools/test/topos/oe-linear-3.json
+++ b/tools/test/topos/oe-linear-3.json
@@ -3,28 +3,33 @@
         {
             "uri": "of:0000ffffffffff01", "mac": "ffffffffffff01", "type": "ROADM",
             "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM1",
-            "annotations": { "latitude": 37.6, "longitude": 122.3, "optical.regens": 0 }
+            "annotations": { "latitude": 37.6, "longitude": 122.3, "optical.regens": 0 },
+            "ports": [ { "port": 10, "speed": 100000, "type": "FIBER" }, { "port": 20, "speed": 0, "type": "FIBER" } ]
         },
         {
             "uri": "of:0000ffffffffff02", "mac": "ffffffffffff02", "type": "ROADM",
             "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM2",
-            "annotations": { "latitude": 37.3, "longitude": 121.9, "optical.regens": 0 }
+            "annotations": { "latitude": 37.3, "longitude": 121.9, "optical.regens": 0 },
+            "ports": [ { "port": 11, "speed": 100000, "type": "FIBER" }, { "port": 21, "speed": 0, "type": "FIBER" } ]
         },
         {
             "uri": "of:0000ffffffffff03", "mac": "ffffffffffff03", "type": "ROADM",
             "mfr": "Linc", "hw": "OE", "sw": "?", "serial": "?", "name": "ROADM3",
-            "annotations": { "latitude": 33.9, "longitude": 118.4, "optical.regens": 2 }
+            "annotations": { "latitude": 33.9, "longitude": 118.4, "optical.regens": 2 },
+            "ports": [ { "port": 30, "speed": 0, "type": "FIBER" }, { "port": 31, "speed": 0, "type": "FIBER" } ]
         },
 
         {
             "uri": "of:0000ffffffff0001", "mac": "ffffffffff0003", "type": "SWITCH",
             "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?", "name": "ROUTER1",
-            "annotations": { "latitude": 37.6, "longitude": 122.3 }
+            "annotations": { "latitude": 37.6, "longitude": 122.3 },
+            "ports": [ { "port": 1, "speed": 10000, "type": "COPPER" }, { "port": 2, "speed": 100000, "type": "FIBER" } ]
         },
         {
             "uri": "of:0000ffffffff0002", "mac": "ffffffffff0002", "type": "SWITCH",
             "mfr": "Linc", "hw": "PK", "sw": "?", "serial": "?", "name": "ROUTER2",
-            "annotations": { "latitude": 37.3, "longitude": 121.9 }
+            "annotations": { "latitude": 37.3, "longitude": 121.9 },
+            "ports": [ { "port": 1, "speed": 10000, "type": "COPPER" }, { "port": 2, "speed": 100000, "type": "FIBER" } ]
         }
     ],
 
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
index b32cbba..e3b5246 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
@@ -101,20 +101,6 @@
         return this.address.toString() + "/" + this.prefixLen;
     }
 
-    /**
-     * Compares the value of two Ip4Prefix objects.
-     * <p/>
-     * Note the value of the IPv4 address is compared directly between the
-     * objects, and must match exactly for the objects to be considered equal.
-     * This may result in objects which represent the same IP prefix being
-     * classified as unequal, because the unsignificant bits of the address
-     * field don't match (the bits to the right of the prefix length).
-     * <p/>
-     * TODO Change this behavior so that objects that represent the same prefix
-     * are classified as equal according to this equals method.
-     *
-     * @see Object#equals(Object)
-     */
     @Override
     public boolean equals(Object other) {
         if (other == this) {
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
index 9603b68..5422ae1 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
@@ -101,20 +101,6 @@
         return this.address.toString() + "/" + this.prefixLen;
     }
 
-    /**
-     * Compares the value of two Ip6Prefix objects.
-     * <p/>
-     * Note the value of the IPv6 address is compared directly between the
-     * objects, and must match exactly for the objects to be considered equal.
-     * This may result in objects which represent the same IP prefix being
-     * classified as unequal, because the unsignificant bits of the address
-     * field don't match (the bits to the right of the prefix length).
-     * <p/>
-     * TODO Change this behavior so that objects that represent the same prefix
-     * are classified as equal according to this equals method.
-     *
-     * @see Object#equals(Object)
-     */
     @Override
     public boolean equals(Object other) {
         if (other == this) {
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
index 9dd97c3..6af87a7 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
@@ -17,6 +17,8 @@
 
 import java.nio.ByteBuffer;
 import java.util.Arrays;
+import java.util.Objects;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
  * A class representing an IPv4 address.
@@ -37,38 +39,71 @@
     /**
      * Constructor for given IP address version and address octets.
      *
-     * @param ver the IP address version
-     * @param octets the IP address octets
-     */
-    private IpAddress(Version ver, byte[] octets) {
-        this.version = ver;
-        this.octets = Arrays.copyOf(octets, INET_BYTE_LENGTH);
-    }
-
-    /**
-     * Converts a byte array into an IP address.
-     *
-     * @param address the IP address value stored in network byte order
+     * @param value the IP address value stored in network byte order
      * (i.e., the most significant byte first)
-     * @return an IP address
+     * @param value the IP address value
      */
-    public static IpAddress valueOf(byte[] address) {
-        return new IpAddress(Version.INET, address);
+    private IpAddress(Version version, byte[] value) {
+        checkNotNull(value);
+
+        this.version = version;
+        this.octets = Arrays.copyOf(value, INET_BYTE_LENGTH);
     }
 
     /**
      * Converts an integer into an IPv4 address.
      *
-     * @param address an integer representing an IPv4 value
+     * @param value an integer representing an IPv4 value
      * @return an IP address
      */
-    public static IpAddress valueOf(int address) {
+    public static IpAddress valueOf(int value) {
         byte[] bytes =
-            ByteBuffer.allocate(INET_BYTE_LENGTH).putInt(address).array();
+            ByteBuffer.allocate(INET_BYTE_LENGTH).putInt(value).array();
         return new IpAddress(Version.INET, bytes);
     }
 
     /**
+     * Converts a byte array into an IP address.
+     *
+     * @param value the IP address value stored in network byte order
+     * (i.e., the most significant byte first)
+     * @return an IP address
+     */
+    public static IpAddress valueOf(byte[] value) {
+        return new IpAddress(Version.INET, value);
+    }
+
+    /**
+     * Converts a byte array and a given offset from the beginning of the
+     * array into an IP address.
+     * <p/>
+     * The IP address is stored in network byte order (i.e., the most
+     * significant byte first).
+     *
+     * @param value the value to use
+     * @param offset the offset in bytes from the beginning of the byte array
+     * @return an IP address
+     */
+    public static IpAddress valueOf(byte[] value, int offset) {
+        // Verify the arguments
+        if ((offset < 0) || (offset + INET_BYTE_LENGTH > value.length)) {
+            String msg;
+            if (value.length < INET_BYTE_LENGTH) {
+                msg = "Invalid IPv4 address array: array length: " +
+                    value.length + ". Must be at least " + INET_BYTE_LENGTH;
+            } else {
+                msg = "Invalid IPv4 address array: array offset: " +
+                    offset + ". Must be in the interval [0, " +
+                    (value.length - INET_BYTE_LENGTH) + "]";
+            }
+            throw new IllegalArgumentException(msg);
+        }
+
+        byte[] bc = Arrays.copyOfRange(value, offset, value.length);
+        return IpAddress.valueOf(bc);
+    }
+
+    /**
      * Converts a dotted-decimal string (x.x.x.x) into an IPv4 address.
      *
      * @param address a IP address in string form, e.g. "10.0.0.1".
@@ -77,8 +112,9 @@
     public static IpAddress valueOf(String address) {
         final String[] net = address.split("\\.");
         if (net.length != INET_BYTE_LENGTH) {
-            throw new IllegalArgumentException("Malformed IP address string; "
-                    + "Address must have four decimal values separated by dots (.)");
+            String msg = "Malformed IPv4 address string; " +
+                "Address must have four decimal values separated by dots (.)";
+            throw new IllegalArgumentException(msg);
         }
         final byte[] bytes = new byte[INET_BYTE_LENGTH];
         for (int i = 0; i < INET_BYTE_LENGTH; i++) {
@@ -115,6 +151,48 @@
         return bb.getInt();
     }
 
+    /**
+     * Creates an IP network mask prefix.
+     *
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 32] for IPv4
+     * @return a new IP address that contains a mask prefix of the
+     * specified length
+     */
+    public static IpAddress makeMaskPrefix(int prefixLen) {
+        // Verify the prefix length
+        if ((prefixLen < 0) || (prefixLen > INET_BIT_LENGTH)) {
+            final String msg = "Invalid IPv4 prefix length: " + prefixLen +
+                ". Must be in the interval [0, 32].";
+            throw new IllegalArgumentException(msg);
+        }
+
+        long v = (0xffffffffL << (INET_BIT_LENGTH - prefixLen)) & 0xffffffffL;
+        return IpAddress.valueOf((int) v);
+    }
+
+    /**
+     * Creates an IP address by masking it with a network mask of given
+     * mask length.
+     *
+     * @param addr the address to mask
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 32] for IPv4
+     * @return a new IP address that is masked with a mask prefix of the
+     * specified length
+     */
+    public static IpAddress makeMaskedAddress(final IpAddress addr,
+                                              int prefixLen) {
+        IpAddress mask = IpAddress.makeMaskPrefix(prefixLen);
+        byte[] net = new byte[INET_BYTE_LENGTH];
+
+        // Mask each byte
+        for (int i = 0; i < INET_BYTE_LENGTH; i++) {
+            net[i] = (byte) (addr.octets[i] & mask.octets[i]);
+        }
+        return IpAddress.valueOf(net);
+    }
+
     @Override
     public int compareTo(IpAddress o) {
         Long lv = ((long) this.toInt()) & 0xffffffffL;
@@ -124,32 +202,20 @@
 
     @Override
     public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + Arrays.hashCode(octets);
-        result = prime * result + ((version == null) ? 0 : version.hashCode());
-        return result;
+        return Objects.hash(version, Arrays.hashCode(octets));
     }
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj) {
+        if (obj == this) {
             return true;
         }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
+        if ((obj == null) || (getClass() != obj.getClass())) {
             return false;
         }
         IpAddress other = (IpAddress) obj;
-        if (!Arrays.equals(octets, other.octets)) {
-            return false;
-        }
-        if (version != other.version) {
-            return false;
-        }
-        return true;
+        return (version == other.version) &&
+            Arrays.equals(octets, other.octets);
     }
 
     @Override
diff --git a/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java b/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java
index 08e3546..57e247f 100644
--- a/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java
+++ b/web/api/src/main/java/org/onlab/onos/rest/ConfigProvider.java
@@ -19,17 +19,21 @@
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Host;
 import org.onlab.onos.net.HostId;
 import org.onlab.onos.net.HostLocation;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.MastershipRole;
+import org.onlab.onos.net.Port;
 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.DeviceProvider;
 import org.onlab.onos.net.device.DeviceProviderRegistry;
 import org.onlab.onos.net.device.DeviceProviderService;
+import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.host.DefaultHostDescription;
 import org.onlab.onos.net.host.HostProvider;
 import org.onlab.onos.net.host.HostProviderRegistry;
@@ -45,7 +49,9 @@
 import org.onlab.packet.VlanId;
 
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onlab.onos.net.DeviceId.deviceId;
@@ -120,7 +126,30 @@
         DeviceDescription desc =
                 new DefaultDeviceDescription(uri, type, mfr, hw, sw, serial,
                                              cid, annotations);
-        dps.deviceConnected(deviceId(uri), desc);
+        DeviceId deviceId = deviceId(uri);
+        dps.deviceConnected(deviceId, desc);
+
+        JsonNode ports = node.get("ports");
+        if (ports != null) {
+            parsePorts(dps, deviceId, ports);
+        }
+    }
+
+    // Parses the given node with list of device ports.
+    private void parsePorts(DeviceProviderService dps, DeviceId deviceId, JsonNode nodes) {
+        List<PortDescription> ports = new ArrayList<>();
+        for (JsonNode node : nodes) {
+            ports.add(parsePort(node));
+        }
+        dps.updatePorts(deviceId, ports);
+    }
+
+    // Parses the given node with port information.
+    private PortDescription parsePort(JsonNode node) {
+        Port.Type type = Port.Type.valueOf(node.path("type").asText("COPPER"));
+        return new DefaultPortDescription(portNumber(node.path("port").asLong(0)),
+                                          node.path("enabled").asBoolean(true),
+                                          type, node.path("speed").asLong(1_000));
     }
 
     // Parses the given JSON and provides links as configured.