Merge "Make logger static final variable"
diff --git a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
index f1d33ed..18f5d0a 100644
--- a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
+++ b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
@@ -132,6 +132,11 @@
             InboundPacket pkt = context.inPacket();
             Ethernet ethPkt = pkt.parsed();
 
+            // Bail if this is deemed to be a control packet.
+            if (isControlPacket(ethPkt)) {
+                return;
+            }
+
             HostId id = HostId.hostId(ethPkt.getDestinationMAC());
 
             // Do not process link-local addresses in any way.
@@ -180,6 +185,13 @@
             // Otherwise forward and be done with it.
             installRule(context, path.src().port());
         }
+
+    }
+
+    // Indicates whether this is a control packet, e.g. LLDP, BDDP
+    private boolean isControlPacket(Ethernet eth) {
+        short type = eth.getEtherType();
+        return type == Ethernet.TYPE_LLDP || type == Ethernet.TYPE_BSN;
     }
 
     // Selects a path from the given set that does not lead back to the
diff --git a/apps/oecfg/pom.xml b/apps/oecfg/pom.xml
index d0cd081..7a84412 100644
--- a/apps/oecfg/pom.xml
+++ b/apps/oecfg/pom.xml
@@ -49,7 +49,6 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
-                <version>2.3</version>
                 <executions>
                     <execution>
                         <phase>package</phase>
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/AddHostToHostIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/AddHostToHostIntentCommand.java
index 7d3bfc2..13f3000 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/AddHostToHostIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/AddHostToHostIntentCommand.java
@@ -15,14 +15,16 @@
  */
 package org.onlab.onos.cli.net;
 
+import java.util.List;
+
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
-import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.net.HostId;
 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;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.HostToHostIntent;
 import org.onlab.onos.net.intent.IntentService;
 
@@ -31,7 +33,7 @@
  */
 @Command(scope = "onos", name = "add-host-intent",
          description = "Installs host-to-host connectivity intent")
-public class AddHostToHostIntentCommand extends AbstractShellCommand {
+public class AddHostToHostIntentCommand extends ConnectivityIntentCommand {
 
     @Argument(index = 0, name = "one", description = "One host ID",
               required = true, multiValued = false)
@@ -50,9 +52,11 @@
 
         TrafficSelector selector = DefaultTrafficSelector.builder().build();
         TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+        List<Constraint> constraints = buildConstraints();
 
         HostToHostIntent intent = new HostToHostIntent(appId(), oneId, twoId,
-                                                       selector, treatment);
+                                                       selector, treatment,
+                                                       constraints);
         service.submit(intent);
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/AddMultiPointToSinglePointIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/AddMultiPointToSinglePointIntentCommand.java
index 9e8e2fc..1cd695e 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/AddMultiPointToSinglePointIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/AddMultiPointToSinglePointIntentCommand.java
@@ -23,11 +23,13 @@
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.Intent;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
 
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import static org.onlab.onos.net.DeviceId.deviceId;
@@ -69,9 +71,11 @@
 
         TrafficSelector selector = buildTrafficSelector();
         TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+        List<Constraint> constraints = buildConstraints();
 
         Intent intent = new MultiPointToSinglePointIntent(appId(), selector, treatment,
-                                                          ingressPoints, egress);
+                                                          ingressPoints, egress,
+                                                          constraints);
         service.submit(intent);
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentCommand.java
index ed98c7e..26bb1c0 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentCommand.java
@@ -15,6 +15,8 @@
  */
 package org.onlab.onos.cli.net;
 
+import java.util.List;
+
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.onlab.onos.net.ConnectPoint;
@@ -22,6 +24,7 @@
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.Intent;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.PointToPointIntent;
@@ -63,8 +66,10 @@
         TrafficSelector selector = buildTrafficSelector();
         TrafficTreatment treatment = builder().build();
 
+        List<Constraint> constraints = buildConstraints();
+
         Intent intent = new PointToPointIntent(appId(), selector, treatment,
-                                               ingress, egress);
+                                               ingress, egress, constraints);
         service.submit(intent);
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
index d8ec3c7..e4fc5aa 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
@@ -15,14 +15,21 @@
  */
 package org.onlab.onos.cli.net;
 
+import java.util.LinkedList;
+import java.util.List;
+
 import org.apache.karaf.shell.commands.Option;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
 import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.intent.Constraint;
+import org.onlab.onos.net.intent.constraint.BandwidthConstraint;
+import org.onlab.onos.net.intent.constraint.LambdaConstraint;
+import org.onlab.onos.net.resource.Bandwidth;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.MacAddress;
 
-import com.google.common.base.Strings;
+import static com.google.common.base.Strings.isNullOrEmpty;
 
 /**
  * Base class for command line operations for connectivity based intents.
@@ -41,6 +48,14 @@
             required = false, multiValued = false)
     private String ethTypeString = "";
 
+    @Option(name = "-b", aliases = "--bandwidth", description = "Bandwidth",
+            required = false, multiValued = false)
+    private String bandwidthString = "";
+
+    @Option(name = "-l", aliases = "--lambda", description = "Lambda",
+            required = false, multiValued = false)
+    private boolean lambda = false;
+
     /**
      * Constructs a traffic selector based on the command line arguments
      * presented to the command.
@@ -50,21 +65,43 @@
         TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
         Short ethType = Ethernet.TYPE_IPV4;
 
-        if (!Strings.isNullOrEmpty(ethTypeString)) {
+        if (!isNullOrEmpty(ethTypeString)) {
             EthType ethTypeParameter = EthType.valueOf(ethTypeString);
             ethType = ethTypeParameter.value();
         }
         selectorBuilder.matchEthType(ethType);
 
-        if (!Strings.isNullOrEmpty(srcMacString)) {
+        if (!isNullOrEmpty(srcMacString)) {
             selectorBuilder.matchEthSrc(MacAddress.valueOf(srcMacString));
         }
 
-        if (!Strings.isNullOrEmpty(dstMacString)) {
+        if (!isNullOrEmpty(dstMacString)) {
             selectorBuilder.matchEthDst(MacAddress.valueOf(dstMacString));
         }
 
         return selectorBuilder.build();
     }
 
+    /**
+     * Builds the constraint list for this command based on the command line
+     * parameters.
+     *
+     * @return List of constraint objects describing the constraints requested
+     */
+    protected List<Constraint> buildConstraints() {
+        final List<Constraint> constraints = new LinkedList<>();
+
+        // Check for a bandwidth specification
+        if (!isNullOrEmpty(bandwidthString)) {
+            final double bandwidthValue = Double.parseDouble(bandwidthString);
+            constraints.add(new BandwidthConstraint(Bandwidth.valueOf(bandwidthValue)));
+        }
+
+        // Check for a lambda specification
+        if (lambda) {
+            constraints.add(new LambdaConstraint(null));
+        }
+
+        return constraints;
+    }
 }
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 1d32aa5..29452f7 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
@@ -25,6 +25,7 @@
 import org.onlab.onos.cli.Comparators;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Port;
+import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DeviceService;
 
 import java.util.ArrayList;
