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