Consolidating null providers and making them fully configurable and integrated with the ConfigProvider to allow arbitrary topologies.

Change-Id: I899e27a9771af4013a3ce6da7f683a4927ffb438
diff --git a/cli/src/main/java/org/onosproject/cli/AbstractChoicesCompleter.java b/cli/src/main/java/org/onosproject/cli/AbstractChoicesCompleter.java
new file mode 100644
index 0000000..b846fde
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/AbstractChoicesCompleter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 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.onosproject.cli;
+
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Abstraction of a completer with preset choices.
+ */
+public abstract class AbstractChoicesCompleter extends AbstractCompleter {
+
+    protected abstract List<String> choices();
+
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        StringsCompleter delegate = new StringsCompleter();
+        SortedSet<String> strings = delegate.getStrings();
+        choices().forEach(strings::add);
+        return delegate.complete(buffer, cursor, candidates);
+    }
+
+}
diff --git a/cli/src/main/java/org/onosproject/cli/net/AbstractCompleter.java b/cli/src/main/java/org/onosproject/cli/AbstractCompleter.java
similarity index 97%
rename from cli/src/main/java/org/onosproject/cli/net/AbstractCompleter.java
rename to cli/src/main/java/org/onosproject/cli/AbstractCompleter.java
index 36c0a04..f232596 100644
--- a/cli/src/main/java/org/onosproject/cli/net/AbstractCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/AbstractCompleter.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.cli.net;
+package org.onosproject.cli;
 
 import org.apache.felix.service.command.CommandSession;
 import org.apache.karaf.shell.console.CommandSessionHolder;
diff --git a/cli/src/main/java/org/onosproject/cli/StartStopCompleter.java b/cli/src/main/java/org/onosproject/cli/StartStopCompleter.java
new file mode 100644
index 0000000..b9d9b0f
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/StartStopCompleter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 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.onosproject.cli;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Start/stop command completer.
+ */
+public class StartStopCompleter extends AbstractChoicesCompleter {
+
+    public static final String START = "start";
+    public static final String STOP = "stop";
+
+    @Override
+    public List<String> choices() {
+        return ImmutableList.of(START, STOP);
+    }
+}
diff --git a/cli/src/main/java/org/onosproject/cli/UpDownCompleter.java b/cli/src/main/java/org/onosproject/cli/UpDownCompleter.java
new file mode 100644
index 0000000..9d8eda5
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/UpDownCompleter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015 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.onosproject.cli;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * Up/down command completer.
+ */
+public class UpDownCompleter extends AbstractChoicesCompleter {
+
+    public static final String UP = "up";
+    public static final String DOWN = "down";
+
+    @Override
+    public List<String> choices() {
+        return ImmutableList.of(UP, DOWN);
+    }
+}
diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
index b9ea774..51611ff 100644
--- a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java
@@ -15,30 +15,20 @@
  */
 package org.onosproject.cli.app;
 
-import org.apache.karaf.shell.console.Completer;
-import org.apache.karaf.shell.console.completer.StringsCompleter;
+import com.google.common.collect.ImmutableList;
+import org.onosproject.cli.AbstractChoicesCompleter;
 
 import java.util.List;
-import java.util.SortedSet;
 
 import static org.onosproject.cli.app.ApplicationCommand.*;
 
 /**
- * Application name completer.
+ * Application command completer.
  */
