Adding command to add routes and to generate flows from them.

Enhanced FlowRuleStore and FlowRuleService with a new method.

Change-Id: I011371c1931294448e361fc1ceb120d89c14489d
diff --git a/apps/test/route-scale/BUCK b/apps/test/route-scale/BUCK
new file mode 100644
index 0000000..d0853d4
--- /dev/null
+++ b/apps/test/route-scale/BUCK
@@ -0,0 +1,20 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:org.apache.karaf.shell.console',
+    '//cli:onos-cli',
+    '//utils/rest:onlab-rest',
+    '//apps/route-service/api:onos-apps-route-service-api',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+)
+
+onos_app (
+    app_name = 'org.onosproject.routescale',
+    title = 'Route and Flow Scalability Test',
+    category = 'Test Utility',
+    url = 'http://onosproject.org',
+    description = 'Route and flow scalability test facility.',
+    required_apps = [ 'org.onosproject.route-service' ],
+)
diff --git a/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateFlows.java b/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateFlows.java
new file mode 100644
index 0000000..6515def
--- /dev/null
+++ b/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateFlows.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.routescale;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+
+/**
+ * Creates the specified number of routes for scale testing.
+ */
+@Command(scope = "onos", name = "scale-create-flows",
+        description = "Creates the specified number of flows for scale testing")
+public class CreateFlows extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "flowCount", description = "Number of flows to create",
+            required = true)
+    int flowCount;
+
+    @Override
+    protected void execute() {
+        ScaleTestManager service = get(ScaleTestManager.class);
+        service.createFlows(flowCount);
+    }
+
+}
diff --git a/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateRoutes.java b/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateRoutes.java
new file mode 100644
index 0000000..b43d2bb
--- /dev/null
+++ b/apps/test/route-scale/src/main/java/org/onosproject/routescale/CreateRoutes.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.routescale;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+
+/**
+ * Creates the specified number of routes for scale testing.
+ */
+@Command(scope = "onos", name = "scale-create-routes",
+        description = "Creates the specified number of routes for scale testing")
+public class CreateRoutes extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "routeCount", description = "Number of routes to create",
+            required = true)
+    int routeCount;
+
+    @Override
+    protected void execute() {
+        ScaleTestManager service = get(ScaleTestManager.class);
+        service.createRoutes(routeCount);
+    }
+
+}
diff --git a/apps/test/route-scale/src/main/java/org/onosproject/routescale/ScaleTestManager.java b/apps/test/route-scale/src/main/java/org/onosproject/routescale/ScaleTestManager.java
new file mode 100644
index 0000000..65c1c7f
--- /dev/null
+++ b/apps/test/route-scale/src/main/java/org/onosproject/routescale/ScaleTestManager.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.routescale;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onosproject.app.ApplicationService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleOperations;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostAdminService;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Random;
+
+@Component(immediate = true)
+@Service(value = ScaleTestManager.class)
+public class ScaleTestManager {
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ApplicationService applicationService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostAdminService hostAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowRuleService flowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RouteAdminService routeAdminService;
+
+    private final Random random = new Random(System.currentTimeMillis());
+
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = applicationService.getId("org.onosproject.routescale");
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        log.info("Stopped");
+    }
+
+    public void createFlows(int flowCount) {
+        for (Device device : deviceService.getAvailableDevices()) {
+            DeviceId id = device.id();
+            FlowRuleOperations.Builder ops = FlowRuleOperations.builder();
+
+            for (int i = 0; i < flowCount; i++) {
+                FlowRule.Builder frb = DefaultFlowRule.builder();
+                frb.fromApp(appId).makePermanent().withPriority(1000 + i);
+                TrafficSelector.Builder tsb = DefaultTrafficSelector.builder();
+                TrafficTreatment.Builder ttb = DefaultTrafficTreatment.builder();
+
+                tsb.matchEthType(Ethernet.TYPE_IPV4);
+                ttb.setEthDst(randomMac()).setEthSrc(randomMac());
+                ttb.setOutput(PortNumber.portNumber(random.nextInt(512)));
+                frb.withSelector(tsb.build()).withTreatment(ttb.build());
+                ops.add(frb.forDevice(id).build());
+            }
+
+            flowRuleService.apply(ops.build());
+
+        }
+    }
+
+    public void createRoutes(int routeCount) {
+        List<Host> hosts = ImmutableList.copyOf(hostAdminService.getHosts());
+        ImmutableSet.Builder<Route> routes = ImmutableSet.builder();
+        for (int i = 0; i < routeCount; i++) {
+            IpPrefix prefix = randomIp().toIpPrefix();
+            IpAddress nextHop = randomIp(hosts);
+            routes.add(new Route(Route.Source.STATIC, prefix, nextHop));
+        }
+        routeAdminService.update(routes.build());
+    }
+
+    private IpAddress randomIp() {
+        byte[] bytes = new byte[4];
+        random.nextBytes(bytes);
+        return IpAddress.valueOf(IpAddress.Version.INET, bytes, 0);
+    }
+
+    private IpAddress randomIp(List<Host> hosts) {
+        Host host = hosts.get(random.nextInt(hosts.size()));
+        return host.ipAddresses().iterator().next();
+    }
+
+    private MacAddress randomMac() {
+        byte[] bytes = new byte[6];
+        random.nextBytes(bytes);
+        return MacAddress.valueOf(bytes);
+    }
+
+}
diff --git a/apps/test/route-scale/src/main/java/org/onosproject/routescale/package-info.java b/apps/test/route-scale/src/main/java/org/onosproject/routescale/package-info.java
new file mode 100644
index 0000000..a9fe7a9
--- /dev/null
+++ b/apps/test/route-scale/src/main/java/org/onosproject/routescale/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Facilities for creating a scale stress test using routes and flows across
+ * the discovered topology.
+ */
+package org.onosproject.routescale;
\ No newline at end of file
diff --git a/apps/test/route-scale/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/test/route-scale/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..0c9dbc5
--- /dev/null
+++ b/apps/test/route-scale/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright 2018-present Open Networking Foundation
+  ~
+  ~ 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.routescale.CreateRoutes"/>
+        </command>
+        <command>
+            <action class="org.onosproject.routescale.CreateFlows"/>
+        </command>
+    </command-bundle>
+
+</blueprint>
diff --git a/core/api/src/main/java/org/onosproject/net/flow/FlowRuleService.java b/core/api/src/main/java/org/onosproject/net/flow/FlowRuleService.java
index a4cfdfd..0ade2ed 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/FlowRuleService.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/FlowRuleService.java
@@ -43,6 +43,16 @@
     int getFlowRuleCount();
 
     /**
+     * Returns the number of flow rules for the given device.
+     *
+     * @param deviceId device identifier
+     * @return number of flow rules for the given device
+     */
+    default int getFlowRuleCount(DeviceId deviceId) {
+        return 0;
+    }
+
+    /**
      * Returns the collection of flow entries applied on the specified device.
      * This will include flow rules which may not yet have been applied to
      * the device.
diff --git a/core/api/src/main/java/org/onosproject/net/flow/FlowRuleStore.java b/core/api/src/main/java/org/onosproject/net/flow/FlowRuleStore.java
index ff0bbe1..449a1fd 100644
--- a/core/api/src/main/java/org/onosproject/net/flow/FlowRuleStore.java
+++ b/core/api/src/main/java/org/onosproject/net/flow/FlowRuleStore.java
@@ -15,26 +15,36 @@
  */
 package org.onosproject.net.flow;
 
