Moved demo app back to main onos repo.

Change-Id: I3e445b28c1a19f9af9b582a05f3f6403af6d19c2
diff --git a/apps/test/demo/app.xml b/apps/test/demo/app.xml
new file mode 100644
index 0000000..0318f9c
--- /dev/null
+++ b/apps/test/demo/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<app name="org.onosproject.demo" origin="ON.Lab" version="1.2.0"
+        features="onos-app-demo">
+    <description>Flow throughput test application</description>
+</app>
diff --git a/apps/test/demo/pom.xml b/apps/test/demo/pom.xml
new file mode 100644
index 0000000..adac250
--- /dev/null
+++ b/apps/test/demo/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-apps-test</artifactId>
+        <version>1.2.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-app-demo</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS demo app bundle</description>
+
+    <properties>
+        <web.context>/onos/demo</web.context>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <_wab>src/main/webapp/</_wab>
+                        <Bundle-SymbolicName>
+                            ${project.groupId}.${project.artifactId}
+                        </Bundle-SymbolicName>
+                        <Import-Package>
+                            org.slf4j,
+                            org.osgi.framework,
+                            javax.ws.rs,javax.ws.rs.core,
+                            com.sun.jersey.api.core,
+                            com.sun.jersey.spi.container.servlet,
+                            com.sun.jersey.server.impl.container.servlet,
+                            com.fasterxml.jackson.databind,
+                            com.fasterxml.jackson.databind.node,
+                            org.apache.commons.lang.math.*,
+                            com.google.common.*,
+                            org.onlab.packet.*,
+                            org.onlab.rest.*,
+                            org.onosproject.*,
+                            org.onlab.util.*,
+                            org.jboss.netty.util.*
+                        </Import-Package>
+                        <Web-ContextPath>${web.context}</Web-ContextPath>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/apps/test/demo/src/main/java/org/onosproject/demo/DemoAPI.java b/apps/test/demo/src/main/java/org/onosproject/demo/DemoAPI.java
new file mode 100644
index 0000000..eded291
--- /dev/null
+++ b/apps/test/demo/src/main/java/org/onosproject/demo/DemoAPI.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.demo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.Optional;
+
+/**
+ * Simple demo api interface.
+ */
+public interface DemoAPI {
+
+    enum InstallType { MESH, RANDOM };
+
+    /**
+     * Tests flow subsystem based on the parameters supplied.
+     *
+     * @param params the test parameters
+     * @return JSON representation
+     */
+    JsonNode flowTest(Optional<JsonNode> params);
+
+    /**
+     * Installs intents based on the installation type.
+     * @param type the installation type.
+     * @param runParams run params
+     */
+    void setup(InstallType type, Optional<JsonNode> runParams);
+
+    /**
+     * Uninstalls all existing intents.
+     */
+    void tearDown();
+
+}
diff --git a/apps/test/demo/src/main/java/org/onosproject/demo/DemoInstaller.java b/apps/test/demo/src/main/java/org/onosproject/demo/DemoInstaller.java
new file mode 100644
index 0000000..1bad7a7
--- /dev/null
+++ b/apps/test/demo/src/main/java/org/onosproject/demo/DemoInstaller.java
@@ -0,0 +1,600 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.demo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Predicate;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.apache.commons.lang.math.RandomUtils;
+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.MacAddress;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.Device;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.MastershipRole;
+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.FlowRuleOperations;
+import org.onosproject.net.flow.FlowRuleOperationsContext;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.Constraint;
+import org.onosproject.net.intent.HostToHostIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentService;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Application to set up demos.
+ */
+@Component(immediate = true)
+@Service
+public class DemoInstaller implements DemoAPI {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentService intentService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected FlowRuleService flowService;
+
+    private ExecutorService worker;
+
+    private ExecutorService installWorker;
+
+    private ApplicationId appId;
+
+    private final Set<Intent> existingIntents = new HashSet<>();
+    private RandomInstaller randomInstaller;
+
+    private ObjectMapper mapper = new ObjectMapper();
+
+
+
+    @Activate
+    public void activate() {
+        String nodeId = clusterService.getLocalNode().ip().toString();
+        appId = coreService.registerApplication("org.onosproject.demo.installer."
+                                                        + nodeId);
+        worker = Executors.newFixedThreadPool(1,
+                                              new ThreadFactoryBuilder()
+                                                      .setNameFormat("demo-app-worker")
+                                                      .build());
+        log.info("Started with Application ID {}", appId.id());
+    }
+
+    @Deactivate
+    public void deactivate() {
+        shutdownAndAwaitTermination(worker);
+        if (installWorker != null && !installWorker.isShutdown()) {
+            shutdownAndAwaitTermination(installWorker);
+        }
+        log.info("Stopped");
+    }
+
+    @Override
+    public JsonNode flowTest(Optional<JsonNode> params) {
+        int flowsPerDevice = 1000;
+        int neighbours = 0;
+        boolean remove = true;
+        if (params.isPresent()) {
+            flowsPerDevice = params.get().get("flowsPerDevice").asInt();
+            neighbours = params.get().get("neighbours").asInt();
+            remove = params.get().get("remove").asBoolean();
+        }
+
+        Future<JsonNode> future = worker.submit(new FlowTest(flowsPerDevice, neighbours, remove));
+
+        try {
+            return future.get(10, TimeUnit.SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            ObjectNode node = mapper.createObjectNode();
+            node.put("Error", e.getMessage());
+            return node;
+        }
+    }
+
+    @Override
+    public void setup(InstallType type, Optional<JsonNode> runParams) {
+        switch (type) {
+            case MESH:
+                log.debug("Installing mesh intents");
+                worker.execute(new MeshInstaller());
+                break;
+            case RANDOM:
+                //check that we do not have a random installer running
+                if (installWorker == null || installWorker.isShutdown()) {
+                    installWorker = Executors.newFixedThreadPool(1,
+                                                                 new ThreadFactoryBuilder()
+                                                                         .setNameFormat("random-worker")
+                                                                         .build());
+                    log.debug("Installing random sequence of intents");
+                    randomInstaller = new RandomInstaller(runParams);
+                    installWorker.execute(randomInstaller);
+                } else {
+                    log.warn("Random installer is already running");
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("What is it you want exactly?");
+        }
+    }
+
+    @Override
+    public void tearDown() {
+        worker.submit(new UnInstaller());
+    }
+
+
+    /**
+     * Simply installs a mesh of intents from all the hosts existing in the network.
+     */
+    private class MeshInstaller implements Runnable {
+
+        @Override
+        public void run() {
+            TrafficSelector selector = DefaultTrafficSelector.emptySelector();
+            TrafficTreatment treatment = DefaultTrafficTreatment.emptyTreatment();
+            List<Constraint> constraint = Lists.newArrayList();
+            List<Host> hosts = Lists.newArrayList(hostService.getHosts());
+            while (!hosts.isEmpty()) {
+                Host src = hosts.remove(0);
+                for (Host dst : hosts) {
+                    HostToHostIntent intent = HostToHostIntent.builder()
+                            .appId(appId)
+                            .one(src.id())
+                            .two(dst.id())
+                            .selector(selector)
+                            .treatment(treatment)
+                            .constraints(constraint)
+                            .build();
+                    existingIntents.add(intent);
+                    intentService.submit(intent);
+                }
+            }
+        }
+    }
+
+    /**
+     * Randomly installs and withdraws intents.
+     */
+    private class RandomInstaller implements Runnable {
+
+        private final boolean isLocal;
+        private final Set<Host> hosts;
+
+        private final Random random = new Random(System.currentTimeMillis());
+
+        private Set<HostPair> uninstalledOrWithdrawn;
+        private Set<HostPair> installed;
+
+        private CountDownLatch latch;
+
+        //used to wait on a batch to be processed.
+        private static final int ITERATIONMAX = 50000000;
+
+
+        public RandomInstaller(Optional<JsonNode> runParams) {
+            /*
+                Check if we have params and honour them. Otherwise
+                    set defaults to processing only local stuff and
+                    all local hosts.
+             */
+            if (runParams.isPresent()) {
+                JsonNode node = runParams.get();
+                isLocal = node.get("local").asBoolean();
+                hosts = node.get("hosts") == null ? Sets.newHashSet(hostService.getHosts()) :
+                        constructHostIds(node.get("hosts").elements());
+            } else {
+                isLocal = true;
+                hosts = Sets.newHashSet(hostService.getHosts());
+            }
+
+            //construct list of intents.
+            installed = Sets.newHashSet();
+            if (isLocal) {
+                uninstalledOrWithdrawn = buildPairs(pruneHostsByMasterShip());
+            } else {
+                uninstalledOrWithdrawn = buildPairs(hosts);
+            }
+
+        }
+
+        private Set<Host> constructHostIds(Iterator<JsonNode> elements) {
+            Set<Host> hostIds = Sets.newHashSet();
+            JsonNode n;
+            while (elements.hasNext()) {
+                n = elements.next();
+                hostIds.add(hostService.getHost(HostId.hostId(n.textValue())));
+            }
+            return hostIds;
+        }
+
+        @Override
+        public void run() {
+            if (!installWorker.isShutdown()) {
+                randomize();
+                latch = new CountDownLatch(1);
+                try {
+                    trackIntents();
+                } catch (InterruptedException e) {
+                    shutdown();
+                }
+            }
+
+        }
+
+
+        /**
+         *   Check whether the previously submitted batch is in progress
+         *   and if yes submit the next one. If things hang, wait for at
+         *   most 5 seconds and bail.
+         * @throws InterruptedException if the thread go interupted
+         */
+        private void trackIntents() throws InterruptedException {
+            //FIXME
+            // TODO generate keys for each set of intents to allow manager to throttle
+            // TODO may also look into the store to see how many operations are pending
+
+            //if everything is good proceed.
+            if (!installWorker.isShutdown()) {
+                installWorker.execute(this);
+            }
+
+        }
+
+        public void shutdown() {
+            log.warn("Shutting down random installer!");
+            cleanUp();
+        }
+
+
+        /**
+         *   Shuffle the uninstalled and installed list (separately) and select
+         *   a random number of them and install or uninstall them respectively.
+         */
+        private void randomize() {
+            List<HostPair> hostList = new LinkedList<>(uninstalledOrWithdrawn);
+            Collections.shuffle(hostList);
+            List<HostPair> toInstall = hostList.subList(0,
+                                                        random.nextInt(hostList.size() - 1));
+            List<HostPair> toRemove;
+            if (!installed.isEmpty()) {
+                hostList = new LinkedList<>(installed);
+                Collections.shuffle(hostList);
+                toRemove = hostList.subList(0,
+                                            random.nextInt(hostList.size() - 1));
+                uninstallIntents(toRemove);
+            }
+            installIntents(toInstall);
+
+        }
+
+        private void installIntents(List<HostPair> toInstall) {
+            for (HostPair pair : toInstall) {
+                installed.add(pair);
+                uninstalledOrWithdrawn.remove(pair);
+                intentService.submit(pair.h2hIntent());
+            }
+        }
+
+        private void uninstallIntents(Collection<HostPair> toRemove) {
+            for (HostPair pair : toRemove) {
+                installed.remove(pair);
+                uninstalledOrWithdrawn.add(pair);
+                intentService.withdraw(pair.h2hIntent());
+            }
+        }
+
+        /**
+         *  Take everything and remove it all.
+         */
+        private void cleanUp() {
+            List<HostPair> allPairs = Lists.newArrayList(installed);
+            allPairs.addAll(uninstalledOrWithdrawn);
+            for (HostPair pair : allPairs) {
+                intentService.withdraw(pair.h2hIntent());
+            }
+        }
+
+
+        private Set<HostPair> buildPairs(Set<Host> hosts) {
+            Set<HostPair> pairs = Sets.newHashSet();
+            Iterator<Host> it = Sets.newHashSet(hosts).iterator();
+            while (it.hasNext()) {
+                Host src = it.next();
+                it.remove();
+                for (Host dst : hosts) {
+                    pairs.add(new HostPair(src, dst));
+                }
+            }
+            return pairs;
+        }
+
+        private Set<Host> pruneHostsByMasterShip() {
+            return FluentIterable.from(hosts)
+                    .filter(hasLocalMaster())
+                    .toSet();
+
+        }
+
+        private Predicate<? super Host> hasLocalMaster() {
+            return new Predicate<Host>() {
+                @Override
+                public boolean apply(Host host) {
+                    return mastershipService.getLocalRole(
+                            host.location().deviceId()).equals(MastershipRole.MASTER);
+                }
+            };
+        }
+
+
+        /**
+         * Simple class representing a pair of hosts and precomputes the associated
+         * h2h intent.
+         */
+        private class HostPair {
+
+            private final Host src;
+            private final Host dst;
+
+            private final TrafficSelector selector = DefaultTrafficSelector.emptySelector();
+            private final TrafficTreatment treatment = DefaultTrafficTreatment.emptyTreatment();
+            private final List<Constraint> constraint = Lists.newArrayList();
+            private final HostToHostIntent intent;
+
+            public HostPair(Host src, Host dst) {
+                this.src = src;
+                this.dst = dst;
+                this.intent = HostToHostIntent.builder()
+                        .appId(appId)
+                        .one(src.id())
+                        .two(dst.id())
+                        .selector(selector)
+                        .treatment(treatment)
+                        .constraints(constraint)
+                        .build();
+            }
+
+            public HostToHostIntent h2hIntent() {
+                return intent;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass()) {
+                    return false;
+                }
+
+                HostPair hostPair = (HostPair) o;
+
+                return Objects.equals(src, hostPair.src) &&
+                        Objects.equals(dst, hostPair.dst);
+
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(src, dst);
+            }
+
+
+        }
+
+    }
+
+    /**
+     * Remove anything that is running and clear it all out.
+     */
+    private class UnInstaller implements Runnable {
+        @Override
+        public void run() {
+            if (!existingIntents.isEmpty()) {
+                clearExistingIntents();
+            }
+
+            if (installWorker != null && !installWorker.isShutdown()) {
+                shutdownAndAwaitTermination(installWorker);
+                randomInstaller.shutdown();
+            }
+        }
+
+        private void clearExistingIntents() {
+            for (Intent i : existingIntents) {
+                intentService.withdraw(i);
+            }
+            existingIntents.clear();
+        }
+    }
+
+    /**
+     * Shutdown a pool cleanly if possible.
+     *
+     * @param pool an executorService
+     */
+    private void shutdownAndAwaitTermination(ExecutorService pool) {
+        pool.shutdown(); // Disable new tasks from being submitted
+        try {
+            // Wait a while for existing tasks to terminate
+            if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
+                pool.shutdownNow(); // Cancel currently executing tasks
+                // Wait a while for tasks to respond to being cancelled
+                if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
+                    log.error("Pool did not terminate");
+                }
+            }
+        } catch (Exception ie) {
+            // (Re-)Cancel if current thread also interrupted
+            pool.shutdownNow();
+            // Preserve interrupt status
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private class FlowTest implements Callable<JsonNode> {
+        private final int flowPerDevice;
+        private final int neighbours;
+        private final boolean remove;
+        private FlowRuleOperations.Builder adds;
+        private FlowRuleOperations.Builder removes;
+
+        public FlowTest(int flowsPerDevice, int neighbours, boolean remove) {
+            this.flowPerDevice = flowsPerDevice;
+            this.neighbours = neighbours;
+            this.remove = remove;
+            prepareInstallation();
+        }
+
+        private void prepareInstallation() {
+            Set<ControllerNode> instances = Sets.newHashSet(clusterService.getNodes());
+            instances.remove(clusterService.getLocalNode());
+            Set<NodeId> acceptableNodes = Sets.newHashSet();
+            if (neighbours >= instances.size()) {
+                instances.forEach(instance -> acceptableNodes.add(instance.id()));
+            } else {
+                Iterator<ControllerNode> nodes = instances.iterator();
+                for (int i = neighbours; i > 0; i--) {
+                    acceptableNodes.add(nodes.next().id());
+                }
+            }
+            acceptableNodes.add(clusterService.getLocalNode().id());
+
+            Set<Device> devices = Sets.newHashSet();
+            for (Device dev : deviceService.getDevices()) {
+                if (acceptableNodes.contains(
+                        mastershipService.getMasterFor(dev.id()))) {
+                    devices.add(dev);
+                }
+            }
+
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
+                    .setOutput(PortNumber.portNumber(RandomUtils.nextInt())).build();
+            TrafficSelector.Builder sbuilder;
+            FlowRuleOperations.Builder rules = FlowRuleOperations.builder();
+            FlowRuleOperations.Builder remove = FlowRuleOperations.builder();
+
+            for (Device d : devices) {
+                for (int i = 0; i < this.flowPerDevice; i++) {
+                    sbuilder = DefaultTrafficSelector.builder();
+
+                    sbuilder.matchEthSrc(MacAddress.valueOf(RandomUtils.nextInt() * i))
+                            .matchEthDst(MacAddress.valueOf((Integer.MAX_VALUE - i) * RandomUtils.nextInt()));
+
+
+                    int randomPriority = RandomUtils.nextInt();
+                    DefaultFlowRule f = new DefaultFlowRule(d.id(), sbuilder.build(), treatment,
+                                                            randomPriority, appId, 10, false);
+                    rules.add(f);
+                    remove.remove(f);
+
+                }
+            }
+
+            this.adds = rules;
+            this.removes = remove;
+        }
+
+        @Override
+        public JsonNode call() throws Exception {
+            ObjectNode node = mapper.createObjectNode();
+            CountDownLatch latch = new CountDownLatch(1);
+            flowService.apply(adds.build(new FlowRuleOperationsContext() {
+
+                private final Stopwatch timer = Stopwatch.createStarted();
+
+                @Override
+                public void onSuccess(FlowRuleOperations ops) {
+
+                    long elapsed = timer.elapsed(TimeUnit.MILLISECONDS);
+                    node.put("elapsed", elapsed);
+
+
+                    latch.countDown();
+                }
+            }));
+
+            latch.await(10, TimeUnit.SECONDS);
+            if (this.remove) {
+                flowService.apply(removes.build());
+            }
+            return node;
+        }
+    }
+}
+
+
diff --git a/apps/test/demo/src/main/java/org/onosproject/demo/DemoResource.java b/apps/test/demo/src/main/java/org/onosproject/demo/DemoResource.java
new file mode 100644
index 0000000..29c55d1
--- /dev/null
+++ b/apps/test/demo/src/main/java/org/onosproject/demo/DemoResource.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.demo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.onlab.rest.BaseResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+/**
+ * Rest API for demos.
+ */
+@Path("intents")
+public class DemoResource extends BaseResource {
+
+
+
+    @POST
+    @Path("flowTest")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response flowTest(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode cfg = mapper.readTree(input);
+        DemoAPI demo = get(DemoAPI.class);
+        return Response.ok(demo.flowTest(Optional.ofNullable(cfg)).toString()).build();
+    }
+
+    @POST
+    @Path("setup")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response setup(InputStream input) throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode cfg = mapper.readTree(input);
+        if (!cfg.has("type")) {
+            return Response.status(Response.Status.BAD_REQUEST)
+                    .entity("Expected type field containing either mesh or random.").build();
+        }
+
+
+        DemoAPI.InstallType type = DemoAPI.InstallType.valueOf(
+                cfg.get("type").asText().toUpperCase());
+        DemoAPI demo = get(DemoAPI.class);
+        demo.setup(type, Optional.ofNullable(cfg.get("runParams")));
+
+        return Response.ok(mapper.createObjectNode().toString()).build();
+    }
+
+    @GET
+    @Path("teardown")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response tearDown() throws IOException {
+        ObjectMapper mapper = new ObjectMapper();
+        DemoAPI demo = get(DemoAPI.class);
+        demo.tearDown();
+        return Response.ok(mapper.createObjectNode().toString()).build();
+    }
+
+}
diff --git a/apps/test/demo/src/main/java/org/onosproject/demo/package-info.java b/apps/test/demo/src/main/java/org/onosproject/demo/package-info.java
new file mode 100644
index 0000000..f583a0e
--- /dev/null
+++ b/apps/test/demo/src/main/java/org/onosproject/demo/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ *  Demo applications live here.
+ */
+package org.onosproject.demo;
diff --git a/apps/test/demo/src/main/webapp/WEB-INF/web.xml b/apps/test/demo/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..29716df
--- /dev/null
+++ b/apps/test/demo/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2014 Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="ONOS" version="2.5">
+    <display-name>ONOS DEMO APP API v1.0</display-name>
+
+    <servlet>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>
+            <param-value>com.sun.jersey.api.core.ClassNamesResourceConfig</param-value>
+        </init-param>
+        <init-param>
+            <param-name>com.sun.jersey.config.property.classnames</param-name>
+            <param-value>
+                org.onosproject.demo.DemoResource
+            </param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+</web-app>
diff --git a/apps/test/pom.xml b/apps/test/pom.xml
index 1d7ba6f..daab99b 100644
--- a/apps/test/pom.xml
+++ b/apps/test/pom.xml
@@ -34,6 +34,7 @@
     <modules>
         <module>election</module>
         <module>intent-perf</module>
