Adding topology auto-layout.

Change-Id: I2b9e0b5808b8d193e8d08b7fe6ffdb007b083809
diff --git a/apps/layout/BUCK b/apps/layout/BUCK
new file mode 100644
index 0000000..87e21e3
--- /dev/null
+++ b/apps/layout/BUCK
@@ -0,0 +1,18 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:org.apache.karaf.shell.console',
+    '//core/common:onos-core-common',
+    '//cli:onos-cli',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+)
+
+onos_app (
+    title = 'UI Auto-Layout',
+    category = 'Utility',
+    url = 'http://onosproject.org',
+    description = 'Automatically lays out the network topology using roles assigned to each ' +
+        'network element via the network configuration. Supports multiple layout variants.',
+)
diff --git a/apps/layout/pom.xml b/apps/layout/pom.xml
new file mode 100644
index 0000000..a3e4a32
--- /dev/null
+++ b/apps/layout/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015-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.
+  -->
+<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</artifactId>
+        <version>1.13.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-app-layout</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>Lays out topology views</description>
+
+    <properties>
+        <onos.app.name>org.onosproject.layout</onos.app.name>
+        <onos.app.title>UI Auto-Layout</onos.app.title>
+        <onos.app.category>Utility</onos.app.category>
+        <onos.app.url>http://onosproject.org</onos.app.url>
+        <onos.app.readme>
+            Automatically lays out the network topology using roles assigned to each
+            network element via the network configuration. Supports multiple layout variants.
+        </onos.app.readme>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/apps/layout/src/main/java/org/onosproject/layout/AccessNetworkLayout.java b/apps/layout/src/main/java/org/onosproject/layout/AccessNetworkLayout.java
new file mode 100644
index 0000000..104861c
--- /dev/null
+++ b/apps/layout/src/main/java/org/onosproject/layout/AccessNetworkLayout.java
@@ -0,0 +1,180 @@
+/*
+ * 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.layout;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
+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.utils.Comparators;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Arranges access network according to roles assigned to devices and hosts.
+ */
+public class AccessNetworkLayout extends LayoutAlgorithm {
+
+    private static final double COMPUTE_Y = -400.0;
+    private static final double SERVICE_Y = -200.0;
+    private static final double SPINE_Y = 0.0;
+    private static final double AGGREGATION_Y = +200.0;
+    private static final double ACCESS_Y = +400.0;
+    private static final double HOSTS_Y = +700.0;
+    private static final double GATEWAY_X = 900.0;
+
+    private static final int HOSTS_PER_ROW = 6;
+    private static final double ROW_GAP = 70;
+    private static final double COL_GAP = 50;
+    private static final double COMPUTE_GAP = 60.0;
+    private static final double COMPUTE_OFFSET = 400.0;
+    private static final double GATEWAY_GAP = 200.0;
+    private static final double GATEWAY_OFFSET = -200.0;
+
+    private int spine, aggregation, accessLeaf, serviceLeaf, compute, gateway;
+
+    @Override
+    protected boolean classify(Device device) {
+        if (!super.classify(device)) {
+            String role;
+
+            // Does the device have any hosts attached? If not, it's a spine
+            if (hostService.getConnectedHosts(device.id()).isEmpty()) {
+                // Does the device have any aggregate links to other devices?
+                Multiset<DeviceId> destinations = HashMultiset.create();
+                linkService.getDeviceEgressLinks(device.id()).stream()
+                        .map(l -> l.dst().deviceId()).forEach(destinations::add);
+
+                // If yes, it's the main spine; otherwise it's an aggregate spine
+                role = destinations.entrySet().stream().anyMatch(e -> e.getCount() > 1) ?
+                        SPINE : AGGREGATION;
+            } else {
+                // Does the device have any multi-home hosts attached?
+                // If yes, it's a service leaf; otherwise it's an access leaf
+                role = hostService.getConnectedHosts(device.id()).stream()
+                        .map(Host::locations).anyMatch(s -> s.size() > 1) ?
+                        LEAF : ACCESS;
+            }
+            deviceCategories.put(role, device.id());
+        }
+        return true;
+    }
+
+    @Override
+    protected boolean classify(Host host) {
+        if (!super.classify(host)) {
+            // Is the host attached to an access leaf?
+            // If so, it's an access host; otherwise it's a service host or gateway
+            String role = host.locations().stream().map(ConnectPoint::deviceId)
+                    .anyMatch(d -> deviceCategories.get(ACCESS)
+                            .contains(deviceService.getDevice(d).id())) ?
+                    ACCESS : COMPUTE;
+            hostCategories.put(role, host.id());
+        }
+        return true;
+    }
+
+    @Override
+    public void apply() {
+        placeSpines();
+        placeServiceLeavesAndHosts();
+        placeAccessLeavesAndHosts();
+    }
+
+    private void placeSpines() {
+        spine = 1;
+        List<DeviceId> spines = deviceCategories.get(SPINE);
+        spines.stream().sorted(Comparators.ELEMENT_ID_COMPARATOR)
+                .forEach(d -> place(d, c(spine++, spines.size()), SPINE_Y));
+    }
+
+    private void placeServiceLeavesAndHosts() {
+        List<DeviceId> leaves = deviceCategories.get(LEAF);
+        List<HostId> computes = hostCategories.get(COMPUTE);
+        List<HostId> gateways = hostCategories.get(GATEWAY);
+        Set<HostId> placed = Sets.newHashSet();
+
+        serviceLeaf = 1;
+        leaves.stream().sorted(Comparators.ELEMENT_ID_COMPARATOR).forEach(id -> {
+            gateway = 1;
+            place(id, c(serviceLeaf++, leaves.size()), SERVICE_Y);
+
+            List<HostId> gwHosts = hostService.getConnectedHosts(id).stream()
+                    .map(Host::id)
+                    .filter(gateways::contains)
+                    .filter(hid -> !placed.contains(hid))
+                    .sorted(Comparators.ELEMENT_ID_COMPARATOR)
+                    .collect(Collectors.toList());
+
+            gwHosts.forEach(hid -> {
+                place(hid, serviceLeaf <= 2 ? -GATEWAY_X : GATEWAY_X,
+                      c(gateway++, gwHosts.size(), GATEWAY_GAP, GATEWAY_OFFSET));
+                placed.add(hid);
+            });
+
+            compute = 1;
+            List<HostId> hosts = hostService.getConnectedHosts(id).stream()
+                    .map(Host::id)
+                    .filter(computes::contains)
+                    .filter(hid -> !placed.contains(hid))
+                    .sorted(Comparators.ELEMENT_ID_COMPARATOR)
+                    .collect(Collectors.toList());
+
+            hosts.forEach(hid -> {
+                place(hid, c(compute++, hosts.size(), COMPUTE_GAP,
+                             serviceLeaf <= 2 ? -COMPUTE_OFFSET : COMPUTE_OFFSET),
+                      COMPUTE_Y);
+                placed.add(hid);
+            });
+
+        });
+    }
+
+    private void placeAccessLeavesAndHosts() {
+        List<DeviceId> spines = deviceCategories.get(AGGREGATION);
+        List<DeviceId> leaves = deviceCategories.get(ACCESS);
+        Set<DeviceId> placed = Sets.newHashSet();
+
+        aggregation = 1;
+        accessLeaf = 1;
+        spines.stream().sorted(Comparators.ELEMENT_ID_COMPARATOR).forEach(id -> {
+            place(id, c(aggregation++, spines.size()), AGGREGATION_Y);
+            linkService.getDeviceEgressLinks(id).stream()
+                    .map(l -> l.dst().deviceId())
+                    .filter(leaves::contains)
+                    .filter(lid -> !placed.contains(lid))
+                    .sorted(Comparators.ELEMENT_ID_COMPARATOR)
+                    .forEach(lid -> {
+                        double x = c(accessLeaf++, leaves.size());
+                        place(lid, x, ACCESS_Y);
+                        placed.add(lid);
+                        placeHostBlock(hostService.getConnectedHosts(lid).stream()
+                                               .map(Host::id)
+                                               .sorted(Comparators.ELEMENT_ID_COMPARATOR)
+                                               .collect(Collectors.toList()), x, HOSTS_Y,
+                                       HOSTS_PER_ROW, ROW_GAP, COL_GAP);
+                    });
+        });
+    }
+
+}
diff --git a/apps/layout/src/main/java/org/onosproject/layout/AutoLayoutCommand.java b/apps/layout/src/main/java/org/onosproject/layout/AutoLayoutCommand.java
new file mode 100644
index 0000000..33705db
--- /dev/null
+++ b/apps/layout/src/main/java/org/onosproject/layout/AutoLayoutCommand.java
@@ -0,0 +1,50 @@
+/*
+ * 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.layout;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+
+/**
+ * Lists all the default lease parameters offered by the DHCP Server.
+ */
+@Command(scope = "onos", name = "topo-layout",
+        description = "Lays out the elements in the topology using the specified algorithm")
+public class AutoLayoutCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "algorithm",
+            description = "Layout algorithm to use for the layout")
+    String algorithm = "access";
+
+    @Override
+    protected void execute() {
+        RoleBasedLayoutManager mgr = get(RoleBasedLayoutManager.class);
+        switch (algorithm) {
+            case "spine-leaf":
+                mgr.layout(new AccessNetworkLayout());
+                break;
+            case "access":
+                mgr.layout(new AccessNetworkLayout());
+                break;
+            case "hag-access":
+                mgr.layout(new AccessNetworkLayout());
+                break;
+            default:
+                print("Unsupported layout algorithm %s", algorithm);
+        }
+    }
+}
diff --git a/apps/layout/src/main/java/org/onosproject/layout/LayoutAlgorithm.java b/apps/layout/src/main/java/org/onosproject/layout/LayoutAlgorithm.java
new file mode 100644
index 0000000..f9e062b
--- /dev/null
+++ b/apps/layout/src/main/java/org/onosproject/layout/LayoutAlgorithm.java
@@ -0,0 +1,225 @@
+/*
+ * 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.layout;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.ElementId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.config.basics.BasicDeviceConfig;
+import org.onosproject.net.config.basics.BasicHostConfig;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.LinkService;
+
+import java.util.Collection;
+
+/**
+ * Represents a topology layout algorithm.
+ */
+public abstract class LayoutAlgorithm {
+
+    public static final String SPINE = "spine";
+    public static final String AGGREGATION = "aggregation";
+    public static final String LEAF = "leaf";
+    public static final String ACCESS = "access";
+    public static final String GATEWAY = "gateway";
+    public static final String COMPUTE = "compute";
+
+    protected DeviceService deviceService;
+    protected HostService hostService;
+    protected LinkService linkService;
+    protected NetworkConfigService netConfigService;
+
+    protected ListMultimap<String, DeviceId> deviceCategories = ArrayListMultimap.create();
+    protected ListMultimap<String, HostId> hostCategories = ArrayListMultimap.create();
+
+
+    /**
+     * Initializes layout algorithm for operating on device and host inventory.
+     *
+     * @param deviceService        device service
+     * @param hostService          host service
+     * @param linkService          link service
+     * @param networkConfigService net config service
+     */
+    protected void init(DeviceService deviceService,
+                        HostService hostService,
+                        LinkService linkService,
+                        NetworkConfigService networkConfigService) {
+        this.deviceService = deviceService;
+        this.hostService = hostService;
+        this.linkService = linkService;
+        this.netConfigService = networkConfigService;
+    }
+
+    /**
+     * Places the specified device on the layout grid.
+     *
+     * @param id device identifier
+     * @param x grid X
+     * @param y grid Y
+     */
+    protected void place(DeviceId id, double x, double y) {
+        netConfigService.addConfig(id, BasicDeviceConfig.class)
+                .gridX(x).gridY(y).locType("grid").apply();
+    }
+
+    /**
+     * Places the specified device on the layout grid.
+     *
+     * @param id host identifier
+     * @param x grid X
+     * @param y grid Y
+     */
+    protected void place(HostId id, double x, double y) {
+        netConfigService.addConfig(id, BasicHostConfig.class)
+                .gridX(x).gridY(y).locType("grid").apply();
+    }
+
+    /**
+     * Computes grid coordinate for the i-th element of n-elements in a tier
+     * using a default gap of 400.
+     *
+     * @param i element index
+     * @param n number of elements
+     * @return grid Y
+     */
+    protected double c(int i, int n) {
+        return c(i, n, 400);
+    }
+
+    /**
+     * Computes grid coordinate for the i-th element of n-elements in a tier.
+     *
+     * @param i   element index
+     * @param n   number of elements
+     * @param gap gap width
+     * @return grid Y
+     */
+    protected double c(int i, int n, double gap) {
+        return c(i, n, gap, 0);
+    }
+
+    /**
+     * Computes grid coordinate for the i-th element of n-elements in a tier.
+     *
+     * @param i      element index
+     * @param n      number of elements
+     * @param gap    gap width
+     * @param offset additional Y offset
+     * @return grid Y
+     */
+    protected double c(int i, int n, double gap, double offset) {
+        return gap * (i - 1) - (gap * (n - 1)) / 2 + offset;
+    }
+
+    /**
+     * Places the specified collection of hosts (all presumably connected to
+     * the same network device) in a block.
+     *
+     * @param hosts       hosts to place
+     * @param gridX       grid X of the top of the block
+     * @param gridY       grid Y of the center of the block
+     * @param hostsPerRow number of hosts in a 'row'
+     * @param rowGap      gap width between rows
+     * @param colGap      gap width between columns
+     */
+    protected void placeHostBlock(Collection<HostId> hosts,
+                                  double gridX, double gridY, int hostsPerRow,
+                                  double rowGap, double colGap) {
+        double yStep = rowGap / hostsPerRow;
+        double y = gridY;
+        double x = gridX - (colGap * (hostsPerRow - 1)) / 2;
+        int i = 1;
+
+        for (HostId id : hosts) {
+            place(id, x, y);
+            if ((i % hostsPerRow) == 0) {
+                x = gridX - (colGap * (hostsPerRow - 1)) / 2;
+            } else {
+                x += colGap;
+                y += yStep;
+            }
+            i++;
+        }
+    }
+
+    /**
+     * Applies device and host classifications.
+     */
+    public void classify() {
+        deviceService.getDevices().forEach(this::classify);
+        hostService.getHosts().forEach(this::classify);
+    }
+
+    /**
+     * Classifies the specified device.
+     *
+     * @param device device to be classified
+     * @return true if classified
+     */
+    protected boolean classify(Device device) {
+        BasicDeviceConfig cfg = netConfigService.getConfig(device.id(), BasicDeviceConfig.class);
+        if (cfg != null && !cfg.roles().isEmpty()) {
+            cfg.roles().forEach(r -> deviceCategories.put(r, device.id()));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Classifies the specified host.
+     *
+     * @param host host to be classified
+     * @return true if classified
+     */
+    protected boolean classify(Host host) {
+        BasicHostConfig cfg = netConfigService.getConfig(host.id(), BasicHostConfig.class);
+        if (cfg != null && !cfg.roles().isEmpty()) {
+            cfg.roles().forEach(r -> hostCategories.put(r, host.id()));
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Applies the specified layout algorithm.
+     */
+    abstract void apply();
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceCategories", count(deviceCategories))
+                .add("hostCategories", count(hostCategories))
+                .toString();
+    }
+
+    private String count(ListMultimap<String, ? extends ElementId> categories) {
+        StringBuilder sb = new StringBuilder("[ ");
+        categories.keySet().forEach(k -> sb.append(k).append("=").append(categories.get(k).size()).append(" "));
+        return sb.append("]").toString();
+    }
+
+}
diff --git a/apps/layout/src/main/java/org/onosproject/layout/RoleBasedLayoutManager.java b/apps/layout/src/main/java/org/onosproject/layout/RoleBasedLayoutManager.java
new file mode 100644
index 0000000..0340c7c
--- /dev/null
+++ b/apps/layout/src/main/java/org/onosproject/layout/RoleBasedLayoutManager.java
@@ -0,0 +1,78 @@
+/*
+ * 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.layout;
+
+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.onosproject.net.config.NetworkConfigService;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.link.LinkService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages automatic layout of the current network elements into one of several
+ * supported layout variants using roles assigned to network elements using
+ * network configuration.
+ */
+@Component(immediate = true)
+@Service(value = RoleBasedLayoutManager.class)
+public class RoleBasedLayoutManager {
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigService networkConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkService linkService;
+
+    @Activate
+    protected void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        log.info("Stopped");
+    }
+
+
+    /**
+     * Executes the specified layout algorithm.
+     *
+     * @param algorithm layout algorithm
+     */
+    public void layout(LayoutAlgorithm algorithm) {
+        algorithm.init(deviceService, hostService, linkService, networkConfigService);
+        algorithm.classify();
+        log.info("Layout classified: {}", algorithm);
+        algorithm.apply();
+    }
+
+}
\ No newline at end of file
diff --git a/apps/layout/src/main/java/org/onosproject/layout/package-info.java b/apps/layout/src/main/java/org/onosproject/layout/package-info.java
new file mode 100644
index 0000000..22406ca
--- /dev/null
+++ b/apps/layout/src/main/java/org/onosproject/layout/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.
+ */
+
+/**
+ * Utility to layout UI topology based on element roles assigned via
+ * network configuration.
+ */
+package org.onosproject.layout;
\ No newline at end of file
diff --git a/apps/layout/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/layout/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..6562fd5
--- /dev/null
+++ b/apps/layout/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ 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.layout.AutoLayoutCommand"/>
+        </command>
+    </command-bundle>
+</blueprint>
\ No newline at end of file
diff --git a/apps/pom.xml b/apps/pom.xml
index fe5661a..a1abb59 100644
--- a/apps/pom.xml
+++ b/apps/pom.xml
@@ -101,6 +101,7 @@
         <module>routeradvertisement</module>
         <module>simplefabric</module>
         <module>mcast</module>
+        <module>layout</module>
     </modules>
 
     <properties>
diff --git a/core/api/src/main/java/org/onosproject/net/config/basics/BasicDeviceConfig.java b/core/api/src/main/java/org/onosproject/net/config/basics/BasicDeviceConfig.java
index cc60be6..0353bf2 100644
--- a/core/api/src/main/java/org/onosproject/net/config/basics/BasicDeviceConfig.java
+++ b/core/api/src/main/java/org/onosproject/net/config/basics/BasicDeviceConfig.java
@@ -50,7 +50,7 @@
 
         return super.isValid()
                 && hasOnlyFields(ALLOWED, NAME, LOC_TYPE, LATITUDE, LONGITUDE,
-                GRID_Y, GRID_X, UI_TYPE, RACK_ADDRESS, OWNER, TYPE, DRIVER,
+                GRID_Y, GRID_X, UI_TYPE, RACK_ADDRESS, OWNER, TYPE, DRIVER, ROLES,
                 MANUFACTURER, HW_VERSION, SW_VERSION, SERIAL,
                 MANAGEMENT_ADDRESS, DEVICE_KEY_ID)
                 && isValidLength(DRIVER, DRIVER_MAX_LENGTH)
diff --git a/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java b/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
index 9647bc9..3879257 100644
--- a/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
+++ b/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
@@ -40,8 +40,8 @@
         // ipAddresses can be absent, but if present must be valid
         this.locations();
         this.ipAddresses();
-        return hasOnlyFields(ALLOWED, NAME, LOC_TYPE, LATITUDE, LONGITUDE,
-                             GRID_Y, GRID_Y, UI_TYPE, RACK_ADDRESS, OWNER, IPS, LOCATIONS);
+        return hasOnlyFields(ALLOWED, NAME, LOC_TYPE, LATITUDE, LONGITUDE, ROLES,
+                             GRID_X, GRID_Y, UI_TYPE, RACK_ADDRESS, OWNER, IPS, LOCATIONS);
     }
 
     @Override
diff --git a/modules.defs b/modules.defs
index 1cb57f4..ec11808 100644
--- a/modules.defs
+++ b/modules.defs
@@ -232,6 +232,7 @@
     '//apps/t3:onos-apps-t3-oar',
     '//apps/simplefabric:onos-apps-simplefabric-oar',
     '//apps/mcast:onos-apps-mcast-oar',
+    '//apps/layout:onos-apps-layout-oar',
 ]
 
 PROTOCOL_APPS = [
diff --git a/tools/test/topos/access-gw.json b/tools/test/topos/access-gw.json
new file mode 100644
index 0000000..7268110
--- /dev/null
+++ b/tools/test/topos/access-gw.json
@@ -0,0 +1,28 @@
+{
+  "hosts" : {
+    "00:00:00:00:00:01/None" : {
+      "basic" : {
+        "name" : "GW-A1",
+        "roles" : [ "gateway" ], "gridX" : 0.0, "gridY" : 0.0, "locType" : "grid", "uiType" : "router"
+      }
+    },
+    "00:00:00:00:00:02/None" : {
+      "basic" : {
+        "name" : "GW-A2",
+        "roles" : [ "gateway" ], "gridX" : 0.0, "gridY" : 0.0, "locType" : "grid", "uiType" : "router"
+      }
+    },
+    "00:00:00:00:00:0D/None" : {
+      "basic" : {
+        "name" : "GW-B1",
+        "roles" : [ "gateway" ], "gridX" : 0.0, "gridY" : 0.0, "locType" : "grid", "uiType" : "router"
+      }
+    },
+    "00:00:00:00:00:0E/None" : {
+      "basic" : {
+        "name" : "GW-B2",
+        "roles" : [ "gateway" ], "gridX" : 0.0, "gridY" : 0.0, "locType" : "grid", "uiType" : "router"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/tools/test/topos/co-fo-access-null b/tools/test/topos/co-fo-access-null
new file mode 100755
index 0000000..3c07f5b
--- /dev/null
+++ b/tools/test/topos/co-fo-access-null
@@ -0,0 +1,156 @@
+#!/bin/bash
+# -----------------------------------------------------------------------------
+# Creates a hierarchical access spine-leaf fabric with large number of hosts
+# using null providers.
+#
+# Default setup as follows:
+#   2 primary spines (or more if needed, but this is typically not the case)
+#   2 aggregating spines per access headend
+#   ~3 access leaves per headend
+#   2 leaf pairs for connecting gateways and compute (services/caching/etc.)
+# -----------------------------------------------------------------------------
+
+function usage {
+    echo "usage: $(basename $0) [options] [onos-ip]"
+    echo ""
+    echo "Options:"
+    echo "  -s spines"
+    echo "  -l spineLinks"
+    echo "  -S serviceHosts"
+    echo "  -G gateways"
+    echo "  -f fieldOffices"
+    echo "  -a accessLeaves"
+    echo "  -A accessHosts"
+    exit 1
+}
+
+spines=2
+spineLinks=2
+serviceLeafGroups="A B"
+serviceHosts=10
+fieldOffices=3
+accessLeaves=3
+accessHosts=60
+gateways=2
+
+# Scan arguments for user/password or other options...
+while getopts s:l:a:f:A:S:G:?h o; do
+    case "$o" in
+        s) spines=$OPTARG;;
+        l) spineLinks=$OPTARG;;
+        a) accessLeaves=$OPTARG;;
+        f) fieldOffices=$OPTARG;;
+        A) accessHosts=$OPTARG;;
+        S) serviceHosts=$OPTARG;;
+        G) gateways=$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 hagPorts=accessLeaves+8      # derive hag ports from access leaves
+
+let totalHags=fieldOffices*spines
+let totalAccess=fieldOffices*accessLeaves
+
+let OPC=$OPTIND-1
+shift $OPC
+
+# Optional ONOS node IP
+node=${1:-$OCI}
+
+# Create the script of ONOS commands first and then execute it all at once.
+export CMDS="/tmp/access-onos.cmds"
+rm -f $CMDS 2>/dev/null
+
+function sim {
+    echo "$@" >> $CMDS
+}
+
+function y {
+    let p="${3:-400} * ($1 - 1) - (${3:-400} * ($2 - 1)) / 2 + ${4:-0}"
+    echo $p
+}
+
+# Create central office spines
+for spine in $(seq 1 $spines); do
+    sim "null-create-device switch Spine-${spine} ${spinePorts} 0 $(y $spine $spines) grid"
+done
+
+# Create 2 leaf pairs with dual links to the spines and a link between the pair
+for pair in $serviceLeafGroups; do
+    [ $pair = A ] && l1=1 || l1=3
+    [ $pair = A ] && l2=2 || l2=4
+    sim "null-create-device switch Leaf-${pair}1 ${leafPorts} -200 $(y $l1 4) grid"
+    sim "null-create-device switch Leaf-${pair}2 ${leafPorts} -200 $(y $l2 4) grid"
+    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 gateways attached to each leaf group; multi-homed to each leaf in the pair
+    [ $pair = A ] && pn=1 || pn=2
+    [ $pair = A ] && gwy=-800 || gwy=800
+    for gw in $(seq 1 $gateways); do
+        sim "null-create-host Leaf-${pair}1,Leaf-${pair}2 10.${pn}.0.${gw} $(y $gw $gateways 200 -200) ${gwy} grid"
+    done
+
+    # Create hosts for each leaf group; multi-homed to each leaf in the pair
+    [ $pair = A ] && offset=-400 || offset=400
+    for host in $(seq 1 $serviceHosts); do
+        sim "null-create-host Leaf-${pair}1,Leaf-${pair}2 10.${pn}.1.${host} -400 $(y $host $serviceHosts 60 $offset) grid"
+    done
+done
+
+# Indexes for HAG and access leaves across all field offices
+let iagg=1
+let iacc=1
+
+# Create field offices
+for field in $(seq $fieldOffices); do
+    # Create HAG spines for each office and connect it to central office spines
+    for spine in $(seq $spines); do
+        sim "null-create-device switch Spine-${field}-${spine} ${hagPorts} 200 $(y $iagg $totalHags) grid"
+        sim "null-create-link direct Spine-${spine} Spine-${field}-${spine}"
+        let iagg=iagg+1
+    done
+
+    # Create single access leafs with links to the spines
+    for access in $(seq $accessLeaves); do
+        sim "null-create-device switch Access-${field}-${access} ${accessPorts} 400 $(y $iacc $totalAccess) grid"
+        for spine in $(seq 1 $spines); do
+            sim "null-create-link direct Spine-${field}-${spine} Access-${field}-${access}"
+        done
+
+        # Create hosts for each access single leaf
+        sim "null-create-hosts Access-${field}-${access} 10.${field}${access}.1.*" $accessHosts 700 $(y $iacc $totalAccess) 6
+
+        let iacc=iacc+1
+    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/tools/test/topos/wipe-ui-annotations b/tools/test/topos/wipe-ui-annotations
new file mode 100755
index 0000000..b6cfd52
--- /dev/null
+++ b/tools/test/topos/wipe-ui-annotations
@@ -0,0 +1,16 @@
+#!/bin/bash
+# -----------------------------------------------------------------------------
+# Wipes out UI annotations from all devices and hosts.
+# -----------------------------------------------------------------------------
+
+aux=/tmp/wua.$$.json
+
+host=${1:-$OCI}
+
+onos $host netcfg devices | sed -E 's/("grid[XY]")( : )([-0-9.]*)/\1\20.0/' > $aux
+onos-netcfg $host $aux devices
+
+onos $host netcfg hosts | sed -E 's/("grid[XY]")( : )([-0-9.]*)/\1\20.0/' > $aux
+onos-netcfg $host $aux hosts
+
+rm -f $aux