-import java.util.List;
-
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.flow.oldbatch.FlowRuleBatchEvent;
 import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
 import org.onosproject.store.Store;
 
+import java.util.List;
+
 /**
  * Manages inventory of flow rules; not intended for direct use.
  */
 public interface FlowRuleStore extends Store<FlowRuleBatchEvent, FlowRuleStoreDelegate> {
 
     /**
-     * Returns the number of flow rule in the store.
+     * Returns the number of flow rules in the store.
      *
      * @return number of flow rules
      */
     int getFlowRuleCount();
 
     /**
+     * Returns the number of flow rules for the given device in the store.
+     *
+     * @param deviceId device identifier
+     * @return number of flow rules for the given device
+     */
+    default int getFlowRuleCount(DeviceId deviceId) {
+        return 0;
+    }
+
+    /**
      * Returns the stored flow.
      *
      * @param rule the rule to look for
diff --git a/core/net/src/main/java/org/onosproject/net/flow/impl/FlowRuleManager.java b/core/net/src/main/java/org/onosproject/net/flow/impl/FlowRuleManager.java
index 9c1a31a..ffcfe0b 100644
--- a/core/net/src/main/java/org/onosproject/net/flow/impl/FlowRuleManager.java
+++ b/core/net/src/main/java/org/onosproject/net/flow/impl/FlowRuleManager.java
@@ -101,6 +101,7 @@
                                                  FlowRuleProvider, FlowRuleProviderService>
         implements FlowRuleService, FlowRuleProviderRegistry {
 
+    public static final String DEVICE_ID_NULL = "Device ID cannot be null";
     private final Logger log = getLogger(getClass());
 
     public static final String FLOW_RULE_NULL = "FlowRule cannot be null";
@@ -233,8 +234,16 @@
     }
 
     @Override
+    public int getFlowRuleCount(DeviceId deviceId) {
+        checkPermission(FLOWRULE_READ);
+        checkNotNull(deviceId, "Device ID cannot be null");
+        return store.getFlowRuleCount(deviceId);
+    }
+
+    @Override
     public Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
         checkPermission(FLOWRULE_READ);
+        checkNotNull(deviceId, DEVICE_ID_NULL);
         return store.getFlowEntries(deviceId);
     }
 
@@ -252,6 +261,7 @@
     @Override
     public void purgeFlowRules(DeviceId deviceId) {
         checkPermission(FLOWRULE_WRITE);
+        checkNotNull(deviceId, DEVICE_ID_NULL);
         store.purgeFlowRule(deviceId);
     }
 
@@ -343,6 +353,7 @@
      */
     @Override
     protected synchronized FlowRuleProvider getProvider(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
         // if device supports FlowRuleProgrammable,
         // use FlowRuleProgrammable via FlowRuleDriverProvider
         return Optional.ofNullable(deviceService.getDevice(deviceId))
@@ -714,11 +725,13 @@
     @Override
     public Iterable<TableStatisticsEntry> getFlowTableStatistics(DeviceId deviceId) {
         checkPermission(FLOWRULE_READ);
+        checkNotNull(deviceId, DEVICE_ID_NULL);
         return store.getTableStatistics(deviceId);
     }
 
     @Override
     public long getActiveFlowRuleCount(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
         return store.getActiveFlowRuleCount(deviceId);
     }
 
diff --git a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
index 64c09cf..e1a8cff 100644
--- a/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onosproject/store/flow/impl/ECFlowRuleStore.java
@@ -186,6 +186,7 @@
             .register(KryoNamespaces.API)
             .register(MastershipBasedTimestamp.class);
 
+    private EventuallyConsistentMap<DeviceId, Integer> flowCounts;
 
     private IdGenerator idGenerator;
     private NodeId local;
@@ -212,6 +213,14 @@
                 backupPeriod,
                 TimeUnit.MILLISECONDS);
 
+        flowCounts = storageService.<DeviceId, Integer>eventuallyConsistentMapBuilder()
+                .withName("onos-flow-counts")
+                .withSerializer(serializerBuilder)
+                .withAntiEntropyPeriod(5, TimeUnit.SECONDS)
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .withTombstonesDisabled()
+                .build();
+
         deviceTableStats = storageService.<DeviceId, List<TableStatisticsEntry>>eventuallyConsistentMapBuilder()
                 .withName("onos-flow-table-stats")
                 .withSerializer(serializerBuilder)
@@ -333,8 +342,14 @@
     @Override
     public int getFlowRuleCount() {
         return Streams.stream(deviceService.getDevices()).parallel()
-                        .mapToInt(device -> Iterables.size(getFlowEntries(device.id())))
-                        .sum();
+                .mapToInt(device -> getFlowRuleCount(device.id()))
+                .sum();
+    }
+
+    @Override
+    public int getFlowRuleCount(DeviceId deviceId) {
+        Integer count = flowCounts.get(deviceId);
+        return count != null ? count : 0;
     }
 
     @Override
@@ -703,7 +718,13 @@
             log.debug("Sending flowEntries for devices {} to {} for backup.", deviceIds, nodeId);
             Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
                     deviceFlowEntries = Maps.newConcurrentMap();
-            deviceIds.forEach(id -> deviceFlowEntries.put(id, getFlowTableCopy(id)));
+            deviceIds.forEach(id -> {
+                Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> copy = getFlowTableCopy(id);
+                int flowCount = copy.entrySet().stream()
+                        .mapToInt(e -> e.getValue().values().size()).sum();
+                flowCounts.put(id, flowCount);
+                deviceFlowEntries.put(id, copy);
+            });
             clusterCommunicator.<Map<DeviceId,
                                  Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>,
                                  Set<DeviceId>>
diff --git a/modules.defs b/modules.defs
index e1b6fdb..65bef76 100644
--- a/modules.defs
+++ b/modules.defs
@@ -179,6 +179,7 @@
     '//apps/test/election:onos-apps-test-election-oar',
     '//apps/test/flow-perf:onos-apps-test-flow-perf-oar',
     '//apps/test/intent-perf:onos-apps-test-intent-perf-oar',
+    '//apps/test/route-scale:onos-apps-test-route-scale-oar',
     '//apps/test/loadtest:onos-apps-test-loadtest-oar',
     '//apps/test/netcfg-monitor:onos-apps-test-netcfg-monitor-oar',
     '//apps/test/messaging-perf:onos-apps-test-messaging-perf-oar',
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
index 2cbb4d0..beaeb6a 100644
--- a/providers/null/src/main/java/org/onosproject/provider/nil/NullFlowRuleProvider.java
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/NullFlowRuleProvider.java
@@ -74,12 +74,12 @@
 
     @Override
     public void applyFlowRule(FlowRule... flowRules) {
-        // FIXME: invoke executeBatch
+        throw new UnsupportedOperationException("Cannot apply individual flow rules");
     }
 
     @Override
     public void removeFlowRule(FlowRule... flowRules) {
-        // FIXME: invoke executeBatch
+        throw new UnsupportedOperationException("Cannot remove individual flow rules");
     }
 
     @Override
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullEntity.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullEntity.java
index 368fa3d..a9efa78 100644
--- a/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullEntity.java
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullEntity.java
@@ -34,9 +34,12 @@
  * Base command for adding simulated entities to the custom topology simulation.
  */
 public abstract class CreateNullEntity extends AbstractShellCommand {
+
     protected static final String GEO = "geo";
     protected static final String GRID = "grid";
 
+    protected static final int MAX_EDGE_PORT_TRIES = 5;
+
     /**
      * Validates that the simulator is custom.
      *
@@ -94,22 +97,32 @@
      * @return connect point available for link or host attachment
      */
     protected ConnectPoint findAvailablePort(DeviceId deviceId, ConnectPoint otherPoint) {
-        EdgePortService eps = get(EdgePortService.class);
         HostService hs = get(HostService.class);
+        return findAvailablePorts(deviceId).stream()
+                .filter(p -> !Objects.equals(p, otherPoint) && hs.getConnectedHosts(p).isEmpty())
+                .findFirst().orElse(null);
+    }
+
+    /**
+     * Finds an available connect points among edge ports of the specified device.
+     *
+     * @param deviceId device identifier
+     * @return list of connect points available for link or host attachments
+     */
+    protected List<ConnectPoint> findAvailablePorts(DeviceId deviceId) {
+        EdgePortService eps = get(EdgePortService.class);
 
         // As there may be a slight delay in edge service getting updated, retry a few times
-        for (int i = 0; i < 3; i++) {
+        for (int i = 0; i < MAX_EDGE_PORT_TRIES; i++) {
             List<ConnectPoint> points = ImmutableList
                     .sortedCopyOf((l, r) -> Longs.compare(l.port().toLong(), r.port().toLong()),
                                   eps.getEdgePoints(deviceId));
-            ConnectPoint point = points.stream()
-                    .filter(p -> !Objects.equals(p, otherPoint) && hs.getConnectedHosts(p).isEmpty())
-                    .findFirst().orElse(null);
-            if (point != null) {
-                return point;
+            if (!points.isEmpty()) {
+                return points;
             }
             Tools.delay(100);
         }
-        return null;
+        return ImmutableList.of();
     }
+
 }
diff --git a/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullHosts.java b/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullHosts.java
new file mode 100644
index 0000000..0495038
--- /dev/null
+++ b/providers/null/src/main/java/org/onosproject/provider/nil/cli/CreateNullHosts.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.onlab.packet.IpAddress;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.provider.nil.CustomTopologySimulator;
+import org.onosproject.provider.nil.NullProviders;
+import org.onosproject.provider.nil.TopologySimulator;
+
+import java.util.List;
+
+/**
+ * Adds a simulated end-station host to the custom topology simulation.
+ */
+@Command(scope = "onos", name = "null-create-hosts",
+        description = "Adds a simulated end-station host to the custom topology simulation")
+public class CreateNullHosts extends CreateNullEntity {
+
+    @Argument(index = 0, name = "deviceName", description = "Name of device where hosts are attached",
+            required = true)
+    String deviceName = null;
+
+    @Argument(index = 1, name = "hostIpPattern", description = "Host IP pattern",
+            required = true)
+    String hostIpPattern = null;
+
+    @Argument(index = 2, name = "hostCount", description = "Number of hosts to create",
+            required = true)
+    int hostCount = 0;
+
+    @Override
+    protected void execute() {
+        NullProviders service = get(NullProviders.class);
+
+        TopologySimulator simulator = service.currentSimulator();
+        if (!validateSimulator(simulator)) {
+            return;
+        }
+
+        CustomTopologySimulator sim = (CustomTopologySimulator) simulator;
+
+        List<ConnectPoint> points = findAvailablePorts(sim.deviceId(deviceName));
+        String pattern = hostIpPattern.replace("*", "%d");
+        for (int h = 0; h < hostCount; h++) {
+            HostLocation location = new HostLocation(points.get(h), System.currentTimeMillis());
+            IpAddress ip = IpAddress.valueOf(String.format(pattern, h));
+            HostId id = sim.nextHostId();
+            sim.createHost(id, location, ip);
+        }
+    }
+
+}
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
index e1abc40..7be09e8 100644
--- a/providers/null/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/providers/null/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -50,6 +50,9 @@
         <command>
             <action class="org.onosproject.provider.nil.cli.CreateNullHost"/>
         </command>
+        <command>
+            <action class="org.onosproject.provider.nil.cli.CreateNullHosts"/>
+        </command>
     </command-bundle>
 
     <bean id="startStopCompleter" class="org.onosproject.cli.StartStopCompleter"/>
diff --git a/tools/test/topos/access-null b/tools/test/topos/access-null
new file mode 100755
index 0000000..0d9a13f
--- /dev/null
+++ b/tools/test/topos/access-null
@@ -0,0 +1,123 @@
+#!/bin/bash
+# -----------------------------------------------------------------------------
+# Creates a spine-leaf fabric with large number of hosts using null providers
+#
+# Default setup as follows:
+#   2 spines, can potentially be few more
+#   12 leaves in total
+#       2 leaf pair
+#       8 non-paired
+#   Host per leaf up to 1K
+#
+# -----------------------------------------------------------------------------
+
+function usage {
+    echo "usage: $(basename $0) [options] [onos-ip]"
+    echo ""
+    echo "Options:"
+    echo "  -s spines"
+    echo "  -l spineLinks"
+    echo "  -S serviceHosts"
+    echo "  -a accessLeaves"
+    echo "  -A accessHosts"
+    exit 1
+}
+
+spines=2
+spineLinks=2
+serviceLeafGroups="A B"
+serviceHosts=10
+accessLeaves=8
+accessHosts=100
+
+# Scan arguments for user/password or other options...
+while getopts s:l:a:A:S:?h o; do
+    case "$o" in
+        s) spines=$OPTARG;;
+        l) spineLinks=$OPTARG;;
+        a) accessLeaves=$OPTARG;;
+        A) accessHosts=$OPTARG;;
+        S) serviceHosts=$OPTARG;;
+        *) usage $0;;
+    esac
+done
+
+spinePorts=48
+let leafPorts=serviceHosts+8     # derive service ports from service hosts
+let accessPorts=accessHosts+8    # derive access ports from access hosts
+
+let OPC=$OPTIND-1
+shift $OPC
+
+# config
+node=${1:-$OCI}
+
+
+# Create the script of ONOS commands first and then execute it all at once.
+export CMDS="/tmp/fab-onos.cmds"
+rm $CMDS
+
+function sim {
+    echo "$@" >> $CMDS
+}
+
+# Create spines
+for spine in $(seq 1 $spines); do
+    sim "null-create-device switch Spine-${spine} ${spinePorts}"
+done
+
+# Create 2 leaf pairs with dual links to the spines and a link between the pair
+for pair in $serviceLeafGroups; do
+    sim "null-create-device switch Leaf-${pair}1 ${leafPorts}"
+    sim "null-create-device switch Leaf-${pair}2 ${leafPorts}"
+    sim "null-create-link direct Leaf-${pair}1 Leaf-${pair}2"
+
+    for spine in $(seq 1 $spines); do
+        for link in $(seq 1 $spineLinks); do
+            sim "null-create-link direct Spine-${spine} Leaf-${pair}1"
+            sim "null-create-link direct Spine-${spine} Leaf-${pair}2"
+        done
+    done
+
+    # Create hosts for each leaf group; multi-homed to each leaf in the pair
+    [ $pair = A ] && pn=1 || pn=2
+    for host in $(seq 1 $serviceHosts); do
+        sim "null-create-host Leaf-${pair}1,Leaf-${pair}2 10.${pn}.1.${host}"
+    done
+done
+
+# Create single access leafs with dual links to the spines
+for access in $(seq $accessLeaves); do
+    sim "null-create-device switch Access-${access} ${accessPorts}"
+
+    for spine in $(seq 1 $spines); do
+        for link in $(seq 1 $spineLinks); do
+            sim "null-create-link direct Spine-${spine} Access-${access}"
+        done
+    done
+
+    # Create hosts for each access single leaf
+    sim "null-create-hosts Access-${access} 10.1${access}.1.*" $accessHosts
+    # sim "null-create-hosts Access-${access} 10.1${access}.2.*" $accessHosts
+done
+
+
+# make sure null providers are activated and any running simulation is stopped
+onos ${node} app activate org.onosproject.null
+sleep 2
+onos ${node} null-simulation stop
+
+# wait until the masterships clear-out across the cluster
+while onos ${node} masters | grep -qv " 0 devices"; do sleep 1; done
+
+# clean-up
+onos ${node} wipe-out please
+sleep 1
+
+# start custom simulation..
+onos ${node} null-simulation start custom
+sleep 2
+
+# Add devices, links, and hosts
+cat $CMDS | onos ${node}
+
diff --git a/tools/test/topos/cfab-null b/tools/test/topos/cfab-null
deleted file mode 100755
index f8eb3f0..0000000
--- a/tools/test/topos/cfab-null
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/bin/bash
-# -----------------------------------------------------------------------------
-# Creates a spine-leaf fabric with large number of hosts using null providers
-# -----------------------------------------------------------------------------
-
-# config
-node=${1:-$OCI}
-
-# Create the script of ONOS commands first and then execute it all at once.
-export CMDS="/tmp/fab-onos.cmds"
-rm $CMDS
-
-function sim {
-    echo "$@" >> $CMDS
-}
-
-# Generate the recipe using the following:
-# 2 spines, can potentially be few more
-# 12 leaves in total
-#     2 leaf pair
-#     8 non-paired
-# Host per leaf up to 1K
-
-spinePorts=48
-leafPorts=64
-accessPorts=128
-
-# Create spines
-for spine in {1..2}; do
-    sim "null-create-device switch Spine-${spine} ${spinePorts}"
-done
-
-# Create 2 leaf pairs with dual links to the spines and a link between the pair
-for pair in A B; do
-    sim "null-create-device switch Leaf-${pair}1 ${leafPorts}"
-    sim "null-create-device switch Leaf-${pair}2 ${leafPorts}"
-    sim "null-create-link direct Leaf-${pair}1 Leaf-${pair}2"
-
-    for spine in {1..2}; do
-        for link in {1..2}; do
-            sim "null-create-link direct Spine-${spine} Leaf-${pair}1"
-            sim "null-create-link direct Spine-${spine} Leaf-${pair}2"
-        done
-    done
-
-    # Create hosts for each leaf group; multi-homed to each leaf in the pair
-    [ $pair = A ] && pn=1 || pn=2
-    for host in {1..10}; do
-        sim "null-create-host Leaf-${pair}1,Leaf-${pair}2 10.${pn}.1.${host}"
-    done
-done
-
-# Create 8 single leafs with dual links to the spines
-for access in {1..8}; do
-    sim "null-create-device switch Access-${access} ${accessPorts}"
-
-    for spine in {1..2}; do
-        for link in {1..2}; do
-            sim "null-create-link direct Spine-${spine} Access-${access}"
-        done
-    done
-
-    # Create hosts for each single leaf
-    for host in {1..50}; do
-        sim "null-create-host Access-${access} 10.0.${access}.${host}"
-    done
-done
-
-
-# make sure null providers are activated and any running simulation is stopped
-onos ${node} app activate org.onosproject.null
-sleep 2
-onos ${node} null-simulation stop
-
-# wait until the masterships clear-out across the cluster
-while onos ${node} masters | grep -qv " 0 devices"; do sleep 1; done
-
-# clean-up
-onos ${node} wipe-out please
-sleep 1
-
-# start custom simulation..
-onos ${node} null-simulation start custom
-sleep 2
-
-# Add devices, links, and hosts
-cat $CMDS | onos ${node}
-
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
index d84fbda..789c252 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java
@@ -43,7 +43,6 @@
 import org.onosproject.net.HostLocation;
 import org.onosproject.net.Link;
 import org.onosproject.net.device.DeviceEvent;
-import org.onosproject.net.flow.FlowEntry;
 import org.onosproject.net.host.HostEvent;
 import org.onosproject.net.link.LinkEvent;
 import org.onosproject.net.provider.ProviderId;
@@ -544,11 +543,7 @@
     }
 
     protected int getFlowCount(DeviceId deviceId) {
-        int count = 0;
-        for (FlowEntry flowEntry : services.flow().getFlowEntries(deviceId)) {
-            count++;
-        }
-        return count;
+        return services.flow().getFlowRuleCount(deviceId);
     }
 
     protected int getTunnelCount(DeviceId deviceId) {