+        <module>demo</module>
     </modules>
 
 </project>
diff --git a/features/features.xml b/features/features.xml
index ef79503..fb76afd 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -241,6 +241,13 @@
         <bundle>mvn:org.onosproject/onos-app-election/@ONOS-VERSION</bundle>
     </feature>
 
+    <feature name="onos-app-demo" version="@FEATURE-VERSION"
+             description="ONOS demo applications">
+        <feature>onos-api</feature>
+        <bundle>mvn:org.onosproject/onlab-misc/@ONOS-VERSION</bundle>
+        <bundle>mvn:org.onosproject/onos-app-demo/@ONOS-VERSION</bundle>
+    </feature>
+
 
     <!-- ONOS sample app features: to be moved to a different repo -->
 
@@ -258,13 +265,6 @@
         <bundle>mvn:org.onosproject/onos-app-ifwd/@ONOS-VERSION</bundle>
     </feature>
 
-    <feature name="onos-app-demo" version="@FEATURE-VERSION"
-             description="ONOS demo applications">
-        <feature>onos-api</feature>
-        <bundle>mvn:org.onosproject/onlab-misc/@ONOS-VERSION</bundle>
-        <bundle>mvn:org.onosproject/onos-app-demo/@ONOS-VERSION</bundle>
-    </feature>
-
     <feature name="onos-app-database-perf" version="@FEATURE-VERSION"
              description="ONOS partitioned database perf application">
         <feature>onos-api</feature>