-public class ApplicationCommandCompleter implements Completer {
+public class ApplicationCommandCompleter extends AbstractChoicesCompleter {
     @Override
-    public int complete(String buffer, int cursor, List<String> candidates) {
-        // Delegate string completer
-        StringsCompleter delegate = new StringsCompleter();
-        SortedSet<String> strings = delegate.getStrings();
-        strings.add(INSTALL);
-        strings.add(UNINSTALL);
-        strings.add(ACTIVATE);
-        strings.add(DEACTIVATE);
-
-        // Now let the completer do the work for figuring out what to offer.
-        return delegate.complete(buffer, cursor, candidates);
+    public List<String> choices() {
+        return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE);
     }
 
 }
diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
index e975be0..9342721 100644
--- a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java
@@ -19,7 +19,7 @@
 import org.apache.karaf.shell.console.completer.StringsCompleter;
 import org.onosproject.app.ApplicationService;
 import org.onosproject.app.ApplicationState;
-import org.onosproject.cli.net.AbstractCompleter;
+import org.onosproject.cli.AbstractCompleter;
 import org.onosproject.core.Application;
 
 import java.util.Iterator;
diff --git a/cli/src/main/java/org/onosproject/cli/cfg/ComponentPropertyNameCompleter.java b/cli/src/main/java/org/onosproject/cli/cfg/ComponentPropertyNameCompleter.java
index 4c6e5e9..98e1969 100644
--- a/cli/src/main/java/org/onosproject/cli/cfg/ComponentPropertyNameCompleter.java
+++ b/cli/src/main/java/org/onosproject/cli/cfg/ComponentPropertyNameCompleter.java
@@ -19,7 +19,7 @@
 import org.apache.karaf.shell.console.completer.StringsCompleter;
 import org.onosproject.cfg.ComponentConfigService;
 import org.onosproject.cfg.ConfigProperty;
-import org.onosproject.cli.net.AbstractCompleter;
+import org.onosproject.cli.AbstractCompleter;
 
 import java.util.List;
 import java.util.Set;
diff --git a/cli/src/main/java/org/onosproject/cli/net/LinkDstCompleter.java b/cli/src/main/java/org/onosproject/cli/net/LinkDstCompleter.java
new file mode 100644
index 0000000..fbf116f
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/net/LinkDstCompleter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2015 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.onosproject.cli.net;
+
+import org.apache.karaf.shell.console.completer.ArgumentCompleter;
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+import org.onosproject.cli.AbstractCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.link.LinkService;
+
+import java.util.List;
+import java.util.SortedSet;
+
+import static org.onosproject.cli.net.AddPointToPointIntentCommand.getDeviceId;
+import static org.onosproject.cli.net.AddPointToPointIntentCommand.getPortNumber;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+
+/**
+ * Link end-point completer.
+ */
+public class LinkDstCompleter extends AbstractCompleter {
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        // Fetch our service and feed it's offerings to the string completer
+        LinkService service = AbstractShellCommand.get(LinkService.class);
+
+        // Link source the previous argument.
+        ArgumentCompleter.ArgumentList list = getArgumentList();
+        String srcArg = list.getArguments()[list.getCursorArgumentIndex() - 1];
+
+        // Generate the device ID/port number identifiers
+        SortedSet<String> strings = delegate.getStrings();
+        try {
+            ConnectPoint src = new ConnectPoint(deviceId(getDeviceId(srcArg)),
+                                                portNumber(getPortNumber(srcArg)));
+            service.getEgressLinks(src)
+                    .forEach(link -> strings.add(link.dst().elementId().toString() +
+                                                         "/" + link.dst().port()));
+        } catch (NumberFormatException e) {
+            System.err.println("Invalid connect-point");
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(buffer, cursor, candidates);
+    }
+
+}
diff --git a/cli/src/main/java/org/onosproject/cli/net/LinkSrcCompleter.java b/cli/src/main/java/org/onosproject/cli/net/LinkSrcCompleter.java
new file mode 100644
index 0000000..dec8c6b
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/net/LinkSrcCompleter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015 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.onosproject.cli.net;
+
+import org.apache.karaf.shell.console.completer.StringsCompleter;
+import org.onosproject.cli.AbstractCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.link.LinkService;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Link end-point completer.
+ */
+public class LinkSrcCompleter extends AbstractCompleter {
+    @Override
+    public int complete(String buffer, int cursor, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        // Fetch our service and feed it's offerings to the string completer
+        LinkService service = AbstractShellCommand.get(LinkService.class);
+
+        // Generate the device ID/port number identifiers
+        SortedSet<String> strings = delegate.getStrings();
+        service.getLinks()
+                .forEach(link -> strings.add(link.src().elementId().toString() +
+                                                     "/" + link.src().port()));
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(buffer, cursor, candidates);
+    }
+
+}
diff --git a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 047016c..0b43f5f 100644
--- a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -332,4 +332,7 @@
     <bean id="ipProtocolCompleter" class="org.onosproject.cli.net.IpProtocolCompleter"/>
     <bean id="driverNameCompleter" class="org.onosproject.cli.net.DriverNameCompleter"/>
 
+    <bean id="startStopCompleter" class="org.onosproject.cli.StartStopCompleter"/>
+    <bean id="upDownCompleter" class="org.onosproject.cli.UpDownCompleter"/>
+
 </blueprint>
diff --git a/core/api/src/main/java/org/onosproject/net/device/DeviceAdminService.java b/core/api/src/main/java/org/onosproject/net/device/DeviceAdminService.java
index e84727c..901df2e 100644
--- a/core/api/src/main/java/org/onosproject/net/device/DeviceAdminService.java
+++ b/core/api/src/main/java/org/onosproject/net/device/DeviceAdminService.java
@@ -20,7 +20,7 @@
 /**
  * Service for administering the inventory of infrastructure devices.
  */
-public interface DeviceAdminService {
+public interface DeviceAdminService extends DeviceService {
 
     /**
      * Removes the device with the specified identifier.
diff --git a/core/api/src/main/java/org/onosproject/net/host/HostAdminService.java b/core/api/src/main/java/org/onosproject/net/host/HostAdminService.java
index 2b20313..86b12b9 100644
--- a/core/api/src/main/java/org/onosproject/net/host/HostAdminService.java
+++ b/core/api/src/main/java/org/onosproject/net/host/HostAdminService.java
@@ -21,7 +21,7 @@
 /**
  * Service for administering the inventory of end-station hosts.
  */
-public interface HostAdminService {
+public interface HostAdminService extends HostService {
 
     /**
      * Removes the end-station host with the specified identifier.
diff --git a/core/api/src/main/java/org/onosproject/net/link/LinkAdminService.java b/core/api/src/main/java/org/onosproject/net/link/LinkAdminService.java
index 8ad8a07..b671719 100644
--- a/core/api/src/main/java/org/onosproject/net/link/LinkAdminService.java
+++ b/core/api/src/main/java/org/onosproject/net/link/LinkAdminService.java
@@ -21,7 +21,7 @@
 /**
  * Service for administering the inventory of infrastructure links.
  */
-public interface LinkAdminService {
+public interface LinkAdminService extends LinkService {
 
     /**
      * Removes all infrastructure links leading to and from the
diff --git a/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java b/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
index ae1669b..1930a47 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/group/impl/DistributedGroupStore.java
@@ -705,7 +705,8 @@
                         remove(new GroupStoreKeyMapKey(deviceId, group.appCookie()));
                 }
             } else {
-                if (deviceAuditStatus.get(deviceId)) {
+                Boolean audited = deviceAuditStatus.get(deviceId);
+                if (audited != null && audited) {
                     log.debug("deviceInitialAuditCompleted: Clearing AUDIT "
                                       + "status for device {}", deviceId);
                     deviceAuditStatus.put(deviceId, false);
@@ -717,8 +718,8 @@
     @Override
     public boolean deviceInitialAuditStatus(DeviceId deviceId) {
         synchronized (deviceAuditStatus) {
-            return (deviceAuditStatus.get(deviceId) != null)
-                    ? deviceAuditStatus.get(deviceId) : false;
+            Boolean audited = deviceAuditStatus.get(deviceId);
+            return audited != null && audited;
         }
     }
 
diff --git a/docs/pom.xml b/docs/pom.xml
index 050362f..7199f5a 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -79,7 +79,7 @@
                         <group>
                             <title>Null Providers</title>
                             <packages>
-                                org.onosproject.provider.nil.*
+                                org.onosproject.provider.nil:org.onosproject.provider.nil.*
                             </packages>
                         </group>
                         <group>
diff --git a/features/features.xml b/features/features.xml
index c30d272..d4d37ff 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -131,13 +131,7 @@
     <feature name="onos-null" version="@FEATURE-VERSION"
              description="ONOS Null providers">
         <feature>onos-api</feature>
-
-        <bundle>mvn:org.onosproject/onos-null-provider-device/@ONOS-VERSION</bundle>
-        <bundle>mvn:org.onosproject/onos-null-provider-link/@ONOS-VERSION</bundle>
-        <bundle>mvn:org.onosproject/onos-null-provider-host/@ONOS-VERSION</bundle>
-        <bundle>mvn:org.onosproject/onos-null-provider-packet/@ONOS-VERSION</bundle>
-        <bundle>mvn:org.onosproject/onos-null-provider-flow/@ONOS-VERSION</bundle>
-
+        <bundle>mvn:org.onosproject/onos-null-provider/@ONOS-VERSION</bundle>
     </feature>
 
     <feature name="onos-openflow" version="@FEATURE-VERSION"
diff --git a/providers/null/pom.xml b/providers/null/pom.xml
index 6a2e34d..fa1b6d3 100644
--- a/providers/null/pom.xml
+++ b/providers/null/pom.xml
@@ -26,20 +26,25 @@
         <relativePath>../pom.xml</relativePath>
     </parent>
 
-    <artifactId>onos-null-providers</artifactId>
-    <packaging>pom</packaging>
+    <artifactId>onos-null-provider</artifactId>
+    <packaging>bundle</packaging>
 
     <description>ONOS null protocol adapters</description>
 
-    <modules>
-        <module>device</module>
-        <module>link</module>
-        <module>host</module>
-        <module>packet</module>
-        <module>flow</module>
-    </modules>
-
     <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
 
         <dependency>
             <groupId>org.onosproject</groupId>
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/CentipedeTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/CentipedeTopologySimulator.java
new file mode 100644
index 0000000..5234d44
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/CentipedeTopologySimulator.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+/**
+ * Linear topology with hosts on every device.
+ */
+public class CentipedeTopologySimulator extends LinearTopologySimulator {
+
+    /**
+     * Creates simulated hosts.
+     */
+    protected void createHosts() {
+        deviceIds.forEach(id -> createHosts(id, infrastructurePorts));
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/ConfiguredTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/ConfiguredTopologySimulator.java
new file mode 100644
index 0000000..ad57bf3
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/ConfiguredTopologySimulator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+/**
+ * Topology simulator which operates on topology configured via the REST API
+ * config service.
+ */
+public class ConfiguredTopologySimulator extends TopologySimulator {
+
+    @Override
+    protected void createDevices() {
+        deviceService.getDevices()
+                .forEach(device -> deviceProviderService
+                        .deviceConnected(device.id(), description(device)));
+    }
+
+    @Override
+    protected void createLinks() {
+        linkService.getLinks()
+                .forEach(link -> linkProviderService
+                        .linkDetected(description(link)));
+    }
+
+    @Override
+    protected void createHosts() {
+        hostService.getHosts()
+                .forEach(host -> hostProviderService
+                        .hostDetected(host.id(), description(host)));
+    }
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/LinearTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/LinearTopologySimulator.java
new file mode 100644
index 0000000..8e8c759
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/LinearTopologySimulator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Linear topology simulator.
+ */
+public class LinearTopologySimulator extends TopologySimulator {
+
+    @Override
+    protected void processTopoShape(String shape) {
+        super.processTopoShape(shape);
+        deviceCount = (topoShape.length == 1) ? deviceCount : Integer.parseInt(topoShape[1]);
+    }
+
+    @Override
+    public void setUpTopology() {
+        checkArgument(deviceCount > 1, "There must be at least 2 devices");
+
+        prepareForDeviceEvents(deviceCount);
+        createDevices();
+        waitForDeviceEvents();
+
+        createLinks();
+        createHosts();
+    }
+
+    @Override
+    protected void createLinks() {
+        for (int i = 0, n = deviceCount - 1; i < n; i++) {
+            createLink(i, i + 1);
+        }
+    }
+
+    @Override
+    protected void createHosts() {
+        createHosts(deviceIds.get(0), infrastructurePorts);
+        createHosts(deviceIds.get(deviceCount - 1), infrastructurePorts);
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/MeshTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/MeshTopologySimulator.java
new file mode 100644
index 0000000..d3f2d6a
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/MeshTopologySimulator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+/**
+ * Full mesh topology with hosts at each device.
+ */
+public class MeshTopologySimulator extends TopologySimulator {
+
+    @Override
+    protected void processTopoShape(String shape) {
+        super.processTopoShape(shape);
+        // FIXME: implement this
+    }
+
+    @Override
+    public void setUpTopology() {
+        // FIXME: implement this
+        // checkArgument(FIXME, "There must be at least ...");
+        super.setUpTopology();
+    }
+
+    @Override
+    protected void createLinks() {
+    }
+
+    @Override
+    protected void createHosts() {
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/NullFlowRuleProvider.java b/providers/null/src/main/java/org/onosproject/provider/nil/NullFlowRuleProvider.java
new file mode 100644
index 0000000..9b01d12
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/NullFlowRuleProvider.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import com.google.common.collect.Sets;
+import org.jboss.netty.util.HashedWheelTimer;
+import org.jboss.netty.util.Timeout;
+import org.jboss.netty.util.TimerTask;
+import org.onlab.util.Timer;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.CompletedBatchOperation;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleBatchEntry;
+import org.onosproject.net.flow.FlowRuleBatchOperation;
+import org.onosproject.net.flow.FlowRuleProvider;
+import org.onosproject.net.flow.FlowRuleProviderService;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Null provider to accept any flow and report them.
+ */
+class NullFlowRuleProvider extends NullProviders.AbstractNullProvider
+        implements FlowRuleProvider {
+
+    private final Logger log = getLogger(getClass());
+
+    private ConcurrentMap<DeviceId, Set<FlowEntry>> flowTable = new ConcurrentHashMap<>();
+
+    private FlowRuleProviderService providerService;
+
+    private HashedWheelTimer timer = Timer.getTimer();
+    private Timeout timeout;
+
+    /**
+     * Starts the flow rule provider simulation.
+     *
+     * @param providerService flow rule provider service
+     */
+    void start(FlowRuleProviderService providerService) {
+        this.providerService = providerService;
+        timeout = timer.newTimeout(new StatisticTask(), 5, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Stops the flow rule provider simulation.
+     */
+    void stop() {
+        timeout.cancel();
+    }
+
+    @Override
+    public void applyFlowRule(FlowRule... flowRules) {
+        // FIXME: invoke executeBatch
+    }
+
+    @Override
+    public void removeFlowRule(FlowRule... flowRules) {
+        // FIXME: invoke executeBatch
+    }
+
+    @Override
+    public void removeRulesById(ApplicationId id, FlowRule... flowRules) {
+        throw new UnsupportedOperationException("Cannot remove by appId from null provider");
+    }
+
+    @Override
+    public void executeBatch(FlowRuleBatchOperation batch) {
+        // TODO: consider checking mastership
+        Set<FlowEntry> entries =
+                flowTable.getOrDefault(batch.deviceId(),
+                                       Sets.newConcurrentHashSet());
+        for (FlowRuleBatchEntry fbe : batch.getOperations()) {
+            switch (fbe.operator()) {
+                case ADD:
+                    entries.add(new DefaultFlowEntry(fbe.target()));
+                    break;
+                case REMOVE:
+                    entries.remove(new DefaultFlowEntry(fbe.target()));
+                    break;
+                case MODIFY:
+                    FlowEntry entry = new DefaultFlowEntry(fbe.target());
+                    entries.remove(entry);
+                    entries.add(entry);
+                    break;
+                default:
+                    log.error("Unknown flow operation: {}", fbe);
+            }
+        }
+        flowTable.put(batch.deviceId(), entries);
+        CompletedBatchOperation op =
+                new CompletedBatchOperation(true, Collections.emptySet(),
+                                            batch.deviceId());
+        providerService.batchOperationCompleted(batch.id(), op);
+    }
+
+    // Periodically reports flow rule statistics.
+    private class StatisticTask implements TimerTask {
+        @Override
+        public void run(Timeout to) throws Exception {
+            for (DeviceId devId : flowTable.keySet()) {
+                Set<FlowEntry> entries =
+                        flowTable.getOrDefault(devId, Collections.emptySet());
+                providerService.pushFlowMetrics(devId, entries);
+            }
+            timeout = timer.newTimeout(to.getTask(), 5, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/NullPacketProvider.java b/providers/null/src/main/java/org/onosproject/provider/nil/NullPacketProvider.java
new file mode 100644
index 0000000..a1baec9
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/NullPacketProvider.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import org.jboss.netty.util.HashedWheelTimer;
+import org.jboss.netty.util.Timeout;
+import org.jboss.netty.util.TimerTask;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.ICMP;
+import org.onlab.util.Timer;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.packet.DefaultInboundPacket;
+import org.onosproject.net.packet.DefaultPacketContext;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketProvider;
+import org.onosproject.net.packet.PacketProviderService;
+import org.slf4j.Logger;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.onosproject.net.MastershipRole.MASTER;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provider which generates simulated packets and acts as a sink for outbound
+ * packets. To be used for benchmarking only.
+ */
+class NullPacketProvider extends NullProviders.AbstractNullProvider
+        implements PacketProvider {
+
+    private static final int INITIAL_DELAY = 5;
+    private final Logger log = getLogger(getClass());
+
+    // Arbitrary host src/dst
+    private static final int SRC_HOST = 2;
+    private static final int DST_HOST = 5;
+
+    // Time between event firing, in milliseconds
+    private int delay;
+
+    // TODO: use host service to pick legitimate hosts connected to devices
+    private HostService hostService;
+    private PacketProviderService providerService;
+
+    private List<Device> devices;
+    private int currentDevice = 0;
+
+    private HashedWheelTimer timer = Timer.getTimer();
+    private Timeout timeout;
+
+    /**
+     * Starts the packet generation process.
+     *
+     * @param packetRate      packets per second
+     * @param hostService     host service
+     * @param deviceService   device service
+     * @param providerService packet provider service
+     */
+    void start(int packetRate, HostService hostService,
+               DeviceAdminService deviceService,
+               PacketProviderService providerService) {
+        this.hostService = hostService;
+        this.providerService = providerService;
+
+        this.devices = copyOf(deviceService.getDevices()).stream()
+                .filter(d -> deviceService.getRole(d.id()) == MASTER)
+                .collect(Collectors.toList());
+
+        adjustRate(packetRate);
+        timeout = timer.newTimeout(new PacketDriverTask(), INITIAL_DELAY, SECONDS);
+    }
+
+    /**
+     * Adjusts packet rate.
+     *
+     * @param packetRate new packet rate
+     */
+    void adjustRate(int packetRate) {
+        delay = 1000 / packetRate;
+        log.info("Settings: packetRate={}, delay={}", packetRate, delay);
+    }
+
+    /**
+     * Stops the packet generation process.
+     */
+    void stop() {
+        if (timeout != null) {
+            timeout.cancel();
+        }
+    }
+
+    @Override
+    public void emit(OutboundPacket packet) {
+        // We don't have a network to emit to. Keep a counter here, maybe?
+    }
+
+    /**
+     * Generates packet events at a given rate.
+     */
+    private class PacketDriverTask implements TimerTask {
+
+        // Filler echo request
+        ICMP icmp;
+        Ethernet eth;
+
+        public PacketDriverTask() {
+            icmp = new ICMP();
+            icmp.setIcmpType((byte) 8).setIcmpCode((byte) 0).setChecksum((short) 0);
+            eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV4);
+            eth.setPayload(icmp);
+        }
+
+        @Override
+        public void run(Timeout to) {
+            if (!devices.isEmpty()) {
+                sendEvent(devices.get(currentDevice));
+                currentDevice = (currentDevice + 1) % devices.size();
+                timeout = timer.newTimeout(to.getTask(), delay, TimeUnit.MILLISECONDS);
+            }
+        }
+
+        private void sendEvent(Device device) {
+            // Make it look like things came from ports attached to hosts
+            eth.setSourceMACAddress("00:00:10:00:00:0" + SRC_HOST)
+                    .setDestinationMACAddress("00:00:10:00:00:0" + DST_HOST);
+            InboundPacket inPkt = new DefaultInboundPacket(
+                    new ConnectPoint(device.id(), PortNumber.portNumber(SRC_HOST)),
+                    eth, ByteBuffer.wrap(eth.serialize()));
+            providerService.processPacket(new NullPacketContext(inPkt, null));
+        }
+    }
+
+     // Minimal PacketContext to make core and applications happy.
+    private final class NullPacketContext extends DefaultPacketContext {
+        private NullPacketContext(InboundPacket inPkt, OutboundPacket outPkt) {
+            super(System.currentTimeMillis(), inPkt, outPkt, false);
+        }
+
+        @Override
+        public void send() {
+            // We don't send anything out.
+        }
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/NullProviders.java b/providers/null/src/main/java/org/onosproject/provider/nil/NullProviders.java
new file mode 100644
index 0000000..d8ccb3e
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/NullProviders.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+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.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.mastership.MastershipAdminService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.MastershipRole;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceProvider;
+import org.onosproject.net.device.DeviceProviderRegistry;
+import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.flow.FlowRuleProviderRegistry;
+import org.onosproject.net.flow.FlowRuleProviderService;
+import org.onosproject.net.host.HostProvider;
+import org.onosproject.net.host.HostProviderRegistry;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.LinkProvider;
+import org.onosproject.net.link.LinkProviderRegistry;
+import org.onosproject.net.link.LinkProviderService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.packet.PacketProviderRegistry;
+import org.onosproject.net.packet.PacketProviderService;
+import org.onosproject.net.provider.AbstractProvider;
+import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+
+import java.util.Dictionary;
+import java.util.Properties;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onlab.util.Tools.delay;
+import static org.onlab.util.Tools.get;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.MastershipRole.MASTER;
+import static org.onosproject.net.MastershipRole.NONE;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provider of a fake network environment, i.e. devices, links, hosts, etc.
+ * To be used for benchmarking only.
+ */
+@Component(immediate = true)
+@Service(value = NullProviders.class)
+public class NullProviders {
+
+    private static final Logger log = getLogger(NullProviders.class);
+
+    static final String SCHEME = "null";
+    static final String PROVIDER_ID = "org.onosproject.provider.nil";
+
+    private static final String FORMAT =
+            "Settings: enabled={}, topoShape={}, deviceCount={}, " +
+                    "hostCount={}, packetRate={}, mutationRate={}";
+
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipAdminService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceAdminService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkService linkService;
+
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceProviderRegistry deviceProviderRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostProviderRegistry hostProviderRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkProviderRegistry linkProviderRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowRuleProviderRegistry flowRuleProviderRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PacketProviderRegistry packetProviderRegistry;
+
+
+    private final NullDeviceProvider deviceProvider = new NullDeviceProvider();
+    private final NullLinkProvider linkProvider = new NullLinkProvider();
+    private final NullHostProvider hostProvider = new NullHostProvider();
+    private final NullFlowRuleProvider flowRuleProvider = new NullFlowRuleProvider();
+    private final NullPacketProvider packetProvider = new NullPacketProvider();
+    private final TopologyMutationDriver topologyMutationDriver = new TopologyMutationDriver();
+
+    private DeviceProviderService deviceProviderService;
+    private HostProviderService hostProviderService;
+    private LinkProviderService linkProviderService;
+    private FlowRuleProviderService flowRuleProviderService;
+    private PacketProviderService packetProviderService;
+
+    private TopologySimulator simulator;
+
+    @Property(name = "enabled", boolValue = false,
+            label = "Enables or disables the provider")
+    private boolean enabled = false;
+
+    private static final String DEFAULT_TOPO_SHAPE = "configured";
+    @Property(name = "topoShape", value = DEFAULT_TOPO_SHAPE,
+            label = "Topology shape: configured, linear, reroute, tree, spineleaf, mesh")
+    private String topoShape = DEFAULT_TOPO_SHAPE;
+
+    private static final int DEFAULT_DEVICE_COUNT = 10;
+    @Property(name = "deviceCount", intValue = DEFAULT_DEVICE_COUNT,
+            label = "Number of devices to generate")
+    private int deviceCount = DEFAULT_DEVICE_COUNT;
+
+    private static final int DEFAULT_HOST_COUNT = 5;
+    @Property(name = "hostCount", intValue = DEFAULT_HOST_COUNT,
+            label = "Number of host to generate per device")
+    private int hostCount = DEFAULT_HOST_COUNT;
+
+    private static final int DEFAULT_PACKET_RATE = 5;
+    @Property(name = "packetRate", intValue = DEFAULT_PACKET_RATE,
+            label = "Packet-in/s rate; 0 for no packets")
+    private int packetRate = DEFAULT_PACKET_RATE;
+
+    private static final double DEFAULT_MUTATION_RATE = 0;
+    @Property(name = "mutationRate", doubleValue = DEFAULT_MUTATION_RATE,
+            label = "Link event/s topology mutation rate; 0 for no mutations")
+    private double mutationRate = DEFAULT_MUTATION_RATE;
+
+    private static final String DEFAULT_MASTERSHIP = "random";
+    @Property(name = "mastership", value = DEFAULT_MASTERSHIP,
+            label = "Mastership given as 'random' or 'node1=dpid,dpid/node2=dpid,...'")
+    private String mastership = DEFAULT_MASTERSHIP;
+
+
+    @Activate
+    public void activate(ComponentContext context) {
+        cfgService.registerProperties(getClass());
+
+        deviceProviderService = deviceProviderRegistry.register(deviceProvider);
+        hostProviderService = hostProviderRegistry.register(hostProvider);
+        linkProviderService = linkProviderRegistry.register(linkProvider);
+        flowRuleProviderService = flowRuleProviderRegistry.register(flowRuleProvider);
+        packetProviderService = packetProviderRegistry.register(packetProvider);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate(ComponentContext context) {
+        cfgService.unregisterProperties(getClass(), false);
+        tearDown();
+
+        deviceProviderRegistry.unregister(deviceProvider);
+        hostProviderRegistry.unregister(hostProvider);
+        linkProviderRegistry.unregister(linkProvider);
+        flowRuleProviderRegistry.unregister(flowRuleProvider);
+        packetProviderRegistry.unregister(packetProvider);
+
+        deviceProviderService = null;
+        hostProviderService = null;
+        linkProviderService = null;
+        flowRuleProviderService = null;
+        packetProviderService = null;
+
+        log.info("Stopped");
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+
+        boolean newEnabled;
+        int newDeviceCount, newHostCount, newPacketRate;
+        double newMutationRate;
+        String newTopoShape, newMastership;
+        try {
+            String s = get(properties, "enabled");
+            newEnabled = isNullOrEmpty(s) ? enabled : Boolean.parseBoolean(s.trim());
+
+            newTopoShape = get(properties, "topoShape");
+            newMastership = get(properties, "mastership");
+
+            s = get(properties, "deviceCount");
+            newDeviceCount = isNullOrEmpty(s) ? deviceCount : Integer.parseInt(s.trim());
+
+            s = get(properties, "hostCount");
+            newHostCount = isNullOrEmpty(s) ? hostCount : Integer.parseInt(s.trim());
+
+            s = get(properties, "packetRate");
+            newPacketRate = isNullOrEmpty(s) ? packetRate : Integer.parseInt(s.trim());
+
+            s = get(properties, "mutationRate");
+            newMutationRate = isNullOrEmpty(s) ? mutationRate : Double.parseDouble(s.trim());
+
+        } catch (NumberFormatException e) {
+            log.warn(e.getMessage());
+            newEnabled = enabled;
+            newTopoShape = topoShape;
+            newDeviceCount = deviceCount;
+            newHostCount = hostCount;
+            newPacketRate = packetRate;
+            newMutationRate = mutationRate;
+            newMastership = mastership;
+        }
+
+        // Any change in the following parameters implies hard restart
+        if (newEnabled != enabled || !newTopoShape.equals(topoShape) ||
+                newDeviceCount != deviceCount || newHostCount != hostCount) {
+            enabled = newEnabled;
+            topoShape = newTopoShape;
+            deviceCount = newDeviceCount;
+            hostCount = newHostCount;
+            packetRate = newPacketRate;
+            mutationRate = newMutationRate;
+            restartSimulation();
+        }
+
+        // Any change in the following parameters implies just a rate change
+        if (newPacketRate != packetRate || newMutationRate != mutationRate) {
+            packetRate = newPacketRate;
+            mutationRate = newMutationRate;
+            adjustRates();
+        }
+
+        // Any change in mastership implies just reassignments.
+        if (!newMastership.equals(mastership)) {
+            mastership = newMastership;
+            reassignMastership();
+        }
+
+        log.info(FORMAT, enabled, topoShape, deviceCount, hostCount,
+                 packetRate, mutationRate);
+    }
+
+    /**
+     * Severs the link between the specified end-points in both directions.
+     *
+     * @param one link endpoint
+     * @param two link endpoint
+     */
+    public void severLink(ConnectPoint one, ConnectPoint two) {
+        if (enabled) {
+            topologyMutationDriver.severLink(one, two);
+        }
+    }
+
+    /**
+     * Severs the link between the specified end-points in both directions.
+     *
+     * @param one link endpoint
+     * @param two link endpoint
+     */
+    public void repairLink(ConnectPoint one, ConnectPoint two) {
+        if (enabled) {
+            topologyMutationDriver.repairLink(one, two);
+        }
+    }
+
+    // Resets simulation based on the current configuration parameters.
+    private void restartSimulation() {
+        tearDown();
+        if (enabled) {
+            setUp();
+        }
+    }
+
+    // Sets up the topology simulation and all providers.
+    private void setUp() {
+        simulator = selectSimulator(topoShape);
+        simulator.init(topoShape, deviceCount, hostCount,
+                       new DefaultServiceDirectory(),
+                       deviceProviderService, hostProviderService,
+                       linkProviderService);
+        simulator.setUpTopology();
+        flowRuleProvider.start(flowRuleProviderService);
+        packetProvider.start(packetRate, hostService, deviceService,
+                             packetProviderService);
+        topologyMutationDriver.start(mutationRate, linkService, deviceService,
+                                     linkProviderService);
+    }
+
+    // Selects the simulator based on the specified name.
+    private TopologySimulator selectSimulator(String topoShape) {
+        if (topoShape.matches("linear([,].*|$)")) {
+            return new LinearTopologySimulator();
+        } else if (topoShape.matches("centipede([,].*|$)")) {
+            return new CentipedeTopologySimulator();
+        } else if (topoShape.matches("reroute([,].*|$)")) {
+            return new RerouteTopologySimulator();
+        } else if (topoShape.matches("tree([,].*|$)")) {
+            return new TreeTopologySimulator();
+        } else if (topoShape.matches("spineleaf([,].*|$)")) {
+            return new SpineLeafTopologySimulator();
+        } else if (topoShape.matches("mesh([,].*|$)")) {
+            return new MeshTopologySimulator();
+        } else {
+            return new ConfiguredTopologySimulator();
+        }
+    }
+
+    // Shuts down the topology simulator and all providers.
+    private void tearDown() {
+        if (simulator != null) {
+            topologyMutationDriver.stop();
+            packetProvider.stop();
+            flowRuleProvider.stop();
+            delay(500);
+            rejectMastership();
+            simulator.tearDownTopology();
+            simulator = null;
+        }
+    }
+
+    // Changes packet and mutation rates.
+    private void adjustRates() {
+        packetProvider.adjustRate(packetRate);
+        topologyMutationDriver.adjustRate(mutationRate);
+    }
+
+    // Re-assigns mastership roles.
+    private void reassignMastership() {
+        if (mastership.equals(DEFAULT_MASTERSHIP)) {
+            mastershipService.balanceRoles();
+        } else {
+            NodeId localNode = clusterService.getLocalNode().id();
+            rejectMastership();
+            String[] nodeSpecs = mastership.split("/");
+            for (int i = 0; i < nodeSpecs.length; i++) {
+                String[] specs = nodeSpecs[i].split("=");
+                if (specs[0].equals(localNode.toString())) {
+                    String[] ids = specs[1].split(",");
+                    for (String id : ids) {
+                        mastershipService.setRole(localNode, deviceId(id), MASTER);
+                    }
+                    break;
+                }
+            }
+        }
+    }
+
+    // Rejects mastership of all devices.
+    private void rejectMastership() {
+        NodeId localNode = clusterService.getLocalNode().id();
+        deviceService.getDevices()
+                .forEach(device -> mastershipService.setRole(localNode, device.id(),
+                                                             NONE));
+    }
+
+    // Null provider base class.
+    abstract static class AbstractNullProvider extends AbstractProvider {
+        protected AbstractNullProvider() {
+            super(new ProviderId(SCHEME, PROVIDER_ID));
+        }
+    }
+
+    // Device provider facade.
+    private class NullDeviceProvider extends AbstractNullProvider implements DeviceProvider {
+        @Override
+        public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
+            deviceProviderService.receivedRoleReply(deviceId, newRole, newRole);
+        }
+
+        @Override
+        public boolean isReachable(DeviceId deviceId) {
+            return topoShape.equals("configured") || deviceService.isAvailable(deviceId);
+        }
+
+        @Override
+        public void triggerProbe(DeviceId deviceId) {
+        }
+    }
+
+    // Host provider facade.
+    private class NullHostProvider extends AbstractNullProvider implements HostProvider {
+        @Override
+        public void triggerProbe(Host host) {
+        }
+    }
+
+    // Host provider facade.
+    private class NullLinkProvider extends AbstractNullProvider implements LinkProvider {
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/RerouteTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/RerouteTopologySimulator.java
new file mode 100644
index 0000000..11dc130
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/RerouteTopologySimulator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Re-routable linear topology simulator with an alternate path in the middle.
+ */
+public class RerouteTopologySimulator extends LinearTopologySimulator {
+
+    @Override
+    protected void processTopoShape(String shape) {
+        super.processTopoShape(shape);
+        infrastructurePorts = 3;
+        deviceCount = (topoShape.length == 1) ? deviceCount : Integer.parseInt(topoShape[1]);
+    }
+
+    @Override
+    public void setUpTopology() {
+        checkArgument(deviceCount > 2, "There must be at least 3 devices");
+        super.setUpTopology();
+    }
+
+    @Override
+    protected void createLinks() {
+        for (int i = 0, n = deviceCount - 2; i < n; i++) {
+            createLink(i, i + 1);
+        }
+        int middle = (deviceCount - 1) / 2;
+        int alternate = deviceCount - 1;
+        createLink(middle - 1, alternate);
+        createLink(middle, alternate);
+    }
+
+    @Override
+    protected void createHosts() {
+        createHosts(deviceIds.get(0), infrastructurePorts);
+        createHosts(deviceIds.get(deviceCount - 2), infrastructurePorts);
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/SpineLeafTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/SpineLeafTopologySimulator.java
new file mode 100644
index 0000000..876fc5d
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/SpineLeafTopologySimulator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+/**
+ * Spine-leaf topology with hosts at the leaf devices.
+ */
+public class SpineLeafTopologySimulator extends TopologySimulator {
+
+    @Override
+    protected void processTopoShape(String shape) {
+        super.processTopoShape(shape);
+        // FIXME: implement this
+    }
+
+    @Override
+    public void setUpTopology() {
+        // checkArgument(FIXME, "There must be at least one spine tier");
+        super.setUpTopology();
+    }
+
+    @Override
+    protected void createLinks() {
+    }
+
+    @Override
+    protected void createHosts() {
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/TopologyMutationDriver.java b/providers/null/src/main/java/org/onosproject/provider/nil/TopologyMutationDriver.java
new file mode 100644
index 0000000..ccf7e08
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/TopologyMutationDriver.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import com.google.common.collect.Lists;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.link.DefaultLinkDescription;
+import org.onosproject.net.link.LinkDescription;
+import org.onosproject.net.link.LinkProviderService;
+import org.onosproject.net.link.LinkService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.util.Tools.delay;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.Link.Type.DIRECT;
+import static org.onosproject.net.MastershipRole.MASTER;
+import static org.onosproject.provider.nil.TopologySimulator.description;
+
+/**
+ * Drives topology mutations at a specified rate of events per second.
+ */
+class TopologyMutationDriver implements Runnable {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final int WAIT_DELAY = 2_000;
+    private static final int MAX_DOWN_LINKS = 5;
+
+    private final Random random = new Random();
+
+    private volatile boolean stopped = true;
+
+    private double mutationRate;
+    private int millis, nanos;
+
+    private LinkService linkService;
+    private DeviceService deviceService;
+    private LinkProviderService linkProviderService;
+
+    private List<LinkDescription> activeLinks;
+    private List<LinkDescription> inactiveLinks;
+
+    private final ExecutorService executor =
+            newSingleThreadScheduledExecutor(groupedThreads("onos/null", "topo-mutator"));
+
+    /**
+     * Starts the mutation process.
+     *
+     * @param mutationRate        link events per second
+     * @param linkService         link service
+     * @param deviceService       device service
+     * @param linkProviderService link provider service
+     */
+    void start(double mutationRate,
+               LinkService linkService, DeviceService deviceService,
+               LinkProviderService linkProviderService) {
+        stopped = false;
+        this.linkService = linkService;
+        this.deviceService = deviceService;
+        this.linkProviderService = linkProviderService;
+        activeLinks = reduceLinks();
+        inactiveLinks = Lists.newArrayList();
+        adjustRate(mutationRate);
+        executor.submit(this);
+    }
+
+    /**
+     * Adjusts the topology mutation rate.
+     *
+     * @param mutationRate new topology mutation rate
+     */
+    void adjustRate(double mutationRate) {
+        this.mutationRate = mutationRate;
+        if (mutationRate > 0) {
+            this.millis = (int) (1_000 / mutationRate / 2);
+            this.nanos = (int) (1_000_000 / mutationRate / 2) % 1_000_000;
+        } else {
+            this.millis = 0;
+            this.nanos = 0;
+        }
+        log.info("Settings: millis={}, nanos={}", millis, nanos);
+    }
+
+    /**
+     * Stops the mutation process.
+     */
+    void stop() {
+        stopped = true;
+    }
+
+    /**
+     * Severs the link between the specified end-points in both directions.
+     *
+     * @param one link endpoint
+     * @param two link endpoint
+     */
+    void severLink(ConnectPoint one, ConnectPoint two) {
+        LinkDescription link = new DefaultLinkDescription(one, two, DIRECT);
+        linkProviderService.linkVanished(link);
+        linkProviderService.linkVanished(reverse(link));
+
+    }
+
+    /**
+     * Repairs the link between the specified end-points in both directions.
+     *
+     * @param one link endpoint
+     * @param two link endpoint
+     */
+    void repairLink(ConnectPoint one, ConnectPoint two) {
+        LinkDescription link = new DefaultLinkDescription(one, two, DIRECT);
+        linkProviderService.linkDetected(link);
+        linkProviderService.linkDetected(reverse(link));
+    }
+
+    @Override
+    public void run() {
+        delay(WAIT_DELAY);
+
+        while (!stopped) {
+            if (mutationRate > 0 && inactiveLinks.isEmpty()) {
+                primeInactiveLinks();
+            } else if (mutationRate <= 0 && !inactiveLinks.isEmpty()) {
+                repairInactiveLinks();
+            } else if (inactiveLinks.isEmpty()) {
+                delay(WAIT_DELAY);
+
+            } else {
+                activeLinks.add(repairLink());
+                pause();
+                inactiveLinks.add(severLink());
+                pause();
+            }
+        }
+    }
+
+    // Primes the inactive links with a few random links.
+    private void primeInactiveLinks() {
+        for (int i = 0, n = Math.min(MAX_DOWN_LINKS, activeLinks.size()); i < n; i++) {
+            inactiveLinks.add(severLink());
+        }
+    }
+
+    // Repairs all inactive links.
+    private void repairInactiveLinks() {
+        while (!inactiveLinks.isEmpty()) {
+            repairLink();
+        }
+    }
+
+    // Picks a random active link and severs it.
+    private LinkDescription severLink() {
+        LinkDescription link = getRandomLink(activeLinks);
+        linkProviderService.linkVanished(link);
+        linkProviderService.linkVanished(reverse(link));
+        return link;
+    }
+
+    // Picks a random inactive link and repairs it.
+    private LinkDescription repairLink() {
+        LinkDescription link = getRandomLink(inactiveLinks);
+        linkProviderService.linkDetected(link);
+        linkProviderService.linkDetected(reverse(link));
+        return link;
+    }
+
+    // Produces a reverse of the specified link.
+    private LinkDescription reverse(LinkDescription link) {
+        return new DefaultLinkDescription(link.dst(), link.src(), link.type());
+    }
+
+    // Returns a random link from the specified list of links.
+    private LinkDescription getRandomLink(List<LinkDescription> links) {
+        return links.remove(random.nextInt(links.size()));
+    }
+
+    // Reduces the given list of links to just a single link in each original pair.
+    private List<LinkDescription> reduceLinks() {
+        List<LinkDescription> links = Lists.newArrayList();
+        linkService.getLinks().forEach(link -> links.add(description(link)));
+        return links.stream()
+                .filter(this::isOurLink)
+                .filter(this::isRightDirection)
+                .collect(Collectors.toList());
+    }
+
+    // Returns true if the specified link is ours.
+    private boolean isOurLink(LinkDescription linkDescription) {
+        return deviceService.getRole(linkDescription.src().deviceId()) == MASTER;
+    }
+
+    // Returns true if the link source is greater than the link destination.
+    private boolean isRightDirection(LinkDescription link) {
+        return link.src().deviceId().toString().compareTo(link.dst().deviceId().toString()) > 0;
+    }
+
+    // Pauses the current thread for the pre-computed time of millis & nanos.
+    private void pause() {
+        delay(millis, nanos);
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/TopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/TopologySimulator.java
new file mode 100644
index 0000000..6e7a9ce
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/TopologySimulator.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import com.google.common.collect.Lists;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DefaultDeviceDescription;
+import org.onosproject.net.device.DefaultPortDescription;
+import org.onosproject.net.device.DeviceAdminService;
+import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.device.PortDescription;
+import org.onosproject.net.host.DefaultHostDescription;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.DefaultLinkDescription;
+import org.onosproject.net.link.LinkProviderService;
+import org.onosproject.net.link.LinkService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.onlab.util.Tools.toHex;
+import static org.onosproject.net.HostId.hostId;
+import static org.onosproject.net.Link.Type.DIRECT;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.net.device.DeviceEvent.Type.*;
+import static org.onosproject.provider.nil.NullProviders.SCHEME;
+
+/**
+ * Abstraction of a provider capable to simulate some network topology.
+ */
+public abstract class TopologySimulator {
+
+    protected final Logger log = LoggerFactory.getLogger(getClass());
+
+    protected String[] topoShape;
+    protected int deviceCount;
+    protected int hostCount;
+
+    protected ServiceDirectory directory;
+    protected NodeId localNode;
+
+    protected ClusterService clusterService;
+    protected MastershipService mastershipService;
+
+    protected DeviceAdminService deviceService;
+    protected HostService hostService;
+    protected LinkService linkService;
+
+    protected DeviceProviderService deviceProviderService;
+    protected HostProviderService hostProviderService;
+    protected LinkProviderService linkProviderService;
+
+    protected int maxWaitSeconds = 1;
+    protected int infrastructurePorts = 2;
+    protected CountDownLatch deviceLatch;
+
+    protected final List<DeviceId> deviceIds = Lists.newArrayList();
+
+    private DeviceListener deviceEventCounter = new DeviceEventCounter();
+
+    /**
+     * Initializes a new topology simulator with access to the specified service
+     * directory and various provider services.
+     *
+     * @param topoShape             topology shape specifier
+     * @param deviceCount           number of devices in the topology
+     * @param hostCount             number of hosts per device
+     * @param directory             service directory
+     * @param deviceProviderService device provider service
+     * @param hostProviderService   host provider service
+     * @param linkProviderService   link provider service
+     */
+    protected void init(String topoShape, int deviceCount, int hostCount,
+                        ServiceDirectory directory,
+                        DeviceProviderService deviceProviderService,
+                        HostProviderService hostProviderService,
+                        LinkProviderService linkProviderService) {
+        this.deviceCount = deviceCount;
+        this.hostCount = hostCount;
+        this.directory = directory;
+
+        this.clusterService = directory.get(ClusterService.class);
+        this.mastershipService = directory.get(MastershipService.class);
+
+        this.deviceService = directory.get(DeviceAdminService.class);
+        this.hostService = directory.get(HostService.class);
+        this.linkService = directory.get(LinkService.class);
+        this.deviceProviderService = deviceProviderService;
+        this.hostProviderService = hostProviderService;
+        this.linkProviderService = linkProviderService;
+
+        localNode = clusterService.getLocalNode().id();
+
+        processTopoShape(topoShape);
+    }
+
+    /**
+     * Processes the topology shape specifier.
+     *
+     * @param shape topology shape specifier
+     */
+    protected void processTopoShape(String shape) {
+        this.topoShape = shape.split(",");
+    }
+
+    /**
+     * Sets up network topology simulation.
+     */
+    public void setUpTopology() {
+        prepareForDeviceEvents(deviceCount);
+        createDevices();
+        waitForDeviceEvents();
+
+        createLinks();
+        createHosts();
+    }
+
+    /**
+     * Creates simulated devices.
+     */
+    protected void createDevices() {
+        for (int i = 0; i < deviceCount; i++) {
+            createDevice(i + 1);
+        }
+    }
+
+    /**
+     * Creates simulated links.
+     */
+    protected abstract void createLinks();
+
+    /**
+     * Creates simulated hosts.
+     */
+    protected abstract void createHosts();
+
+    /**
+     * Creates simulated device.
+     *
+     * @param i index of the device id in the list.
+     */
+    protected void createDevice(int i) {
+        DeviceId id = DeviceId.deviceId(SCHEME + ":" + toHex(i));
+        DeviceDescription desc =
+                new DefaultDeviceDescription(id.uri(), Device.Type.SWITCH,
+                                             "ON.Lab", "0.1", "0.1", "1234",
+                                             new ChassisId(i));
+        deviceProviderService.deviceConnected(id, desc);
+        deviceProviderService.updatePorts(id, buildPorts(hostCount + infrastructurePorts));
+        deviceIds.add(id);
+    }
+
+    /**
+     * Creates simulated link between two devices.
+     *
+     * @param i index of one simulated device
+     * @param j index of another simulated device
+     */
+    protected void createLink(int i, int j) {
+        ConnectPoint one = new ConnectPoint(deviceIds.get(i), PortNumber.portNumber(1));
+        ConnectPoint two = new ConnectPoint(deviceIds.get(j), PortNumber.portNumber(2));
+        linkProviderService.linkDetected(new DefaultLinkDescription(one, two, DIRECT));
+        linkProviderService.linkDetected(new DefaultLinkDescription(two, one, DIRECT));
+    }
+
+    /**
+     * Creates simularted hosts for the specified device.
+     *
+     * @param deviceId   device identifier
+     * @param portOffset port offset where to start attaching hosts
+     */
+    protected void createHosts(DeviceId deviceId, int portOffset) {
+        String s = deviceId.toString();
+        byte dByte = Byte.parseByte(s.substring(s.length() - 1), 16);
+        // TODO: this limits the simulation to 256 devices & 256 hosts/device.
+        byte[] macBytes = new byte[]{0, 0, 0, 0, dByte, 0};
+        byte[] ipBytes = new byte[]{(byte) 192, (byte) 168, dByte, 0};
+
+        for (int i = 0; i < hostCount; i++) {
+            int port = portOffset + i + 1;
+            macBytes[5] = (byte) (i + 1);
+            ipBytes[3] = (byte) (i + 1);
+            HostId id = hostId(MacAddress.valueOf(macBytes), VlanId.NONE);
+            IpAddress ip = IpAddress.valueOf(IpAddress.Version.INET, ipBytes);
+            hostProviderService.hostDetected(id, description(id, ip, deviceId, port));
+        }
+    }
+
+    /**
+     * Prepares to count device added/available/removed events.
+     *
+     * @param count number of events to count
+     */
+    protected void prepareForDeviceEvents(int count) {
+        deviceLatch = new CountDownLatch(count);
+        deviceService.addListener(deviceEventCounter);
+    }
+
+    /**
+     * Waits for all expected device added/available/removed events.
+     */
+    protected void waitForDeviceEvents() {
+        try {
+            deviceLatch.await(maxWaitSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.warn("Device events did not arrive in time");
+        }
+        deviceService.removeListener(deviceEventCounter);
+    }
+
+
+    /**
+     * Tears down network topology simulation.
+     */
+    public void tearDownTopology() {
+        removeHosts();
+        removeLinks();
+        removeDevices();
+    }
+
+    /**
+     * Removes any hosts previously advertised by this provider.
+     */
+    protected void removeHosts() {
+        hostService.getHosts()
+                .forEach(host -> hostProviderService.hostVanished(host.id()));
+    }
+
+    /**
+     * Removes any links previously advertised by this provider.
+     */
+    protected void removeLinks() {
+        linkService.getLinks()
+                .forEach(link -> linkProviderService.linkVanished(description(link)));
+    }
+
+    /**
+     * Removes any devices previously advertised by this provider.
+     */
+    protected void removeDevices() {
+        prepareForDeviceEvents(deviceService.getDeviceCount());
+        deviceService.getDevices()
+                .forEach(device -> deviceService.removeDevice(device.id()));
+        waitForDeviceEvents();
+    }
+
+
+    /**
+     * Produces a device description from the given device.
+     *
+     * @param device device to copy
+     * @return device description
+     */
+    static DeviceDescription description(Device device) {
+        return new DefaultDeviceDescription(device.id().uri(), device.type(),
+                                            device.manufacturer(),
+                                            device.hwVersion(), device.swVersion(),
+                                            device.serialNumber(), device.chassisId());
+    }
+
+    /**
+     * Produces a link description from the given link.
+     *
+     * @param link link to copy
+     * @return link description
+     */
+    static DefaultLinkDescription description(Link link) {
+        return new DefaultLinkDescription(link.src(), link.dst(), link.type());
+    }
+
+    /**
+     * Produces a host description from the given host.
+     *
+     * @param host host to copy
+     * @return host description
+     */
+    static DefaultHostDescription description(Host host) {
+        return new DefaultHostDescription(host.mac(), host.vlan(), host.location(),
+                                          host.ipAddresses());
+    }
+
+    /**
+     * Generates a host description from the given id and location information.
+     *
+     * @param hostId   host identifier
+     * @param ip       host IP
+     * @param deviceId edge device
+     * @param port     edge port
+     * @return host description
+     */
+    static HostDescription description(HostId hostId, IpAddress ip,
+                                       DeviceId deviceId, int port) {
+        HostLocation location = new HostLocation(deviceId, portNumber(port), 0L);
+        return new DefaultHostDescription(hostId.mac(), hostId.vlanId(), location, ip);
+    }
+
+    /**
+     * Generates a list of a configured number of ports.
+     *
+     * @param portCount number of ports
+     * @return list of ports
+     */
+    protected List<PortDescription> buildPorts(int portCount) {
+        List<PortDescription> ports = Lists.newArrayList();
+        for (int i = 0; i < portCount; i++) {
+            ports.add(new DefaultPortDescription(PortNumber.portNumber(i), true,
+                                                 Port.Type.COPPER, 0));
+        }
+        return ports;
+    }
+
+    // Counts down number of device added/available/removed events.
+    private class DeviceEventCounter implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceEvent.Type type = event.type();
+            if (type == DEVICE_ADDED || type == DEVICE_REMOVED ||
+                    type == DEVICE_AVAILABILITY_CHANGED) {
+                deviceLatch.countDown();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/TreeTopologySimulator.java b/providers/null/src/main/java/org/onosproject/provider/nil/TreeTopologySimulator.java
new file mode 100644
index 0000000..bed1390
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/TreeTopologySimulator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Tree topology with hosts at the leaf devices.
+ */
+public class TreeTopologySimulator extends TopologySimulator {
+
+    private int[] tierMultiplier;
+    private int[] tierDeviceCount;
+    private int[] tierOffset;
+
+    @Override
+    protected void processTopoShape(String shape) {
+        super.processTopoShape(shape);
+        tierOffset = new int[topoShape.length];
+        tierMultiplier = new int[topoShape.length];
+        tierDeviceCount = new int[topoShape.length];
+
+        deviceCount = 1;
+
+        tierOffset[0] = 0;
+        tierMultiplier[0] = 1;
+        tierDeviceCount[0] = deviceCount;
+
+        for (int i = 1; i < topoShape.length; i++) {
+            tierOffset[i] = deviceCount;
+            tierMultiplier[i] = Integer.parseInt(topoShape[i]);
+            tierDeviceCount[i] = tierDeviceCount[i - 1] * tierMultiplier[i];
+            deviceCount = deviceCount + tierDeviceCount[i];
+        }
+    }
+
+    @Override
+    public void setUpTopology() {
+        checkArgument(tierDeviceCount.length > 0, "There must be at least one tree tier");
+        super.setUpTopology();
+    }
+
+    @Override
+    protected void createLinks() {
+        for (int t = 1; t < tierOffset.length; t++) {
+            int child = tierOffset[t];
+            for (int parent = tierOffset[t - 1]; parent < tierOffset[t]; parent++) {
+                for (int i = 0; i < tierMultiplier[t]; i++) {
+                    createLink(parent, child);
+                    child++;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void createHosts() {
+        for (int i = tierOffset[tierOffset.length - 1]; i < deviceCount; i++) {
+            createHosts(deviceIds.get(i), hostCount);
+        }
+    }
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullControlCommand.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullControlCommand.java
new file mode 100644
index 0000000..261f46d
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullControlCommand.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.provider.nil.NullProviders;
+
+import static org.onosproject.cli.StartStopCompleter.START;
+
+/**
+ * Starts or stops topology simulation.
+ */
+@Command(scope = "onos", name = "null-simulation",
+        description = "Starts or stops topology simulation")
+public class NullControlCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "cmd", description = "Control command: start/stop",
+            required = true, multiValued = false)
+    String cmd = null;
+
+    @Argument(index = 1, name = "topoShape",
+            description = "Topology shape: e.g. configured, linear, reroute, centipede, tree, spineleaf, mesh",
+            required = false, multiValued = false)
+    String topoShape = null;
+
+    @Override
+    protected void execute() {
+        ComponentConfigService service = get(ComponentConfigService.class);
+        if (topoShape != null) {
+            service.setProperty(NullProviders.class.getName(), "topoShape", topoShape);
+        }
+        service.setProperty(NullProviders.class.getName(), "enabled",
+                            cmd.equals(START) ? "true" : "false");
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullLinkCommand.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullLinkCommand.java
new file mode 100644
index 0000000..6018b8d
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/NullLinkCommand.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.provider.nil.NullProviders;
+
+import static org.onosproject.cli.UpDownCompleter.DOWN;
+import static org.onosproject.cli.UpDownCompleter.UP;
+import static org.onosproject.cli.net.AddPointToPointIntentCommand.getDeviceId;
+import static org.onosproject.cli.net.AddPointToPointIntentCommand.getPortNumber;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+
+/**
+ * Servers or repairs a simulated link.
+ */
+@Command(scope = "onos", name = "null-link",
+        description = "Severs or repairs a simulated link")
+public class NullLinkCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "one", description = "One link end-point as device/port",
+            required = true, multiValued = false)
+    String one = null;
+
+    @Argument(index = 1, name = "two", description = "Another link end-point as device/port",
+            required = true, multiValued = false)
+    String two = null;
+
+    @Argument(index = 2, name = "cmd", description = "up/down",
+            required = true, multiValued = false)
+    String cmd = null;
+
+
+    @Override
+    protected void execute() {
+        NullProviders service = get(NullProviders.class);
+
+        try {
+            DeviceId oneId = deviceId(getDeviceId(one));
+            PortNumber onePort = portNumber(getPortNumber(one));
+            ConnectPoint onePoint = new ConnectPoint(oneId, onePort);
+
+            DeviceId twoId = deviceId(getDeviceId(two));
+            PortNumber twoPort = portNumber(getPortNumber(two));
+            ConnectPoint twoPoint = new ConnectPoint(twoId, twoPort);
+
+            if (cmd.equals(UP)) {
+                service.repairLink(onePoint, twoPoint);
+            } else if (cmd.equals(DOWN)) {
+                service.severLink(onePoint, twoPoint);
+            } else {
+                error("Illegal command %s; must be up or down", cmd);
+            }
+        } catch (NumberFormatException e) {
+            error("Invalid port number specified", e);
+        }
+    }
+
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/TopologyShapeCompleter.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/TopologyShapeCompleter.java
new file mode 100644
index 0000000..fad29b6
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/TopologyShapeCompleter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015 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.onosproject.provider.nil.cli;
+
+import com.google.common.collect.ImmutableList;
+import org.onosproject.cli.AbstractChoicesCompleter;
+
+import java.util.List;
+
+/**
+ * Topology shape completer.
+ */
+public class TopologyShapeCompleter extends AbstractChoicesCompleter {
+    @Override
+    public List<String> choices() {
+        return ImmutableList.of("configured", "linear", "reroute", "centipede",
+                                "tree", "spineleaf", "mesh");
+    }
+}
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/package-info.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/package-info.java
new file mode 100644
index 0000000..f876ced
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015 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.
+ */
+
+/**
+ * Null provider CLI commands and completers.
+ */
+package org.onosproject.provider.nil.cli;
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/package-info.java b/providers/null/src/main/java/org/onosproject/provider/nil/package-info.java
new file mode 100644
index 0000000..e8d309f
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2015 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.
+ */
+
+/**
+ * Set of null south-bound providers which permit simulating a network
+ * topology using fake devices, links, hosts, etc.
+ */
+package org.onosproject.provider.nil;
diff --git a/providers/null/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/providers/null/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..f96f10c
--- /dev/null
+++ b/providers/null/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,44 @@
+<!--
+  ~ Copyright 2015 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.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+        <command>
+            <action class="org.onosproject.provider.nil.cli.NullControlCommand"/>
+            <completers>
+                <ref component-id="startStopCompleter"/>
+                <ref component-id="topoShapeCompleter"/>
+                <null/>
+            </completers>
+        </command>
+        <command>
+            <action class="org.onosproject.provider.nil.cli.NullLinkCommand"/>
+            <completers>
+                <ref component-id="linkSrcCompleter"/>
+                <ref component-id="linkDstCompleter"/>
+                <ref component-id="upDownCompleter"/>
+                <null/>
+            </completers>
+        </command>
+    </command-bundle>
+
+    <bean id="startStopCompleter" class="org.onosproject.cli.StartStopCompleter"/>
+    <bean id="upDownCompleter" class="org.onosproject.cli.UpDownCompleter"/>
+    <bean id="topoShapeCompleter" class="org.onosproject.provider.nil.cli.TopologyShapeCompleter"/>
+    <bean id="linkSrcCompleter" class="org.onosproject.cli.net.LinkSrcCompleter"/>
+    <bean id="linkDstCompleter" class="org.onosproject.cli.net.LinkDstCompleter"/>
+
+</blueprint>
diff --git a/tools/test/topos/attmpls-sim.json b/tools/test/topos/attmpls-sim.json
new file mode 100644
index 0000000..457eb6e
--- /dev/null
+++ b/tools/test/topos/attmpls-sim.json
@@ -0,0 +1,116 @@
+{
+  "devices": [
+    { "alias":  "s1", "uri": "null:0000000000000001", "mac": "000000000001", "annotations": { "name": "CMBR", "latitude": 42.373730, "longitude": -71.109734  }, "type": "SWITCH" },
+    { "alias":  "s2", "uri": "null:0000000000000002", "mac": "000000000002", "annotations": { "name": "CHCG", "latitude": 41.877461, "longitude": -87.642892  }, "type": "SWITCH" },
+    { "alias":  "s3", "uri": "null:0000000000000003", "mac": "000000000003", "annotations": { "name": "CLEV", "latitude": 41.498928, "longitude": -81.695217  }, "type": "SWITCH" },
+    { "alias":  "s4", "uri": "null:0000000000000004", "mac": "000000000004", "annotations": { "name": "RLGH", "latitude": 35.780150, "longitude": -78.644026  }, "type": "SWITCH" },
+    { "alias":  "s5", "uri": "null:0000000000000005", "mac": "000000000005", "annotations": { "name": "ATLN", "latitude": 33.749017, "longitude": -84.394168  }, "type": "SWITCH" },
+    { "alias":  "s6", "uri": "null:0000000000000006", "mac": "000000000006", "annotations": { "name": "PHLA", "latitude": 39.952906, "longitude": -75.172278  }, "type": "SWITCH" },
+    { "alias":  "s7", "uri": "null:0000000000000007", "mac": "000000000007", "annotations": { "name": "WASH", "latitude": 38.906696, "longitude": -77.035509  }, "type": "SWITCH" },
+    { "alias":  "s8", "uri": "null:0000000000000008", "mac": "000000000008", "annotations": { "name": "NSVL", "latitude": 36.166410, "longitude": -86.787305  }, "type": "SWITCH" },
+    { "alias":  "s9", "uri": "null:0000000000000009", "mac": "000000000009", "annotations": { "name": "STLS", "latitude": 38.626418, "longitude": -90.198143  }, "type": "SWITCH" },
+    { "alias": "s10", "uri": "null:000000000000000a", "mac": "00000000000a", "annotations": { "name": "NWOR", "latitude": 29.951475, "longitude": -90.078434  }, "type": "SWITCH" },
+    { "alias": "s11", "uri": "null:000000000000000b", "mac": "00000000000b", "annotations": { "name": "HSTN", "latitude": 29.763249, "longitude": -95.368332  }, "type": "SWITCH" },
+    { "alias": "s12", "uri": "null:000000000000000c", "mac": "00000000000c", "annotations": { "name": "SNAN", "latitude": 29.424331, "longitude": -98.491745  }, "type": "SWITCH" },
+    { "alias": "s13", "uri": "null:000000000000000d", "mac": "00000000000d", "annotations": { "name": "DLLS", "latitude": 32.777665, "longitude": -96.802064  }, "type": "SWITCH" },
+    { "alias": "s14", "uri": "null:000000000000000e", "mac": "00000000000e", "annotations": { "name": "ORLD", "latitude": 28.538641, "longitude": -81.381110  }, "type": "SWITCH" },
+    { "alias": "s15", "uri": "null:000000000000000f", "mac": "00000000000f", "annotations": { "name": "DNVR", "latitude": 39.736623, "longitude": -104.984887 }, "type": "SWITCH" },
+    { "alias": "s16", "uri": "null:0000000000000010", "mac": "000000000010", "annotations": { "name": "KSCY", "latitude": 39.100725, "longitude": -94.581228  }, "type": "SWITCH" },
+    { "alias": "s17", "uri": "null:0000000000000011", "mac": "000000000011", "annotations": { "name": "SNFN", "latitude": 37.779751, "longitude": -122.409791 }, "type": "SWITCH" },
+    { "alias": "s18", "uri": "null:0000000000000012", "mac": "000000000012", "annotations": { "name": "SCRM", "latitude": 38.581001, "longitude": -121.497844 }, "type": "SWITCH" },
+    { "alias": "s19", "uri": "null:0000000000000013", "mac": "000000000013", "annotations": { "name": "PTLD", "latitude": 45.523317, "longitude": -122.677768 }, "type": "SWITCH" },
+    { "alias": "s20", "uri": "null:0000000000000014", "mac": "000000000014", "annotations": { "name": "STTL", "latitude": 47.607326, "longitude": -122.331786 }, "type": "SWITCH" },
+    { "alias": "s21", "uri": "null:0000000000000015", "mac": "000000000015", "annotations": { "name": "SLKC", "latitude": 40.759577, "longitude": -111.895079 }, "type": "SWITCH" },
+    { "alias": "s22", "uri": "null:0000000000000016", "mac": "000000000016", "annotations": { "name": "LA03", "latitude": 34.056346, "longitude": -118.235951 }, "type": "SWITCH" },
+    { "alias": "s23", "uri": "null:0000000000000017", "mac": "000000000017", "annotations": { "name": "SNDG", "latitude": 32.714564, "longitude": -117.153528 }, "type": "SWITCH" },
+    { "alias": "s24", "uri": "null:0000000000000018", "mac": "000000000018", "annotations": { "name": "PHNX", "latitude": 33.448289, "longitude": -112.076299 }, "type": "SWITCH" },
+    { "alias": "s25", "uri": "null:0000000000000019", "mac": "000000000019", "annotations": { "name": "NY54", "latitude": 40.728270, "longitude": -73.994483  }, "type": "SWITCH" }
+  ],
+
+  "hosts": [
+    { "alias":  "h1", "mac": "00:00:00:00:00:01", "vlan": -1, "location": "null:0000000000000001/1", "ip": "10.0.0.1",  "annotations": { "name": "CMBR", "latitude": 43.355715, "longitude":  -69.528243 } },
+    { "alias":  "h2", "mac": "00:00:00:00:00:02", "vlan": -1, "location": "null:0000000000000002/1", "ip": "10.0.0.2",  "annotations": { "name": "CHCG", "latitude": 43.632679, "longitude":  -88.772526 } },
+    { "alias":  "h3", "mac": "00:00:00:00:00:03", "vlan": -1, "location": "null:0000000000000003/1", "ip": "10.0.0.3",  "annotations": { "name": "CLEV", "latitude": 42.756945, "longitude":  -79.831317 } },
+    { "alias":  "h4", "mac": "00:00:00:00:00:04", "vlan": -1, "location": "null:0000000000000004/1", "ip": "10.0.0.4",  "annotations": { "name": "RLGH", "latitude": 36.972249, "longitude":  -76.667163 } },
+    { "alias":  "h5", "mac": "00:00:00:00:00:05", "vlan": -1, "location": "null:0000000000000005/1", "ip": "10.0.0.5",  "annotations": { "name": "ATLN", "latitude": 35.427493, "longitude":  -83.885831 } },
+    { "alias":  "h6", "mac": "00:00:00:00:00:06", "vlan": -1, "location": "null:0000000000000006/1", "ip": "10.0.0.6",  "annotations": { "name": "PHLA", "latitude": 39.208113, "longitude":  -73.421341 } },
+    { "alias":  "h7", "mac": "00:00:00:00:00:07", "vlan": -1, "location": "null:0000000000000007/1", "ip": "10.0.0.7",  "annotations": { "name": "WASH", "latitude": 40.133860, "longitude":  -79.238299 } },
+    { "alias":  "h8", "mac": "00:00:00:00:00:08", "vlan": -1, "location": "null:0000000000000008/1", "ip": "10.0.0.8",  "annotations": { "name": "NSVL", "latitude": 37.407589, "longitude":  -84.415068 } },
+    { "alias":  "h9", "mac": "00:00:00:00:00:09", "vlan": -1, "location": "null:0000000000000009/1", "ip": "10.0.0.9",  "annotations": { "name": "STLS", "latitude": 40.066810, "longitude":  -90.932405 } },
+    { "alias": "h10", "mac": "00:00:00:00:00:0a", "vlan": -1, "location": "null:000000000000000a/1", "ip": "10.0.0.10", "annotations": { "name": "NWOR", "latitude": 31.470982, "longitude":  -88.779353 } },
+    { "alias": "h11", "mac": "00:00:00:00:00:0b", "vlan": -1, "location": "null:000000000000000b/1", "ip": "10.0.0.11", "annotations": { "name": "HSTN", "latitude": 31.136858, "longitude":  -94.351656 } },
+    { "alias": "h12", "mac": "00:00:00:00:00:0c", "vlan": -1, "location": "null:000000000000000c/1", "ip": "10.0.0.12", "annotations": { "name": "SNAN", "latitude": 28.040975, "longitude":  -99.169527 } },
+    { "alias": "h13", "mac": "00:00:00:00:00:0d", "vlan": -1, "location": "null:000000000000000d/1", "ip": "10.0.0.13", "annotations": { "name": "DLLS", "latitude": 31.899825, "longitude":  -99.287263 } },
+    { "alias": "h14", "mac": "00:00:00:00:00:0e", "vlan": -1, "location": "null:000000000000000e/1", "ip": "10.0.0.14", "annotations": { "name": "ORLD", "latitude": 26.670509, "longitude":  -81.291920 } },
+    { "alias": "h15", "mac": "00:00:00:00:00:0f", "vlan": -1, "location": "null:000000000000000f/1", "ip": "10.0.0.15", "annotations": { "name": "DNVR", "latitude": 40.888148, "longitude": -103.459878 } },
+    { "alias": "h16", "mac": "00:00:00:00:00:10", "vlan": -1, "location": "null:0000000000000010/1", "ip": "10.0.0.16", "annotations": { "name": "KSCY", "latitude": 40.545088, "longitude":  -93.734002 } },
+    { "alias": "h17", "mac": "00:00:00:00:00:11", "vlan": -1, "location": "null:0000000000000011/1", "ip": "10.0.0.17", "annotations": { "name": "SNFN", "latitude": 39.081743, "longitude": -124.330172 } },
+    { "alias": "h18", "mac": "00:00:00:00:00:12", "vlan": -1, "location": "null:0000000000000012/1", "ip": "10.0.0.18", "annotations": { "name": "SCRM", "latitude": 40.107468, "longitude": -120.424689 } },
+    { "alias": "h19", "mac": "00:00:00:00:00:13", "vlan": -1, "location": "null:0000000000000013/1", "ip": "10.0.0.19", "annotations": { "name": "PTLD", "latitude": 44.383051, "longitude": -124.767594 } },
+    { "alias": "h20", "mac": "00:00:00:00:00:14", "vlan": -1, "location": "null:0000000000000014/1", "ip": "10.0.0.20", "annotations": { "name": "STTL", "latitude": 48.832627, "longitude": -120.298441 } },
+    { "alias": "h21", "mac": "00:00:00:00:00:15", "vlan": -1, "location": "null:0000000000000015/1", "ip": "10.0.0.21", "annotations": { "name": "SLKC", "latitude": 42.301734, "longitude": -111.217297 } },
+    { "alias": "h22", "mac": "00:00:00:00:00:16", "vlan": -1, "location": "null:0000000000000016/1", "ip": "10.0.0.22", "annotations": { "name": "LA03", "latitude": 33.224634, "longitude": -121.532943 } },
+    { "alias": "h23", "mac": "00:00:00:00:00:17", "vlan": -1, "location": "null:0000000000000017/1", "ip": "10.0.0.23", "annotations": { "name": "SNDG", "latitude": 31.834607, "longitude": -118.847982 } },
+    { "alias": "h24", "mac": "00:00:00:00:00:18", "vlan": -1, "location": "null:0000000000000018/1", "ip": "10.0.0.24", "annotations": { "name": "PHNX", "latitude": 34.662290, "longitude": -110.946662 } },
+    { "alias": "h25", "mac": "00:00:00:00:00:19", "vlan": -1, "location": "null:0000000000000019/1", "ip": "10.0.0.25", "annotations": { "name": "NY54", "latitude": 42.395459, "longitude":  -75.293563 } }
+  ],
+
+  "links": [
+    { "src": "null:0000000000000019/2", "dst": "null:0000000000000001/2",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000019/3", "dst": "null:0000000000000002/10", "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000019/4", "dst": "null:0000000000000006/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000019/5", "dst": "null:0000000000000007/2",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000001/2", "dst": "null:0000000000000006/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/2", "dst": "null:0000000000000003/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/3", "dst": "null:0000000000000006/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/4", "dst": "null:0000000000000009/2",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/5", "dst": "null:000000000000000f/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/6", "dst": "null:0000000000000010/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/7", "dst": "null:0000000000000011/7",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/8", "dst": "null:0000000000000014/2",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000002/9", "dst": "null:0000000000000015/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000003/2", "dst": "null:0000000000000008/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000003/3", "dst": "null:0000000000000009/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000003/4", "dst": "null:0000000000000006/6",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000004/2", "dst": "null:0000000000000005/7",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000004/3", "dst": "null:0000000000000007/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000005/2", "dst": "null:0000000000000007/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000005/3", "dst": "null:0000000000000008/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000005/4", "dst": "null:0000000000000009/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000005/5", "dst": "null:000000000000000d/6",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000005/6", "dst": "null:000000000000000e/2",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000006/2", "dst": "null:0000000000000007/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000008/2", "dst": "null:0000000000000009/6",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000008/3", "dst": "null:000000000000000d/7",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000009/2", "dst": "null:000000000000000d/8",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000009/3", "dst": "null:0000000000000010/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000009/4", "dst": "null:0000000000000016/5",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000a/2", "dst": "null:000000000000000b/5",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000a/3", "dst": "null:000000000000000d/9",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000a/4", "dst": "null:000000000000000e/3",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000b/2", "dst": "null:000000000000000c/4",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000b/3", "dst": "null:000000000000000d/10", "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000b/4", "dst": "null:000000000000000e/4",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000c/2", "dst": "null:0000000000000018/2",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000c/3", "dst": "null:000000000000000d/6",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000d/2", "dst": "null:000000000000000f/5",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000d/3", "dst": "null:0000000000000010/5",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000d/4", "dst": "null:0000000000000011/8",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000d/5", "dst": "null:0000000000000016/6",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000f/2", "dst": "null:0000000000000010/6",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000f/3", "dst": "null:0000000000000011/9",  "annotations": { "durable": "true" }},
+    { "src": "null:000000000000000f/4", "dst": "null:0000000000000015/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000010/2", "dst": "null:0000000000000011/10", "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000011/2", "dst": "null:0000000000000012/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000011/3", "dst": "null:0000000000000013/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000011/4", "dst": "null:0000000000000014/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000011/5", "dst": "null:0000000000000015/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000011/6", "dst": "null:0000000000000016/7",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000012/2", "dst": "null:0000000000000015/5",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000013/2", "dst": "null:0000000000000014/4",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000015/2", "dst": "null:0000000000000016/8",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000016/2", "dst": "null:0000000000000017/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000016/3", "dst": "null:0000000000000018/3",  "annotations": { "durable": "true" }},
+    { "src": "null:0000000000000017/2", "dst": "null:0000000000000018/5",  "annotations": { "durable": "true" }}
+  ]
+}
diff --git a/utils/misc/src/main/java/org/onlab/util/Tools.java b/utils/misc/src/main/java/org/onlab/util/Tools.java
index 62330fb..bff3953 100644
--- a/utils/misc/src/main/java/org/onlab/util/Tools.java
+++ b/utils/misc/src/main/java/org/onlab/util/Tools.java
@@ -192,6 +192,20 @@
     }
 
     /**
+     * Suspends the current thread for a specified number of millis and nanos.
+     *
+     * @param ms    number of millis
+     * @param nanos number of nanos
+     */
+    public static void delay(int ms, int nanos) {
+        try {
+            Thread.sleep(ms, nanos);
+        } catch (InterruptedException e) {
+            throw new RuntimeException("Interrupted", e);
+        }
+    }
+
+    /**
      * Slurps the contents of a file into a list of strings, one per line.
      *
      * @param path file path
diff --git a/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java b/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
index bec1941..3974f17 100644
--- a/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
+++ b/web/api/src/main/java/org/onosproject/rest/ConfigProvider.java
@@ -16,6 +16,11 @@
 package org.onosproject.rest;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+import org.onlab.packet.ChassisId;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DefaultAnnotations;
 import org.onosproject.net.Device;
@@ -30,9 +35,12 @@
 import org.onosproject.net.device.DefaultDeviceDescription;
 import org.onosproject.net.device.DefaultPortDescription;
 import org.onosproject.net.device.DeviceDescription;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
 import org.onosproject.net.device.DeviceProvider;
 import org.onosproject.net.device.DeviceProviderRegistry;
 import org.onosproject.net.device.DeviceProviderService;
+import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.device.PortDescription;
 import org.onosproject.net.host.DefaultHostDescription;
 import org.onosproject.net.host.HostProvider;
@@ -43,10 +51,8 @@
 import org.onosproject.net.link.LinkProviderRegistry;
 import org.onosproject.net.link.LinkProviderService;
 import org.onosproject.net.provider.ProviderId;
-import org.onlab.packet.ChassisId;
-import org.onlab.packet.IpAddress;
-import org.onlab.packet.MacAddress;
-import org.onlab.packet.VlanId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.net.URI;
 import java.util.ArrayList;
@@ -54,37 +60,60 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.onosproject.net.DeviceId.deviceId;
 import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_AVAILABILITY_CHANGED;
 
 /**
  * Provider of devices and links parsed from a JSON configuration structure.
  */
 class ConfigProvider implements DeviceProvider, LinkProvider, HostProvider {
 
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
     private static final ProviderId PID =
             new ProviderId("cfg", "org.onosproject.rest", true);
 
+    private static final String UNKNOWN = "unknown";
+
+    private CountDownLatch deviceLatch;
+
     private final JsonNode cfg;
+    private final DeviceService deviceService;
+
     private final DeviceProviderRegistry deviceProviderRegistry;
     private final LinkProviderRegistry linkProviderRegistry;
     private final HostProviderRegistry hostProviderRegistry;
 
+    private DeviceProviderService deviceProviderService;
+    private LinkProviderService linkProviderService;
+    private HostProviderService hostProviderService;
+
+    private DeviceListener deviceEventCounter = new DeviceEventCounter();
+    private List<ConnectPoint> connectPoints = Lists.newArrayList();
+
     /**
      * Creates a new configuration provider.
      *
      * @param cfg                    JSON configuration
+     * @param deviceService          device service
      * @param deviceProviderRegistry device provider registry
      * @param linkProviderRegistry   link provider registry
      * @param hostProviderRegistry   host provider registry
      */
     ConfigProvider(JsonNode cfg,
+                   DeviceService deviceService,
                    DeviceProviderRegistry deviceProviderRegistry,
                    LinkProviderRegistry linkProviderRegistry,
                    HostProviderRegistry hostProviderRegistry) {
         this.cfg = checkNotNull(cfg, "Configuration cannot be null");
+        this.deviceService = checkNotNull(deviceService, "Device service cannot be null");
         this.deviceProviderRegistry = checkNotNull(deviceProviderRegistry, "Device provider registry cannot be null");
         this.linkProviderRegistry = checkNotNull(linkProviderRegistry, "Link provider registry cannot be null");
         this.hostProviderRegistry = checkNotNull(hostProviderRegistry, "Host provider registry cannot be null");
@@ -94,56 +123,74 @@
      * Parses the given JSON and provides links as configured.
      */
     void parse() {
-        parseDevices();
-        parseLinks();
-        parseHosts();
+        try {
+            register();
+            parseDevices();
+            parseLinks();
+            parseHosts();
+            addMissingPorts();
+        } finally {
+            unregister();
+        }
+    }
+
+    private void register() {
+        deviceProviderService = deviceProviderRegistry.register(this);
+        linkProviderService = linkProviderRegistry.register(this);
+        hostProviderService = hostProviderRegistry.register(this);
+    }
+
+    private void unregister() {
+        deviceProviderRegistry.unregister(this);
+        linkProviderRegistry.unregister(this);
+        hostProviderRegistry.unregister(this);
     }
 
     // Parses the given JSON and provides devices.
     private void parseDevices() {
         try {
-            DeviceProviderService dps = deviceProviderRegistry.register(this);
             JsonNode nodes = cfg.get("devices");
             if (nodes != null) {
+                prepareForDeviceEvents(nodes.size());
                 for (JsonNode node : nodes) {
-                    parseDevice(dps, node);
+                    parseDevice(node);
                 }
             }
         } finally {
-            deviceProviderRegistry.unregister(this);
+            waitForDeviceEvents();
         }
     }
 
     // Parses the given node with device data and supplies the device.
-    private void parseDevice(DeviceProviderService dps, JsonNode node) {
+    private void parseDevice(JsonNode node) {
         URI uri = URI.create(get(node, "uri"));
-        Device.Type type = Device.Type.valueOf(get(node, "type"));
-        String mfr = get(node, "mfr");
-        String hw = get(node, "hw");
-        String sw = get(node, "sw");
-        String serial = get(node, "serial");
-        ChassisId cid = new ChassisId(get(node, "mac"));
+        Device.Type type = Device.Type.valueOf(get(node, "type", "SWITCH"));
+        String mfr = get(node, "mfr", UNKNOWN);
+        String hw = get(node, "hw", UNKNOWN);
+        String sw = get(node, "sw", UNKNOWN);
+        String serial = get(node, "serial", UNKNOWN);
+        ChassisId cid = new ChassisId(get(node, "mac", "000000000000"));
         SparseAnnotations annotations = annotations(node.get("annotations"));
 
         DeviceDescription desc =
                 new DefaultDeviceDescription(uri, type, mfr, hw, sw, serial,
                                              cid, annotations);
         DeviceId deviceId = deviceId(uri);
-        dps.deviceConnected(deviceId, desc);
+        deviceProviderService.deviceConnected(deviceId, desc);
 
         JsonNode ports = node.get("ports");
         if (ports != null) {
-            parsePorts(dps, deviceId, ports);
+            parsePorts(deviceId, ports);
         }
     }
 
     // Parses the given node with list of device ports.
-    private void parsePorts(DeviceProviderService dps, DeviceId deviceId, JsonNode nodes) {
+    private void parsePorts(DeviceId deviceId, JsonNode nodes) {
         List<PortDescription> ports = new ArrayList<>();
         for (JsonNode node : nodes) {
             ports.add(parsePort(node));
         }
-        dps.updatePorts(deviceId, ports);
+        deviceProviderService.updatePorts(deviceId, ports);
     }
 
     // Parses the given node with port information.
@@ -156,43 +203,41 @@
 
     // Parses the given JSON and provides links as configured.
     private void parseLinks() {
-        try {
-            LinkProviderService lps = linkProviderRegistry.register(this);
-            JsonNode nodes = cfg.get("links");
-            if (nodes != null) {
-                for (JsonNode node : nodes) {
-                    parseLink(lps, node, false);
-                    if (!node.has("halfplex")) {
-                        parseLink(lps, node, true);
-                    }
+        JsonNode nodes = cfg.get("links");
+        if (nodes != null) {
+            for (JsonNode node : nodes) {
+                parseLink(node, false);
+                if (!node.has("halfplex")) {
+                    parseLink(node, true);
                 }
             }
-        } finally {
-            linkProviderRegistry.unregister(this);
         }
     }
 
     // Parses the given node with link data and supplies the link.
-    private void parseLink(LinkProviderService lps, JsonNode node, boolean reverse) {
+    private void parseLink(JsonNode node, boolean reverse) {
         ConnectPoint src = connectPoint(get(node, "src"));
         ConnectPoint dst = connectPoint(get(node, "dst"));
-        Link.Type type = Link.Type.valueOf(get(node, "type"));
+        Link.Type type = Link.Type.valueOf(get(node, "type", "DIRECT"));
         SparseAnnotations annotations = annotations(node.get("annotations"));
 
         DefaultLinkDescription desc = reverse ?
                 new DefaultLinkDescription(dst, src, type, annotations) :
                 new DefaultLinkDescription(src, dst, type, annotations);
-        lps.linkDetected(desc);
+        linkProviderService.linkDetected(desc);
+
+        connectPoints.add(src);
+        connectPoints.add(dst);
     }
 
     // Parses the given JSON and provides hosts as configured.
     private void parseHosts() {
         try {
-            HostProviderService hps = hostProviderRegistry.register(this);
             JsonNode nodes = cfg.get("hosts");
             if (nodes != null) {
                 for (JsonNode node : nodes) {
-                    parseHost(hps, node);
+                    parseHost(node);
+                    parseHost(node); // FIXME: hack to make sure host positions take
                 }
             }
         } finally {
@@ -201,14 +246,14 @@
     }
 
     // Parses the given node with host data and supplies the host.
-    private void parseHost(HostProviderService hps, JsonNode node) {
+    private void parseHost(JsonNode node) {
         MacAddress mac = MacAddress.valueOf(get(node, "mac"));
-        VlanId vlanId = VlanId.vlanId(node.get("vlan").shortValue());
+        VlanId vlanId = VlanId.vlanId((short) node.get("vlan").asInt(VlanId.UNTAGGED));
         HostId hostId = HostId.hostId(mac, vlanId);
         SparseAnnotations annotations = annotations(node.get("annotations"));
         HostLocation location = new HostLocation(connectPoint(get(node, "location")), 0);
 
-        String[] ipStrings = get(node, "ip").split(",");
+        String[] ipStrings = get(node, "ip", "").split(",");
         Set<IpAddress> ips = new HashSet<>();
         for (String ip : ipStrings) {
             ips.add(IpAddress.valueOf(ip.trim()));
@@ -216,9 +261,45 @@
 
         DefaultHostDescription desc =
                 new DefaultHostDescription(mac, vlanId, location, ips, annotations);
-        hps.hostDetected(hostId, desc);
+        hostProviderService.hostDetected(hostId, desc);
+
+        connectPoints.add(location);
     }
 
+    // Adds any missing device ports for configured links and host locations.
+    private void addMissingPorts() {
+        deviceService.getDevices().forEach(this::addMissingPorts);
+    }
+
+    // Adds any missing device ports.
+    private void addMissingPorts(Device device) {
+        List<Port> ports = deviceService.getPorts(device.id());
+        Set<ConnectPoint> existing = ports.stream()
+                .map(p -> new ConnectPoint(device.id(), p.number()))
+                .collect(Collectors.toSet());
+        Set<ConnectPoint> missing = connectPoints.stream()
+                .filter(cp -> !existing.contains(cp))
+                .collect(Collectors.toSet());
+
+        if (!missing.isEmpty()) {
+            List<PortDescription> newPorts = Lists.newArrayList();
+            ports.forEach(p -> newPorts.add(description(p)));
+            missing.forEach(cp -> newPorts.add(description(cp)));
+            deviceProviderService.updatePorts(device.id(), newPorts);
+        }
+    }
+
+    // Creates a port description from the specified port.
+    private PortDescription description(Port p) {
+        return new DefaultPortDescription(p.number(), p.isEnabled(), p.type(), p.portSpeed());
+    }
+
+    // Creates a port description from the specified connection point.
+    private PortDescription description(ConnectPoint cp) {
+        return new DefaultPortDescription(cp.port(), true);
+    }
+
+
     // Produces set of annotations from the given JSON node.
     private SparseAnnotations annotations(JsonNode node) {
         if (node == null) {
@@ -246,12 +327,18 @@
         return node.path(name).asText();
     }
 
-    @Override
-    public void triggerProbe(DeviceId deviceId) {
+    // Returns string form of the named property in the given JSON object.
+    private String get(JsonNode node, String name, String defaultValue) {
+        return node.path(name).asText(defaultValue);
     }
 
     @Override
     public void roleChanged(DeviceId device, MastershipRole newRole) {
+        deviceProviderService.receivedRoleReply(device, newRole, newRole);
+    }
+
+    @Override
+    public void triggerProbe(DeviceId deviceId) {
     }
 
     @Override
@@ -265,6 +352,40 @@
 
     @Override
     public boolean isReachable(DeviceId device) {
-        return false;
+        return true;
     }
+
+    /**
+     * Prepares to count device added/available/removed events.
+     *
+     * @param count number of events to count
+     */
+    protected void prepareForDeviceEvents(int count) {
+        deviceLatch = new CountDownLatch(count);
+        deviceService.addListener(deviceEventCounter);
+    }
+
+    /**
+     * Waits for all expected device added/available/removed events.
+     */
+    protected void waitForDeviceEvents() {
+        try {
+            deviceLatch.await(2, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.warn("Device events did not arrive in time");
+        }
+        deviceService.removeListener(deviceEventCounter);
+    }
+
+    // Counts down number of device added/available/removed events.
+    private class DeviceEventCounter implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceEvent.Type type = event.type();
+            if (type == DEVICE_ADDED || type == DEVICE_AVAILABILITY_CHANGED) {
+                deviceLatch.countDown();
+            }
+        }
+    }
+
 }
diff --git a/web/api/src/main/java/org/onosproject/rest/ConfigResource.java b/web/api/src/main/java/org/onosproject/rest/ConfigWebResource.java
similarity index 87%
rename from web/api/src/main/java/org/onosproject/rest/ConfigResource.java
rename to web/api/src/main/java/org/onosproject/rest/ConfigWebResource.java
index fc20659..42f9fbf 100644
--- a/web/api/src/main/java/org/onosproject/rest/ConfigResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/ConfigWebResource.java
@@ -18,6 +18,7 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.onosproject.net.device.DeviceProviderRegistry;
+import org.onosproject.net.device.DeviceService;
 import org.onosproject.net.host.HostProviderRegistry;
 import org.onosproject.net.link.LinkProviderRegistry;
 import org.onlab.rest.BaseResource;
@@ -40,9 +41,9 @@
  * devices, ports and links.
  */
 @Path("config")
-public class ConfigResource extends BaseResource {
+public class ConfigWebResource extends BaseResource {
 
-    private static Logger log = LoggerFactory.getLogger(ConfigResource.class);
+    private static Logger log = LoggerFactory.getLogger(ConfigWebResource.class);
 
     @POST
     @Path("topology")
@@ -52,7 +53,8 @@
         try {
             ObjectMapper mapper = new ObjectMapper();
             JsonNode cfg = mapper.readTree(input);
-            new ConfigProvider(cfg, get(DeviceProviderRegistry.class),
+            new ConfigProvider(cfg, get(DeviceService.class),
+                               get(DeviceProviderRegistry.class),
                                get(LinkProviderRegistry.class),
                                get(HostProviderRegistry.class)).parse();
             return Response.ok().build();
diff --git a/web/api/src/main/webapp/WEB-INF/web.xml b/web/api/src/main/webapp/WEB-INF/web.xml
index 82ad21e..e018c6f 100644
--- a/web/api/src/main/webapp/WEB-INF/web.xml
+++ b/web/api/src/main/webapp/WEB-INF/web.xml
@@ -72,7 +72,7 @@
                 org.onosproject.rest.IntentsWebResource,
                 org.onosproject.rest.FlowsWebResource,
                 org.onosproject.rest.TopologyWebResource,
-                org.onosproject.rest.ConfigResource,
+                org.onosproject.rest.ConfigWebResource,
                 org.onosproject.rest.PathsWebResource
             </param-value>
         </init-param>