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>