@@ -108,7 +109,7 @@
         for (Port port : service.getPorts(device.id())) {
             if (isIncluded(port)) {
                 ports.add(mapper.createObjectNode()
-                                  .put("port", port.number().toString())
+                                  .put("port", portName(port.number()))
                                   .put("isEnabled", port.isEnabled())
                                   .put("type", port.type().toString().toLowerCase())
                                   .put("portSpeed", port.portSpeed())
@@ -120,6 +121,10 @@
         return result;
     }
 
+    private String portName(PortNumber port) {
+        return port.equals(PortNumber.LOCAL) ? "local" : port.toString();
+    }
+
     // Determines if a port should be included in output.
     private boolean isIncluded(Port port) {
         return enabled && port.isEnabled() || disabled && !port.isEnabled() ||
@@ -133,7 +138,8 @@
         Collections.sort(ports, Comparators.PORT_COMPARATOR);
         for (Port port : ports) {
             if (isIncluded(port)) {
-                print(FMT, port.number(), port.isEnabled() ? "enabled" : "disabled",
+                print(FMT, portName(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/intent/HostToHostIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
index 3fad93d..893270a 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
@@ -105,6 +105,7 @@
                 .add("appId", appId())
                 .add("selector", selector())
                 .add("treatment", treatment())
+                .add("constraints", constraints())
                 .add("one", one)
                 .add("two", two)
                 .toString();
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
index 0c47aad..90907fb 100644
--- 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
@@ -22,6 +22,7 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 
+import java.util.List;
 import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -65,6 +66,38 @@
     }
 
     /**
+     * Creates a new multi-to-single point connectivity intent for the specified
+     * traffic selector and treatment.
+     *
+     * @param appId         application identifier
+     * @param selector      traffic selector
+     * @param treatment     treatment
+     * @param ingressPoints set of ports from which ingress traffic originates
+     * @param egressPoint   port to which traffic will egress
+     * @param constraints   constraints to apply to the intent
+     * @throws NullPointerException     if {@code ingressPoints} or
+     *                                  {@code egressPoint} is null.
+     * @throws IllegalArgumentException if the size of {@code ingressPoints} is
+     *                                  not more than 1
+     */
+    public MultiPointToSinglePointIntent(ApplicationId appId,
+                                         TrafficSelector selector,
+                                         TrafficTreatment treatment,
+                                         Set<ConnectPoint> ingressPoints,
+                                         ConnectPoint egressPoint,
+                                         List<Constraint> constraints) {
+        super(id(MultiPointToSinglePointIntent.class, selector, treatment,
+                ingressPoints, egressPoint), appId, null, selector, treatment,
+                constraints);
+
+        checkNotNull(ingressPoints);
+        checkArgument(!ingressPoints.isEmpty(), "Ingress point set cannot be empty");
+
+        this.ingressPoints = Sets.newHashSet(ingressPoints);
+        this.egressPoint = checkNotNull(egressPoint);
+    }
+
+    /**
      * Constructor for serializer.
      */
     protected MultiPointToSinglePointIntent() {
@@ -101,6 +134,7 @@
                 .add("treatment", treatment())
                 .add("ingress", ingressPoints())
                 .add("egress", egressPoint())
+                .add("constraints", constraints())
                 .toString();
     }
 }
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
index 9189bae..9f8816d 100644
--- 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
@@ -15,6 +15,8 @@
  */
 package org.onlab.onos.net.intent;
 
+import java.util.List;
+
 import com.google.common.base.MoreObjects;
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.net.Path;
@@ -46,6 +48,24 @@
     }
 
     /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports and using the specified explicit path.
+     *
+     * @param appId     application identifier
+     * @param selector  traffic selector
+     * @param treatment treatment
+     * @param path      traversed links
+     * @param constraints  optional list of constraints
+     * @throws NullPointerException {@code path} is null
+     */
+    public PathIntent(ApplicationId appId, TrafficSelector selector,
+                      TrafficTreatment treatment, Path path, List<Constraint> constraints) {
+        super(id(PathIntent.class, selector, treatment, path, constraints), appId,
+                resources(path.links()), selector, treatment, constraints);
+        this.path = path;
+    }
+
+    /**
      * Constructor for serializer.
      */
     protected PathIntent() {
@@ -75,6 +95,7 @@
                 .add("appId", appId())
                 .add("selector", selector())
                 .add("treatment", treatment())
+                .add("constraints", constraints())
                 .add("path", path)
                 .toString();
     }
diff --git a/core/api/src/main/java/org/onlab/onos/net/proxyarp/ProxyArpService.java b/core/api/src/main/java/org/onlab/onos/net/proxyarp/ProxyArpService.java
index 3a93b30..509c6ac 100644
--- a/core/api/src/main/java/org/onlab/onos/net/proxyarp/ProxyArpService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/proxyarp/ProxyArpService.java
@@ -18,7 +18,7 @@
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.packet.PacketContext;
 import org.onlab.packet.Ethernet;
-import org.onlab.packet.IpAddress;
+import org.onlab.packet.Ip4Address;
 
 /**
  * Service for processing arp requests on behalf of applications.
@@ -27,12 +27,12 @@
 public interface ProxyArpService {
 
     /**
-     * Returns whether this particular ip address is known to the system.
+     * Returns whether this particular IPv4 address is known to the system.
      *
-     * @param addr a ip address
+     * @param addr an IPv4 address
      * @return true if know, false otherwise
      */
-    boolean known(IpAddress addr);
+    boolean known(Ip4Address addr);
 
     /**
      * Sends a reply for a given request. If the host is not known then the arp
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
index 4cf1830..8a31900 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
@@ -118,13 +118,14 @@
 
         @Override
         public double weight(TopologyEdge edge) {
-            if (constraints == null) {
+            if (constraints == null || !constraints.iterator().hasNext()) {
                 return 1.0;
             }
 
             // iterate over all constraints in order and return the weight of
             // the first one with fast fail over the first failure
             Iterator<Constraint> it = constraints.iterator();
+
             double cost = it.next().cost(edge.link(), resourceService);
             while (it.hasNext() && cost > 0) {
                 if (it.next().cost(edge.link(), resourceService) < 0) {
@@ -132,6 +133,7 @@
                 }
             }
             return cost;
+
         }
     }
 
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
index 605d3c7..4d989ac 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
@@ -70,7 +70,8 @@
                                     HostToHostIntent intent) {
         TrafficSelector selector = builder(intent.selector())
                 .matchEthSrc(src.mac()).matchEthDst(dst.mac()).build();
-        return new PathIntent(intent.appId(), selector, intent.treatment(), path);
+        return new PathIntent(intent.appId(), selector, intent.treatment(),
+                              path, intent.constraints());
     }
 
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
index c32c8ee..5c8eb94 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
@@ -77,7 +77,8 @@
     private Intent createPathIntent(Path path,
                                     PointToPointIntent intent) {
         return new PathIntent(intent.appId(),
-                              intent.selector(), intent.treatment(), path);
+                              intent.selector(), intent.treatment(), path,
+                              intent.constraints());
     }
 
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
index 49528d0..c0a3f16 100644
--- a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
@@ -56,6 +56,7 @@
 import org.onlab.packet.ARP;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.Ip4Address;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 import org.slf4j.Logger;
@@ -113,7 +114,7 @@
     }
 
     @Override
-    public boolean known(IpAddress addr) {
+    public boolean known(Ip4Address addr) {
         checkNotNull(addr, MAC_ADDR_NULL);
         Set<Host> hosts = hostService.getHostsByIp(addr);
         return !hosts.isEmpty();
@@ -131,9 +132,8 @@
         // If the request came from outside the network, only reply if it was
         // for one of our external addresses.
         if (isOutsidePort(inPort)) {
-            IpAddress target =
-                IpAddress.valueOf(IpAddress.Version.INET,
-                                  arp.getTargetProtocolAddress());
+            Ip4Address target =
+                Ip4Address.valueOf(arp.getTargetProtocolAddress());
             Set<PortAddresses> addressSet =
                 hostService.getAddressBindingsForPort(inPort);
 
@@ -141,7 +141,7 @@
                 for (InterfaceIpAddress ia : addresses.ipAddresses()) {
                     if (ia.ipAddress().equals(target)) {
                         Ethernet arpReply =
-                            buildArpReply(ia.ipAddress(), addresses.mac(), eth);
+                            buildArpReply(target, addresses.mac(), eth);
                         sendTo(arpReply, inPort);
                     }
                 }
@@ -151,9 +151,8 @@
             // If the source address matches one of our external addresses
             // it could be a request from an internal host to an external
             // address. Forward it over to the correct port.
-            IpAddress source =
-                IpAddress.valueOf(IpAddress.Version.INET,
-                                  arp.getSenderProtocolAddress());
+            Ip4Address source =
+                Ip4Address.valueOf(arp.getSenderProtocolAddress());
             PortAddresses sourceAddresses = findPortInSubnet(source);
             if (sourceAddresses != null) {
                 for (InterfaceIpAddress ia : sourceAddresses.ipAddresses()) {
@@ -168,9 +167,8 @@
         // Continue with normal proxy ARP case
 
         VlanId vlan = VlanId.vlanId(eth.getVlanID());
-        Set<Host> hosts =
-            hostService.getHostsByIp(IpAddress.valueOf(IpAddress.Version.INET,
-                                        arp.getTargetProtocolAddress()));
+        Set<Host> hosts = hostService.getHostsByIp(
+                        Ip4Address.valueOf(arp.getTargetProtocolAddress()));
 
         Host dst = null;
         Host src = hostService.getHost(HostId.hostId(eth.getSourceMAC(),
@@ -188,11 +186,19 @@
             return;
         }
 
-        // TODO find the correct IP address
-        IpAddress ipAddress = dst.ipAddresses().iterator().next();
-        Ethernet arpReply = buildArpReply(ipAddress, dst.mac(), eth);
-        // TODO: check send status with host service.
-        sendTo(arpReply, src.location());
+        //
+        // TODO find the correct IP address.
+        // Right now we use the first IPv4 address that is found.
+        //
+        for (IpAddress ipAddress : dst.ipAddresses()) {
+            Ip4Address ip4Address = ipAddress.getIp4Address();
+            if (ip4Address != null) {
+                Ethernet arpReply = buildArpReply(ip4Address, dst.mac(), eth);
+                // TODO: check send status with host service.
+                sendTo(arpReply, src.location());
+                break;
+            }
+        }
     }
 
     /**
@@ -223,7 +229,7 @@
      * @param target the target address to find a matching port for
      * @return a PortAddresses object if one was found, otherwise null
      */
-    private PortAddresses findPortInSubnet(IpAddress target) {
+    private PortAddresses findPortInSubnet(Ip4Address target) {
         for (PortAddresses addresses : hostService.getAddressBindings()) {
             for (InterfaceIpAddress ia : addresses.ipAddresses()) {
                 if (ia.subnetAddress().contains(target)) {
@@ -358,7 +364,7 @@
      * @param request the ARP request we got
      * @return an Ethernet frame containing the ARP reply
      */
-    private Ethernet buildArpReply(IpAddress srcIp, MacAddress srcMac,
+    private Ethernet buildArpReply(Ip4Address srcIp, MacAddress srcMac,
             Ethernet request) {
 
         Ethernet eth = new Ethernet();
@@ -372,7 +378,7 @@
         arp.setProtocolType(ARP.PROTO_TYPE_IP);
         arp.setHardwareType(ARP.HW_TYPE_ETHERNET);
 
-        arp.setProtocolAddressLength((byte) IpAddress.INET_BYTE_LENGTH);
+        arp.setProtocolAddressLength((byte) Ip4Address.BYTE_LENGTH);
         arp.setHardwareAddressLength((byte) Ethernet.DATALAYER_ADDRESS_LENGTH);
         arp.setSenderHardwareAddress(srcMac.toBytes());
         arp.setTargetHardwareAddress(request.getSourceMACAddress());
diff --git a/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
index 8beac4a..bfb659e 100644
--- a/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
@@ -57,8 +57,8 @@
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.packet.ARP;
 import org.onlab.packet.Ethernet;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.IpPrefix;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 
@@ -74,8 +74,8 @@
     private static final int NUM_ADDRESS_PORTS = NUM_DEVICES / 2;
     private static final int NUM_FLOOD_PORTS = 3;
 
-    private static final IpAddress IP1 = IpAddress.valueOf("192.168.1.1");
-    private static final IpAddress IP2 = IpAddress.valueOf("192.168.1.2");
+    private static final Ip4Address IP1 = Ip4Address.valueOf("192.168.1.1");
+    private static final Ip4Address IP2 = Ip4Address.valueOf("192.168.1.2");
 
     private static final ProviderId PID = new ProviderId("of", "foo");
 
@@ -204,10 +204,12 @@
 
         for (int i = 1; i <= NUM_ADDRESS_PORTS; i++) {
             ConnectPoint cp = new ConnectPoint(getDeviceId(i), P1);
-            IpPrefix prefix1 = IpPrefix.valueOf("10.0." + (2 * i - 1) + ".0/24");
-            IpAddress addr1 = IpAddress.valueOf("10.0." + (2 * i - 1) + ".1");
-            IpPrefix prefix2 = IpPrefix.valueOf("10.0." + (2 * i) + ".0/24");
-            IpAddress addr2 = IpAddress.valueOf("10.0." + (2 * i) + ".1");
+            Ip4Prefix prefix1 =
+                Ip4Prefix.valueOf("10.0." + (2 * i - 1) + ".0/24");
+            Ip4Address addr1 =
+                Ip4Address.valueOf("10.0." + (2 * i - 1) + ".1");
+            Ip4Prefix prefix2 = Ip4Prefix.valueOf("10.0." + (2 * i) + ".0/24");
+            Ip4Address addr2 = Ip4Address.valueOf("10.0." + (2 * i) + ".1");
             InterfaceIpAddress ia1 = new InterfaceIpAddress(addr1, prefix1);
             InterfaceIpAddress ia2 = new InterfaceIpAddress(addr2, prefix2);
             PortAddresses pa1 =
@@ -235,7 +237,7 @@
     }
 
     /**
-     * Tests {@link ProxyArpManager#known(IpAddress)} in the case where the
+     * Tests {@link ProxyArpManager#known(Ip4Address)} in the case where the
      * IP address is not known.
      * Verifies the method returns false.
      */
@@ -248,7 +250,7 @@
     }
 
     /**
-     * Tests {@link ProxyArpManager#known(IpAddress)} in the case where the
+     * Tests {@link ProxyArpManager#known(Ip4Address)} in the case where the
      * IP address is known.
      * Verifies the method returns true.
      */
@@ -344,9 +346,9 @@
 
     @Test
     public void testReplyToRequestForUs() {
-        IpAddress theirIp = IpAddress.valueOf("10.0.1.254");
-        IpAddress ourFirstIp = IpAddress.valueOf("10.0.1.1");
-        IpAddress ourSecondIp = IpAddress.valueOf("10.0.2.1");
+        Ip4Address theirIp = Ip4Address.valueOf("10.0.1.254");
+        Ip4Address ourFirstIp = Ip4Address.valueOf("10.0.1.1");
+        Ip4Address ourSecondIp = Ip4Address.valueOf("10.0.2.1");
         MacAddress firstMac = MacAddress.valueOf(1L);
         MacAddress secondMac = MacAddress.valueOf(2L);
 
@@ -379,11 +381,11 @@
     public void testReplyExternalPortBadRequest() {
         replay(hostService); // no further host service expectations
 
-        IpAddress theirIp = IpAddress.valueOf("10.0.1.254");
+        Ip4Address theirIp = Ip4Address.valueOf("10.0.1.254");
 
         // Request for a valid external IP address but coming in the wrong port
         Ethernet arpRequest = buildArp(ARP.OP_REQUEST, MAC1, null, theirIp,
-                IpAddress.valueOf("10.0.3.1"));
+                Ip4Address.valueOf("10.0.3.1"));
         proxyArp.reply(arpRequest, LOC1);
         assertEquals(0, packetService.packets.size());
 
@@ -398,9 +400,9 @@
     public void testReplyToRequestFromUs() {
         replay(hostService); // no further host service expectations
 
-        IpAddress ourIp = IpAddress.valueOf("10.0.1.1");
+        Ip4Address ourIp = Ip4Address.valueOf("10.0.1.1");
         MacAddress ourMac = MacAddress.valueOf(1L);
-        IpAddress theirIp = IpAddress.valueOf("10.0.1.100");
+        Ip4Address theirIp = Ip4Address.valueOf("10.0.1.100");
 
         // This is a request from something inside our network (like a BGP
         // daemon) to an external host.
@@ -523,7 +525,7 @@
      * @return the ARP packet
      */
     private Ethernet buildArp(short opcode, MacAddress srcMac, MacAddress dstMac,
-            IpAddress srcIp, IpAddress dstIp) {
+            Ip4Address srcIp, Ip4Address dstIp) {
         Ethernet eth = new Ethernet();
 
         if (dstMac == null) {
@@ -541,7 +543,7 @@
         arp.setProtocolType(ARP.PROTO_TYPE_IP);
         arp.setHardwareType(ARP.HW_TYPE_ETHERNET);
 
-        arp.setProtocolAddressLength((byte) IpAddress.INET_BYTE_LENGTH);
+        arp.setProtocolAddressLength((byte) Ip4Address.BYTE_LENGTH);
         arp.setHardwareAddressLength((byte) Ethernet.DATALAYER_ADDRESS_LENGTH);
         arp.setSenderHardwareAddress(srcMac.toBytes());
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
index 38e1322..849ad17 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
@@ -159,7 +159,7 @@
             return messagingService.sendAndReceive(nodeEp, message.subject().value(), SERIALIZER.encode(message));
 
         } catch (IOException e) {
-            log.error("Failed interaction with remote nodeId: " + toNodeId, e);
+            log.trace("Failed interaction with remote nodeId: " + toNodeId, e);
             throw e;
         }
     }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
index c561221..56dba79 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
@@ -149,12 +149,12 @@
 
     @Activate
     public void activate() {
-        log.info("Started.");
+        log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
-        log.info("Stopped.");
+        log.info("Stopped");
     }
 
     @Override
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
index 183a6db..2779b35 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
@@ -5,20 +5,26 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 import net.kuujo.copycat.Copycat;
 import net.kuujo.copycat.StateMachine;
+import net.kuujo.copycat.cluster.ClusterConfig;
 import net.kuujo.copycat.cluster.TcpCluster;
 import net.kuujo.copycat.cluster.TcpClusterConfig;
 import net.kuujo.copycat.cluster.TcpMember;
 import net.kuujo.copycat.log.InMemoryLog;
 import net.kuujo.copycat.log.Log;
+
 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.cluster.ClusterEvent;
+import org.onlab.onos.cluster.ClusterEventListener;
 import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.store.service.DatabaseAdminService;
@@ -35,8 +41,6 @@
 import org.onlab.onos.store.service.WriteResult;
 import org.slf4j.Logger;
 
-import com.google.common.collect.Lists;
-
 /**
  * Strongly consistent and durable state management service based on
  * Copycat implementation of Raft consensus protocol.
@@ -58,17 +62,34 @@
     private Copycat copycat;
     private DatabaseClient client;
 
+    // guarded by synchronized block
+    private ClusterConfig<TcpMember> clusterConfig;
+
+    private CountDownLatch clusterEventLatch;
+
+    private ClusterEventListener clusterEventListener;
+
     @Activate
     public void activate() {
-        log.info("Starting.");
 
-        // TODO: Not every node can be part of the consensus ring.
+        // TODO: Not every node should be part of the consensus ring.
 
+        final ControllerNode localNode = clusterService.getLocalNode();
         TcpMember localMember =
                 new TcpMember(
-                        clusterService.getLocalNode().ip().toString(),
-                        clusterService.getLocalNode().tcpPort());
-        List<TcpMember> remoteMembers = Lists.newArrayList();
+                        localNode.ip().toString(),
+                        localNode.tcpPort());
+
+        clusterConfig = new TcpClusterConfig();
+        clusterConfig.setLocalMember(localMember);
+
+        List<TcpMember> remoteMembers = new ArrayList<>(clusterService.getNodes().size());
+
+        clusterEventLatch = new CountDownLatch(1);
+        clusterEventListener = new InternalClusterEventListener();
+        clusterService.addListener(clusterEventListener);
+
+        // note: from this point beyond, clusterConfig requires synchronization
 
         for (ControllerNode node : clusterService.getNodes()) {
             TcpMember member = new TcpMember(node.ip().toString(), node.tcpPort());
@@ -77,20 +98,37 @@
             }
         }
 
-        // Configure the cluster.
-        TcpClusterConfig config = new TcpClusterConfig();
+        if (remoteMembers.isEmpty()) {
+            log.info("This node is the only node in the cluster.  "
+                    + "Waiting for others to show up.");
+            // FIXME: hack trying to relax cases forming multiple consensus rings.
+            // add seed node configuration to avoid this
 
-        config.setLocalMember(localMember);
-        config.setRemoteMembers(remoteMembers.toArray(new TcpMember[]{}));
+            // If the node is alone on it's own, wait some time
+            // hoping other will come up soon
+            try {
+                if (!clusterEventLatch.await(120, TimeUnit.SECONDS)) {
+                    log.info("Starting as single node cluster");
+                }
+            } catch (InterruptedException e) {
+                log.info("Interrupted waiting for others", e);
+            }
+        }
 
-        // Create the cluster.
-        TcpCluster cluster = new TcpCluster(config);
+        final TcpCluster cluster;
+        synchronized (clusterConfig) {
+            clusterConfig.addRemoteMembers(remoteMembers);
+
+            // Create the cluster.
+            cluster = new TcpCluster(clusterConfig);
+        }
+        log.info("Starting cluster: {}", cluster);
+
 
         StateMachine stateMachine = new DatabaseStateMachine();
-        ControllerNode thisNode = clusterService.getLocalNode();
         // FIXME resolve Chronicle + OSGi issue
         //Log consensusLog = new ChronicleLog(LOG_FILE_PREFIX + "_" + thisNode.id());
-        Log consensusLog = new InMemoryLog();
+        Log consensusLog = new KryoRegisteredInMemoryLog();
 
         copycat = new Copycat(stateMachine, consensusLog, cluster, copycatMessagingProtocol);
         copycat.start();
@@ -102,6 +140,7 @@
 
     @Deactivate
     public void deactivate() {
+        clusterService.removeListener(clusterEventListener);
         copycat.stop();
         log.info("Stopped.");
     }
@@ -179,6 +218,53 @@
 
     }
 
+    private final class InternalClusterEventListener
+            implements ClusterEventListener {
+
+        @Override
+        public void event(ClusterEvent event) {
+            // TODO: Not every node should be part of the consensus ring.
+
+            final ControllerNode node = event.subject();
+            final TcpMember tcpMember = new TcpMember(node.ip().toString(),
+                                                      node.tcpPort());
+
+            log.trace("{}", event);
+            switch (event.type()) {
+            case INSTANCE_ACTIVATED:
+            case INSTANCE_ADDED:
+                log.info("{} was added to the cluster", tcpMember);
+                synchronized (clusterConfig) {
+                    clusterConfig.addRemoteMember(tcpMember);
+                }
+                break;
+            case INSTANCE_DEACTIVATED:
+            case INSTANCE_REMOVED:
+                log.info("{} was removed from the cluster", tcpMember);
+                synchronized (clusterConfig) {
+                    clusterConfig.removeRemoteMember(tcpMember);
+                }
+                break;
+            default:
+                break;
+            }
+            if (copycat != null) {
+                log.debug("Current cluster: {}", copycat.cluster());
+            }
+            clusterEventLatch.countDown();
+        }
+
+    }
+
+    public static final class KryoRegisteredInMemoryLog extends InMemoryLog {
+        public KryoRegisteredInMemoryLog() {
+            super();
+            // required to deserialize object across bundles
+            super.kryo.register(TcpMember.class, new TcpMemberSerializer());
+            super.kryo.register(TcpClusterConfig.class, new TcpClusterConfigSerializer());
+        }
+    }
+
     private class DatabaseOperationResult<R, E extends DatabaseException> implements OptionalResult<R, E> {
 
         private final R result;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
index ad6773e..2822f25 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.store.service.impl;
 
+import static org.slf4j.LoggerFactory.getLogger;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -16,6 +18,7 @@
 import org.onlab.onos.store.service.WriteRequest;
 import org.onlab.onos.store.service.WriteResult;
 import org.onlab.util.KryoNamespace;
+import org.slf4j.Logger;
 
 import com.google.common.collect.Maps;
 
@@ -28,6 +31,8 @@
  */
 public class DatabaseStateMachine implements StateMachine {
 
+    private final Logger log = getLogger(getClass());
+
     public static final KryoSerializer SERIALIZER = new KryoSerializer() {
         @Override
         protected void setupKryoPool() {
@@ -161,7 +166,7 @@
         try {
             return SERIALIZER.encode(state);
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Snapshot serialization error", e);
             return null;
         }
     }
@@ -171,7 +176,7 @@
         try {
             this.state = SERIALIZER.decode(data);
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("Snapshot deserialization error", e);
         }
     }
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpClusterConfigSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpClusterConfigSerializer.java
new file mode 100644
index 0000000..48887b9
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpClusterConfigSerializer.java
@@ -0,0 +1,30 @@
+package org.onlab.onos.store.service.impl;
+
+import java.util.Collection;
+
+import net.kuujo.copycat.cluster.TcpClusterConfig;
+import net.kuujo.copycat.cluster.TcpMember;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+public class TcpClusterConfigSerializer extends Serializer<TcpClusterConfig> {
+
+    @Override
+    public void write(Kryo kryo, Output output, TcpClusterConfig object) {
+        kryo.writeClassAndObject(output, object.getLocalMember());
+        kryo.writeClassAndObject(output, object.getRemoteMembers());
+    }
+
+    @Override
+    public TcpClusterConfig read(Kryo kryo, Input input,
+                                 Class<TcpClusterConfig> type) {
+        TcpMember localMember = (TcpMember) kryo.readClassAndObject(input);
+        @SuppressWarnings("unchecked")
+        Collection<TcpMember> remoteMembers = (Collection<TcpMember>) kryo.readClassAndObject(input);
+        return new TcpClusterConfig(localMember, remoteMembers);
+    }
+
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpMemberSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpMemberSerializer.java
new file mode 100644
index 0000000..e729f9b
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/TcpMemberSerializer.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.store.service.impl;
+
+import net.kuujo.copycat.cluster.TcpMember;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+public class TcpMemberSerializer extends Serializer<TcpMember> {
+
+    @Override
+    public void write(Kryo kryo, Output output, TcpMember object) {
+        output.writeString(object.host());
+        output.writeInt(object.port());
+    }
+
+    @Override
+    public TcpMember read(Kryo kryo, Input input, Class<TcpMember> type) {
+        String host = input.readString();
+        int port = input.readInt();
+        return new TcpMember(host, port);
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4AddressSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4AddressSerializer.java
new file mode 100644
index 0000000..1fee80f
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4AddressSerializer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.store.serializers;
+
+import org.onlab.packet.Ip4Address;
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Kryo Serializer for {@link Ip4Address}.
+ */
+public class Ip4AddressSerializer extends Serializer<Ip4Address> {
+
+    /**
+     * Creates {@link Ip4Address} serializer instance.
+     */
+    public Ip4AddressSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output, Ip4Address object) {
+        byte[] octs = object.toOctets();
+        // TODO: Writing (and reading) the number of octets is redundant:
+        // It is always Ip4Address.BYTE_LENGTH
+        output.writeInt(octs.length);
+        output.writeBytes(octs);
+    }
+
+    @Override
+    public Ip4Address read(Kryo kryo, Input input, Class<Ip4Address> type) {
+        final int octLen = input.readInt();
+        byte[] octs = new byte[octLen];
+        input.readBytes(octs);
+        return Ip4Address.valueOf(octs);
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4PrefixSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4PrefixSerializer.java
new file mode 100644
index 0000000..577d20b
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip4PrefixSerializer.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.store.serializers;
+
+import org.onlab.packet.Ip4Prefix;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Kryo Serializer for {@link Ip4Prefix}.
+ */
+public final class Ip4PrefixSerializer extends Serializer<Ip4Prefix> {
+
+    /**
+     * Creates {@link Ip4Prefix} serializer instance.
+     */
+    public Ip4PrefixSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output,
+            Ip4Prefix object) {
+        byte[] octs = object.address().toOctets();
+        // TODO: Writing (and reading) the number of octets is redundant:
+        // It is always Ip6Address.BYTE_LENGTH
+        output.writeInt(octs.length);
+        output.writeBytes(octs);
+        output.writeInt(object.prefixLength());
+    }
+
+    @Override
+    public Ip4Prefix read(Kryo kryo, Input input,
+            Class<Ip4Prefix> type) {
+        int octLen = input.readInt();
+        byte[] octs = new byte[octLen];
+        input.readBytes(octs);
+        int prefLen = input.readInt();
+        return Ip4Prefix.valueOf(octs, prefLen);
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6AddressSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6AddressSerializer.java
new file mode 100644
index 0000000..83c6a56
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6AddressSerializer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.store.serializers;
+
+import org.onlab.packet.Ip6Address;
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Kryo Serializer for {@link Ip6Address}.
+ */
+public class Ip6AddressSerializer extends Serializer<Ip6Address> {
+
+    /**
+     * Creates {@link Ip6Address} serializer instance.
+     */
+    public Ip6AddressSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output, Ip6Address object) {
+        byte[] octs = object.toOctets();
+        // TODO: Writing (and reading) the number of octets is redundant:
+        // It is always Ip6Address.BYTE_LENGTH
+        output.writeInt(octs.length);
+        output.writeBytes(octs);
+    }
+
+    @Override
+    public Ip6Address read(Kryo kryo, Input input, Class<Ip6Address> type) {
+        final int octLen = input.readInt();
+        byte[] octs = new byte[octLen];
+        input.readBytes(octs);
+        return Ip6Address.valueOf(octs);
+    }
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6PrefixSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6PrefixSerializer.java
new file mode 100644
index 0000000..8ecd13a
--- /dev/null
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/Ip6PrefixSerializer.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.store.serializers;
+
+import org.onlab.packet.Ip6Prefix;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Kryo Serializer for {@link Ip6Prefix}.
+ */
+public final class Ip6PrefixSerializer extends Serializer<Ip6Prefix> {
+
+    /**
+     * Creates {@link Ip6Prefix} serializer instance.
+     */
+    public Ip6PrefixSerializer() {
+        // non-null, immutable
+        super(false, true);
+    }
+
+    @Override
+    public void write(Kryo kryo, Output output,
+            Ip6Prefix object) {
+        byte[] octs = object.address().toOctets();
+        // TODO: Writing (and reading) the number of octets is redundant:
+        // It is always Ip6Address.BYTE_LENGTH
+        output.writeInt(octs.length);
+        output.writeBytes(octs);
+        output.writeInt(object.prefixLength());
+    }
+
+    @Override
+    public Ip6Prefix read(Kryo kryo, Input input,
+            Class<Ip6Prefix> type) {
+        int octLen = input.readInt();
+        byte[] octs = new byte[octLen];
+        input.readBytes(octs);
+        int prefLen = input.readInt();
+        return Ip6Prefix.valueOf(octs, prefLen);
+    }
+}
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 80db22e..4cba9f0 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
@@ -88,7 +88,11 @@
 import org.onlab.onos.store.Timestamp;
 import org.onlab.packet.ChassisId;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpPrefix;
+import org.onlab.packet.Ip4Prefix;
+import org.onlab.packet.Ip6Prefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 import org.onlab.util.KryoNamespace;
@@ -99,12 +103,30 @@
 
 public final class KryoNamespaces {
 
+    public static final KryoNamespace BASIC = KryoNamespace.newBuilder()
+            .register(ImmutableMap.class, new ImmutableMapSerializer())
+            .register(ImmutableList.class, new ImmutableListSerializer())
+            .register(ImmutableSet.class, new ImmutableSetSerializer())
+            .register(
+                    ArrayList.class,
+                    Arrays.asList().getClass(),
+                    HashMap.class,
+                    HashSet.class,
+                    LinkedList.class,
+                    byte[].class
+                    )
+            .build();
+
     /**
      * KryoNamespace which can serialize ON.lab misc classes.
      */
     public static final KryoNamespace MISC = KryoNamespace.newBuilder()
             .register(IpPrefix.class, new IpPrefixSerializer())
+            .register(Ip4Prefix.class, new Ip4PrefixSerializer())
+            .register(Ip6Prefix.class, new Ip6PrefixSerializer())
             .register(IpAddress.class, new IpAddressSerializer())
+            .register(Ip4Address.class, new Ip4AddressSerializer())
+            .register(Ip6Address.class, new Ip6AddressSerializer())
             .register(MacAddress.class, new MacAddressSerializer())
             .register(VlanId.class)
             .build();
@@ -115,19 +137,8 @@
      */
     public static final KryoNamespace API = KryoNamespace.newBuilder()
             .register(MISC)
-            .register(ImmutableMap.class, new ImmutableMapSerializer())
-            .register(ImmutableList.class, new ImmutableListSerializer())
-            .register(ImmutableSet.class, new ImmutableSetSerializer())
+            .register(BASIC)
             .register(
-                    //
-                    ArrayList.class,
-                    Arrays.asList().getClass(),
-                    HashMap.class,
-                    HashSet.class,
-                    LinkedList.class,
-                    byte[].class,
-                    //
-                    //
                     ControllerNode.State.class,
                     Device.Type.class,
                     Port.Type.class,
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 654ab11..82588b7 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
@@ -43,7 +43,11 @@
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.packet.ChassisId;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpPrefix;
+import org.onlab.packet.Ip4Prefix;
+import org.onlab.packet.Ip6Prefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.util.KryoNamespace;
 
@@ -168,11 +172,31 @@
     }
 
     @Test
+    public void testIp4Prefix() {
+        testSerialized(Ip4Prefix.valueOf("192.168.0.1/24"));
+    }
+
+    @Test
+    public void testIp6Prefix() {
+        testSerialized(Ip6Prefix.valueOf("1111:2222::/120"));
+    }
+
+    @Test
     public void testIpAddress() {
         testSerialized(IpAddress.valueOf("192.168.0.1"));
     }
 
     @Test
+    public void testIp4Address() {
+        testSerialized(Ip4Address.valueOf("192.168.0.1"));
+    }
+
+    @Test
+    public void testIp6Address() {
+        testSerialized(Ip6Address.valueOf("1111:2222::"));
+    }
+
+    @Test
     public void testMacAddress() {
         testSerialized(MacAddress.valueOf("12:34:56:78:90:ab"));
     }
diff --git a/pom.xml b/pom.xml
index ee2a3bb..28b387e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,8 @@
         <repository>
             <id>onlab-temp</id>
             <name>ON.lab temporary repository</name>
-            <url>http://mavenrepo.onlab.us:8081/nexus/content/groups/public</url>
+            <url>http://mavenrepo.onlab.us:8081/nexus/content/groups/public
+            </url>
             <snapshots>
                 <enabled>true</enabled>
             </snapshots>
@@ -421,6 +422,12 @@
                 </plugin>
 
                 <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-shade-plugin</artifactId>
+                    <version>2.3</version>
+                </plugin>
+
+                <plugin>
                     <groupId>org.codehaus.mojo</groupId>
                     <artifactId>findbugs-maven-plugin</artifactId>
                     <version>3.0.0</version>
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
index e5131df..16a32db 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
@@ -31,8 +31,8 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.openflow.controller.Dpid;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.IpPrefix;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 import org.projectfloodlight.openflow.protocol.OFFlowRemoved;
@@ -168,12 +168,12 @@
             case SET_NW_DST:
                 OFActionSetNwDst nwdst = (OFActionSetNwDst) act;
                 IPv4Address di = nwdst.getNwAddr();
-                builder.setIpDst(IpAddress.valueOf(di.getInt()));
+                builder.setIpDst(Ip4Address.valueOf(di.getInt()));
                 break;
             case SET_NW_SRC:
                 OFActionSetNwSrc nwsrc = (OFActionSetNwSrc) act;
                 IPv4Address si = nwsrc.getNwAddr();
-                builder.setIpSrc(IpAddress.valueOf(si.getInt()));
+                builder.setIpSrc(Ip4Address.valueOf(si.getInt()));
                 break;
             case EXPERIMENTER:
                 OFActionExperimenter exp = (OFActionExperimenter) act;
@@ -237,33 +237,33 @@
                 builder.matchEthType((short) ethType);
                 break;
             case IPV4_DST:
-                IpPrefix dip;
+                Ip4Prefix dip;
                 if (match.isPartiallyMasked(MatchField.IPV4_DST)) {
                     Masked<IPv4Address> maskedIp = match.getMasked(MatchField.IPV4_DST);
 
-                    dip = IpPrefix.valueOf(
+                    dip = Ip4Prefix.valueOf(
                             maskedIp.getValue().getInt(),
                             maskedIp.getMask().asCidrMaskLength());
                 } else {
-                    dip = IpPrefix.valueOf(
+                    dip = Ip4Prefix.valueOf(
                             match.get(MatchField.IPV4_DST).getInt(),
-                            IpPrefix.MAX_INET_MASK_LENGTH);
+                            Ip4Prefix.MAX_MASK_LENGTH);
                 }
 
                 builder.matchIPDst(dip);
                 break;
             case IPV4_SRC:
-                IpPrefix sip;
+                Ip4Prefix sip;
                 if (match.isPartiallyMasked(MatchField.IPV4_SRC)) {
                     Masked<IPv4Address> maskedIp = match.getMasked(MatchField.IPV4_SRC);
 
-                    sip = IpPrefix.valueOf(
+                    sip = Ip4Prefix.valueOf(
                             maskedIp.getValue().getInt(),
                             maskedIp.getMask().asCidrMaskLength());
                 } else {
-                    sip = IpPrefix.valueOf(
+                    sip = Ip4Prefix.valueOf(
                             match.get(MatchField.IPV4_SRC).getInt(),
-                            IpPrefix.MAX_INET_MASK_LENGTH);
+                            Ip4Prefix.MAX_MASK_LENGTH);
                 }
 
                 builder.matchIPSrc(sip);
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
index 0caf06b..1e9256f 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
@@ -30,8 +30,8 @@
 import org.onlab.onos.net.flow.criteria.Criteria.VlanIdCriterion;
 import org.onlab.onos.net.flow.criteria.Criteria.VlanPcpCriterion;
 import org.onlab.onos.net.flow.criteria.Criterion;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.IpPrefix;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
 import org.projectfloodlight.openflow.protocol.OFFactory;
 import org.projectfloodlight.openflow.protocol.OFFlowAdd;
 import org.projectfloodlight.openflow.protocol.OFFlowDelete;
@@ -124,6 +124,7 @@
         Match.Builder mBuilder = factory.buildMatch();
         EthCriterion eth;
         IPCriterion ip;
+        Ip4Prefix ip4Prefix;
         TcpPortCriterion tp;
         for (Criterion c : selector.criteria()) {
             switch (c.type()) {
@@ -145,32 +146,32 @@
                 break;
             case IPV4_DST:
                 ip = (IPCriterion) c;
-                if (ip.ip().prefixLength() != IpPrefix.MAX_INET_MASK_LENGTH) {
-                    IpAddress maskAddr =
-                        IpAddress.makeMaskPrefix(ip.ip().address().version(),
-                                                 ip.ip().prefixLength());
+                ip4Prefix = ip.ip().getIp4Prefix();
+                if (ip4Prefix.prefixLength() != Ip4Prefix.MAX_MASK_LENGTH) {
+                    Ip4Address maskAddr =
+                        Ip4Address.makeMaskPrefix(ip4Prefix.prefixLength());
                     Masked<IPv4Address> maskedIp =
-                        Masked.of(IPv4Address.of(ip.ip().address().toInt()),
+                        Masked.of(IPv4Address.of(ip4Prefix.address().toInt()),
                                   IPv4Address.of(maskAddr.toInt()));
                     mBuilder.setMasked(MatchField.IPV4_DST, maskedIp);
                 } else {
                     mBuilder.setExact(MatchField.IPV4_DST,
-                                IPv4Address.of(ip.ip().address().toInt()));
+                                IPv4Address.of(ip4Prefix.address().toInt()));
                 }
                 break;
             case IPV4_SRC:
                 ip = (IPCriterion) c;
-                if (ip.ip().prefixLength() != IpPrefix.MAX_INET_MASK_LENGTH) {
-                    IpAddress maskAddr =
-                        IpAddress.makeMaskPrefix(ip.ip().address().version(),
-                                                 ip.ip().prefixLength());
+                ip4Prefix = ip.ip().getIp4Prefix();
+                if (ip4Prefix.prefixLength() != Ip4Prefix.MAX_MASK_LENGTH) {
+                    Ip4Address maskAddr =
+                        Ip4Address.makeMaskPrefix(ip4Prefix.prefixLength());
                     Masked<IPv4Address> maskedIp =
-                        Masked.of(IPv4Address.of(ip.ip().address().toInt()),
+                        Masked.of(IPv4Address.of(ip4Prefix.address().toInt()),
                                   IPv4Address.of(maskAddr.toInt()));
                     mBuilder.setMasked(MatchField.IPV4_SRC, maskedIp);
                 } else {
                     mBuilder.setExact(MatchField.IPV4_SRC,
-                                IPv4Address.of(ip.ip().address().toInt()));
+                                IPv4Address.of(ip4Prefix.address().toInt()));
                 }
                 break;
             case IP_PROTO:
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer10.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer10.java
index 416dc0b..ec315f5 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer10.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer10.java
@@ -29,6 +29,7 @@
 import org.onlab.onos.net.flow.instructions.L2ModificationInstruction.ModVlanPcpInstruction;
 import org.onlab.onos.net.flow.instructions.L3ModificationInstruction;
 import org.onlab.onos.net.flow.instructions.L3ModificationInstruction.ModIPInstruction;
+import org.onlab.packet.Ip4Address;
 import org.projectfloodlight.openflow.protocol.OFFactory;
 import org.projectfloodlight.openflow.protocol.OFFlowAdd;
 import org.projectfloodlight.openflow.protocol.OFFlowDelete;
@@ -166,13 +167,16 @@
     private OFAction buildL3Modification(Instruction i) {
         L3ModificationInstruction l3m = (L3ModificationInstruction) i;
         ModIPInstruction ip;
+        Ip4Address ip4;
         switch (l3m.subtype()) {
         case IP_DST:
             ip = (ModIPInstruction) i;
-            return factory().actions().setNwDst(IPv4Address.of(ip.ip().toInt()));
+            ip4 = ip.ip().getIp4Address();
+            return factory().actions().setNwDst(IPv4Address.of(ip4.toInt()));
         case IP_SRC:
             ip = (ModIPInstruction) i;
-            return factory().actions().setNwSrc(IPv4Address.of(ip.ip().toInt()));
+            ip4 = ip.ip().getIp4Address();
+            return factory().actions().setNwSrc(IPv4Address.of(ip4.toInt()));
         default:
             log.warn("Unimplemented action type {}.", l3m.subtype());
             break;
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
index 9aff8b7..88dfd1c 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilderVer13.java
@@ -31,6 +31,7 @@
 import org.onlab.onos.net.flow.instructions.L2ModificationInstruction.ModVlanPcpInstruction;
 import org.onlab.onos.net.flow.instructions.L3ModificationInstruction;
 import org.onlab.onos.net.flow.instructions.L3ModificationInstruction.ModIPInstruction;
+import org.onlab.packet.Ip4Address;
 import org.projectfloodlight.openflow.protocol.OFFactory;
 import org.projectfloodlight.openflow.protocol.OFFlowAdd;
 import org.projectfloodlight.openflow.protocol.OFFlowDelete;
@@ -230,14 +231,17 @@
     private OFAction buildL3Modification(Instruction i) {
         L3ModificationInstruction l3m = (L3ModificationInstruction) i;
         ModIPInstruction ip;
+        Ip4Address ip4;
         OFOxm<?> oxm = null;
         switch (l3m.subtype()) {
         case IP_DST:
             ip = (ModIPInstruction) i;
-            oxm = factory().oxms().ipv4Dst(IPv4Address.of(ip.ip().toInt()));
+            ip4 = ip.ip().getIp4Address();
+            oxm = factory().oxms().ipv4Dst(IPv4Address.of(ip4.toInt()));
         case IP_SRC:
             ip = (ModIPInstruction) i;
-            oxm = factory().oxms().ipv4Src(IPv4Address.of(ip.ip().toInt()));
+            ip4 = ip.ip().getIp4Address();
+            oxm = factory().oxms().ipv4Src(IPv4Address.of(ip4.toInt()));
         default:
             log.warn("Unimplemented action type {}.", l3m.subtype());
             break;
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 427a23f..375fe6b 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -407,7 +407,8 @@
                     height: this.height,
                     uid: this.uid,
                     setRadio: this.setRadio,
-                    setKeys: this.setKeys
+                    setKeys: this.setKeys,
+                    dataLoadError: this.dataLoadError
                 }
             },
 
@@ -498,6 +499,16 @@
 
             uid: function (id) {
                 return makeUid(this, id);
+            },
+
+            // TODO : implement custom dialogs (don't use alerts)
+
+            dataLoadError: function (err, url) {
+                var msg = 'Data Load Error\n\n' +
+                    err.status + ' -- ' + err.statusText + '\n\n' +
+                    'relative-url: "' + url + '"\n\n' +
+                    'complete-url: "' + err.responseURL + '"';
+                alert(msg);
             }
 
             // TODO: consider schedule, clearTimer, etc.
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 88fcd94..eee9244 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,3 +24,6 @@
     opacity: 0.5;
 }
 
+svg .node {
+    fill: #03c;
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 7ac9adc..f5e1792 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -56,12 +56,24 @@
             opt: 'img/opt.png'
         },
         force: {
-            marginLR: 20,
-            marginTB: 20,
+            note: 'node.class or link.class is used to differentiate',
+            linkDistance: {
+                infra: 200,
+                host: 40
+            },
+            linkStrength: {
+                infra: 1.0,
+                host: 1.0
+            },
+            charge: {
+                device: -400,
+                host: -100
+            },
+            pad: 20,
             translate: function() {
                 return 'translate(' +
-                    config.force.marginLR + ',' +
-                    config.force.marginTB + ')';
+                    config.force.pad + ',' +
+                    config.force.pad + ')';
             }
         }
     };
@@ -94,7 +106,11 @@
     // D3 selections
     var svg,
         bgImg,
-        topoG;
+        topoG,
+        nodeG,
+        linkG,
+        node,
+        link;
 
     // ==============================
     // For Debugging / Development
@@ -175,22 +191,145 @@
     // ==============================
     // Private functions
 
-    // set the size of the given element to that of the view
-    function setSize(el, view) {
+    // set the size of the given element to that of the view (reduced if padded)
+    function setSize(el, view, pad) {
+        var padding = pad ? pad * 2 : 0;
         el.attr({
-            width: view.width(),
-            height: view.height()
+            width: view.width() - padding,
+            height: view.height() - padding
         });
     }
 
-
     function getNetworkData(view) {
         var url = getTopoUrl();
 
-        // TODO ...
-
+        console.log('Fetching JSON: ' + url);
+        d3.json(url, function(err, data) {
+            if (err) {
+                view.dataLoadError(err, url);
+            } else {
+                network.data = data;
+                drawNetwork(view);
+            }
+        });
     }
 
+    function drawNetwork(view) {
+        preprocessData(view);
+        updateLayout(view);
+    }
+
+    function preprocessData(view) {
+        var w = view.width(),
+            h = view.height(),
+            hDevice = h * 0.6,
+            hHost = h * 0.3,
+            data = network.data,
+            deviceLayout = computeInitLayout(w, hDevice, data.devices.length),
+            hostLayout = computeInitLayout(w, hHost, data.hosts.length);
+
+        network.lookup = {};
+        network.nodes = [];
+        network.links = [];
+        // we created new arrays, so need to set the refs in the force layout
+        network.force.nodes(network.nodes);
+        network.force.links(network.links);
+
+        // let's just start with the nodes
+
+        // note that both 'devices' and 'hosts' get mapped into the nodes array
+        function makeNode(d, cls, layout) {
+            var node = {
+                    id: d.id,
+                    labels: d.labels,
+                    class: cls,
+                    icon: cls,
+                    type: d.type,
+                    x: layout.x(),
+                    y: layout.y()
+                };
+            network.lookup[d.id] = node;
+            network.nodes.push(node);
+        }
+
+        // first the devices...
+        network.data.devices.forEach(function (d) {
+            makeNode(d, 'device', deviceLayout);
+        });
+
+        // then the hosts...
+        network.data.hosts.forEach(function (d) {
+            makeNode(d, 'host', hostLayout);
+        });
+
+        // TODO: process links
+    }
+
+    function computeInitLayout(w, h, n) {
+        var maxdw = 60,
+            compdw, dw, ox, layout;
+
+        if (n < 2) {
+            layout = { ox: w/2, dw: 0 }
+        } else {
+            compdw = (0.8 * w) / (n - 1);
+            dw = Math.min(maxdw, compdw);
+            ox = w/2 - ((n - 1)/2 * dw);
+            layout = { ox: ox, dw: dw }
+        }
+
+        layout.i = 0;
+
+        layout.x = function () {
+            var x = layout.ox + layout.i*layout.dw;
+            layout.i++;
+            return x;
+        };
+
+        layout.y = function () {
+            return h;
+        };
+
+        return layout;
+    }
+
+    function linkId(d) {
+        return d.source.id + '~' + d.target.id;
+    }
+
+    function nodeId(d) {
+        return d.id;
+    }
+
+    function updateLayout(view) {
+        link = link.data(network.force.links(), linkId);
+        link.enter().append('line')
+            .attr('class', 'link');
+        link.exit().remove();
+
+        node = node.data(network.force.nodes(), nodeId);
+        node.enter().append('circle')
+            .attr('id', function (d) { return 'nodeId-' + d.id; })
+            .attr('class', function (d) { return 'node'; })
+            .attr('r', 12);
+
+        network.force.start();
+    }
+
+
+    function tick() {
+        node.attr({
+            cx: function(d) { return d.x; },
+            cy: function(d) { return d.y; }
+        });
+
+        link.attr({
+            x1: function (d) { return d.source.x; },
+            y1: function (d) { return d.source.y; },
+            x2: function (d) { return d.target.x; },
+            y2: function (d) { return d.target.y; }
+        });
+    }
 
     // ==============================
     // View life-cycle callbacks
@@ -199,15 +338,15 @@
         var w = view.width(),
             h = view.height(),
             idBg = view.uid('bg'),
-            showBg = config.options.showBackground ? 'visible' : 'hidden';
+            showBg = config.options.showBackground ? 'visible' : 'hidden',
+            fcfg = config.force,
+            fpad = fcfg.pad,
+            forceDim = [w - 2*fpad, h - 2*fpad];
 
         // NOTE: view.$div is a D3 selection of the view's div
         svg = view.$div.append('svg');
         setSize(svg, view);
 
-        topoG = svg.append('g')
-            .attr('transform', config.force.translate());
-
         // load the background image
         bgImg = svg.append('svg:image')
             .attr({
@@ -219,6 +358,28 @@
             .style({
                 visibility: showBg
             });
+
+        // group for the topology
+        topoG = svg.append('g')
+            .attr('transform', fcfg.translate());
+
+        // subgroups for links and nodes
+        linkG = topoG.append('g').attr('id', 'links');
+        nodeG = topoG.append('g').attr('id', 'nodes');
+
+        // selection of nodes and links
+        link = linkG.selectAll('.link');
+        node = nodeG.selectAll('.node');
+
+        // set up the force layout
+        network.force = d3.layout.force()
+            .size(forceDim)
+            .nodes(network.nodes)
+            .links(network.links)
+            .charge(function (d) { return fcfg.charge[d.class]; })
+            .linkDistance(function (d) { return fcfg.linkDistance[d.class]; })
+            .linkStrength(function (d) { return fcfg.linkStrength[d.class]; })
+            .on('tick', tick);
     }