Detangling incubator: virtual nets, tunnels, resource labels, oh my

- virtual networking moved to /apps/virtual; with CLI & REST API
- tunnels and labels moved to /apps/tunnel; with CLI & REST API; UI disabled for now
- protobuf/models moved to /core/protobuf/models
- defunct grpc/rpc registry stuff left under /graveyard
- compile dependencies on /incubator moved to respective modules for compilation
- run-time dependencies will need to be re-tested for dependent apps

- /graveyard will be removed in not-too-distant future

Change-Id: I0a0b995c635487edcf95a352f50dd162186b0b39
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantAddCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantAddCommand.java
new file mode 100644
index 0000000..a449afb
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantAddCommand.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+/**
+ * Creates a new virtual network tenant.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-add-tenant",
+        description = "Creates a new virtual network tenant.")
+
+public class TenantAddCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "id", description = "Tenant ID",
+            required = true, multiValued = false)
+    String id = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.registerTenantId(TenantId.tenantId(id));
+        print("Tenant successfully added.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantCompleter.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantCompleter.java
new file mode 100644
index 0000000..29a625c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantCompleter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Tenant Id completer.
+ */
+@Service
+public class TenantCompleter 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
+        VirtualNetworkAdminService service = AbstractShellCommand.get(VirtualNetworkAdminService.class);
+
+        SortedSet<String> strings = delegate.getStrings();
+
+        for (TenantId tenantId : service.getTenantIds()) {
+            strings.add(tenantId.id());
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantListCommand.java
new file mode 100644
index 0000000..6590090
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantListCommand.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.utils.Comparators;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Lists all tenants.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-tenants",
+        description = "Lists all virtual network tenants.")
+public class TenantListCommand extends AbstractShellCommand {
+
+    private static final String FMT_TENANT = "tenantId=%s";
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        List<TenantId> tenants = new ArrayList<>();
+        tenants.addAll(service.getTenantIds());
+        Collections.sort(tenants, Comparators.TENANT_ID_COMPARATOR);
+
+        tenants.forEach(this::printTenant);
+    }
+
+    private void printTenant(TenantId tenantId) {
+        print(FMT_TENANT, tenantId.id());
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantRemoveCommand.java
new file mode 100644
index 0000000..2877f59
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/TenantRemoveCommand.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+/**
+ * Creates a new virtual network tenant.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-remove-tenant",
+        description = "Removes a virtual network tenant.")
+
+public class TenantRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "id", description = "Tenant ID",
+            required = true, multiValued = false)
+    @Completion(TenantCompleter.class)
+    String id = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.unregisterTenantId(TenantId.tenantId(id));
+        print("Tenant successfully removed.");
+    }
+}
\ No newline at end of file
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCompleter.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCompleter.java
new file mode 100644
index 0000000..410b483
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCompleter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import static org.onlab.osgi.DefaultServiceDirectory.getService;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractChoicesCompleter;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Virtual device completer.
+ *
+ * Assumes the first argument which can be parsed to a number is network id.
+ */
+@Service
+public class VirtualDeviceCompleter extends AbstractChoicesCompleter {
+    @Override
+    protected List<String> choices() {
+        //parse argument list for network id
+        String[] argsArray = commandLine.getArguments();
+        for (String str : argsArray) {
+            if (str.matches("[0-9]+")) {
+                long networkId = Long.valueOf(str);
+                return getSortedVirtualDevices(networkId).stream()
+                        .map(virtualDevice -> virtualDevice.id().toString())
+                        .collect(Collectors.toList());
+            }
+        }
+        return Collections.singletonList("Missing network id");
+    }
+
+    /**
+     * Returns the list of virtual devices sorted using the network identifier.
+     *
+     * @param networkId network id
+     * @return sorted virtual device list
+     */
+    private List<VirtualDevice> getSortedVirtualDevices(long networkId) {
+        VirtualNetworkService service = getService(VirtualNetworkService.class);
+
+        List<VirtualDevice> virtualDevices = new ArrayList<>();
+        virtualDevices.addAll(service.getVirtualDevices(NetworkId.networkId(networkId)));
+        Collections.sort(virtualDevices, Comparators.VIRTUAL_DEVICE_COMPARATOR);
+        return virtualDevices;
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCreateCommand.java
new file mode 100644
index 0000000..187c0e1
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceCreateCommand.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Creates a new virtual device.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-create-device",
+        description = "Creates a new virtual device in a network.")
+public class VirtualDeviceCreateCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Device ID",
+            required = true, multiValued = false)
+    String deviceId = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.createVirtualDevice(NetworkId.networkId(networkId), DeviceId.deviceId(deviceId));
+        print("Virtual device successfully created.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceListCommand.java
new file mode 100644
index 0000000..4a4acfc
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceListCommand.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Lists all virtual devices for the network ID.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-devices",
+        description = "Lists all virtual devices in a virtual network.")
+public class VirtualDeviceListCommand extends AbstractShellCommand {
+
+    private static final String FMT_VIRTUAL_DEVICE =
+            "deviceId=%s";
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Override
+    protected void doExecute() {
+
+        getSortedVirtualDevices().forEach(this::printVirtualDevice);
+    }
+
+    /**
+     * Returns the list of virtual devices sorted using the device identifier.
+     *
+     * @return sorted virtual device list
+     */
+    private List<VirtualDevice> getSortedVirtualDevices() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+
+        List<VirtualDevice> virtualDevices = new ArrayList<>();
+        virtualDevices.addAll(service.getVirtualDevices(NetworkId.networkId(networkId)));
+        Collections.sort(virtualDevices, Comparators.VIRTUAL_DEVICE_COMPARATOR);
+        return virtualDevices;
+    }
+
+    /**
+     * Prints out each virtual device.
+     *
+     * @param virtualDevice virtual device
+     */
+    private void printVirtualDevice(VirtualDevice virtualDevice) {
+        print(FMT_VIRTUAL_DEVICE, virtualDevice.id());
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceRemoveCommand.java
new file mode 100644
index 0000000..f6213c7
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualDeviceRemoveCommand.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.DeviceId;
+
+/**
+ * Removes a virtual device.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-remove-device",
+        description = "Removes a virtual device.")
+public class VirtualDeviceRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Device ID",
+            required = true, multiValued = false)
+    @Completion(DeviceIdCompleter.class)
+    String deviceId = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.removeVirtualDevice(NetworkId.networkId(networkId), DeviceId.deviceId(deviceId));
+        print("Virtual device successfully removed.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualFlowsListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualFlowsListCommand.java
new file mode 100644
index 0000000..05da804
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualFlowsListCommand.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2017-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.incubator.net.virtual.cli;
+
+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.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.apache.karaf.shell.api.action.Option;
+import org.onlab.util.StringFilter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cli.PlaceholderCompleter;
+import org.onosproject.cli.net.FlowRuleStatusCompleter;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowEntry.FlowEntryState;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.utils.Comparators;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+
+/**
+ * Lists all currently-known flows.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-flows",
+         description = "Lists all currently-known flows for a virtual network.")
+public class VirtualFlowsListCommand extends AbstractShellCommand {
+
+    private static final Predicate<FlowEntry> TRUE_PREDICATE = f -> true;
+
+    public static final String ANY = "any";
+
+    private static final String LONG_FORMAT = "    id=%s, state=%s, bytes=%s, "
+            + "packets=%s, duration=%s, liveType=%s, priority=%s, tableId=%s, appId=%s, "
+            + "payLoad=%s, selector=%s, treatment=%s";
+
+    private static final String SHORT_FORMAT = "    %s, bytes=%s, packets=%s, "
+            + "table=%s, priority=%s, selector=%s, treatment=%s";
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "state", description = "Flow Rule state",
+            required = false, multiValued = false)
+    @Completion(FlowRuleStatusCompleter.class)
+    String state = null;
+
+    @Argument(index = 2, name = "uri", description = "Device ID",
+              required = false, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String uri = null;
+
+    @Argument(index = 3, name = "table", description = "Table ID",
+            required = false, multiValued = false)
+    @Completion(PlaceholderCompleter.class)
+    String table = null;
+
+    @Option(name = "-s", aliases = "--short",
+            description = "Print more succinct output for each flow",
+            required = false, multiValued = false)
+    private boolean shortOutput = false;
+
+    @Option(name = "-c", aliases = "--count",
+            description = "Print flow count only",
+            required = false, multiValued = false)
+    private boolean countOnly = false;
+
+    @Option(name = "-f", aliases = "--filter",
+            description = "Filter flows by specific key",
+            required = false, multiValued = true)
+    private List<String> filter = new ArrayList<>();
+
+    private Predicate<FlowEntry> predicate = TRUE_PREDICATE;
+
+    private StringFilter contentFilter;
+
+    @Override
+    protected void doExecute() {
+        CoreService coreService = get(CoreService.class);
+
+        VirtualNetworkService vnetservice = get(VirtualNetworkService.class);
+        DeviceService deviceService = vnetservice.get(NetworkId.networkId(networkId),
+                                                      DeviceService.class);
+        FlowRuleService service = vnetservice.get(NetworkId.networkId(networkId),
+                                                  FlowRuleService.class);
+        contentFilter = new StringFilter(filter, StringFilter.Strategy.AND);
+
+        compilePredicate();
+
+        SortedMap<Device, List<FlowEntry>> flows = getSortedFlows(deviceService, service);
+
+        if (outputJson()) {
+            print("%s", json(flows.keySet(), flows));
+        } else {
+            flows.forEach((device, flow) -> printFlows(device, flow, coreService));
+        }
+    }
+
+    /**
+     * Produces a JSON array of flows grouped by the each device.
+     *
+     * @param devices     collection of devices to group flow by
+     * @param flows       collection of flows per each device
+     * @return JSON array
+     */
+    private JsonNode json(Iterable<Device> devices,
+                          Map<Device, List<FlowEntry>> flows) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        for (Device device : devices) {
+            result.add(json(mapper, device, flows.get(device)));
+        }
+        return result;
+    }
+
+    /**
+     * Compiles a predicate to find matching flows based on the command
+     * arguments.
+     */
+    private void compilePredicate() {
+        if (state != null && !state.equals(ANY)) {
+            final FlowEntryState feState = FlowEntryState.valueOf(state.toUpperCase());
+            predicate = predicate.and(f -> f.state().equals(feState));
+        }
+
+        if (table != null) {
+            final int tableId = Integer.parseInt(table);
+            predicate = predicate.and(f -> f.tableId() == tableId);
+        }
+    }
+
+    // Produces JSON object with the flows of the given device.
+    private ObjectNode json(ObjectMapper mapper,
+                            Device device, List<FlowEntry> flows) {
+        ObjectNode result = mapper.createObjectNode();
+        ArrayNode array = mapper.createArrayNode();
+
+        flows.forEach(flow -> array.add(jsonForEntity(flow, FlowEntry.class)));
+
+        result.put("device", device.id().toString())
+                .put("flowCount", flows.size())
+                .set("flows", array);
+        return result;
+    }
+
+    /**
+     * Returns the list of devices sorted using the device ID URIs.
+     *
+     * @param deviceService device service
+     * @param service flow rule service
+     * @return sorted device list
+     */
+    protected SortedMap<Device, List<FlowEntry>> getSortedFlows(DeviceService deviceService,
+                                                          FlowRuleService service) {
+        SortedMap<Device, List<FlowEntry>> flows = new TreeMap<>(Comparators.ELEMENT_COMPARATOR);
+        List<FlowEntry> rules;
+
+        Iterable<Device> devices = null;
+        if (uri == null) {
+            devices = deviceService.getDevices();
+        } else {
+            Device dev = deviceService.getDevice(DeviceId.deviceId(uri));
+            devices = (dev == null) ? deviceService.getDevices()
+                                    : Collections.singletonList(dev);
+        }
+
+        for (Device d : devices) {
+            if (predicate.equals(TRUE_PREDICATE)) {
+                rules = newArrayList(service.getFlowEntries(d.id()));
+            } else {
+                rules = newArrayList();
+                for (FlowEntry f : service.getFlowEntries(d.id())) {
+                    if (predicate.test(f)) {
+                        rules.add(f);
+                    }
+                }
+            }
+            rules.sort(Comparators.FLOW_RULE_COMPARATOR);
+
+            flows.put(d, rules);
+        }
+        return flows;
+    }
+
+    /**
+     * Prints flows.
+     *
+     * @param d     the device
+     * @param flows the set of flows for that device
+     * @param coreService core system service
+     */
+    protected void printFlows(Device d, List<FlowEntry> flows,
+                              CoreService coreService) {
+        List<FlowEntry> filteredFlows = flows.stream().
+                filter(f -> contentFilter.filter(f)).collect(Collectors.toList());
+        boolean empty = filteredFlows == null || filteredFlows.isEmpty();
+        print("deviceId=%s, flowRuleCount=%d", d.id(), empty ? 0 : filteredFlows.size());
+        if (empty || countOnly) {
+            return;
+        }
+
+        for (FlowEntry f : filteredFlows) {
+            if (shortOutput) {
+                print(SHORT_FORMAT, f.state(), f.bytes(), f.packets(),
+                        f.tableId(), f.priority(), f.selector().criteria(),
+                        printTreatment(f.treatment()));
+            } else {
+                ApplicationId appId = coreService.getAppId(f.appId());
+                print(LONG_FORMAT, Long.toHexString(f.id().value()), f.state(),
+                        f.bytes(), f.packets(), f.life(), f.liveType(), f.priority(), f.tableId(),
+                        appId != null ? appId.name() : "<none>",
+                        f.payLoad() == null ? null : Arrays.toString(f.payLoad().payLoad()),
+                        f.selector().criteria(), f.treatment());
+            }
+        }
+    }
+
+    private String printTreatment(TrafficTreatment treatment) {
+        final String delimiter = ", ";
+        StringBuilder builder = new StringBuilder("[");
+        if (!treatment.immediate().isEmpty()) {
+            builder.append("immediate=" + treatment.immediate() + delimiter);
+        }
+        if (!treatment.deferred().isEmpty()) {
+            builder.append("deferred=" + treatment.deferred() + delimiter);
+        }
+        if (treatment.clearedDeferred()) {
+            builder.append("clearDeferred" + delimiter);
+        }
+        if (treatment.tableTransition() != null) {
+            builder.append("transition=" + treatment.tableTransition() + delimiter);
+        }
+        if (treatment.metered() != null) {
+            builder.append("meter=" + treatment.metered() + delimiter);
+        }
+        if (treatment.writeMetadata() != null) {
+            builder.append("metadata=" + treatment.writeMetadata() + delimiter);
+        }
+        // Chop off last delimiter
+        builder.replace(builder.length() - delimiter.length(), builder.length(), "");
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCompleter.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCompleter.java
new file mode 100644
index 0000000..c8f6c44
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCompleter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import org.onosproject.cli.AbstractChoicesCompleter;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.onlab.osgi.DefaultServiceDirectory.getService;
+
+/**
+ * Virtual host completer.
+ *
+ * Assumes the first argument which can be parsed to a number is network id.
+ */
+public class VirtualHostCompleter extends AbstractChoicesCompleter {
+    @Override
+    protected List<String> choices() {
+        //parse argument list for network id
+        String[] argsArray = commandLine.getArguments();
+        for (String str : argsArray) {
+            if (str.matches("[0-9]+")) {
+                long networkId = Long.valueOf(str);
+                return getSortedVirtualHosts(networkId).stream()
+                        .map(virtualHost -> virtualHost.id().toString())
+                        .collect(Collectors.toList());
+            }
+        }
+        return Collections.singletonList("Missing network id");
+    }
+
+    /**
+     * Returns the list of virtual hosts sorted using the host identifier.
+     *
+     * @param networkId network id
+     * @return virtual host list
+     */
+    private List<VirtualHost> getSortedVirtualHosts(long networkId) {
+        VirtualNetworkService service = getService(VirtualNetworkService.class);
+
+        List<VirtualHost> virtualHosts = new ArrayList<>();
+        virtualHosts.addAll(service.getVirtualHosts(NetworkId.networkId(networkId)));
+        return virtualHosts;
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCreateCommand.java
new file mode 100644
index 0000000..da198b1
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostCreateCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.apache.karaf.shell.api.action.Option;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Creates a new virtual host.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-create-host",
+        description = "Creates a new virtual host in a network.")
+public class VirtualHostCreateCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "mac", description = "Mac address",
+            required = true, multiValued = false)
+    String mac = null;
+
+    @Argument(index = 2, name = "vlan", description = "Vlan",
+            required = true, multiValued = false)
+    short vlan;
+
+    @Argument(index = 3, name = "hostLocationDeviceId", description = "Host location device ID",
+            required = true, multiValued = false)
+    String hostLocationDeviceId;
+
+    @Argument(index = 4, name = "hostLocationPortNumber", description = "Host location port number",
+            required = true, multiValued = false)
+    long hostLocationPortNumber;
+
+    // ip addresses
+    @Option(name = "--hostIp", description = "Host IP addresses.  Can be specified multiple times.",
+            required = false, multiValued = true)
+    protected String[] hostIpStrings;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+
+        Set<IpAddress> hostIps = new HashSet<>();
+        if (hostIpStrings != null) {
+            Arrays.stream(hostIpStrings).forEach(s -> hostIps.add(IpAddress.valueOf(s)));
+        }
+        HostLocation hostLocation = new HostLocation(DeviceId.deviceId(hostLocationDeviceId),
+                                                     PortNumber.portNumber(hostLocationPortNumber),
+                                                     System.currentTimeMillis());
+        MacAddress macAddress = MacAddress.valueOf(mac);
+        VlanId vlanId = VlanId.vlanId(vlan);
+        service.createVirtualHost(NetworkId.networkId(networkId),
+                                  HostId.hostId(macAddress, vlanId), macAddress, vlanId,
+                                  hostLocation, hostIps);
+        print("Virtual host successfully created.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostListCommand.java
new file mode 100644
index 0000000..38533ee
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostListCommand.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Lists all virtual hosts for the network ID.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-hosts",
+        description = "Lists all virtual hosts in a virtual network.")
+public class VirtualHostListCommand extends AbstractShellCommand {
+
+    private static final String FMT_VIRTUAL_HOST =
+            "id=%s, mac=%s, vlan=%s, location=%s, ips=%s";
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Override
+    protected void doExecute() {
+        getSortedVirtualHosts().forEach(this::printVirtualHost);
+    }
+
+    /**
+     * Returns the list of virtual hosts sorted using the device identifier.
+     *
+     * @return virtual host list
+     */
+    private List<VirtualHost> getSortedVirtualHosts() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+
+        List<VirtualHost> virtualHosts = new ArrayList<>();
+        virtualHosts.addAll(service.getVirtualHosts(NetworkId.networkId(networkId)));
+        return virtualHosts;
+    }
+
+    /**
+     * Prints out each virtual host.
+     *
+     * @param virtualHost virtual host
+     */
+    private void printVirtualHost(VirtualHost virtualHost) {
+        print(FMT_VIRTUAL_HOST, virtualHost.id().toString(), virtualHost.mac().toString(),
+              virtualHost.vlan().toString(), virtualHost.location().toString(),
+              virtualHost.ipAddresses().toString());
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostRemoveCommand.java
new file mode 100644
index 0000000..329e4a3
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualHostRemoveCommand.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.HostId;
+//import org.onosproject.net.HostId;
+
+/**
+ * Removes a virtual host.
+ */
+
+@Service
+@Command(scope = "onos", name = "vnet-remove-host",
+        description = "Removes a virtual host.")
+public class VirtualHostRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "id", description = "Host ID",
+              required = true, multiValued = false)
+    @Completion(VirtualHostCompleter.class)
+    String id = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.removeVirtualHost(NetworkId.networkId(networkId), HostId.hostId(id));
+        print("Virtual host successfully removed.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkCreateCommand.java
new file mode 100644
index 0000000..4bc6ea5
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkCreateCommand.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.apache.karaf.shell.api.action.Option;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Creates a new virtual link.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-create-link",
+        description = "Creates a new virtual link in a network.")
+public class VirtualLinkCreateCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "srcDeviceId", description = "Source device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String srcDeviceId = null;
+
+    @Argument(index = 2, name = "srcPortNum", description = "Source port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer srcPortNum = null;
+
+    @Argument(index = 3, name = "dstDeviceId", description = "Destination device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String dstDeviceId = null;
+
+    @Argument(index = 4, name = "dstPortNum", description = "Destination port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer dstPortNum = null;
+
+    @Option(name = "-b", aliases = "--bidirectional",
+            description = "If this argument is passed in then the virtual link created will be bidirectional, " +
+                    "otherwise the link will be unidirectional.",
+            required = false, multiValued = false)
+    boolean bidirectional = false;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        ConnectPoint src = new ConnectPoint(DeviceId.deviceId(srcDeviceId), PortNumber.portNumber(srcPortNum));
+        ConnectPoint dst = new ConnectPoint(DeviceId.deviceId(dstDeviceId), PortNumber.portNumber(dstPortNum));
+
+        service.createVirtualLink(NetworkId.networkId(networkId), src, dst);
+        if (bidirectional) {
+            service.createVirtualLink(NetworkId.networkId(networkId), dst, src);
+        }
+        print("Virtual link successfully created.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkListCommand.java
new file mode 100644
index 0000000..a69e0b7
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkListCommand.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualLink;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Lists all virtual links for the network ID.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-links",
+        description = "Lists all virtual links in a virtual network.")
+public class VirtualLinkListCommand extends AbstractShellCommand {
+
+    private static final String FMT_VIRTUAL_LINK =
+            "src=%s, dst=%s, state=%s, tunnelId=%s";
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Override
+    protected void doExecute() {
+
+        getSortedVirtualLinks().forEach(this::printVirtualLink);
+    }
+
+    /**
+     * Returns the list of virtual links sorted using the device identifier.
+     *
+     * @return virtual link list
+     */
+    private List<VirtualLink> getSortedVirtualLinks() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+
+        List<VirtualLink> virtualLinks = new ArrayList<>();
+        virtualLinks.addAll(service.getVirtualLinks(NetworkId.networkId(networkId)));
+        return virtualLinks;
+    }
+
+    /**
+     * Prints out each virtual link.
+     *
+     * @param virtualLink virtual link
+     */
+    private void printVirtualLink(VirtualLink virtualLink) {
+        print(FMT_VIRTUAL_LINK, virtualLink.src().toString(), virtualLink.dst().toString(),
+              virtualLink.state(),
+              virtualLink.tunnelId() == null ? null : virtualLink.tunnelId().toString());
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkRemoveCommand.java
new file mode 100644
index 0000000..5f613f4
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualLinkRemoveCommand.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.apache.karaf.shell.api.action.Option;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Removes a virtual link.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-remove-link",
+        description = "Removes a virtual link.")
+public class VirtualLinkRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "srcDeviceId", description = "Source device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String srcDeviceId = null;
+
+    @Argument(index = 2, name = "srcPortNum", description = "Source port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer srcPortNum = null;
+
+    @Argument(index = 3, name = "dstDeviceId", description = "Destination device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String dstDeviceId = null;
+
+    @Argument(index = 4, name = "dstPortNum", description = "Destination port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer dstPortNum = null;
+
+    @Option(name = "-b", aliases = "--bidirectional",
+            description = "If this argument is passed in then then bidirectional virtual link will be removed, " +
+                    "otherwise the unidirectional link will be removed.",
+            required = false, multiValued = false)
+    boolean bidirectional = false;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        ConnectPoint src = new ConnectPoint(DeviceId.deviceId(srcDeviceId), PortNumber.portNumber(srcPortNum));
+        ConnectPoint dst = new ConnectPoint(DeviceId.deviceId(dstDeviceId), PortNumber.portNumber(dstPortNum));
+
+        service.removeVirtualLink(NetworkId.networkId(networkId), src, dst);
+        if (bidirectional) {
+            service.removeVirtualLink(NetworkId.networkId(networkId), dst, src);
+        }
+        print("Virtual link successfully removed.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkBalanceMastersCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkBalanceMastersCommand.java
new file mode 100644
index 0000000..51a0c96
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkBalanceMastersCommand.java
@@ -0,0 +1,45 @@
+/*
+ * 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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.mastership.MastershipAdminService;
+
+/**
+ * Forces virtual network device mastership rebalancing.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-balance-masters",
+        description = "Forces virtual network device mastership rebalancing")
+public class VirtualNetworkBalanceMastersCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+    @Override
+    protected void doExecute() {
+        VirtualNetworkService vnetService = get(VirtualNetworkService.class);
+        MastershipAdminService mastershipAdminService = vnetService
+                .get(NetworkId.networkId(networkId), MastershipAdminService.class);
+        mastershipAdminService.balanceRoles();
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCompleter.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCompleter.java
new file mode 100644
index 0000000..1a3e7a2
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCompleter.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+import java.util.List;
+import java.util.Set;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.SortedSet;
+
+/**
+ * Virtual network completer.
+ */
+@Service
+public class VirtualNetworkCompleter 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
+        VirtualNetworkAdminService service = AbstractShellCommand.get(VirtualNetworkAdminService.class);
+
+        List<VirtualNetwork> virtualNetworks = new ArrayList<>();
+
+        Set<TenantId> tenantSet = service.getTenantIds();
+        tenantSet.forEach(tenantId -> virtualNetworks.addAll(service.getVirtualNetworks(tenantId)));
+
+        Collections.sort(virtualNetworks, Comparators.VIRTUAL_NETWORK_COMPARATOR);
+
+        SortedSet<String> strings = delegate.getStrings();
+        virtualNetworks.forEach(virtualNetwork -> strings.add(virtualNetwork.id().toString()));
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCreateCommand.java
new file mode 100644
index 0000000..d1ebe47
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkCreateCommand.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+/**
+ * Creates a new virtual network.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-create",
+        description = "Creates a new virtual network.")
+public class VirtualNetworkCreateCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "id", description = "Tenant ID",
+            required = true, multiValued = false)
+    @Completion(TenantCompleter.class)
+    String id = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.createVirtualNetwork(TenantId.tenantId(id));
+        print("Virtual network successfully created.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentCreateCommand.java
new file mode 100644
index 0000000..4a8b400
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentCreateCommand.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.net.ConnectivityIntentCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.intent.Constraint;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentService;
+
+import java.util.List;
+
+/**
+ * Installs virtual network intents.
+ */
+@Service
+@Command(scope = "onos", name = "add-vnet-intent",
+        description = "Installs virtual network connectivity intent")
+public class VirtualNetworkIntentCreateCommand extends ConnectivityIntentCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "ingressDevice",
+            description = "Ingress Device/Port Description",
+            required = true, multiValued = false)
+    String ingressDeviceString = null;
+
+    @Argument(index = 2, name = "egressDevice",
+            description = "Egress Device/Port Description",
+            required = true, multiValued = false)
+    String egressDeviceString = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        IntentService virtualNetworkIntentService = service.get(NetworkId.networkId(networkId), IntentService.class);
+
+        ConnectPoint ingress = ConnectPoint.deviceConnectPoint(ingressDeviceString);
+        ConnectPoint egress = ConnectPoint.deviceConnectPoint(egressDeviceString);
+
+        TrafficSelector selector = buildTrafficSelector();
+        TrafficTreatment treatment = buildTrafficTreatment();
+
+        List<Constraint> constraints = buildConstraints();
+
+        Intent intent = VirtualNetworkIntent.builder()
+                .networkId(NetworkId.networkId(networkId))
+                .appId(appId())
+                .key(key())
+                .selector(selector)
+                .treatment(treatment)
+                .ingressPoint(ingress)
+                .egressPoint(egress)
+                .constraints(constraints)
+                .priority(priority())
+                .build();
+        virtualNetworkIntentService.submit(intent);
+        print("Virtual intent submitted:\n%s", intent.toString());
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentRemoveCommand.java
new file mode 100644
index 0000000..eea3d45
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkIntentRemoveCommand.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.apache.karaf.shell.api.action.Option;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentListener;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.IntentState;
+import org.onosproject.net.intent.Key;
+
+import java.math.BigInteger;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.net.intent.IntentState.FAILED;
+import static org.onosproject.net.intent.IntentState.WITHDRAWN;
+
+/**
+ * Removes a virtual network intent.
+ */
+@Service
+@Command(scope = "onos", name = "remove-vnet-intent",
+        description = "Removes the virtual network intent")
+public class VirtualNetworkIntentRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "app",
+            description = "Application ID",
+            required = false, multiValued = false)
+    String applicationIdString = null;
+
+    @Argument(index = 2, name = "key",
+            description = "Intent Key",
+            required = false, multiValued = false)
+    String keyString = null;
+
+    @Option(name = "-p", aliases = "--purge",
+            description = "Purge the intent from the store after removal",
+            required = false, multiValued = false)
+    private boolean purgeAfterRemove = false;
+
+    @Option(name = "-s", aliases = "--sync",
+            description = "Waits for the removal before returning",
+            required = false, multiValued = false)
+    private boolean sync = false;
+
+    private static final EnumSet<IntentState> CAN_PURGE = EnumSet.of(WITHDRAWN, FAILED);
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        IntentService intentService = service.get(NetworkId.networkId(networkId), IntentService.class);
+        CoreService coreService = get(CoreService.class);
+
+        if (purgeAfterRemove || sync) {
+            print("Using \"sync\" to remove/purge intents - this may take a while...");
+            print("Check \"summary\" to see remove/purge progress.");
+        }
+
+        ApplicationId appId = appId();
+        if (!isNullOrEmpty(applicationIdString)) {
+            appId = coreService.getAppId(applicationIdString);
+            if (appId == null) {
+                print("Cannot find application Id %s", applicationIdString);
+                return;
+            }
+        }
+
+        if (isNullOrEmpty(keyString)) {
+            for (Intent intent : intentService.getIntents()) {
+                if (intent.appId().equals(appId)) {
+                    removeIntent(intentService, intent);
+                }
+            }
+
+        } else {
+            final Key key;
+            if (keyString.startsWith("0x")) {
+                // The intent uses a LongKey
+                keyString = keyString.replaceFirst("0x", "");
+                key = Key.of(new BigInteger(keyString, 16).longValue(), appId);
+            } else {
+                // The intent uses a StringKey
+                key = Key.of(keyString, appId);
+            }
+
+            Intent intent = intentService.getIntent(key);
+            if (intent != null) {
+                removeIntent(intentService, intent);
+            } else {
+                print("Intent not found!");
+            }
+        }
+    }
+
+    /**
+     * Removes the intent using the specified intentService.
+     *
+     * @param intentService intent service
+     * @param intent        intent
+     */
+    private void removeIntent(IntentService intentService, Intent intent) {
+        IntentListener listener = null;
+        Key key = intent.key();
+        final CountDownLatch withdrawLatch, purgeLatch;
+        if (purgeAfterRemove || sync) {
+            // set up latch and listener to track uninstall progress
+            withdrawLatch = new CountDownLatch(1);
+            purgeLatch = purgeAfterRemove ? new CountDownLatch(1) : null;
+            listener = (IntentEvent event) -> {
+                if (Objects.equals(event.subject().key(), key)) {
+                    if (event.type() == IntentEvent.Type.WITHDRAWN ||
+                            event.type() == IntentEvent.Type.FAILED) {
+                        withdrawLatch.countDown();
+                    } else if (purgeLatch != null && purgeAfterRemove &&
+                            event.type() == IntentEvent.Type.PURGED) {
+                        purgeLatch.countDown();
+                    }
+                }
+            };
+            intentService.addListener(listener);
+        } else {
+            purgeLatch = null;
+            withdrawLatch = null;
+        }
+
+        // request the withdraw
+        intentService.withdraw(intent);
+
+        if ((purgeAfterRemove || sync) && purgeLatch != null) {
+            try { // wait for withdraw event
+                withdrawLatch.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                print("Timed out waiting for intent {} withdraw", key);
+            }
+            if (purgeAfterRemove && CAN_PURGE.contains(intentService.getIntentState(key))) {
+                intentService.purge(intent);
+                if (sync) { // wait for purge event
+                    /* TODO
+                       Technically, the event comes before map.remove() is called.
+                       If we depend on sync and purge working together, we will
+                       need to address this.
+                    */
+                    try {
+                        purgeLatch.await(5, TimeUnit.SECONDS);
+                    } catch (InterruptedException e) {
+                        print("Timed out waiting for intent {} purge", key);
+                    }
+                }
+            }
+        }
+
+        if (listener != null) {
+            // clean up the listener
+            intentService.removeListener(listener);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkListCommand.java
new file mode 100644
index 0000000..df1172c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkListCommand.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Lists all virtual networks for the tenant ID.
+ */
+@Service
+@Command(scope = "onos", name = "vnets",
+        description = "Lists all virtual networks.")
+public class VirtualNetworkListCommand extends AbstractShellCommand {
+
+    private static final String FMT_VIRTUAL_NETWORK =
+            "tenantId=%s, networkId=%s";
+
+    @Override
+    protected void doExecute() {
+
+        getSortedVirtualNetworks().forEach(this::printVirtualNetwork);
+    }
+
+    /**
+     * Returns the list of virtual networks sorted using the tenant identifier.
+     *
+     * @return sorted virtual network list
+     */
+    private List<VirtualNetwork> getSortedVirtualNetworks() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        VirtualNetworkAdminService adminService = get(VirtualNetworkAdminService.class);
+
+        List<VirtualNetwork> virtualNetworks = new ArrayList<>();
+
+        Set<TenantId> tenantSet = adminService.getTenantIds();
+        tenantSet.forEach(tenantId -> virtualNetworks.addAll(service.getVirtualNetworks(tenantId)));
+
+        Collections.sort(virtualNetworks, Comparators.VIRTUAL_NETWORK_COMPARATOR);
+        return virtualNetworks;
+    }
+
+    /**
+     * Prints out each virtual network.
+     *
+     * @param virtualNetwork virtual network
+     */
+    private void printVirtualNetwork(VirtualNetwork virtualNetwork) {
+        print(FMT_VIRTUAL_NETWORK, virtualNetwork.tenantId(), virtualNetwork.id());
+    }
+}
+
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkPacketRequestCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkPacketRequestCommand.java
new file mode 100644
index 0000000..059829d
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkPacketRequestCommand.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+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.apache.karaf.shell.api.action.Option;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.TpPort;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cli.net.EthType;
+import org.onosproject.cli.net.EthTypeCompleter;
+import org.onosproject.cli.net.ExtHeader;
+import org.onosproject.cli.net.ExtHeaderCompleter;
+import org.onosproject.cli.net.Icmp6Code;
+import org.onosproject.cli.net.Icmp6CodeCompleter;
+import org.onosproject.cli.net.Icmp6Type;
+import org.onosproject.cli.net.Icmp6TypeCompleter;
+import org.onosproject.cli.net.IpProtocol;
+import org.onosproject.cli.net.IpProtocolCompleter;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.packet.PacketRequest;
+import org.onosproject.net.packet.PacketService;
+
+import java.util.List;
+import java.util.Optional;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Tests virtual network packet requests.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-packet",
+        description = "Tests virtual network packet requests")
+public class VirtualNetworkPacketRequestCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "command",
+            description = "Command name (requestPackets|getRequests|cancelPackets)",
+            required = true, multiValued = false)
+    private String command = null;
+
+    @Argument(index = 1, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    private Long networkId = null;
+
+    @Option(name = "--deviceId", description = "Device ID",
+            required = false, multiValued = false)
+    private String deviceIdString = null;
+
+    // Traffic selector
+    @Option(name = "-s", aliases = "--ethSrc", description = "Source MAC Address",
+            required = false, multiValued = false)
+    private String srcMacString = null;
+
+    @Option(name = "-d", aliases = "--ethDst", description = "Destination MAC Address",
+            required = false, multiValued = false)
+    private String dstMacString = null;
+
+    @Option(name = "-t", aliases = "--ethType", description = "Ethernet Type",
+            required = false, multiValued = false)
+    @Completion(EthTypeCompleter.class)
+    private String ethTypeString = null;
+
+    @Option(name = "-v", aliases = "--vlan", description = "VLAN ID",
+            required = false, multiValued = false)
+    private String vlanString = null;
+
+    @Option(name = "--ipProto", description = "IP Protocol",
+            required = false, multiValued = false)
+    @Completion(IpProtocolCompleter.class)
+    private String ipProtoString = null;
+
+    @Option(name = "--ipSrc", description = "Source IP Prefix",
+            required = false, multiValued = false)
+    private String srcIpString = null;
+
+    @Option(name = "--ipDst", description = "Destination IP Prefix",
+            required = false, multiValued = false)
+    private String dstIpString = null;
+
+    @Option(name = "--fLabel", description = "IPv6 Flow Label",
+            required = false, multiValued = false)
+    private String fLabelString = null;
+
+    @Option(name = "--icmp6Type", description = "ICMPv6 Type",
+            required = false, multiValued = false)
+    @Completion(Icmp6TypeCompleter.class)
+    private String icmp6TypeString = null;
+
+    @Option(name = "--icmp6Code", description = "ICMPv6 Code",
+            required = false, multiValued = false)
+    @Completion(Icmp6CodeCompleter.class)
+    private String icmp6CodeString = null;
+
+    @Option(name = "--ndTarget", description = "IPv6 Neighbor Discovery Target Address",
+            required = false, multiValued = false)
+    private String ndTargetString = null;
+
+    @Option(name = "--ndSLL", description = "IPv6 Neighbor Discovery Source Link-Layer",
+            required = false, multiValued = false)
+    private String ndSllString = null;
+
+    @Option(name = "--ndTLL", description = "IPv6 Neighbor Discovery Target Link-Layer",
+            required = false, multiValued = false)
+    private String ndTllString = null;
+
+    @Option(name = "--tcpSrc", description = "Source TCP Port",
+            required = false, multiValued = false)
+    private String srcTcpString = null;
+
+    @Option(name = "--tcpDst", description = "Destination TCP Port",
+            required = false, multiValued = false)
+    private String dstTcpString = null;
+
+    @Option(name = "--extHdr", description = "IPv6 Extension Header Pseudo-field",
+            required = false, multiValued = true)
+    @Completion(ExtHeaderCompleter.class)
+    private List<String> extHdrStringList = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        PacketService virtualPacketService = service.get(NetworkId.networkId(networkId), PacketService.class);
+
+        if (command == null) {
+            print("Command is not defined");
+            return;
+        }
+
+        if (command.equals("getRequests")) {
+            getRequests(virtualPacketService);
+            return;
+        }
+
+        TrafficSelector selector = buildTrafficSelector();
+        PacketPriority packetPriority = PacketPriority.CONTROL; //TODO allow user to specify
+        Optional<DeviceId> optionalDeviceId = null;
+        if (!isNullOrEmpty(deviceIdString)) {
+            optionalDeviceId = Optional.of(DeviceId.deviceId(deviceIdString));
+        }
+
+        if (command.equals("requestPackets")) {
+            if (optionalDeviceId != null) {
+                virtualPacketService.requestPackets(selector, packetPriority, appId(), optionalDeviceId);
+            } else {
+                virtualPacketService.requestPackets(selector, packetPriority, appId());
+            }
+            print("Virtual packet requested:\n%s", selector);
+            return;
+        }
+
+       if (command.equals("cancelPackets")) {
+            if (optionalDeviceId != null) {
+                virtualPacketService.cancelPackets(selector, packetPriority, appId(), optionalDeviceId);
+            } else {
+                virtualPacketService.cancelPackets(selector, packetPriority, appId());
+            }
+            print("Virtual packet cancelled:\n%s", selector);
+            return;
+        }
+
+        print("Unsupported command %s", command);
+    }
+
+    private void getRequests(PacketService packetService) {
+        List<PacketRequest> packetRequests = packetService.getRequests();
+        if (outputJson()) {
+            print("%s", json(packetRequests));
+        } else {
+            packetRequests.forEach(packetRequest -> print(packetRequest.toString()));
+        }
+    }
+
+    private JsonNode json(List<PacketRequest> packetRequests) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+        packetRequests.forEach(packetRequest ->
+                                       result.add(jsonForEntity(packetRequest, PacketRequest.class)));
+        return result;
+    }
+
+    /**
+     * Constructs a traffic selector based on the command line arguments
+     * presented to the command.
+     * @return traffic selector
+     */
+    private TrafficSelector buildTrafficSelector() {
+        IpPrefix srcIpPrefix = null;
+        IpPrefix dstIpPrefix = null;
+
+        TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
+
+        if (!isNullOrEmpty(srcIpString)) {
+            srcIpPrefix = IpPrefix.valueOf(srcIpString);
+            if (srcIpPrefix.isIp4()) {
+                selectorBuilder.matchIPSrc(srcIpPrefix);
+            } else {
+                selectorBuilder.matchIPv6Src(srcIpPrefix);
+            }
+        }
+
+        if (!isNullOrEmpty(dstIpString)) {
+            dstIpPrefix = IpPrefix.valueOf(dstIpString);
+            if (dstIpPrefix.isIp4()) {
+                selectorBuilder.matchIPDst(dstIpPrefix);
+            } else {
+                selectorBuilder.matchIPv6Dst(dstIpPrefix);
+            }
+        }
+
+        if ((srcIpPrefix != null) && (dstIpPrefix != null) &&
+            (srcIpPrefix.version() != dstIpPrefix.version())) {
+            // ERROR: IP src/dst version mismatch
+            throw new IllegalArgumentException(
+                        "IP source and destination version mismatch");
+        }
+
+        //
+        // Set the default EthType based on the IP version if the matching
+        // source or destination IP prefixes.
+        //
+        Short ethType = null;
+        if ((srcIpPrefix != null) && srcIpPrefix.isIp6()) {
+            ethType = EthType.IPV6.value();
+        }
+        if ((dstIpPrefix != null) && dstIpPrefix.isIp6()) {
+            ethType = EthType.IPV6.value();
+        }
+        if (!isNullOrEmpty(ethTypeString)) {
+            ethType = EthType.parseFromString(ethTypeString);
+        }
+        if (ethType != null) {
+            selectorBuilder.matchEthType(ethType);
+        }
+        if (!isNullOrEmpty(vlanString)) {
+            selectorBuilder.matchVlanId(VlanId.vlanId(Short.parseShort(vlanString)));
+        }
+        if (!isNullOrEmpty(srcMacString)) {
+            selectorBuilder.matchEthSrc(MacAddress.valueOf(srcMacString));
+        }
+
+        if (!isNullOrEmpty(dstMacString)) {
+            selectorBuilder.matchEthDst(MacAddress.valueOf(dstMacString));
+        }
+
+        if (!isNullOrEmpty(ipProtoString)) {
+            short ipProtoShort = IpProtocol.parseFromString(ipProtoString);
+            selectorBuilder.matchIPProtocol((byte) ipProtoShort);
+        }
+
+        if (!isNullOrEmpty(fLabelString)) {
+            selectorBuilder.matchIPv6FlowLabel(Integer.parseInt(fLabelString));
+        }
+
+        if (!isNullOrEmpty(icmp6TypeString)) {
+            byte icmp6Type = Icmp6Type.parseFromString(icmp6TypeString);
+            selectorBuilder.matchIcmpv6Type(icmp6Type);
+        }
+
+        if (!isNullOrEmpty(icmp6CodeString)) {
+            byte icmp6Code = Icmp6Code.parseFromString(icmp6CodeString);
+            selectorBuilder.matchIcmpv6Code(icmp6Code);
+        }
+
+        if (!isNullOrEmpty(ndTargetString)) {
+            selectorBuilder.matchIPv6NDTargetAddress(Ip6Address.valueOf(ndTargetString));
+        }
+
+        if (!isNullOrEmpty(ndSllString)) {
+            selectorBuilder.matchIPv6NDSourceLinkLayerAddress(MacAddress.valueOf(ndSllString));
+        }
+
+        if (!isNullOrEmpty(ndTllString)) {
+            selectorBuilder.matchIPv6NDTargetLinkLayerAddress(MacAddress.valueOf(ndTllString));
+        }
+
+        if (!isNullOrEmpty(srcTcpString)) {
+            selectorBuilder.matchTcpSrc(TpPort.tpPort(Integer.parseInt(srcTcpString)));
+        }
+
+        if (!isNullOrEmpty(dstTcpString)) {
+            selectorBuilder.matchTcpDst(TpPort.tpPort(Integer.parseInt(dstTcpString)));
+        }
+
+        if (extHdrStringList != null) {
+            short extHdr = 0;
+            for (String extHdrString : extHdrStringList) {
+                extHdr = (short) (extHdr | ExtHeader.parseFromString(extHdrString));
+            }
+            selectorBuilder.matchIPv6ExthdrFlags(extHdr);
+        }
+
+        return selectorBuilder.build();
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkRemoveCommand.java
new file mode 100644
index 0000000..2b28044
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualNetworkRemoveCommand.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+
+/**
+ * Removes a virtual network.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-remove",
+        description = "Removes a virtual network.")
+public class VirtualNetworkRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "id", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long id;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.removeVirtualNetwork(NetworkId.networkId(id));
+        print("Virtual network successfully removed.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortBindCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortBindCommand.java
new file mode 100644
index 0000000..7013a01
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortBindCommand.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.cli.net.PortNumberCompleter;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Binds an existing virtual port with a physical port.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-bind-port",
+        description = "Binds an existing virtual port with a physical port.")
+public class VirtualPortBindCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Virtual Device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String deviceId = null;
+
+    @Argument(index = 2, name = "portNum", description = "Virtual device port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer portNum = null;
+
+    @Argument(index = 3, name = "physDeviceId", description = "Physical Device ID",
+            required = true, multiValued = false)
+    @Completion(DeviceIdCompleter.class)
+    String physDeviceId = null;
+
+    @Argument(index = 4, name = "physPortNum", description = "Physical device port number",
+            required = true, multiValued = false)
+    @Completion(PortNumberCompleter.class)
+    Integer physPortNum = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        DeviceService deviceService = get(DeviceService.class);
+
+        VirtualPort vPort = getVirtualPort(PortNumber.portNumber(portNum));
+        checkNotNull(vPort, "The virtual Port does not exist");
+
+        ConnectPoint realizedBy = new ConnectPoint(DeviceId.deviceId(physDeviceId),
+                                      PortNumber.portNumber(physPortNum));
+        service.bindVirtualPort(NetworkId.networkId(networkId), DeviceId.deviceId(deviceId),
+                                  PortNumber.portNumber(portNum), realizedBy);
+        print("Virtual port is successfully bound.");
+    }
+
+    /**
+     * Returns the virtual port matching the device and port identifier.
+     *
+     * @param aPortNumber port identifier
+     * @return matching virtual port, or null.
+     */
+    private VirtualPort getVirtualPort(PortNumber aPortNumber) {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        Set<VirtualPort> ports = service.getVirtualPorts(NetworkId.networkId(networkId),
+                                                    DeviceId.deviceId(deviceId));
+        return ports.stream().filter(p -> p.number().equals(aPortNumber))
+                .findFirst().get();
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCompleter.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCompleter.java
new file mode 100644
index 0000000..141c964
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCompleter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+import static org.onlab.osgi.DefaultServiceDirectory.getService;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractChoicesCompleter;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.net.DeviceId;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+/**
+ * Virtual port completer.
+ *
+ * Assumes the first argument which can be parsed to a number is network id
+ * and the argument right before the one being completed is device id
+ */
+@Service
+public class VirtualPortCompleter extends AbstractChoicesCompleter {
+    @Override
+    protected List<String> choices() {
+        //parse argument list for network id
+        String[] argsArray = commandLine.getArguments();
+        for (String str : argsArray) {
+            if (str.matches("[0-9]+")) {
+                long networkId = Long.valueOf(str);
+                String deviceId = argsArray[argsArray.length - 1];
+                return getSortedVirtualPorts(networkId, deviceId).stream()
+                        .map(virtualPort -> virtualPort.number().toString())
+                        .collect(Collectors.toList());
+            }
+        }
+
+        return Collections.singletonList("Missing network id");
+    }
+
+    /**
+     * Returns the list of virtual ports sorted using the port number.
+     *
+     * @param networkId network id.
+     * @param deviceId device id
+     * @return sorted virtual port list
+     */
+    private List<VirtualPort> getSortedVirtualPorts(long networkId, String deviceId) {
+        VirtualNetworkService service = getService(VirtualNetworkService.class);
+
+        List<VirtualPort> virtualPorts = new ArrayList<>();
+        virtualPorts.addAll(service.getVirtualPorts(NetworkId.networkId(networkId),
+                DeviceId.deviceId(deviceId)));
+        Collections.sort(virtualPorts, Comparators.VIRTUAL_PORT_COMPARATOR);
+        return virtualPorts;
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCreateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCreateCommand.java
new file mode 100644
index 0000000..a466a64
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortCreateCommand.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.cli.net.PortNumberCompleter;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Creates a new virtual port.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-create-port",
+        description = "Creates a new virtual port in a network.")
+public class VirtualPortCreateCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Virtual Device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String deviceId = null;
+
+    @Argument(index = 2, name = "portNum", description = "Virtual device port number",
+            required = true, multiValued = false)
+    Integer portNum = null;
+
+    @Argument(index = 3, name = "physDeviceId", description = "Physical Device ID",
+            required = false, multiValued = false)
+    @Completion(DeviceIdCompleter.class)
+    String physDeviceId = null;
+
+    @Argument(index = 4, name = "physPortNum", description = "Physical device port number",
+            required = false, multiValued = false)
+    @Completion(PortNumberCompleter.class)
+    Integer physPortNum = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        DeviceService deviceService = get(DeviceService.class);
+
+        VirtualDevice virtualDevice = getVirtualDevice(DeviceId.deviceId(deviceId));
+        checkNotNull(virtualDevice, "The virtual device does not exist.");
+
+        ConnectPoint realizedBy = null;
+        if (physDeviceId != null && physPortNum != null) {
+            checkNotNull(physPortNum, "The physical port does not specified.");
+            realizedBy = new ConnectPoint(DeviceId.deviceId(physDeviceId),
+                                               PortNumber.portNumber(physPortNum));
+            checkNotNull(realizedBy, "The physical port does not exist.");
+        }
+
+        service.createVirtualPort(NetworkId.networkId(networkId), DeviceId.deviceId(deviceId),
+                                  PortNumber.portNumber(portNum), realizedBy);
+        print("Virtual port successfully created.");
+    }
+
+    /**
+     * Returns the virtual device matching the device identifier.
+     *
+     * @param aDeviceId device identifier
+     * @return matching virtual device, or null.
+     */
+    private VirtualDevice getVirtualDevice(DeviceId aDeviceId) {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+
+        Set<VirtualDevice> virtualDevices = service.getVirtualDevices(NetworkId.networkId(networkId));
+
+        for (VirtualDevice virtualDevice : virtualDevices) {
+            if (virtualDevice.id().equals(aDeviceId)) {
+                return virtualDevice;
+            }
+        }
+        return null;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortListCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortListCommand.java
new file mode 100644
index 0000000..f7a6c08
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortListCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.Comparators;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.net.DeviceId;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Lists all virtual ports for the network ID.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-ports",
+        description = "Lists all virtual ports in a virtual network.")
+public class VirtualPortListCommand extends AbstractShellCommand {
+
+    private static final String FMT_VIRTUAL_PORT =
+            "virtual portNumber=%s, physical deviceId=%s, portNumber=%s, isEnabled=%s";
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Virtual Device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String deviceId = null;
+
+    @Override
+    protected void doExecute() {
+
+        getSortedVirtualPorts().forEach(this::printVirtualPort);
+    }
+
+    /**
+     * Returns the list of virtual ports sorted using the network identifier.
+     *
+     * @return sorted virtual port list
+     */
+    private List<VirtualPort> getSortedVirtualPorts() {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+
+        List<VirtualPort> virtualPorts = new ArrayList<>();
+        virtualPorts.addAll(service.getVirtualPorts(NetworkId.networkId(networkId),
+                                                    DeviceId.deviceId(deviceId)));
+        Collections.sort(virtualPorts, Comparators.VIRTUAL_PORT_COMPARATOR);
+        return virtualPorts;
+    }
+
+    /**
+     * Prints out each virtual port.
+     *
+     * @param virtualPort virtual port
+     */
+    private void printVirtualPort(VirtualPort virtualPort) {
+        if (virtualPort.realizedBy() == null) {
+            print(FMT_VIRTUAL_PORT, virtualPort.number(), "None", "None", virtualPort.isEnabled());
+        } else {
+            print(FMT_VIRTUAL_PORT, virtualPort.number(),
+                  virtualPort.realizedBy().deviceId(),
+                  virtualPort.realizedBy().port(),
+                  virtualPort.isEnabled());
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortRemoveCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortRemoveCommand.java
new file mode 100644
index 0000000..6c79276
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortRemoveCommand.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Removes a virtual port.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-remove-port",
+        description = "Removes a virtual port.")
+public class VirtualPortRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String deviceId = null;
+
+    @Argument(index = 2, name = "portNum", description = "Device port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer portNum = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.removeVirtualPort(NetworkId.networkId(networkId), DeviceId.deviceId(deviceId),
+                                  PortNumber.portNumber(portNum));
+        print("Virtual port successfully removed.");
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortStateCommand.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortStateCommand.java
new file mode 100644
index 0000000..56af2ba
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/VirtualPortStateCommand.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017-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.incubator.net.virtual.cli;
+
+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.cli.AbstractShellCommand;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Administratively enables or disables state of an existing virtual port.
+ */
+@Service
+@Command(scope = "onos", name = "vnet-port-state",
+        description = "Administratively enables or disables state of an existing virtual port.")
+public class VirtualPortStateCommand extends AbstractShellCommand {
+    @Argument(index = 0, name = "networkId", description = "Network ID",
+            required = true, multiValued = false)
+    @Completion(VirtualNetworkCompleter.class)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "deviceId", description = "Virtual Device ID",
+            required = true, multiValued = false)
+    @Completion(VirtualDeviceCompleter.class)
+    String deviceId = null;
+
+    @Argument(index = 2, name = "portNum", description = "Virtual device port number",
+            required = true, multiValued = false)
+    @Completion(VirtualPortCompleter.class)
+    Integer portNum = null;
+
+    @Argument(index = 3, name = "portState",
+            description = "Desired State. Either \"enable\" or \"disable\".",
+            required = true, multiValued = false)
+    String portState = null;
+
+    @Override
+    protected void doExecute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+
+        VirtualPort vPort = getVirtualPort(PortNumber.portNumber(portNum));
+        checkNotNull(vPort, "The virtual Port does not exist");
+
+        boolean isEnabled;
+        if ("enable".equals(portState)) {
+            isEnabled = true;
+        } else if ("disable".equals(portState)) {
+            isEnabled = false;
+        } else {
+            print("State must be enable or disable");
+            return;
+        }
+
+        service.updatePortState(NetworkId.networkId(networkId),
+                                DeviceId.deviceId(deviceId), vPort.number(), isEnabled);
+        print("Virtual port state updated.");
+    }
+
+    /**
+     * Returns the virtual port matching the device and port identifier.
+     *
+     * @param aPortNumber port identifier
+     * @return matching virtual port, or null.
+     */
+    private VirtualPort getVirtualPort(PortNumber aPortNumber) {
+        VirtualNetworkService service = get(VirtualNetworkService.class);
+        Set<VirtualPort> ports = service.getVirtualPorts(NetworkId.networkId(networkId),
+                                                    DeviceId.deviceId(deviceId));
+        return ports.stream().filter(p -> p.number().equals(aPortNumber))
+                .findFirst().get();
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/package-info.java
new file mode 100644
index 0000000..049ee4f
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/**
+ * CLI commands for querying and administering virtual networks.
+ */
+package org.onosproject.incubator.net.virtual.cli;
\ No newline at end of file