[WIP] Upgrade ONOS to karaf version 4.2.1

Change-Id: I7cd40c995bdf1c80f94b1895fb3344e32404c7fa
diff --git a/cli2/src/main/java/org/onosproject/cli2/AbstractChoicesCompleter.java b/cli2/src/main/java/org/onosproject/cli2/AbstractChoicesCompleter.java
new file mode 100644
index 0000000..7082ece
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/AbstractChoicesCompleter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2;
+
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Abstraction of a completer with preset choices.
+ */
+public abstract class AbstractChoicesCompleter extends AbstractCompleter {
+
+    protected abstract List<String> choices();
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        StringsCompleter delegate = new StringsCompleter();
+        SortedSet<String> strings = delegate.getStrings();
+        choices().forEach(strings::add);
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/AbstractCompleter.java b/cli2/src/main/java/org/onosproject/cli2/AbstractCompleter.java
new file mode 100644
index 0000000..3f10abc
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/AbstractCompleter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.impl.action.command.ArgumentCompleter;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+import java.util.List;
+
+/**
+ * Abstract argument completer.
+ */
+public abstract class AbstractCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        StringsCompleter delegate = new StringsCompleter();
+
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/AbstractShellCommand.java b/cli2/src/main/java/org/onosproject/cli2/AbstractShellCommand.java
new file mode 100644
index 0000000..0046e03
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/AbstractShellCommand.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.Action;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onlab.osgi.ServiceNotFoundException;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.CodecService;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.Annotations;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.net.DefaultAnnotations;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Base abstraction of Karaf shell commands.
+ */
+public abstract class AbstractShellCommand implements Action, CodecContext {
+
+    @Option(name = "-j", aliases = "--json", description = "Output JSON",
+            required = false, multiValued = false)
+    private boolean json = false;
+
+    /**
+     * Returns the reference to the implementation of the specified service.
+     *
+     * @param serviceClass service class
+     * @param <T>          type of service
+     * @return service implementation
+     * @throws org.onlab.osgi.ServiceNotFoundException if service is unavailable
+     */
+    public static <T> T get(Class<T> serviceClass) {
+        return DefaultServiceDirectory.getService(serviceClass);
+    }
+
+    /**
+     * Returns application ID for the CLI.
+     *
+     * @return command-line application identifier
+     */
+    protected ApplicationId appId() {
+        return get(CoreService.class)
+               .registerApplication("org.onosproject.cli");
+    }
+
+    /**
+     * Prints the arguments using the specified format.
+     *
+     * @param format format string; see {@link String#format}
+     * @param args   arguments
+     */
+    public void print(String format, Object... args) {
+        System.out.println(String.format(format, args));
+    }
+
+    /**
+     * Prints the arguments using the specified format to error stream.
+     *
+     * @param format format string; see {@link String#format}
+     * @param args   arguments
+     */
+    public void error(String format, Object... args) {
+        System.err.println(String.format(format, args));
+    }
+
+    /**
+     * Produces a string image of the specified key/value annotations.
+     *
+     * @param annotations key/value annotations
+     * @return string image with ", k1=v1, k2=v2, ..." pairs
+     */
+    public static String annotations(Annotations annotations) {
+        if (annotations == null) {
+            annotations = DefaultAnnotations.EMPTY;
+        }
+        StringBuilder sb = new StringBuilder();
+        Set<String> keys = new TreeSet<>(annotations.keys());
+        for (String key : keys) {
+            sb.append(", ").append(key).append('=').append(annotations.value(key));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Produces a string image of the specified key/value annotations.
+     * Excludes the keys in the given Set.
+     *
+     * @param annotations  key/value annotations
+     * @param excludedKeys keys not to add in the resulting string
+     * @return string image with ", k1=v1, k2=v2, ..." pairs
+     */
+    public static String annotations(Annotations annotations, Set<String> excludedKeys) {
+        StringBuilder sb = new StringBuilder();
+        Set<String> keys = new TreeSet<>(annotations.keys());
+        keys.removeAll(excludedKeys);
+        for (String key : keys) {
+            sb.append(", ").append(key).append('=').append(annotations.value(key));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Produces a JSON object from the specified key/value annotations.
+     *
+     * @param mapper ObjectMapper to use while converting to JSON
+     * @param annotations key/value annotations
+     * @return JSON object
+     */
+    public static ObjectNode annotations(ObjectMapper mapper, Annotations annotations) {
+        ObjectNode result = mapper.createObjectNode();
+        for (String key : annotations.keys()) {
+            result.put(key, annotations.value(key));
+        }
+        return result;
+    }
+
+    /**
+     * Indicates whether JSON format should be output.
+     *
+     * @return true if JSON is requested
+     */
+    protected boolean outputJson() {
+        return json;
+    }
+
+    @Override
+    public Object execute() throws Exception {
+        try {
+            doExecute();
+        } catch (ServiceNotFoundException e) {
+            error(e.getMessage());
+        }
+        return null;
+    }
+
+    protected void doExecute() throws Exception {
+        try {
+            execute();
+        } catch (ServiceNotFoundException e) {
+            error(e.getMessage());
+        }
+    }
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Override
+    public ObjectMapper mapper() {
+        return mapper;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> JsonCodec<T> codec(Class<T> entityClass) {
+        return get(CodecService.class).getCodec(entityClass);
+    }
+
+    @Override
+    public <T> T getService(Class<T> serviceClass) {
+        return get(serviceClass);
+    }
+
+    /**
+     * Generates a Json representation of an object.
+     *
+     * @param entity object to generate JSON for
+     * @param entityClass class to format with - this chooses which codec to use
+     * @param <T> Type of the object being formatted
+     * @return JSON object representation
+     */
+    public <T> ObjectNode jsonForEntity(T entity, Class<T> entityClass) {
+        return codec(entityClass).encode(entity, this);
+    }
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/CliComponent.java b/cli2/src/main/java/org/onosproject/cli2/CliComponent.java
new file mode 100644
index 0000000..e5af520
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/CliComponent.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2;
+
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.onosproject.core.CoreService;
+
+/**
+ * OSGI Component for the ONOS CLI.
+ */
+
+@Component(immediate = true)
+public class CliComponent {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Activate
+    public void activate() {
+        coreService
+            .registerApplication("org.onosproject.cli");
+    }
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/MastersListCommand.java b/cli2/src/main/java/org/onosproject/cli2/MastersListCommand.java
new file mode 100644
index 0000000..ae24484
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/MastersListCommand.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.collect.Lists;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.action.Command;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.utils.Comparators;
+
+import java.util.Collections;
+import java.util.List;
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Lists device mastership information.
+ */
+@Command(scope = "onos", name = "masters",
+         description = "Lists device mastership information")
+@Service
+public class MastersListCommand extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() {
+        ClusterService service = get(ClusterService.class);
+        MastershipService mastershipService = get(MastershipService.class);
+        DeviceService deviceService = get(DeviceService.class);
+        List<ControllerNode> nodes = newArrayList(service.getNodes());
+        Collections.sort(nodes, Comparators.NODE_COMPARATOR);
+
+        if (outputJson()) {
+            print("%s", json(service, mastershipService, nodes));
+        } else {
+            for (ControllerNode node : nodes) {
+                List<DeviceId> ids = Lists.newArrayList(mastershipService.getDevicesOf(node.id()));
+                ids.removeIf(did -> deviceService.getDevice(did) == null);
+                Collections.sort(ids, Comparators.ELEMENT_ID_COMPARATOR);
+                print("%s: %d devices", node.id(), ids.size());
+                for (DeviceId deviceId : ids) {
+                    print("  %s", deviceId);
+                }
+            }
+        }
+    }
+
+    // Produces JSON structure.
+    private JsonNode json(ClusterService service, MastershipService mastershipService,
+                          List<ControllerNode> nodes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (ControllerNode node : nodes) {
+            List<DeviceId> ids = Lists.newArrayList(mastershipService.getDevicesOf(node.id()));
+            result.add(mapper.createObjectNode()
+                               .put("id", node.id().toString())
+                               .put("size", ids.size())
+                               .set("devices", json(mapper, ids)));
+        }
+        return result;
+    }
+
+    /**
+     * Produces a JSON array containing the specified device identifiers.
+     *
+     * @param mapper object mapper
+     * @param ids    collection of device identifiers
+     * @return JSON array
+     */
+    public static JsonNode json(ObjectMapper mapper, Iterable<DeviceId> ids) {
+        ArrayNode result = mapper.createArrayNode();
+        for (DeviceId deviceId : ids) {
+            result.add(deviceId.toString());
+        }
+        return result;
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/NodeAddCommand.java b/cli2/src/main/java/org/onosproject/cli2/NodeAddCommand.java
new file mode 100644
index 0000000..99f94bb
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/NodeAddCommand.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ClusterAdminService;
+import org.onosproject.cluster.DefaultControllerNode;
+import org.onosproject.cluster.NodeId;
+
+/**
+ * Adds a new controller cluster node.
+ */
+@Service
+@Command(scope = "onos", name = "add-node",
+         description = "Adds a new controller cluster node")
+public class NodeAddCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "nodeId", description = "Node ID",
+              required = true, multiValued = false)
+    String nodeId = null;
+
+    @Argument(index = 1, name = "ip", description = "Node IP address",
+              required = true, multiValued = false)
+    String ip = null;
+
+    @Argument(index = 2, name = "tcpPort", description = "Node TCP listen port",
+              required = false, multiValued = false)
+    int tcpPort = DefaultControllerNode.DEFAULT_PORT;
+
+    @Override
+    protected void doExecute() {
+        ClusterAdminService service = get(ClusterAdminService.class);
+        service.addNode(new NodeId(nodeId), IpAddress.valueOf(ip), tcpPort);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/NodeIdCompleter.java b/cli2/src/main/java/org/onosproject/cli2/NodeIdCompleter.java
new file mode 100644
index 0000000..0438f76
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/NodeIdCompleter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Node ID completer.
+ */
+@Service
+public class NodeIdCompleter implements Completer {
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        // Fetch our service and feed it's offerings to the string completer
+        ClusterService service = AbstractShellCommand.get(ClusterService.class);
+        Iterator<ControllerNode> it = service.getNodes().iterator();
+        SortedSet<String> strings = delegate.getStrings();
+        while (it.hasNext()) {
+            strings.add(it.next().id().toString());
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/NodeRemoveCommand.java b/cli2/src/main/java/org/onosproject/cli2/NodeRemoveCommand.java
new file mode 100644
index 0000000..80dd25d
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/NodeRemoveCommand.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cluster.ClusterAdminService;
+import org.onosproject.cluster.NodeId;
+
+/**
+ * Removes a controller cluster node.
+ */
+@Service
+@Command(scope = "onos", name = "remove-node",
+         description = "Removes a new controller cluster node")
+public class NodeRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "nodeId", description = "Node ID",
+              required = true, multiValued = false)
+    @Completion(NodeIdCompleter.class)
+    String nodeId = null;
+
+    @Override
+    protected void doExecute() {
+        ClusterAdminService service = get(ClusterAdminService.class);
+        service.removeNode(new NodeId(nodeId));
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/NodesListCommand.java b/cli2/src/main/java/org/onosproject/cli2/NodesListCommand.java
new file mode 100644
index 0000000..fbd6778
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/NodesListCommand.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cluster.ClusterAdminService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.core.Version;
+import org.onosproject.utils.Comparators;
+
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+/**
+ * Lists all controller cluster nodes.
+ */
+@Service
+@Command(scope = "onos", name = "nodes",
+        description = "Lists all controller cluster nodes")
+public class NodesListCommand extends AbstractShellCommand {
+
+    private static final String FMT = "id=%s, address=%s:%s, state=%s, version=%s, updated=%s %s";
+
+    @Override
+    protected void doExecute() {
+        ClusterAdminService service = get(ClusterAdminService.class);
+        List<ControllerNode> nodes = newArrayList(service.getNodes());
+        Collections.sort(nodes, Comparators.NODE_COMPARATOR);
+        if (outputJson()) {
+            print("%s", json(service, nodes));
+        } else {
+            ControllerNode self = service.getLocalNode();
+            for (ControllerNode node : nodes) {
+                String timeAgo = service.localStatus(node.id());
+                Version version = service.getVersion(node.id());
+                print(FMT, node.id(), node.ip(), node.tcpPort(),
+                        service.getState(node.id()),
+                        version == null ? "unknown" : version,
+                        timeAgo,
+                        node.equals(self) ? "*" : "");
+            }
+        }
+    }
+
+    // Produces JSON structure.
+    private JsonNode json(ClusterAdminService service, List<ControllerNode> nodes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        ControllerNode self = service.getLocalNode();
+        for (ControllerNode node : nodes) {
+            ControllerNode.State nodeState = service.getState(node.id());
+            Version nodeVersion = service.getVersion(node.id());
+            ObjectNode newNode = mapper.createObjectNode()
+                    .put("id", node.id().toString())
+                    .put("ip", node.ip().toString())
+                    .put("tcpPort", node.tcpPort())
+                    .put("self", node.equals(self));
+            if (nodeState != null) {
+                newNode.put("state", nodeState.toString());
+            }
+            if (nodeVersion != null) {
+                newNode.put("version", nodeVersion.toString());
+            }
+            result.add(newNode);
+        }
+        return result;
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/SummaryCommand.java b/cli2/src/main/java/org/onosproject/cli2/SummaryCommand.java
new file mode 100644
index 0000000..86e75be
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/SummaryCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2014-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.cli2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ClusterMetadataService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.Version;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.topology.TopologyService;
+
+import java.util.Set;
+
+/**
+ * Provides summary of ONOS model.
+ */
+@Command(scope = "onos", name = "summary",
+         description = "Provides summary of ONOS model")
+@Service
+public class SummaryCommand extends AbstractShellCommand {
+
+    /**
+     * Count the active ONOS controller nodes.
+     *
+     * @param nodes set of all of the controller nodes in the cluster
+     * @return count of active nodes
+     */
+    private long activeNodes(Set<ControllerNode> nodes) {
+        ClusterService clusterService = get(ClusterService.class);
+
+        return nodes.stream()
+                .map(node -> clusterService.getState(node.id()))
+                .filter(nodeState -> nodeState.isActive())
+                .count();
+    }
+
+    @Override
+    protected void doExecute() {
+        IpAddress nodeIp = get(ClusterService.class).getLocalNode().ip();
+        Version version = get(CoreService.class).version();
+        long numNodes = activeNodes(get(ClusterService.class).getNodes());
+        int numDevices = get(DeviceService.class).getDeviceCount();
+        int numLinks = get(LinkService.class).getLinkCount();
+        int numHosts = get(HostService.class).getHostCount();
+        int numScc = get(TopologyService.class).currentTopology().clusterCount();
+        int numFlows = get(FlowRuleService.class).getFlowRuleCount();
+        long numIntents = get(IntentService.class).getIntentCount();
+        String clusterId = get(ClusterMetadataService.class).getClusterMetadata().getName();
+
+        if (outputJson()) {
+            print("%s", new ObjectMapper().createObjectNode()
+                    .put("node", nodeIp.toString())
+                    .put("version", version.toString())
+                    .put("clusterId", clusterId)
+                    .put("nodes", numNodes)
+                    .put("devices", numDevices)
+                    .put("links", numLinks)
+                    .put("hosts", numHosts)
+                    .put("SCC(s)", numScc)
+                    .put("flows", numFlows)
+                    .put("intents", numIntents));
+        } else {
+            print("node=%s, version=%s clusterId=%s", nodeIp, version, clusterId);
+            print("nodes=%d, devices=%d, links=%d, hosts=%d, SCC(s)=%s, flows=%d, intents=%d",
+                  numNodes, numDevices, numLinks, numHosts, numScc, numFlows, numIntents);
+        }
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommand.java b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommand.java
new file mode 100644
index 0000000..35afe52
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommand.java
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2.app;
+
+import com.google.common.io.ByteStreams;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.app.ApplicationAdminService;
+import org.onosproject.cli2.AbstractShellCommand;
+import org.onosproject.core.Application;
+import org.onosproject.core.ApplicationId;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Manages application inventory.
+ */
+@Service
+@Command(scope = "onos", name = "app",
+        description = "Manages application inventory")
+public class ApplicationCommand extends AbstractShellCommand {
+
+    static final String INSTALL = "install";
+    static final String UNINSTALL = "uninstall";
+    static final String ACTIVATE = "activate";
+    static final String DEACTIVATE = "deactivate";
+    static final String DOWNLOAD = "download";
+
+    @Argument(index = 0, name = "command",
+            description = "Command name (install|activate|deactivate|uninstall|download)",
+            required = true, multiValued = false)
+    @Completion(ApplicationCommandCompleter.class)
+    String command = null;
+
+    @Argument(index = 1, name = "names", description = "Application name(s) or URL(s)",
+            required = true, multiValued = true)
+    @Completion(ApplicationNameCompleter.class)
+    String[] names = null;
+
+    @Override
+    protected void doExecute() {
+        ApplicationAdminService service = get(ApplicationAdminService.class);
+        if (command.equals(INSTALL)) {
+            for (String name : names) {
+                if (!installApp(service, name)) {
+                    return;
+                }
+            }
+
+        } else if (command.equals(DOWNLOAD)) {
+            for (String name : names) {
+                if (!downloadApp(service, name)) {
+                    return;
+                }
+            }
+        } else {
+                for (String name : names) {
+                    if (!manageApp(service, name)) {
+                        return;
+                    }
+                }
+            }
+            return;
+        }
+
+    // Installs the application from input of the specified URL
+    private boolean installApp(ApplicationAdminService service, String url) {
+        try {
+            if ("-".equals(url)) {
+                service.install(System.in);
+            } else {
+                service.install(new URL(url).openStream());
+            }
+        } catch (IOException e) {
+            error("Unable to get URL: %s", url);
+            return false;
+        }
+        return true;
+    }
+
+    // Downloads the application bits to the standard output.
+    private boolean downloadApp(ApplicationAdminService service, String name) {
+        try {
+            ByteStreams.copy(service.getApplicationArchive(service.getId(name)),
+                             System.out);
+        } catch (IOException e) {
+            error("Unable to download bits for application %s", name);
+            return false;
+        }
+        return true;
+    }
+
+    // Manages the specified application.
+    private boolean manageApp(ApplicationAdminService service, String name) {
+        ApplicationId appId = service.getId(name);
+        if (appId == null) {
+            List<Application> matches = service.getApplications().stream()
+                .filter(app -> app.id().name().matches(".*\\." + name + "$"))
+                .collect(Collectors.toList());
+
+            if (matches.size() == 1) {
+                // Found match
+                appId = matches.iterator().next().id();
+            } else if (!matches.isEmpty()) {
+                print("Did you mean one of: %s",
+                      matches.stream()
+                          .map(Application::id)
+                          .map(ApplicationId::name)
+                          .collect(Collectors.toList()));
+                return false;
+            }
+        }
+        if (appId == null) {
+            print("No such application: %s", name);
+            return false;
+        }
+
+        String action;
+        if (command.equals(UNINSTALL)) {
+            service.uninstall(appId);
+            action = "Uninstalled";
+        } else if (command.equals(ACTIVATE)) {
+            service.activate(appId);
+            action = "Activated";
+        } else if (command.equals(DEACTIVATE)) {
+            service.deactivate(appId);
+            action = "Deactivated";
+        } else {
+            print("Unsupported command: %s", command);
+            return false;
+        }
+        print("%s %s", action, appId.name());
+        return true;
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommandCompleter.java b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommandCompleter.java
new file mode 100644
index 0000000..f4d3782
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationCommandCompleter.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2.app;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli2.AbstractChoicesCompleter;
+
+import java.util.List;
+
+import static org.onosproject.cli2.app.ApplicationCommand.*;
+
+/**
+ * Application command completer.
+ */
+@Service
+public class ApplicationCommandCompleter extends AbstractChoicesCompleter {
+    @Override
+    public List<String> choices() {
+        return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE, DOWNLOAD);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/app/ApplicationNameCompleter.java b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationNameCompleter.java
new file mode 100644
index 0000000..5f002ff
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/app/ApplicationNameCompleter.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.onosproject.cli2.app;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.app.ApplicationService;
+import org.onosproject.app.ApplicationState;
+import org.onosproject.cli2.AbstractCompleter;
+import org.onosproject.core.Application;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.SortedSet;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+import static org.onosproject.app.ApplicationState.ACTIVE;
+import static org.onosproject.app.ApplicationState.INSTALLED;
+import static org.onosproject.cli2.AbstractShellCommand.get;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Application name completer.
+ */
+@Service
+public class ApplicationNameCompleter extends AbstractCompleter {
+    private static final Logger log = getLogger(ApplicationNameCompleter.class);
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        // Command name is the second argument.
+        String cmd = commandLine.getArguments()[1];
+        log.info("Command is {}", cmd);
+
+        // Grab apps already on the command (to prevent tab-completed duplicates)
+        // FIXME: This does not work.
+//        final Set previousApps;
+//        if (list.getArguments().length > 2) {
+//            previousApps = Sets.newHashSet(
+//                    Arrays.copyOfRange(list.getArguments(), 2, list.getArguments().length));
+//        } else {
+//            previousApps = Collections.emptySet();
+//        }
+
+        // Fetch our service and feed it's offerings to the string completer
+        ApplicationService service = get(ApplicationService.class);
+        Iterator<Application> it = service.getApplications().iterator();
+        SortedSet<String> strings = delegate.getStrings();
+        int c = 0;
+        log.info("Processing apps");
+        while (it.hasNext()) {
+            Application app = it.next();
+            log.info("app #{} is {}", c, app.id().name());
+            c++;
+            ApplicationState state = service.getState(app.id());
+//            if (previousApps.contains(app.id().name())) {
+//                continue;
+//            }
+            if ("uninstall".equals(cmd) || "download".equals(cmd) ||
+                    ("activate".equals(cmd) && state == INSTALLED) ||
+                    ("deactivate".equals(cmd) && state == ACTIVE)) {
+                strings.add(app.id().name());
+            }
+        }
+
+        // add unique suffix to candidates, if user has something in buffer
+        log.info("Command line buffer {} position {}", commandLine.getBuffer(), commandLine.getBufferPosition());
+        if (!commandLine.getBuffer().endsWith(cmd)) {
+            List<String> suffixCandidates = strings.stream()
+                    // remove onos common prefix
+                    .map(full -> full.replaceFirst("org\\.onosproject\\.", ""))
+                    // a.b.c -> [c, b.c, a.b.c]
+                    .flatMap(appName -> {
+                        List<String> suffixes = new ArrayList<>();
+                        Deque<String> frags = new ArrayDeque<>();
+                        // a.b.c -> [c, b, a] -> [c, b.c, a.b.c]
+                        Lists.reverse(asList(appName.split("\\."))).forEach(frag -> {
+                            frags.addFirst(frag);
+                            suffixes.add(frags.stream().collect(Collectors.joining(".")));
+                        });
+                        return suffixes.stream();
+                    })
+                    // convert to occurrence map
+                    .collect(Collectors.groupingBy(e -> e, Collectors.counting()))
+                    .entrySet().stream()
+                    // only accept unique suffix
+                    .filter(e -> e.getValue() == 1L)
+                    .map(Entry::getKey)
+                    .collect(Collectors.toList());
+
+            delegate.getStrings().addAll(suffixCandidates);
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/cli2/src/main/java/org/onosproject/cli2/package-info.java b/cli2/src/main/java/org/onosproject/cli2/package-info.java
new file mode 100644
index 0000000..2531a52
--- /dev/null
+++ b/cli2/src/main/java/org/onosproject/cli2/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014-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.
+ */
+
+/**
+ * Administrative console command-line extensions.
+ */
+package org.onosproject.cli;