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
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java
new file mode 100644
index 0000000..21ba3b9
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkDeviceManager.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.ImmutableList;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetworkEvent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkListener;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceEvent.Type;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.device.PortStatistics;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Device service implementation built on the virtual network service.
+ */
+public class VirtualNetworkDeviceManager
+        extends AbstractVirtualListenerManager<DeviceEvent, DeviceListener>
+        implements DeviceService {
+
+    private static final String TYPE_NULL = "Type cannot be null";
+    private static final String DEVICE_NULL = "Device cannot be null";
+    private static final String PORT_NUMBER_NULL = "PortNumber cannot be null";
+    private VirtualNetworkListener virtualNetworkListener = new InternalVirtualNetworkListener();
+
+    /**
+     * Creates a new VirtualNetworkDeviceService object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkDeviceManager(VirtualNetworkService virtualNetworkManager,
+                                       NetworkId networkId) {
+        super(virtualNetworkManager, networkId, DeviceEvent.class);
+        manager.addListener(virtualNetworkListener);
+    }
+
+    @Override
+    public int getDeviceCount() {
+        return manager.getVirtualDevices(this.networkId).size();
+    }
+
+    @Override
+    public Iterable<Device> getDevices() {
+        return manager.getVirtualDevices(
+                this.networkId).stream().collect(Collectors.toSet());
+    }
+
+    @Override
+    public Iterable<Device> getDevices(Device.Type type) {
+        checkNotNull(type, TYPE_NULL);
+        return manager.getVirtualDevices(this.networkId)
+                .stream()
+                .filter(device -> type.equals(device.type()))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Iterable<Device> getAvailableDevices() {
+        return getDevices();
+    }
+
+    @Override
+    public Iterable<Device> getAvailableDevices(Device.Type type) {
+        return getDevices(type);
+    }
+
+    @Override
+    public Device getDevice(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        Optional<VirtualDevice> foundDevice =
+                manager.getVirtualDevices(this.networkId)
+                .stream()
+                .filter(device -> deviceId.equals(device.id()))
+                .findFirst();
+        if (foundDevice.isPresent()) {
+            return foundDevice.get();
+        }
+        return null;
+    }
+
+    @Override
+    public MastershipRole getRole(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        // TODO hard coded to master for now.
+        return MastershipRole.MASTER;
+    }
+
+    @Override
+    public List<Port> getPorts(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        return manager.getVirtualPorts(this.networkId, deviceId)
+                .stream()
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<PortStatistics> getPortStatistics(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        // TODO not supported at the moment.
+        return ImmutableList.of();
+    }
+
+    @Override
+    public List<PortStatistics> getPortDeltaStatistics(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        // TODO not supported at the moment.
+        return ImmutableList.of();
+    }
+
+    @Override
+    public PortStatistics getStatisticsForPort(DeviceId deviceId,
+                                               PortNumber portNumber) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(deviceId, PORT_NUMBER_NULL);
+        // TODO not supported at the moment.
+        return null;
+    }
+
+    @Override
+    public PortStatistics getDeltaStatisticsForPort(DeviceId deviceId,
+                                                    PortNumber portNumber) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(deviceId, PORT_NUMBER_NULL);
+        // TODO not supported at the moment.
+        return null;
+    }
+
+    @Override
+    public Port getPort(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId, DEVICE_NULL);
+
+        Optional<VirtualPort> foundPort =
+                manager.getVirtualPorts(this.networkId, deviceId)
+                .stream()
+                .filter(port -> port.number().equals(portNumber))
+                .findFirst();
+        if (foundPort.isPresent()) {
+            return foundPort.get();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean isAvailable(DeviceId deviceId) {
+        return getDevice(deviceId) != null;
+    }
+
+    @Override
+    public String localStatus(DeviceId deviceId) {
+        // TODO not supported at this time
+        return null;
+    }
+
+    @Override
+    public long getLastUpdatedInstant(DeviceId deviceId) {
+        // TODO not supported at this time
+        return 0;
+    }
+
+    /**
+     * Translates VirtualNetworkEvent to DeviceEvent.
+     */
+    private class InternalVirtualNetworkListener implements VirtualNetworkListener {
+        @Override
+        public boolean isRelevant(VirtualNetworkEvent event) {
+            return networkId().equals(event.subject());
+        }
+
+        @Override
+        public void event(VirtualNetworkEvent event) {
+            switch (event.type()) {
+                case VIRTUAL_DEVICE_ADDED:
+                    post(new DeviceEvent(Type.DEVICE_ADDED, event.virtualDevice()));
+                    break;
+                case VIRTUAL_DEVICE_UPDATED:
+                    post(new DeviceEvent(Type.DEVICE_UPDATED, event.virtualDevice()));
+                    break;
+                case VIRTUAL_DEVICE_REMOVED:
+                    post(new DeviceEvent(Type.DEVICE_REMOVED, event.virtualDevice()));
+                    break;
+                case VIRTUAL_PORT_ADDED:
+                    post(new DeviceEvent(Type.PORT_ADDED, event.virtualDevice(), event.virtualPort()));
+                    break;
+                case VIRTUAL_PORT_UPDATED:
+                    post(new DeviceEvent(Type.PORT_UPDATED, event.virtualDevice(), event.virtualPort()));
+                    break;
+                case VIRTUAL_PORT_REMOVED:
+                    post(new DeviceEvent(Type.PORT_REMOVED, event.virtualDevice(), event.virtualPort()));
+                    break;
+                case NETWORK_UPDATED:
+                case NETWORK_REMOVED:
+                case NETWORK_ADDED:
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowObjectiveManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowObjectiveManager.java
new file mode 100644
index 0000000..c4e2031
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowObjectiveManager.java
@@ -0,0 +1,716 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalCause;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.incubator.net.virtual.AbstractVnetService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowObjectiveStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.NextGroup;
+import org.onosproject.net.behaviour.Pipeliner;
+import org.onosproject.net.behaviour.PipelinerContext;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.AbstractHandlerBehaviour;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleOperations;
+import org.onosproject.net.flow.FlowRuleOperationsContext;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flowobjective.FilteringObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.FlowObjectiveStore;
+import org.onosproject.net.flowobjective.FlowObjectiveStoreDelegate;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.NextObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.flowobjective.ObjectiveEvent;
+import org.onosproject.net.group.DefaultGroupKey;
+import org.onosproject.net.group.GroupKey;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.BoundedThreadPool.newFixedThreadPool;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provides implementation of the flow objective programming service for virtual networks.
+ */
+// NOTE: This manager is designed to provide flow objective programming service
+// for virtual networks. Actually, virtual networks don't need to consider
+// the different implementation of data-path pipeline. But, the interfaces
+// and usages of flow objective service are still valuable for virtual network.
+// This manager is working as an interpreter from FlowObjective to FlowRules
+// to provide symmetric interfaces with ONOS core services.
+// The behaviours are based on DefaultSingleTablePipeline.
+
+public class VirtualNetworkFlowObjectiveManager extends AbstractVnetService
+        implements FlowObjectiveService {
+
+    public static final int INSTALL_RETRY_ATTEMPTS = 5;
+    public static final long INSTALL_RETRY_INTERVAL = 1000; // ms
+
+    private final Logger log = getLogger(getClass());
+
+    protected DeviceService deviceService;
+
+    // Note: The following dependencies are added on behalf of the pipeline
+    // driver behaviours to assure these services are available for their
+    // initialization.
+    protected FlowRuleService flowRuleService;
+
+    protected VirtualNetworkFlowObjectiveStore virtualFlowObjectiveStore;
+    protected FlowObjectiveStore flowObjectiveStore;
+    private final FlowObjectiveStoreDelegate delegate;
+
+    private final PipelinerContext context = new InnerPipelineContext();
+
+    private final Map<DeviceId, Pipeliner> pipeliners = Maps.newConcurrentMap();
+
+    // local stores for queuing fwd and next objectives that are waiting for an
+    // associated next objective execution to complete. The signal for completed
+    // execution comes from a pipeline driver, in this or another controller
+    // instance, via the DistributedFlowObjectiveStore.
+    private final Map<Integer, Set<PendingFlowObjective>> pendingForwards =
+            Maps.newConcurrentMap();
+    private final Map<Integer, Set<PendingFlowObjective>> pendingNexts =
+            Maps.newConcurrentMap();
+
+    // local store to track which nextObjectives were sent to which device
+    // for debugging purposes
+    private Map<Integer, DeviceId> nextToDevice = Maps.newConcurrentMap();
+
+    private ExecutorService executorService;
+
+    public VirtualNetworkFlowObjectiveManager(VirtualNetworkService manager,
+                                              NetworkId networkId) {
+        super(manager, networkId);
+
+        deviceService = manager.get(networkId(), DeviceService.class);
+        flowRuleService = manager.get(networkId(), FlowRuleService.class);
+
+        executorService = newFixedThreadPool(4, groupedThreads("onos/virtual/objective-installer", "%d", log));
+
+        virtualFlowObjectiveStore =
+                serviceDirectory.get(VirtualNetworkFlowObjectiveStore.class);
+        delegate = new InternalStoreDelegate();
+        virtualFlowObjectiveStore.setDelegate(networkId(), delegate);
+        flowObjectiveStore = new StoreConvertor();
+    }
+
+    @Override
+    public void filter(DeviceId deviceId, FilteringObjective filteringObjective) {
+        executorService.execute(new ObjectiveInstaller(deviceId, filteringObjective));
+    }
+
+    @Override
+    public void forward(DeviceId deviceId, ForwardingObjective forwardingObjective) {
+        if (forwardingObjective.nextId() == null ||
+                forwardingObjective.op() == Objective.Operation.REMOVE ||
+                flowObjectiveStore.getNextGroup(forwardingObjective.nextId()) != null ||
+                !queueFwdObjective(deviceId, forwardingObjective)) {
+            // fast path
+            executorService.execute(new ObjectiveInstaller(deviceId, forwardingObjective));
+        }
+    }
+
+    @Override
+    public void next(DeviceId deviceId, NextObjective nextObjective) {
+        nextToDevice.put(nextObjective.id(), deviceId);
+        if (nextObjective.op() == Objective.Operation.ADD ||
+                flowObjectiveStore.getNextGroup(nextObjective.id()) != null ||
+                !queueNextObjective(deviceId, nextObjective)) {
+            // either group exists or we are trying to create it - let it through
+            executorService.execute(new ObjectiveInstaller(deviceId, nextObjective));
+        }
+    }
+
+    @Override
+    public int allocateNextId() {
+        return flowObjectiveStore.allocateNextId();
+    }
+
+    @Override
+    public void initPolicy(String policy) {
+
+    }
+
+    @Override
+    public List<String> getNextMappings() {
+        List<String> mappings = new ArrayList<>();
+        Map<Integer, NextGroup> allnexts = flowObjectiveStore.getAllGroups();
+        // XXX if the NextGroup after de-serialization actually stored info of the deviceId
+        // then info on any nextObj could be retrieved from one controller instance.
+        // Right now the drivers on one instance can only fetch for next-ids that came
+        // to them.
+        // Also, we still need to send the right next-id to the right driver as potentially
+        // there can be different drivers for different devices. But on that account,
+        // no instance should be decoding for another instance's nextIds.
+
+        for (Map.Entry<Integer, NextGroup> e : allnexts.entrySet()) {
+            // get the device this next Objective was sent to
+            DeviceId deviceId = nextToDevice.get(e.getKey());
+            mappings.add("NextId " + e.getKey() + ": " +
+                                 ((deviceId != null) ? deviceId : "nextId not in this onos instance"));
+            if (deviceId != null) {
+                // this instance of the controller sent the nextObj to a driver
+                Pipeliner pipeliner = getDevicePipeliner(deviceId);
+                List<String> nextMappings = pipeliner.getNextMappings(e.getValue());
+                if (nextMappings != null) {
+                    mappings.addAll(nextMappings);
+                }
+            }
+        }
+        return mappings;
+    }
+
+    @Override
+    public List<String> getPendingFlowObjectives() {
+        List<String> pendingFlowObjectives = new ArrayList<>();
+
+        for (Integer nextId : pendingForwards.keySet()) {
+            Set<PendingFlowObjective> pfwd = pendingForwards.get(nextId);
+            StringBuilder pend = new StringBuilder();
+            pend.append("NextId: ")
+                    .append(nextId);
+            for (PendingFlowObjective pf : pfwd) {
+                pend.append("\n    FwdId: ")
+                        .append(String.format("%11s", pf.flowObjective().id()))
+                        .append(", DeviceId: ")
+                        .append(pf.deviceId())
+                        .append(", Selector: ")
+                        .append(((ForwardingObjective) pf.flowObjective())
+                                        .selector().criteria());
+            }
+            pendingFlowObjectives.add(pend.toString());
+        }
+
+        for (Integer nextId : pendingNexts.keySet()) {
+            Set<PendingFlowObjective> pnext = pendingNexts.get(nextId);
+            StringBuilder pend = new StringBuilder();
+            pend.append("NextId: ")
+                    .append(nextId);
+            for (PendingFlowObjective pn : pnext) {
+                pend.append("\n    NextOp: ")
+                        .append(pn.flowObjective().op())
+                        .append(", DeviceId: ")
+                        .append(pn.deviceId())
+                        .append(", Treatments: ")
+                        .append(((NextObjective) pn.flowObjective())
+                                        .next());
+            }
+            pendingFlowObjectives.add(pend.toString());
+        }
+
+        return pendingFlowObjectives;
+    }
+
+    private boolean queueFwdObjective(DeviceId deviceId, ForwardingObjective fwd) {
+        boolean queued = false;
+        synchronized (pendingForwards) {
+            // double check the flow objective store, because this block could run
+            // after a notification arrives
+            if (flowObjectiveStore.getNextGroup(fwd.nextId()) == null) {
+                pendingForwards.compute(fwd.nextId(), (id, pending) -> {
+                    PendingFlowObjective pendfo = new PendingFlowObjective(deviceId, fwd);
+                    if (pending == null) {
+                        return Sets.newHashSet(pendfo);
+                    } else {
+                        pending.add(pendfo);
+                        return pending;
+                    }
+                });
+                queued = true;
+            }
+        }
+        if (queued) {
+            log.debug("Queued forwarding objective {} for nextId {} meant for device {}",
+                      fwd.id(), fwd.nextId(), deviceId);
+        }
+        return queued;
+    }
+
+    private boolean queueNextObjective(DeviceId deviceId, NextObjective next) {
+
+        // we need to hold off on other operations till we get notified that the
+        // initial group creation has succeeded
+        boolean queued = false;
+        synchronized (pendingNexts) {
+            // double check the flow objective store, because this block could run
+            // after a notification arrives
+            if (flowObjectiveStore.getNextGroup(next.id()) == null) {
+                pendingNexts.compute(next.id(), (id, pending) -> {
+                    PendingFlowObjective pendfo = new PendingFlowObjective(deviceId, next);
+                    if (pending == null) {
+                        return Sets.newHashSet(pendfo);
+                    } else {
+                        pending.add(pendfo);
+                        return pending;
+                    }
+                });
+                queued = true;
+            }
+        }
+        if (queued) {
+            log.debug("Queued next objective {} with operation {} meant for device {}",
+                      next.id(), next.op(), deviceId);
+        }
+        return queued;
+    }
+
+    /**
+     * Task that passes the flow objective down to the driver. The task will
+     * make a few attempts to find the appropriate driver, then eventually give
+     * up and report an error if no suitable driver could be found.
+     */
+    private class ObjectiveInstaller implements Runnable {
+        private final DeviceId deviceId;
+        private final Objective objective;
+
+        private final int numAttempts;
+
+        public ObjectiveInstaller(DeviceId deviceId, Objective objective) {
+            this(deviceId, objective, 1);
+        }
+
+        public ObjectiveInstaller(DeviceId deviceId, Objective objective, int attemps) {
+            this.deviceId = checkNotNull(deviceId);
+            this.objective = checkNotNull(objective);
+            this.numAttempts = attemps;
+        }
+
+        @Override
+        public void run() {
+            try {
+                Pipeliner pipeliner = getDevicePipeliner(deviceId);
+
+                if (pipeliner != null) {
+                    if (objective instanceof NextObjective) {
+                        nextToDevice.put(objective.id(), deviceId);
+                        pipeliner.next((NextObjective) objective);
+                    } else if (objective instanceof ForwardingObjective) {
+                        pipeliner.forward((ForwardingObjective) objective);
+                    } else {
+                        pipeliner.filter((FilteringObjective) objective);
+                    }
+                    //Attempts to check if pipeliner is null for retry attempts
+                } else if (numAttempts < INSTALL_RETRY_ATTEMPTS) {
+                    Thread.sleep(INSTALL_RETRY_INTERVAL);
+                    executorService.execute(new ObjectiveInstaller(deviceId, objective, numAttempts + 1));
+                } else {
+                    // Otherwise we've tried a few times and failed, report an
+                    // error back to the user.
+                    objective.context().ifPresent(
+                            c -> c.onError(objective, ObjectiveError.NOPIPELINER));
+                }
+                //Exception thrown
+            } catch (Exception e) {
+                log.warn("Exception while installing flow objective", e);
+            }
+        }
+    }
+
+    private class InternalStoreDelegate implements FlowObjectiveStoreDelegate {
+        @Override
+        public void notify(ObjectiveEvent event) {
+            if (event.type() == ObjectiveEvent.Type.ADD) {
+                log.debug("Received notification of obj event {}", event);
+                Set<PendingFlowObjective> pending;
+
+                // first send all pending flows
+                synchronized (pendingForwards) {
+                    // needs to be synchronized for queueObjective lookup
+                    pending = pendingForwards.remove(event.subject());
+                }
+                if (pending == null) {
+                    log.debug("No forwarding objectives pending for this "
+                                      + "obj event {}", event);
+                } else {
+                    log.debug("Processing {} pending forwarding objectives for nextId {}",
+                              pending.size(), event.subject());
+                    pending.forEach(p -> getDevicePipeliner(p.deviceId())
+                            .forward((ForwardingObjective) p.flowObjective()));
+                }
+
+                // now check for pending next-objectives
+                synchronized (pendingNexts) {
+                    // needs to be synchronized for queueObjective lookup
+                    pending = pendingNexts.remove(event.subject());
+                }
+                if (pending == null) {
+                    log.debug("No next objectives pending for this "
+                                      + "obj event {}", event);
+                } else {
+                    log.debug("Processing {} pending next objectives for nextId {}",
+                              pending.size(), event.subject());
+                    pending.forEach(p -> getDevicePipeliner(p.deviceId())
+                            .next((NextObjective) p.flowObjective()));
+                }
+            }
+        }
+    }
+
+    /**
+     * Retrieves (if it exists) the device pipeline behaviour from the cache.
+     * Otherwise it warms the caches and triggers the init method of the Pipeline.
+     * For virtual network, it returns OVS pipeliner.
+     *
+     * @param deviceId the id of the device associated to the pipeline
+     * @return the implementation of the Pipeliner behaviour
+     */
+    private Pipeliner getDevicePipeliner(DeviceId deviceId) {
+        return pipeliners.computeIfAbsent(deviceId, this::initPipelineHandler);
+    }
+
+    /**
+     * Creates and initialize {@link Pipeliner}.
+     * <p>
+     * Note: Expected to be called under per-Device lock.
+     *      e.g., {@code pipeliners}' Map#compute family methods
+     *
+     * @param deviceId Device to initialize pipeliner
+     * @return {@link Pipeliner} instance or null
+     */
+    private Pipeliner initPipelineHandler(DeviceId deviceId) {
+        //FIXME: do we need a standard pipeline for virtual device?
+        Pipeliner pipeliner = new DefaultVirtualDevicePipeline();
+        pipeliner.init(deviceId, context);
+        return pipeliner;
+    }
+
+    // Processing context for initializing pipeline driver behaviours.
+    private class InnerPipelineContext implements PipelinerContext {
+        @Override
+        public ServiceDirectory directory() {
+            return serviceDirectory;
+        }
+
+        @Override
+        public FlowObjectiveStore store() {
+            return flowObjectiveStore;
+        }
+    }
+
+    /**
+     * Data class used to hold a pending flow objective that could not
+     * be processed because the associated next object was not present.
+     * Note that this pending flow objective could be a forwarding objective
+     * waiting for a next objective to complete execution. Or it could a
+     * next objective (with a different operation - remove, addToExisting, or
+     * removeFromExisting) waiting for a next objective with the same id to
+     * complete execution.
+     */
+    private class PendingFlowObjective {
+        private final DeviceId deviceId;
+        private final Objective flowObj;
+
+        public PendingFlowObjective(DeviceId deviceId, Objective flowObj) {
+            this.deviceId = deviceId;
+            this.flowObj = flowObj;
+        }
+
+        public DeviceId deviceId() {
+            return deviceId;
+        }
+
+        public Objective flowObjective() {
+            return flowObj;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(deviceId, flowObj);
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof PendingFlowObjective)) {
+                return false;
+            }
+            final PendingFlowObjective other = (PendingFlowObjective) obj;
+            if (this.deviceId.equals(other.deviceId) &&
+                    this.flowObj.equals(other.flowObj)) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * This class is a wrapping class from VirtualNetworkFlowObjectiveStore
+     * to FlowObjectiveStore for PipelinerContext.
+     */
+    private class StoreConvertor implements FlowObjectiveStore {
+
+        @Override
+        public void setDelegate(FlowObjectiveStoreDelegate delegate) {
+            virtualFlowObjectiveStore.setDelegate(networkId(), delegate);
+        }
+
+        @Override
+        public void unsetDelegate(FlowObjectiveStoreDelegate delegate) {
+            virtualFlowObjectiveStore.unsetDelegate(networkId(), delegate);
+        }
+
+        @Override
+        public boolean hasDelegate() {
+            return virtualFlowObjectiveStore.hasDelegate(networkId());
+        }
+
+        @Override
+        public void putNextGroup(Integer nextId, NextGroup group) {
+            virtualFlowObjectiveStore.putNextGroup(networkId(), nextId, group);
+        }
+
+        @Override
+        public NextGroup getNextGroup(Integer nextId) {
+            return virtualFlowObjectiveStore.getNextGroup(networkId(), nextId);
+        }
+
+        @Override
+        public NextGroup removeNextGroup(Integer nextId) {
+            return virtualFlowObjectiveStore.removeNextGroup(networkId(), nextId);
+        }
+
+        @Override
+        public Map<Integer, NextGroup> getAllGroups() {
+            return virtualFlowObjectiveStore.getAllGroups(networkId());
+        }
+
+        @Override
+        public int allocateNextId() {
+            return virtualFlowObjectiveStore.allocateNextId(networkId());
+        }
+    }
+
+    /**
+     * Simple single table pipeline abstraction for virtual networks.
+     */
+    private class DefaultVirtualDevicePipeline
+            extends AbstractHandlerBehaviour implements Pipeliner {
+
+        private final Logger log = getLogger(getClass());
+
+        private DeviceId deviceId;
+
+        private Cache<Integer, NextObjective> pendingNext;
+
+        private KryoNamespace appKryo = new KryoNamespace.Builder()
+                .register(GroupKey.class)
+                .register(DefaultGroupKey.class)
+                .register(SingleGroup.class)
+                .register(byte[].class)
+                .build("DefaultVirtualDevicePipeline");
+
+        @Override
+        public void init(DeviceId deviceId, PipelinerContext context) {
+            this.deviceId = deviceId;
+
+            pendingNext = CacheBuilder.newBuilder()
+                    .expireAfterWrite(20, TimeUnit.SECONDS)
+                    .removalListener((RemovalNotification<Integer, NextObjective> notification) -> {
+                        if (notification.getCause() == RemovalCause.EXPIRED) {
+                            notification.getValue().context()
+                                    .ifPresent(c -> c.onError(notification.getValue(),
+                                                              ObjectiveError.FLOWINSTALLATIONFAILED));
+                        }
+                    }).build();
+        }
+
+        @Override
+        public void filter(FilteringObjective filter) {
+
+            TrafficTreatment.Builder actions;
+            switch (filter.type()) {
+                case PERMIT:
+                    actions = (filter.meta() == null) ?
+                            DefaultTrafficTreatment.builder().punt() :
+                            DefaultTrafficTreatment.builder(filter.meta());
+                    break;
+                case DENY:
+                    actions = (filter.meta() == null) ?
+                            DefaultTrafficTreatment.builder() :
+                            DefaultTrafficTreatment.builder(filter.meta());
+                    actions.drop();
+                    break;
+                default:
+                    log.warn("Unknown filter type: {}", filter.type());
+                    actions = DefaultTrafficTreatment.builder().drop();
+            }
+
+            TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
+
+            filter.conditions().forEach(selector::add);
+
+            if (filter.key() != null) {
+                selector.add(filter.key());
+            }
+
+            FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                    .forDevice(deviceId)
+                    .withSelector(selector.build())
+                    .withTreatment(actions.build())
+                    .fromApp(filter.appId())
+                    .withPriority(filter.priority());
+
+            if (filter.permanent()) {
+                ruleBuilder.makePermanent();
+            } else {
+                ruleBuilder.makeTemporary(filter.timeout());
+            }
+
+            installObjective(ruleBuilder, filter);
+        }
+
+        @Override
+        public void forward(ForwardingObjective fwd) {
+            TrafficSelector selector = fwd.selector();
+
+            if (fwd.treatment() != null) {
+                // Deal with SPECIFIC and VERSATILE in the same manner.
+                FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                        .forDevice(deviceId)
+                        .withSelector(selector)
+                        .fromApp(fwd.appId())
+                        .withPriority(fwd.priority())
+                        .withTreatment(fwd.treatment());
+
+                if (fwd.permanent()) {
+                    ruleBuilder.makePermanent();
+                } else {
+                    ruleBuilder.makeTemporary(fwd.timeout());
+                }
+                installObjective(ruleBuilder, fwd);
+
+            } else {
+                NextObjective nextObjective = pendingNext.getIfPresent(fwd.nextId());
+                if (nextObjective != null) {
+                    pendingNext.invalidate(fwd.nextId());
+                    nextObjective.next().forEach(treat -> {
+                        FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                                .forDevice(deviceId)
+                                .withSelector(selector)
+                                .fromApp(fwd.appId())
+                                .withPriority(fwd.priority())
+                                .withTreatment(treat);
+
+                        if (fwd.permanent()) {
+                            ruleBuilder.makePermanent();
+                        } else {
+                            ruleBuilder.makeTemporary(fwd.timeout());
+                        }
+                        installObjective(ruleBuilder, fwd);
+                    });
+                } else {
+                    fwd.context().ifPresent(c -> c.onError(fwd,
+                                                           ObjectiveError.GROUPMISSING));
+                }
+            }
+        }
+
+        private void installObjective(FlowRule.Builder ruleBuilder, Objective objective) {
+            FlowRuleOperations.Builder flowBuilder = FlowRuleOperations.builder();
+            switch (objective.op()) {
+
+                case ADD:
+                    flowBuilder.add(ruleBuilder.build());
+                    break;
+                case REMOVE:
+                    flowBuilder.remove(ruleBuilder.build());
+                    break;
+                default:
+                    log.warn("Unknown operation {}", objective.op());
+            }
+
+            flowRuleService.apply(flowBuilder.build(new FlowRuleOperationsContext() {
+                @Override
+                public void onSuccess(FlowRuleOperations ops) {
+                    objective.context().ifPresent(context -> context.onSuccess(objective));
+                }
+
+                @Override
+                public void onError(FlowRuleOperations ops) {
+                    objective.context()
+                            .ifPresent(context ->
+                                               context.onError(objective,
+                                                                  ObjectiveError.FLOWINSTALLATIONFAILED));
+                }
+            }));
+        }
+
+        @Override
+        public void next(NextObjective nextObjective) {
+
+            pendingNext.put(nextObjective.id(), nextObjective);
+            flowObjectiveStore.putNextGroup(nextObjective.id(),
+                                            new SingleGroup(
+                                                    new DefaultGroupKey(
+                                                            appKryo.serialize(nextObjective.id()))));
+            nextObjective.context().ifPresent(context -> context.onSuccess(nextObjective));
+        }
+
+        @Override
+        public List<String> getNextMappings(NextGroup nextGroup) {
+            // Default single table pipeline does not use nextObjectives or groups
+            return null;
+        }
+
+        private class SingleGroup implements NextGroup {
+
+            private final GroupKey key;
+
+            public SingleGroup(GroupKey key) {
+                this.key = key;
+            }
+
+            public GroupKey key() {
+                return key;
+            }
+
+            @Override
+            public byte[] data() {
+                return appKryo.serialize(key);
+            }
+        }
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowRuleManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowRuleManager.java
new file mode 100644
index 0000000..f501696
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkFlowRuleManager.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.IdGenerator;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowRuleStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualFlowRuleProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualFlowRuleProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.CompletedBatchOperation;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEvent;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchRequest;
+import org.onosproject.net.flow.FlowRuleEvent;
+import org.onosproject.net.flow.FlowRuleListener;
+import org.onosproject.net.flow.FlowRuleOperation;
+import org.onosproject.net.flow.FlowRuleOperations;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.FlowRuleStoreDelegate;
+import org.onosproject.net.flow.TableStatisticsEntry;
+import org.onosproject.net.provider.ProviderId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_ADD_REQUESTED;
+import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_REMOVE_REQUESTED;
+
+/**
+ * Flow rule service implementation built on the virtual network service.
+ */
+public class VirtualNetworkFlowRuleManager
+        extends AbstractVirtualListenerManager<FlowRuleEvent, FlowRuleListener>
+        implements FlowRuleService {
+
+    private static final String VIRTUAL_FLOW_OP_TOPIC = "virtual-flow-ops-ids";
+    private static final String THREAD_GROUP_NAME = "onos/virtual-flowservice";
+    private static final String DEVICE_INSTALLER_PATTERN = "device-installer-%d";
+    private static final String OPERATION_PATTERN = "operations-%d";
+    public static final String FLOW_RULE_NULL = "FlowRule cannot be null";
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final VirtualNetworkFlowRuleStore store;
+    private final DeviceService deviceService;
+
+    protected ExecutorService deviceInstallers =
+            Executors.newFixedThreadPool(32,
+                                         groupedThreads(THREAD_GROUP_NAME,
+                                                        DEVICE_INSTALLER_PATTERN, log));
+    protected ExecutorService operationsService =
+            Executors.newFixedThreadPool(32,
+                                         groupedThreads(THREAD_GROUP_NAME,
+                                                        OPERATION_PATTERN, log));
+    private IdGenerator idGenerator;
+
+    private final Map<Long, FlowOperationsProcessor> pendingFlowOperations = new ConcurrentHashMap<>();
+
+    private VirtualProviderRegistryService providerRegistryService = null;
+    private InternalFlowRuleProviderService innerProviderService = null;
+
+    private final FlowRuleStoreDelegate storeDelegate;
+
+    /**
+     * Creates a new VirtualNetworkFlowRuleService object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkFlowRuleManager(VirtualNetworkService virtualNetworkManager,
+                                         NetworkId networkId) {
+        super(virtualNetworkManager, networkId, FlowRuleEvent.class);
+
+        store = serviceDirectory.get(VirtualNetworkFlowRuleStore.class);
+
+        idGenerator = serviceDirectory.get(CoreService.class)
+                .getIdGenerator(VIRTUAL_FLOW_OP_TOPIC + networkId().toString());
+        providerRegistryService =
+                serviceDirectory.get(VirtualProviderRegistryService.class);
+        innerProviderService = new InternalFlowRuleProviderService();
+        providerRegistryService.registerProviderService(networkId(), innerProviderService);
+
+        this.deviceService = manager.get(networkId, DeviceService.class);
+        this.storeDelegate = new InternalStoreDelegate();
+        store.setDelegate(networkId, this.storeDelegate);
+    }
+
+    @Override
+    public int getFlowRuleCount() {
+        return store.getFlowRuleCount(networkId());
+    }
+
+    @Override
+    public Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
+        return store.getFlowEntries(networkId(), deviceId);
+    }
+
+    @Override
+    public void applyFlowRules(FlowRule... flowRules) {
+        FlowRuleOperations.Builder builder = FlowRuleOperations.builder();
+        for (FlowRule flowRule : flowRules) {
+            builder.add(flowRule);
+        }
+        apply(builder.build());
+    }
+
+    @Override
+    public void purgeFlowRules(DeviceId deviceId) {
+        store.purgeFlowRule(networkId(), deviceId);
+    }
+
+    @Override
+    public void removeFlowRules(FlowRule... flowRules) {
+        FlowRuleOperations.Builder builder = FlowRuleOperations.builder();
+        for (FlowRule flowRule : flowRules) {
+            builder.remove(flowRule);
+        }
+        apply(builder.build());
+    }
+
+    @Override
+    public void removeFlowRulesById(ApplicationId id) {
+        removeFlowRules(Iterables.toArray(getFlowRulesById(id), FlowRule.class));
+    }
+
+    @Override
+    public Iterable<FlowRule> getFlowRulesById(ApplicationId id) {
+        DeviceService deviceService = manager.get(networkId(), DeviceService.class);
+
+        Set<FlowRule> flowEntries = Sets.newHashSet();
+        for (Device d : deviceService.getDevices()) {
+            for (FlowEntry flowEntry : store.getFlowEntries(networkId(), d.id())) {
+                if (flowEntry.appId() == id.id()) {
+                    flowEntries.add(flowEntry);
+                }
+            }
+        }
+        return flowEntries;
+    }
+
+    @Override
+    public Iterable<FlowEntry> getFlowEntriesById(ApplicationId id) {
+        DeviceService deviceService = manager.get(networkId(), DeviceService.class);
+
+        Set<FlowEntry> flowEntries = Sets.newHashSet();
+        for (Device d : deviceService.getDevices()) {
+            for (FlowEntry flowEntry : store.getFlowEntries(networkId(), d.id())) {
+                if (flowEntry.appId() == id.id()) {
+                    flowEntries.add(flowEntry);
+                }
+            }
+        }
+        return flowEntries;
+    }
+
+    @Override
+    public Iterable<FlowRule> getFlowRulesByGroupId(ApplicationId appId, short groupId) {
+        DeviceService deviceService = manager.get(networkId(), DeviceService.class);
+
+        Set<FlowRule> matches = Sets.newHashSet();
+        long toLookUp = ((long) appId.id() << 16) | groupId;
+        for (Device d : deviceService.getDevices()) {
+            for (FlowEntry flowEntry : store.getFlowEntries(networkId(), d.id())) {
+                if ((flowEntry.id().value() >>> 32) == toLookUp) {
+                    matches.add(flowEntry);
+                }
+            }
+        }
+        return matches;
+    }
+
+    @Override
+    public void apply(FlowRuleOperations ops) {
+        operationsService.execute(new FlowOperationsProcessor(ops));
+    }
+
+    @Override
+    public Iterable<TableStatisticsEntry> getFlowTableStatistics(DeviceId deviceId) {
+        return store.getTableStatistics(networkId(), deviceId);
+    }
+
+    private static FlowRuleBatchEntry.FlowRuleOperation mapOperationType(FlowRuleOperation.Type input) {
+        switch (input) {
+            case ADD:
+                return FlowRuleBatchEntry.FlowRuleOperation.ADD;
+            case MODIFY:
+                return FlowRuleBatchEntry.FlowRuleOperation.MODIFY;
+            case REMOVE:
+                return FlowRuleBatchEntry.FlowRuleOperation.REMOVE;
+            default:
+                throw new UnsupportedOperationException("Unknown flow rule type " + input);
+        }
+    }
+
+    private class FlowOperationsProcessor implements Runnable {
+        // Immutable
+        private final FlowRuleOperations fops;
+
+        // Mutable
+        private final List<Set<FlowRuleOperation>> stages;
+        private final Set<DeviceId> pendingDevices = new HashSet<>();
+        private boolean hasFailed = false;
+
+        FlowOperationsProcessor(FlowRuleOperations ops) {
+            this.stages = Lists.newArrayList(ops.stages());
+            this.fops = ops;
+        }
+
+        @Override
+        public synchronized void run() {
+            if (!stages.isEmpty()) {
+                process(stages.remove(0));
+            } else if (!hasFailed) {
+                fops.callback().onSuccess(fops);
+            }
+        }
+
+        private void process(Set<FlowRuleOperation> ops) {
+            Multimap<DeviceId, FlowRuleBatchEntry> perDeviceBatches = ArrayListMultimap.create();
+
+            for (FlowRuleOperation op : ops) {
+                perDeviceBatches.put(op.rule().deviceId(),
+                                     new FlowRuleBatchEntry(mapOperationType(op.type()), op.rule()));
+            }
+            pendingDevices.addAll(perDeviceBatches.keySet());
+
+            for (DeviceId deviceId : perDeviceBatches.keySet()) {
+                long id = idGenerator.getNewId();
+                final FlowRuleBatchOperation b = new FlowRuleBatchOperation(perDeviceBatches.get(deviceId),
+                                                                            deviceId, id);
+                pendingFlowOperations.put(id, this);
+                deviceInstallers.execute(() -> store.storeBatch(networkId(), b));
+            }
+        }
+
+        synchronized void satisfy(DeviceId devId) {
+            pendingDevices.remove(devId);
+            if (pendingDevices.isEmpty()) {
+                operationsService.execute(this);
+            }
+        }
+
+        synchronized void fail(DeviceId devId, Set<? extends FlowRule> failures) {
+            hasFailed = true;
+            pendingDevices.remove(devId);
+            if (pendingDevices.isEmpty()) {
+                operationsService.execute(this);
+            }
+
+            FlowRuleOperations.Builder failedOpsBuilder = FlowRuleOperations.builder();
+            failures.forEach(failedOpsBuilder::add);
+
+            fops.callback().onError(failedOpsBuilder.build());
+        }
+    }
+
+    private final class InternalFlowRuleProviderService
+            extends AbstractVirtualProviderService<VirtualFlowRuleProvider>
+            implements VirtualFlowRuleProviderService {
+
+        final Map<FlowEntry, Long> firstSeen = Maps.newConcurrentMap();
+        final Map<FlowEntry, Long> lastSeen = Maps.newConcurrentMap();
+
+        private InternalFlowRuleProviderService() {
+            //TODO: find a proper virtual provider.
+            Set<ProviderId> providerIds =
+                    providerRegistryService.getProvidersByService(this);
+            ProviderId providerId = providerIds.stream().findFirst().get();
+            VirtualFlowRuleProvider provider = (VirtualFlowRuleProvider)
+                    providerRegistryService.getProvider(providerId);
+            setProvider(provider);
+        }
+
+        @Override
+        public void flowRemoved(FlowEntry flowEntry) {
+            checkNotNull(flowEntry, FLOW_RULE_NULL);
+            checkValidity();
+
+            lastSeen.remove(flowEntry);
+            firstSeen.remove(flowEntry);
+            FlowEntry stored = store.getFlowEntry(networkId(), flowEntry);
+            if (stored == null) {
+                log.debug("Rule already evicted from store: {}", flowEntry);
+                return;
+            }
+            if (flowEntry.reason() == FlowEntry.FlowRemoveReason.HARD_TIMEOUT) {
+                ((DefaultFlowEntry) stored).setState(FlowEntry.FlowEntryState.REMOVED);
+            }
+
+            //FIXME: obtains provider from devices providerId()
+            FlowRuleEvent event = null;
+            switch (stored.state()) {
+                case ADDED:
+                case PENDING_ADD:
+                    provider().applyFlowRule(networkId(), stored);
+                    break;
+                case PENDING_REMOVE:
+                case REMOVED:
+                    event = store.removeFlowRule(networkId(), stored);
+                    break;
+                default:
+                    break;
+
+            }
+            if (event != null) {
+                log.debug("Flow {} removed", flowEntry);
+                post(event);
+            }
+        }
+
+        private void flowMissing(FlowEntry flowRule) {
+            checkNotNull(flowRule, FLOW_RULE_NULL);
+            checkValidity();
+
+            FlowRuleEvent event = null;
+            switch (flowRule.state()) {
+                case PENDING_REMOVE:
+                case REMOVED:
+                    event = store.removeFlowRule(networkId(), flowRule);
+                    break;
+                case ADDED:
+                case PENDING_ADD:
+                    event = store.pendingFlowRule(networkId(), flowRule);
+
+                    try {
+                        provider().applyFlowRule(networkId(), flowRule);
+                    } catch (UnsupportedOperationException e) {
+                        log.warn(e.getMessage());
+                        if (flowRule instanceof DefaultFlowEntry) {
+                            //FIXME modification of "stored" flow entry outside of store
+                            ((DefaultFlowEntry) flowRule).setState(FlowEntry.FlowEntryState.FAILED);
+                        }
+                    }
+                    break;
+                default:
+                    log.debug("Flow {} has not been installed.", flowRule);
+            }
+
+            if (event != null) {
+                log.debug("Flow {} removed", flowRule);
+                post(event);
+            }
+        }
+
+        private void extraneousFlow(FlowRule flowRule) {
+            checkNotNull(flowRule, FLOW_RULE_NULL);
+            checkValidity();
+
+            provider().removeFlowRule(networkId(), flowRule);
+            log.debug("Flow {} is on switch but not in store.", flowRule);
+        }
+
+        private void flowAdded(FlowEntry flowEntry) {
+            checkNotNull(flowEntry, FLOW_RULE_NULL);
+
+            if (checkRuleLiveness(flowEntry, store.getFlowEntry(networkId(), flowEntry))) {
+                FlowRuleEvent event = store.addOrUpdateFlowRule(networkId(), flowEntry);
+                if (event == null) {
+                    log.debug("No flow store event generated.");
+                } else {
+                    log.trace("Flow {} {}", flowEntry, event.type());
+                    post(event);
+                }
+            } else {
+                log.debug("Removing flow rules....");
+                removeFlowRules(flowEntry);
+            }
+        }
+
+        private boolean checkRuleLiveness(FlowEntry swRule, FlowEntry storedRule) {
+            if (storedRule == null) {
+                return false;
+            }
+            if (storedRule.isPermanent()) {
+                return true;
+            }
+
+            final long timeout = storedRule.timeout() * 1000L;
+            final long currentTime = System.currentTimeMillis();
+
+            // Checking flow with hardTimeout
+            if (storedRule.hardTimeout() != 0) {
+                if (!firstSeen.containsKey(storedRule)) {
+                    // First time rule adding
+                    firstSeen.put(storedRule, currentTime);
+                } else {
+                    Long first = firstSeen.get(storedRule);
+                    final long hardTimeout = storedRule.hardTimeout() * 1000L;
+                    if ((currentTime - first) > hardTimeout) {
+                        return false;
+                    }
+                }
+            }
+
+            if (storedRule.packets() != swRule.packets()) {
+                lastSeen.put(storedRule, currentTime);
+                return true;
+            }
+            if (!lastSeen.containsKey(storedRule)) {
+                // checking for the first time
+                lastSeen.put(storedRule, storedRule.lastSeen());
+                // Use following if lastSeen attr. was removed.
+                //lastSeen.put(storedRule, currentTime);
+            }
+            Long last = lastSeen.get(storedRule);
+
+            // concurrently removed? let the liveness check fail
+            return last != null && (currentTime - last) <= timeout;
+        }
+
+        @Override
+        public void pushFlowMetrics(DeviceId deviceId, Iterable<FlowEntry> flowEntries) {
+            pushFlowMetricsInternal(deviceId, flowEntries, true);
+        }
+
+        @Override
+        public void pushFlowMetricsWithoutFlowMissing(DeviceId deviceId, Iterable<FlowEntry> flowEntries) {
+            pushFlowMetricsInternal(deviceId, flowEntries, false);
+        }
+
+        private void pushFlowMetricsInternal(DeviceId deviceId, Iterable<FlowEntry> flowEntries,
+                                             boolean useMissingFlow) {
+            Map<FlowEntry, FlowEntry> storedRules = Maps.newHashMap();
+            store.getFlowEntries(networkId(), deviceId).forEach(f -> storedRules.put(f, f));
+
+            for (FlowEntry rule : flowEntries) {
+                try {
+                    FlowEntry storedRule = storedRules.remove(rule);
+                    if (storedRule != null) {
+                        if (storedRule.id().equals(rule.id())) {
+                            // we both have the rule, let's update some info then.
+                            flowAdded(rule);
+                        } else {
+                            // the two rules are not an exact match - remove the
+                            // switch's rule and install our rule
+                            extraneousFlow(rule);
+                            flowMissing(storedRule);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.debug("Can't process added or extra rule {}", e.getMessage());
+                }
+            }
+
+            // DO NOT reinstall
+            if (useMissingFlow) {
+                for (FlowEntry rule : storedRules.keySet()) {
+                    try {
+                        // there are rules in the store that aren't on the switch
+                        log.debug("Adding rule in store, but not on switch {}", rule);
+                        flowMissing(rule);
+                    } catch (Exception e) {
+                        log.debug("Can't add missing flow rule:", e);
+                    }
+                }
+            }
+        }
+
+        public void batchOperationCompleted(long batchId, CompletedBatchOperation operation) {
+            store.batchOperationComplete(networkId(), FlowRuleBatchEvent.completed(
+                    new FlowRuleBatchRequest(batchId, Collections.emptySet()),
+                    operation
+            ));
+        }
+
+        @Override
+        public void pushTableStatistics(DeviceId deviceId,
+                                        List<TableStatisticsEntry> tableStats) {
+            store.updateTableStatistics(networkId(), deviceId, tableStats);
+        }
+    }
+
+    // Store delegate to re-post events emitted from the store.
+    private class InternalStoreDelegate implements FlowRuleStoreDelegate {
+
+        // TODO: Right now we only dispatch events at individual flowEntry level.
+        // It may be more efficient for also dispatch events as a batch.
+        @Override
+        public void notify(FlowRuleBatchEvent event) {
+            final FlowRuleBatchRequest request = event.subject();
+            switch (event.type()) {
+                case BATCH_OPERATION_REQUESTED:
+                    // Request has been forwarded to MASTER Node, and was
+                    request.ops().forEach(
+                            op -> {
+                                switch (op.operator()) {
+                                    case ADD:
+                                        post(new FlowRuleEvent(RULE_ADD_REQUESTED, op.target()));
+                                        break;
+                                    case REMOVE:
+                                        post(new FlowRuleEvent(RULE_REMOVE_REQUESTED, op.target()));
+                                        break;
+                                    case MODIFY:
+                                        //TODO: do something here when the time comes.
+                                        break;
+                                    default:
+                                        log.warn("Unknown flow operation operator: {}", op.operator());
+                                }
+                            }
+                    );
+
+                    DeviceId deviceId = event.deviceId();
+                    FlowRuleBatchOperation batchOperation = request.asBatchOperation(deviceId);
+
+                    VirtualFlowRuleProvider provider = innerProviderService.provider();
+                    if (provider != null) {
+                        provider.executeBatch(networkId, batchOperation);
+                    }
+
+                    break;
+
+                case BATCH_OPERATION_COMPLETED:
+                    FlowOperationsProcessor fops = pendingFlowOperations.remove(
+                            event.subject().batchId());
+                    if (fops == null) {
+                       return;
+                    }
+
+                    if (event.result().isSuccess()) {
+                            fops.satisfy(event.deviceId());
+                    } else {
+                        fops.fail(event.deviceId(), event.result().failedItems());
+                    }
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkGroupManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkGroupManager.java
new file mode 100644
index 0000000..5fed4a8
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkGroupManager.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onosproject.core.ApplicationId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkGroupStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualGroupProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualGroupProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupKey;
+import org.onosproject.net.group.GroupListener;
+import org.onosproject.net.group.GroupOperation;
+import org.onosproject.net.group.GroupOperations;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.net.group.GroupStoreDelegate;
+import org.onosproject.net.provider.ProviderId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Group service implementation built on the virtual network service.
+ */
+public class VirtualNetworkGroupManager
+        extends AbstractVirtualListenerManager<GroupEvent, GroupListener>
+        implements GroupService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final VirtualNetworkGroupStore store;
+
+    private VirtualProviderRegistryService providerRegistryService = null;
+    private VirtualGroupProviderService innerProviderService;
+    private InternalStoreDelegate storeDelegate;
+    private DeviceService deviceService;
+
+    //TODO: make this configurable
+    private boolean purgeOnDisconnection = false;
+
+    public VirtualNetworkGroupManager(VirtualNetworkService manager, NetworkId networkId) {
+        super(manager, networkId, GroupEvent.class);
+
+        store = serviceDirectory.get(VirtualNetworkGroupStore.class);
+        deviceService = manager.get(networkId, DeviceService.class);
+
+        providerRegistryService =
+                serviceDirectory.get(VirtualProviderRegistryService.class);
+        innerProviderService = new InternalGroupProviderService();
+        providerRegistryService.registerProviderService(networkId(), innerProviderService);
+
+        this.storeDelegate = new InternalStoreDelegate();
+        store.setDelegate(networkId, this.storeDelegate);
+
+        log.info("Started");
+    }
+
+    @Override
+    public void addGroup(GroupDescription groupDesc) {
+        store.storeGroupDescription(networkId(), groupDesc);
+    }
+
+    @Override
+    public Group getGroup(DeviceId deviceId, GroupKey appCookie) {
+        return store.getGroup(networkId(), deviceId, appCookie);
+    }
+
+    @Override
+    public void addBucketsToGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+                                  GroupKey newCookie, ApplicationId appId) {
+        store.updateGroupDescription(networkId(),
+                                     deviceId,
+                                     oldCookie,
+                                     VirtualNetworkGroupStore.UpdateType.ADD,
+                                     buckets,
+                                     newCookie);
+    }
+
+    @Override
+    public void removeBucketsFromGroup(DeviceId deviceId, GroupKey oldCookie,
+                                       GroupBuckets buckets, GroupKey newCookie,
+                                       ApplicationId appId) {
+        store.updateGroupDescription(networkId(),
+                                     deviceId,
+                                     oldCookie,
+                                     VirtualNetworkGroupStore.UpdateType.REMOVE,
+                                     buckets,
+                                     newCookie);
+
+    }
+
+    @Override
+    public void setBucketsForGroup(DeviceId deviceId,
+                                   GroupKey oldCookie,
+                                   GroupBuckets buckets,
+                                   GroupKey newCookie,
+                                   ApplicationId appId) {
+        store.updateGroupDescription(networkId(),
+                                     deviceId,
+                                     oldCookie,
+                                     VirtualNetworkGroupStore.UpdateType.SET,
+                                     buckets,
+                                     newCookie);
+    }
+
+    @Override
+    public void purgeGroupEntries(DeviceId deviceId) {
+        store.purgeGroupEntry(networkId(), deviceId);
+    }
+
+    @Override
+    public void purgeGroupEntries() {
+        store.purgeGroupEntries(networkId());
+    }
+
+    @Override
+    public void removeGroup(DeviceId deviceId, GroupKey appCookie, ApplicationId appId) {
+        store.deleteGroupDescription(networkId(), deviceId, appCookie);
+    }
+
+    @Override
+    public Iterable<Group> getGroups(DeviceId deviceId, ApplicationId appId) {
+        return store.getGroups(networkId(), deviceId);
+    }
+
+    @Override
+    public Iterable<Group> getGroups(DeviceId deviceId) {
+        return store.getGroups(networkId(), deviceId);
+    }
+
+    private class InternalGroupProviderService
+            extends AbstractVirtualProviderService<VirtualGroupProvider>
+            implements VirtualGroupProviderService {
+
+        protected InternalGroupProviderService() {
+            Set<ProviderId> providerIds =
+                    providerRegistryService.getProvidersByService(this);
+            ProviderId providerId = providerIds.stream().findFirst().get();
+            VirtualGroupProvider provider = (VirtualGroupProvider)
+                    providerRegistryService.getProvider(providerId);
+            setProvider(provider);
+        }
+
+        @Override
+        public void groupOperationFailed(DeviceId deviceId,
+                                         GroupOperation operation) {
+            store.groupOperationFailed(networkId(), deviceId, operation);
+        }
+
+        @Override
+        public void pushGroupMetrics(DeviceId deviceId, Collection<Group> groupEntries) {
+            log.trace("Received group metrics from device {}", deviceId);
+            checkValidity();
+            store.pushGroupMetrics(networkId(), deviceId, groupEntries);
+        }
+
+        @Override
+        public void notifyOfFailovers(Collection<Group> failoverGroups) {
+            store.notifyOfFailovers(networkId(), failoverGroups);
+        }
+    }
+
+    private class InternalStoreDelegate implements GroupStoreDelegate {
+        @Override
+        public void notify(GroupEvent event) {
+            final Group group = event.subject();
+            VirtualGroupProvider groupProvider = innerProviderService.provider();
+            GroupOperations groupOps = null;
+            switch (event.type()) {
+                case GROUP_ADD_REQUESTED:
+                    log.debug("GROUP_ADD_REQUESTED for Group {} on device {}",
+                              group.id(), group.deviceId());
+                    GroupOperation groupAddOp = GroupOperation.
+                            createAddGroupOperation(group.id(),
+                                                    group.type(),
+                                                    group.buckets());
+                    groupOps = new GroupOperations(
+                            Collections.singletonList(groupAddOp));
+                    groupProvider.performGroupOperation(networkId(), group.deviceId(),
+                                                        groupOps);
+                    break;
+
+                case GROUP_UPDATE_REQUESTED:
+                    log.debug("GROUP_UPDATE_REQUESTED for Group {} on device {}",
+                              group.id(), group.deviceId());
+                    GroupOperation groupModifyOp = GroupOperation.
+                            createModifyGroupOperation(group.id(),
+                                                       group.type(),
+                                                       group.buckets());
+                    groupOps = new GroupOperations(
+                            Collections.singletonList(groupModifyOp));
+                    groupProvider.performGroupOperation(networkId(), group.deviceId(),
+                                                        groupOps);
+                    break;
+
+                case GROUP_REMOVE_REQUESTED:
+                    log.debug("GROUP_REMOVE_REQUESTED for Group {} on device {}",
+                              group.id(), group.deviceId());
+                    GroupOperation groupDeleteOp = GroupOperation.
+                            createDeleteGroupOperation(group.id(),
+                                                       group.type());
+                    groupOps = new GroupOperations(
+                            Collections.singletonList(groupDeleteOp));
+                    groupProvider.performGroupOperation(networkId(), group.deviceId(),
+                                                        groupOps);
+                    break;
+
+                case GROUP_ADDED:
+                case GROUP_UPDATED:
+                case GROUP_REMOVED:
+                case GROUP_ADD_FAILED:
+                case GROUP_UPDATE_FAILED:
+                case GROUP_REMOVE_FAILED:
+                case GROUP_BUCKET_FAILOVER:
+                    post(event);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            switch (event.type()) {
+                case DEVICE_REMOVED:
+                case DEVICE_AVAILABILITY_CHANGED:
+                    DeviceId deviceId = event.subject().id();
+                    if (!deviceService.isAvailable(deviceId)) {
+                        log.debug("Device {} became un available; clearing initial audit status",
+                                  event.type(), event.subject().id());
+                        store.deviceInitialAuditCompleted(networkId(), event.subject().id(), false);
+
+                        if (purgeOnDisconnection) {
+                            store.purgeGroupEntry(networkId(), deviceId);
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkHostManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkHostManager.java
new file mode 100644
index 0000000..5d14d4a
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkHostManager.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Host service implementation built on the virtual network service.
+ */
+public class VirtualNetworkHostManager
+        extends AbstractVirtualListenerManager<HostEvent, HostListener>
+        implements HostService {
+
+    private static final String HOST_NULL = "Host ID cannot be null";
+
+    /**
+     * Creates a new virtual network host service object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkHostManager(VirtualNetworkService virtualNetworkManager,
+                                     NetworkId networkId) {
+        super(virtualNetworkManager, networkId, HostEvent.class);
+    }
+
+
+    @Override
+    public int getHostCount() {
+        return manager.getVirtualHosts(this.networkId()).size();
+    }
+
+    @Override
+    public Iterable<Host> getHosts() {
+        return getHostsColl();
+    }
+
+    @Override
+    public Host getHost(HostId hostId) {
+        checkNotNull(hostId, HOST_NULL);
+        Optional<VirtualHost> foundHost =
+                manager.getVirtualHosts(this.networkId())
+                .stream()
+                .filter(host -> hostId.equals(host.id()))
+                .findFirst();
+        if (foundHost.isPresent()) {
+            return foundHost.get();
+        }
+        return null;
+    }
+
+    /**
+     * Gets a collection of virtual hosts.
+     *
+     * @return collection of virtual hosts.
+     */
+    private Collection<Host> getHostsColl() {
+        return manager.getVirtualHosts(this.networkId())
+                .stream().collect(Collectors.toSet());
+    }
+
+    /**
+     * Filters specified collection.
+     *
+     * @param collection collection of hosts to filter
+     * @param predicate condition to filter on
+     * @return collection of virtual hosts that satisfy the filter condition
+     */
+    private Set<Host> filter(Collection<Host> collection, Predicate<Host> predicate) {
+        return collection.stream().filter(predicate).collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Host> getHostsByVlan(VlanId vlanId) {
+        checkNotNull(vlanId, "VLAN identifier cannot be null");
+        return filter(getHostsColl(), host -> Objects.equals(host.vlan(), vlanId));
+    }
+
+    @Override
+    public Set<Host> getHostsByMac(MacAddress mac) {
+        checkNotNull(mac, "MAC address cannot be null");
+        return filter(getHostsColl(), host -> Objects.equals(host.mac(), mac));
+    }
+
+    @Override
+    public Set<Host> getHostsByIp(IpAddress ip) {
+        checkNotNull(ip, "IP address cannot be null");
+        return filter(getHostsColl(), host -> host.ipAddresses().contains(ip));
+    }
+
+    @Override
+    public Set<Host> getConnectedHosts(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, "Connect point cannot be null");
+        return filter(getHostsColl(), host -> host.location().equals(connectPoint));
+    }
+
+    @Override
+    public Set<Host> getConnectedHosts(DeviceId deviceId) {
+        checkNotNull(deviceId, "Device identifier cannot be null");
+        return filter(getHostsColl(), host -> host.location().deviceId().equals(deviceId));
+    }
+
+    @Override
+    public void startMonitoringIp(IpAddress ip) {
+        //TODO check what needs to be done here
+    }
+
+    @Override
+    public void stopMonitoringIp(IpAddress ip) {
+        //TODO check what needs to be done here
+    }
+
+    @Override
+    public void requestMac(IpAddress ip) {
+        //TODO check what needs to be done here
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkIntentManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkIntentManager.java
new file mode 100644
index 0000000..f4c1137
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkIntentManager.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onlab.util.Tools;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntentStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkStore;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.incubator.net.virtual.VnetService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualFinalIntentProcessPhase;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentInstallCoordinator;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentAccumulator;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentCompilerRegistry;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentInstallerRegistry;
+import org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualIntentProcessPhase;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentSkipped;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Port;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentBatchDelegate;
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentListener;
+import org.onosproject.net.intent.IntentStoreDelegate;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.IntentState;
+import org.onosproject.net.intent.Key;
+import org.onosproject.net.resource.ResourceConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.*;
+import static org.onlab.util.BoundedThreadPool.newFixedThreadPool;
+import static org.onlab.util.BoundedThreadPool.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualIntentProcessPhase.newInitialPhase;
+import static org.onosproject.net.intent.IntentState.FAILED;
+
+/**
+ * Intent service implementation built on the virtual network service.
+ */
+public class VirtualNetworkIntentManager
+        extends AbstractVirtualListenerManager<IntentEvent, IntentListener>
+        implements IntentService, VnetService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final int DEFAULT_NUM_THREADS = 12;
+    private int numThreads = DEFAULT_NUM_THREADS;
+
+    private static final String NETWORK_ID_NULL = "Network ID cannot be null";
+    private static final String DEVICE_NULL = "Device cannot be null";
+    private static final String INTENT_NULL = "Intent cannot be null";
+    private static final String KEY_NULL = "Key cannot be null";
+    private static final String APP_ID_NULL = "Intent app identifier cannot be null";
+    private static final String INTENT_KEY_NULL = "Intent key cannot be null";
+    private static final String CP_NULL = "Connect Point cannot be null";
+
+    //FIXME: Tracker service for vnet.
+
+    //ONOS core services
+    protected VirtualNetworkStore virtualNetworkStore;
+    protected VirtualNetworkIntentStore intentStore;
+
+    //Virtual network services
+    protected GroupService groupService;
+
+    private final IntentBatchDelegate batchDelegate = new InternalBatchDelegate();
+    private final InternalIntentProcessor processor = new InternalIntentProcessor();
+    private final IntentStoreDelegate delegate = new InternalStoreDelegate();
+    private final VirtualIntentCompilerRegistry compilerRegistry =
+            VirtualIntentCompilerRegistry.getInstance();
+    private final VirtualIntentInstallerRegistry installerRegistry =
+            VirtualIntentInstallerRegistry.getInstance();
+    private final VirtualIntentAccumulator accumulator =
+            new VirtualIntentAccumulator(batchDelegate);
+
+    private VirtualIntentInstallCoordinator installCoordinator;
+    private ExecutorService batchExecutor;
+    private ExecutorService workerExecutor;
+
+    /**
+     * Creates a new VirtualNetworkIntentService object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkIntentManager(VirtualNetworkService virtualNetworkManager,
+                                       NetworkId networkId) {
+
+        super(virtualNetworkManager, networkId, IntentEvent.class);
+
+        this.virtualNetworkStore = serviceDirectory.get(VirtualNetworkStore.class);
+        this.intentStore = serviceDirectory.get(VirtualNetworkIntentStore.class);
+
+        this.groupService = manager.get(networkId, GroupService.class);
+
+        intentStore.setDelegate(networkId, delegate);
+        batchExecutor = newSingleThreadExecutor(groupedThreads("onos/intent", "batch", log));
+        workerExecutor = newFixedThreadPool(numThreads, groupedThreads("onos/intent", "worker-%d", log));
+
+        installCoordinator = new VirtualIntentInstallCoordinator(networkId, installerRegistry, intentStore);
+        log.info("Started");
+
+    }
+
+    @Override
+    public void submit(Intent intent) {
+        checkNotNull(intent, INTENT_NULL);
+        checkState(intent instanceof VirtualNetworkIntent, "Only VirtualNetworkIntent is supported.");
+        checkArgument(validateIntent((VirtualNetworkIntent) intent), "Invalid Intent");
+
+        IntentData data = IntentData.submit(intent);
+        intentStore.addPending(networkId, data);
+    }
+
+    /**
+     * Returns true if the virtual network intent is valid.
+     *
+     * @param intent virtual network intent
+     * @return true if intent is valid
+     */
+    private boolean validateIntent(VirtualNetworkIntent intent) {
+        checkNotNull(intent, INTENT_NULL);
+        checkNotNull(intent.networkId(), NETWORK_ID_NULL);
+        checkNotNull(intent.appId(), APP_ID_NULL);
+        checkNotNull(intent.key(), INTENT_KEY_NULL);
+        ConnectPoint ingressPoint = intent.ingressPoint();
+        ConnectPoint egressPoint = intent.egressPoint();
+
+        return (validateConnectPoint(ingressPoint) && validateConnectPoint(egressPoint));
+    }
+
+    /**
+     * Returns true if the connect point is valid.
+     *
+     * @param connectPoint connect point
+     * @return true if connect point is valid
+     */
+    private boolean validateConnectPoint(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CP_NULL);
+        Port port = getPort(connectPoint.deviceId(), connectPoint.port());
+        return port != null;
+    }
+
+    /**
+     * Returns the virtual port for the given device identifier and port number.
+     *
+     * @param deviceId   virtual device identifier
+     * @param portNumber virtual port number
+     * @return virtual port
+     */
+    private Port getPort(DeviceId deviceId, PortNumber portNumber) {
+        checkNotNull(deviceId, DEVICE_NULL);
+
+        Optional<VirtualPort> foundPort = manager.getVirtualPorts(this.networkId(), deviceId)
+                .stream()
+                .filter(port -> port.number().equals(portNumber))
+                .findFirst();
+        if (foundPort.isPresent()) {
+            return foundPort.get();
+        }
+        return null;
+    }
+
+    @Override
+    public void withdraw(Intent intent) {
+        checkNotNull(intent, INTENT_NULL);
+        IntentData data = IntentData.withdraw(intent);
+        intentStore.addPending(networkId, data);
+    }
+
+    @Override
+    public void purge(Intent intent) {
+        checkNotNull(intent, INTENT_NULL);
+
+        IntentData data = IntentData.purge(intent);
+        intentStore.addPending(networkId, data);
+
+        // remove associated group if there is one
+        // FIXME: Remove P2P intent for vnets
+    }
+
+    @Override
+    public Intent getIntent(Key key) {
+        checkNotNull(key, KEY_NULL);
+        return intentStore.getIntent(networkId, key);
+    }
+
+    @Override
+    public Iterable<Intent> getIntents() {
+        return intentStore.getIntents(networkId);
+    }
+
+    @Override
+    public void addPending(IntentData intentData) {
+        checkNotNull(intentData, INTENT_NULL);
+        //TODO we might consider further checking / assertions
+        intentStore.addPending(networkId, intentData);
+    }
+
+    @Override
+    public Iterable<IntentData> getIntentData() {
+        return intentStore.getIntentData(networkId, false, 0);
+    }
+
+    @Override
+    public long getIntentCount() {
+        return intentStore.getIntentCount(networkId);
+    }
+
+    @Override
+    public IntentState getIntentState(Key intentKey) {
+        checkNotNull(intentKey, KEY_NULL);
+        return intentStore.getIntentState(networkId, intentKey);
+    }
+
+    @Override
+    public List<Intent> getInstallableIntents(Key intentKey) {
+        return intentStore.getInstallableIntents(networkId, intentKey);
+    }
+
+    @Override
+    public boolean isLocal(Key intentKey) {
+        return intentStore.isMaster(networkId, intentKey);
+    }
+
+    @Override
+    public Iterable<Intent> getPending() {
+        return intentStore.getPending(networkId);
+    }
+
+    // Store delegate to re-post events emitted from the store.
+    private class InternalStoreDelegate implements IntentStoreDelegate {
+        @Override
+        public void notify(IntentEvent event) {
+            post(event);
+            switch (event.type()) {
+                case WITHDRAWN:
+                    //FIXME: release resources
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void process(IntentData data) {
+            accumulator.add(data);
+        }
+
+        @Override
+        public void onUpdate(IntentData intentData) {
+            //FIXME: track intent
+        }
+
+        private void releaseResources(Intent intent) {
+            // If a resource group is set on the intent, the resource consumer is
+            // set equal to it. Otherwise it's set to the intent key
+            ResourceConsumer resourceConsumer =
+                    intent.resourceGroup() != null ? intent.resourceGroup() : intent.key();
+
+            // By default the resource doesn't get released
+            boolean removeResource = false;
+
+            if (intent.resourceGroup() == null) {
+                // If the intent doesn't have a resource group, it means the
+                // resource was registered using the intent key, so it can be
+                // released
+                removeResource = true;
+            } else {
+                // When a resource group is set, we make sure there are no other
+                // intents using the same resource group, before deleting the
+                // related resources.
+                Long remainingIntents =
+                        Tools.stream(intentStore.getIntents(networkId))
+                                .filter(i -> {
+                                    return i.resourceGroup() != null
+                                            && i.resourceGroup().equals(intent.resourceGroup());
+                                })
+                                .count();
+                if (remainingIntents == 0) {
+                    removeResource = true;
+                }
+            }
+
+            if (removeResource) {
+                // Release resources allocated to withdrawn intent
+                // FIXME: confirm resources are released
+            }
+        }
+    }
+
+    private class InternalBatchDelegate implements IntentBatchDelegate {
+        @Override
+        public void execute(Collection<IntentData> operations) {
+            log.debug("Execute {} operation(s).", operations.size());
+            log.trace("Execute operations: {}", operations);
+
+            // batchExecutor is single-threaded, so only one batch is in flight at a time
+            CompletableFuture.runAsync(() -> {
+                // process intent until the phase reaches one of the final phases
+                List<CompletableFuture<IntentData>> futures = operations.stream()
+                        .map(data -> {
+                            log.debug("Start processing of {} {}@{}", data.request(), data.key(), data.version());
+                            return data;
+                        })
+                        .map(x -> CompletableFuture.completedFuture(x)
+                                .thenApply(VirtualNetworkIntentManager.this::createInitialPhase)
+                                .thenApplyAsync(VirtualIntentProcessPhase::process, workerExecutor)
+                                .thenApply(VirtualFinalIntentProcessPhase::data)
+                                .exceptionally(e -> {
+                                    // When the future fails, we update the Intent to simulate the failure of
+                                    // the installation/withdrawal phase and we save in the current map. In
+                                    // the next round the CleanUp Thread will pick this Intent again.
+                                    log.warn("Future failed", e);
+                                    log.warn("Intent {} - state {} - request {}",
+                                             x.key(), x.state(), x.request());
+                                    switch (x.state()) {
+                                        case INSTALL_REQ:
+                                        case INSTALLING:
+                                        case WITHDRAW_REQ:
+                                        case WITHDRAWING:
+                                            // TODO should we swtich based on current
+                                            IntentData current = intentStore.getIntentData(networkId, x.key());
+                                            return IntentData.nextState(current, FAILED);
+                                        default:
+                                            return null;
+                                    }
+                                }))
+                        .collect(Collectors.toList());
+
+                // write multiple data to store in order
+                intentStore.batchWrite(networkId, Tools.allOf(futures).join().stream()
+                                         .filter(Objects::nonNull)
+                                         .collect(Collectors.toList()));
+            }, batchExecutor).exceptionally(e -> {
+                log.error("Error submitting batches:", e);
+                // FIXME incomplete Intents should be cleaned up
+                //       (transition to FAILED, etc.)
+
+                // the batch has failed
+                // TODO: maybe we should do more?
+                log.error("Walk the plank, matey...");
+                return null;
+            }).thenRun(accumulator::ready);
+
+        }
+    }
+
+    private VirtualIntentProcessPhase createInitialPhase(IntentData data) {
+        IntentData pending = intentStore.getPendingData(networkId, data.key());
+        if (pending == null || pending.version().isNewerThan(data.version())) {
+            /*
+                If the pending map is null, then this intent was compiled by a
+                previous batch iteration, so we can skip it.
+                If the pending map has a newer request, it will get compiled as
+                part of the next batch, so we can skip it.
+             */
+            return VirtualIntentSkipped.getPhase();
+        }
+        IntentData current = intentStore.getIntentData(networkId, data.key());
+        return newInitialPhase(networkId, processor, data, current);
+    }
+
+    private class InternalIntentProcessor implements VirtualIntentProcessor {
+        @Override
+        public List<Intent> compile(NetworkId networkId,
+                                    Intent intent,
+                                    List<Intent> previousInstallables) {
+            return compilerRegistry.compile(networkId, intent, previousInstallables);
+        }
+
+        @Override
+        public void apply(NetworkId networkId,
+                          Optional<IntentData> toUninstall,
+                          Optional<IntentData> toInstall) {
+
+            installCoordinator.installIntents(toUninstall, toInstall);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkLinkManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkLinkManager.java
new file mode 100644
index 0000000..1a1c2bf
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkLinkManager.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualLink;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
+import org.onosproject.net.link.LinkService;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Link service implementation built on the virtual network service.
+ */
+public class VirtualNetworkLinkManager
+        extends AbstractVirtualListenerManager<LinkEvent, LinkListener>
+        implements LinkService {
+
+    private static final String DEVICE_NULL = "Device cannot be null";
+    private static final String CONNECT_POINT_NULL = "Connect point cannot be null";
+
+    /**
+     * Creates a new VirtualNetworkLinkService object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual networkIdentifier
+     */
+    public VirtualNetworkLinkManager(VirtualNetworkService virtualNetworkManager,
+                                     NetworkId networkId) {
+        super(virtualNetworkManager, networkId, LinkEvent.class);
+    }
+
+    @Override
+    public int getLinkCount() {
+        return manager.getVirtualLinks(this.networkId()).size();
+    }
+
+    @Override
+    public Iterable<Link> getLinks() {
+        return manager.getVirtualLinks(this.networkId())
+                .stream().collect(Collectors.toSet());
+    }
+
+    @Override
+    public Iterable<Link> getActiveLinks() {
+
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (link.state().equals(Link.State.ACTIVE)))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getDeviceLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (deviceId.equals(link.src().elementId()) ||
+                        deviceId.equals(link.dst().elementId())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getDeviceEgressLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (deviceId.equals(link.dst().elementId())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getDeviceIngressLinks(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (deviceId.equals(link.src().elementId())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (connectPoint.equals(link.src()) ||
+                        connectPoint.equals(link.dst())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getEgressLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (connectPoint.equals(link.dst())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Set<Link> getIngressLinks(ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECT_POINT_NULL);
+        return manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (connectPoint.equals(link.src())))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Link getLink(ConnectPoint src, ConnectPoint dst) {
+        checkNotNull(src, CONNECT_POINT_NULL);
+        checkNotNull(dst, CONNECT_POINT_NULL);
+        Optional<VirtualLink> foundLink =  manager.getVirtualLinks(this.networkId())
+                .stream()
+                .filter(link -> (src.equals(link.src()) &&
+                        dst.equals(link.dst())))
+                .findFirst();
+
+        if (foundLink.isPresent()) {
+            return foundLink.get();
+        }
+        return null;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkManager.java
new file mode 100644
index 0000000..3b4ed8c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkManager.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.tunnel.TunnelId;
+import org.onosproject.incubator.net.virtual.DefaultVirtualLink;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualLink;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkEvent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkListener;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkStoreDelegate;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.incubator.net.virtual.VnetService;
+import org.onosproject.incubator.net.virtual.event.VirtualEvent;
+import org.onosproject.incubator.net.virtual.event.VirtualListenerRegistryManager;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProviderRegistry;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProviderService;
+import org.onosproject.mastership.MastershipAdminService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.mastership.MastershipTermService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.meter.MeterService;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.provider.AbstractListenerProviderRegistry;
+import org.onosproject.net.provider.AbstractProviderService;
+import org.onosproject.net.topology.PathService;
+import org.onosproject.net.topology.TopologyService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Implementation of the virtual network service.
+ */
+@Component(service = {
+                   VirtualNetworkService.class,
+                   VirtualNetworkAdminService.class,
+                   VirtualNetworkService.class,
+                   VirtualNetworkProviderRegistry.class
+            })
+public class VirtualNetworkManager
+        extends AbstractListenerProviderRegistry<VirtualNetworkEvent,
+        VirtualNetworkListener, VirtualNetworkProvider, VirtualNetworkProviderService>
+        implements VirtualNetworkService, VirtualNetworkAdminService, VirtualNetworkProviderRegistry {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private static final String TENANT_NULL = "Tenant ID cannot be null";
+    private static final String NETWORK_NULL = "Network ID cannot be null";
+    private static final String DEVICE_NULL = "Device ID cannot be null";
+    private static final String LINK_POINT_NULL = "Link end-point cannot be null";
+
+    private static final String VIRTUAL_NETWORK_APP_ID_STRING =
+            "org.onosproject.virtual-network";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualNetworkStore store;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    private VirtualNetworkStoreDelegate delegate = this::post;
+
+    private ServiceDirectory serviceDirectory = new DefaultServiceDirectory();
+    private ApplicationId appId;
+
+    // TODO: figure out how to coordinate "implementation" of a virtual network in a cluster
+
+    /**
+     * Only used for Junit test methods outside of this package.
+     *
+     * @param store virtual network store
+     */
+    public void setStore(VirtualNetworkStore store) {
+        this.store = store;
+    }
+
+    @Activate
+    public void activate() {
+        eventDispatcher.addSink(VirtualNetworkEvent.class, listenerRegistry);
+        eventDispatcher.addSink(VirtualEvent.class,
+                                VirtualListenerRegistryManager.getInstance());
+        store.setDelegate(delegate);
+        appId = coreService.registerApplication(VIRTUAL_NETWORK_APP_ID_STRING);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        store.unsetDelegate(delegate);
+        eventDispatcher.removeSink(VirtualNetworkEvent.class);
+        eventDispatcher.removeSink(VirtualEvent.class);
+        log.info("Stopped");
+    }
+
+    @Override
+    public void registerTenantId(TenantId tenantId) {
+        checkNotNull(tenantId, TENANT_NULL);
+        store.addTenantId(tenantId);
+    }
+
+    @Override
+    public void unregisterTenantId(TenantId tenantId) {
+        checkNotNull(tenantId, TENANT_NULL);
+        store.removeTenantId(tenantId);
+    }
+
+    @Override
+    public Set<TenantId> getTenantIds() {
+        return store.getTenantIds();
+    }
+
+    @Override
+    public VirtualNetwork createVirtualNetwork(TenantId tenantId) {
+        checkNotNull(tenantId, TENANT_NULL);
+        return store.addNetwork(tenantId);
+    }
+
+    @Override
+    public void removeVirtualNetwork(NetworkId networkId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        store.removeNetwork(networkId);
+    }
+
+    @Override
+    public VirtualDevice createVirtualDevice(NetworkId networkId, DeviceId deviceId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        return store.addDevice(networkId, deviceId);
+    }
+
+    @Override
+    public void removeVirtualDevice(NetworkId networkId, DeviceId deviceId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        store.removeDevice(networkId, deviceId);
+    }
+
+    @Override
+    public VirtualHost createVirtualHost(NetworkId networkId, HostId hostId,
+                                         MacAddress mac, VlanId vlan,
+                                         HostLocation location, Set<IpAddress> ips) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(hostId, DEVICE_NULL);
+        return store.addHost(networkId, hostId, mac, vlan, location, ips);
+    }
+
+    @Override
+    public void removeVirtualHost(NetworkId networkId, HostId hostId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(hostId, DEVICE_NULL);
+        store.removeHost(networkId, hostId);
+    }
+
+    @Override
+    public VirtualLink createVirtualLink(NetworkId networkId,
+                                         ConnectPoint src, ConnectPoint dst) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(src, LINK_POINT_NULL);
+        checkNotNull(dst, LINK_POINT_NULL);
+        ConnectPoint physicalSrc = mapVirtualToPhysicalPort(networkId, src);
+        checkNotNull(physicalSrc, LINK_POINT_NULL);
+        ConnectPoint physicalDst = mapVirtualToPhysicalPort(networkId, dst);
+        checkNotNull(physicalDst, LINK_POINT_NULL);
+
+        VirtualNetworkProvider provider = getProvider(DefaultVirtualLink.PID);
+        Link.State state = Link.State.INACTIVE;
+        if (provider != null) {
+            boolean traversable = provider.isTraversable(physicalSrc, physicalDst);
+            state = traversable ? Link.State.ACTIVE : Link.State.INACTIVE;
+        }
+        return store.addLink(networkId, src, dst, state, null);
+    }
+
+    /**
+     * Maps the virtual connect point to a physical connect point.
+     *
+     * @param networkId network identifier
+     * @param virtualCp virtual connect point
+     * @return physical connect point
+     */
+    private ConnectPoint mapVirtualToPhysicalPort(NetworkId networkId,
+                                                  ConnectPoint virtualCp) {
+        Set<VirtualPort> ports = store.getPorts(networkId, virtualCp.deviceId());
+        for (VirtualPort port : ports) {
+            if (port.number().equals(virtualCp.port())) {
+                return new ConnectPoint(port.realizedBy().deviceId(),
+                                        port.realizedBy().port());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Maps the physical connect point to a virtual connect point.
+     *
+     * @param networkId  network identifier
+     * @param physicalCp physical connect point
+     * @return virtual connect point
+     */
+    private ConnectPoint mapPhysicalToVirtualToPort(NetworkId networkId,
+                                                    ConnectPoint physicalCp) {
+        Set<VirtualPort> ports = store.getPorts(networkId, null);
+        for (VirtualPort port : ports) {
+            if (port.realizedBy().deviceId().equals(physicalCp.elementId()) &&
+                    port.realizedBy().port().equals(physicalCp.port())) {
+                return new ConnectPoint(port.element().id(), port.number());
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void removeVirtualLink(NetworkId networkId, ConnectPoint src,
+                                  ConnectPoint dst) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(src, LINK_POINT_NULL);
+        checkNotNull(dst, LINK_POINT_NULL);
+        store.removeLink(networkId, src, dst);
+    }
+
+    @Override
+    public VirtualPort createVirtualPort(NetworkId networkId, DeviceId deviceId,
+                                         PortNumber portNumber, ConnectPoint realizedBy) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(portNumber, "Port description cannot be null");
+        return store.addPort(networkId, deviceId, portNumber, realizedBy);
+    }
+
+    @Override
+    public void bindVirtualPort(NetworkId networkId, DeviceId deviceId,
+                PortNumber portNumber, ConnectPoint realizedBy) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(portNumber, "Port description cannot be null");
+        checkNotNull(realizedBy, "Physical port description cannot be null");
+
+        store.bindPort(networkId, deviceId, portNumber, realizedBy);
+    }
+
+    @Override
+    public void updatePortState(NetworkId networkId, DeviceId deviceId,
+                PortNumber portNumber, boolean isEnabled) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(portNumber, "Port description cannot be null");
+
+        store.updatePortState(networkId, deviceId, portNumber, isEnabled);
+    }
+
+    @Override
+    public void removeVirtualPort(NetworkId networkId, DeviceId deviceId,
+                                  PortNumber portNumber) {
+        checkNotNull(networkId, NETWORK_NULL);
+        checkNotNull(deviceId, DEVICE_NULL);
+        checkNotNull(portNumber, "Port number cannot be null");
+        store.removePort(networkId, deviceId, portNumber);
+    }
+
+    @Override
+    public ServiceDirectory getServiceDirectory() {
+        return serviceDirectory;
+    }
+
+    @Override
+    public Set<VirtualNetwork> getVirtualNetworks(TenantId tenantId) {
+        checkNotNull(tenantId, TENANT_NULL);
+        return store.getNetworks(tenantId);
+    }
+
+    @Override
+    public VirtualNetwork getVirtualNetwork(NetworkId networkId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        return store.getNetwork(networkId);
+    }
+
+    @Override
+    public TenantId getTenantId(NetworkId networkId) {
+        VirtualNetwork virtualNetwork = getVirtualNetwork(networkId);
+        checkNotNull(virtualNetwork, "The network does not exist.");
+        return virtualNetwork.tenantId();
+    }
+
+    @Override
+    public Set<VirtualDevice> getVirtualDevices(NetworkId networkId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        return store.getDevices(networkId);
+    }
+
+    @Override
+    public Set<VirtualHost> getVirtualHosts(NetworkId networkId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        return store.getHosts(networkId);
+    }
+
+    @Override
+    public Set<VirtualLink> getVirtualLinks(NetworkId networkId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        return store.getLinks(networkId);
+    }
+
+    @Override
+    public Set<VirtualPort> getVirtualPorts(NetworkId networkId, DeviceId deviceId) {
+        checkNotNull(networkId, NETWORK_NULL);
+        return store.getPorts(networkId, deviceId);
+    }
+
+    @Override
+    public Set<DeviceId> getPhysicalDevices(NetworkId networkId, DeviceId deviceId) {
+        checkNotNull(networkId, "Network ID cannot be null");
+        checkNotNull(deviceId, "Virtual device ID cannot be null");
+        Set<VirtualPort> virtualPortSet = getVirtualPorts(networkId, deviceId);
+        Set<DeviceId> physicalDeviceSet = new HashSet<>();
+
+        virtualPortSet.forEach(virtualPort -> {
+            if (virtualPort.realizedBy() != null) {
+                physicalDeviceSet.add(virtualPort.realizedBy().deviceId());
+            }
+        });
+
+        return ImmutableSet.copyOf(physicalDeviceSet);
+    }
+
+    private final Map<ServiceKey, VnetService> networkServices = Maps.newConcurrentMap();
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T get(NetworkId networkId, Class<T> serviceClass) {
+        checkNotNull(networkId, NETWORK_NULL);
+        ServiceKey serviceKey = networkServiceKey(networkId, serviceClass);
+        VnetService service = lookup(serviceKey);
+        if (service == null) {
+            service = create(serviceKey);
+        }
+        return (T) service;
+    }
+
+    @Override
+    public ApplicationId getVirtualNetworkApplicationId(NetworkId networkId) {
+        return appId;
+    }
+
+    /**
+     * Returns the Vnet service matching the service key.
+     *
+     * @param serviceKey service key
+     * @return vnet service
+     */
+    private VnetService lookup(ServiceKey serviceKey) {
+        return networkServices.get(serviceKey);
+    }
+
+    /**
+     * Creates a new service key using the specified network identifier and service class.
+     *
+     * @param networkId    network identifier
+     * @param serviceClass service class
+     * @param <T>          type of service
+     * @return service key
+     */
+    private <T> ServiceKey networkServiceKey(NetworkId networkId, Class<T> serviceClass) {
+        return new ServiceKey(networkId, serviceClass);
+    }
+
+
+    /**
+     * Create a new vnet service instance.
+     *
+     * @param serviceKey service key
+     * @return vnet service
+     */
+    private VnetService create(ServiceKey serviceKey) {
+        VirtualNetwork network = getVirtualNetwork(serviceKey.networkId());
+        checkNotNull(network, NETWORK_NULL);
+
+        VnetService service;
+        if (serviceKey.serviceClass.equals(DeviceService.class)) {
+            service = new VirtualNetworkDeviceManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(LinkService.class)) {
+            service = new VirtualNetworkLinkManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(TopologyService.class)) {
+            service = new VirtualNetworkTopologyManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(IntentService.class)) {
+            service = new VirtualNetworkIntentManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(HostService.class)) {
+            service = new VirtualNetworkHostManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(PathService.class)) {
+            service = new VirtualNetworkPathManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(FlowRuleService.class)) {
+            service = new VirtualNetworkFlowRuleManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(PacketService.class)) {
+            service = new VirtualNetworkPacketManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(GroupService.class)) {
+            service = new VirtualNetworkGroupManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(MeterService.class)) {
+            service = new VirtualNetworkMeterManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(FlowObjectiveService.class)) {
+            service = new VirtualNetworkFlowObjectiveManager(this, network.id());
+        } else if (serviceKey.serviceClass.equals(MastershipService.class) ||
+                serviceKey.serviceClass.equals(MastershipAdminService.class) ||
+                serviceKey.serviceClass.equals(MastershipTermService.class)) {
+            service = new VirtualNetworkMastershipManager(this, network.id());
+        } else {
+            return null;
+        }
+        networkServices.put(serviceKey, service);
+        return service;
+    }
+
+    /**
+     * Service key class.
+     */
+    private static class ServiceKey {
+        final NetworkId networkId;
+        final Class serviceClass;
+
+        /**
+         * Constructor for service key.
+         *
+         * @param networkId    network identifier
+         * @param serviceClass service class
+         */
+        ServiceKey(NetworkId networkId, Class serviceClass) {
+
+            checkNotNull(networkId, NETWORK_NULL);
+            this.networkId = networkId;
+            this.serviceClass = serviceClass;
+        }
+
+        /**
+         * Returns the network identifier.
+         *
+         * @return network identifier
+         */
+        public NetworkId networkId() {
+            return networkId;
+        }
+
+        /**
+         * Returns the service class.
+         *
+         * @return service class
+         */
+        public Class serviceClass() {
+            return serviceClass;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(networkId, serviceClass);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof ServiceKey) {
+                ServiceKey that = (ServiceKey) obj;
+                return Objects.equals(this.networkId, that.networkId) &&
+                        Objects.equals(this.serviceClass, that.serviceClass);
+            }
+            return false;
+        }
+    }
+
+    @Override
+    protected VirtualNetworkProviderService
+    createProviderService(VirtualNetworkProvider provider) {
+        return new InternalVirtualNetworkProviderService(provider);
+    }
+
+    /**
+     * Service issued to registered virtual network providers so that they
+     * can interact with the core.
+     */
+    private class InternalVirtualNetworkProviderService
+            extends AbstractProviderService<VirtualNetworkProvider>
+            implements VirtualNetworkProviderService {
+        /**
+         * Constructor.
+         * @param provider virtual network provider
+         */
+        InternalVirtualNetworkProviderService(VirtualNetworkProvider provider) {
+            super(provider);
+        }
+
+        @Override
+        public void topologyChanged(Set<Set<ConnectPoint>> clusters) {
+            Set<TenantId> tenantIds = getTenantIds();
+            tenantIds.forEach(tenantId -> {
+                Set<VirtualNetwork> virtualNetworks = getVirtualNetworks(tenantId);
+
+                virtualNetworks.forEach(virtualNetwork -> {
+                    Set<VirtualLink> virtualLinks = getVirtualLinks(virtualNetwork.id());
+
+                    virtualLinks.forEach(virtualLink -> {
+                        if (isVirtualLinkInCluster(virtualNetwork.id(),
+                                                   virtualLink, clusters)) {
+                            store.updateLink(virtualLink, virtualLink.tunnelId(),
+                                             Link.State.ACTIVE);
+                        } else {
+                            store.updateLink(virtualLink, virtualLink.tunnelId(),
+                                             Link.State.INACTIVE);
+                        }
+                    });
+                });
+            });
+        }
+
+        /**
+         * Determines if the virtual link (both source and destination connect point)
+         * is in a cluster.
+         *
+         * @param networkId   virtual network identifier
+         * @param virtualLink virtual link
+         * @param clusters    topology clusters
+         * @return true if the virtual link is in a cluster.
+         */
+        private boolean isVirtualLinkInCluster(NetworkId networkId, VirtualLink virtualLink,
+                                               Set<Set<ConnectPoint>> clusters) {
+            ConnectPoint srcPhysicalCp =
+                    mapVirtualToPhysicalPort(networkId, virtualLink.src());
+            ConnectPoint dstPhysicalCp =
+                    mapVirtualToPhysicalPort(networkId, virtualLink.dst());
+
+            final boolean[] foundSrc = {false};
+            final boolean[] foundDst = {false};
+            clusters.forEach(connectPoints -> {
+                connectPoints.forEach(connectPoint -> {
+                    if (connectPoint.equals(srcPhysicalCp)) {
+                        foundSrc[0] = true;
+                    } else if (connectPoint.equals(dstPhysicalCp)) {
+                        foundDst[0] = true;
+                    }
+                });
+                if (foundSrc[0] && foundDst[0]) {
+                    return;
+                }
+            });
+            return foundSrc[0] && foundDst[0];
+        }
+
+        @Override
+        public void tunnelUp(NetworkId networkId, ConnectPoint src,
+                             ConnectPoint dst, TunnelId tunnelId) {
+            ConnectPoint srcVirtualCp = mapPhysicalToVirtualToPort(networkId, src);
+            ConnectPoint dstVirtualCp = mapPhysicalToVirtualToPort(networkId, dst);
+            if ((srcVirtualCp == null) || (dstVirtualCp == null)) {
+                log.error("Src or dst virtual connection point was not found.");
+            }
+
+            VirtualLink virtualLink = store.getLink(networkId, srcVirtualCp, dstVirtualCp);
+            if (virtualLink != null) {
+                store.updateLink(virtualLink, tunnelId, Link.State.ACTIVE);
+            }
+        }
+
+        @Override
+        public void tunnelDown(NetworkId networkId, ConnectPoint src,
+                               ConnectPoint dst, TunnelId tunnelId) {
+            ConnectPoint srcVirtualCp = mapPhysicalToVirtualToPort(networkId, src);
+            ConnectPoint dstVirtualCp = mapPhysicalToVirtualToPort(networkId, dst);
+            if ((srcVirtualCp == null) || (dstVirtualCp == null)) {
+                log.error("Src or dst virtual connection point was not found.");
+            }
+
+            VirtualLink virtualLink = store.getLink(networkId, srcVirtualCp, dstVirtualCp);
+            if (virtualLink != null) {
+                store.updateLink(virtualLink, tunnelId, Link.State.INACTIVE);
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMastershipManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMastershipManager.java
new file mode 100644
index 0000000..57c0774
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMastershipManager.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import org.onlab.metrics.MetricsService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.cluster.RoleInfo;
+import org.onosproject.core.MetricsHelper;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetworkMastershipStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.mastership.MastershipAdminService;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipInfo;
+import org.onosproject.mastership.MastershipListener;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.mastership.MastershipStoreDelegate;
+import org.onosproject.mastership.MastershipTerm;
+import org.onosproject.mastership.MastershipTermService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.slf4j.Logger;
+import com.codahale.metrics.Timer;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.metrics.MetricsUtil.startTimer;
+import static org.onlab.metrics.MetricsUtil.stopTimer;
+import static org.slf4j.LoggerFactory.getLogger;
+
+public class VirtualNetworkMastershipManager
+        extends AbstractVirtualListenerManager<MastershipEvent, MastershipListener>
+        implements MastershipService, MastershipAdminService, MastershipTermService,
+        MetricsHelper {
+
+    private static final String NODE_ID_NULL = "Node ID cannot be null";
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String ROLE_NULL = "Mastership role cannot be null";
+
+    private final Logger log = getLogger(getClass());
+
+    protected ClusterService clusterService;
+
+    VirtualNetworkMastershipStore store;
+    MastershipStoreDelegate storeDelegate;
+
+    private NodeId localNodeId;
+    private Timer requestRoleTimer;
+
+    /**
+     * Creates a new VirtualNetworkMastershipManager object.
+     *
+     * @param manager virtual network manager service
+     * @param networkId virtual network identifier
+     */
+    public VirtualNetworkMastershipManager(VirtualNetworkService manager, NetworkId networkId) {
+        super(manager, networkId, MastershipEvent.class);
+
+        clusterService = serviceDirectory.get(ClusterService.class);
+
+        store = serviceDirectory.get(VirtualNetworkMastershipStore.class);
+        this.storeDelegate = new InternalDelegate();
+        store.setDelegate(networkId, this.storeDelegate);
+
+        requestRoleTimer = createTimer("Virtual-mastership", "requestRole", "responseTime");
+        localNodeId = clusterService.getLocalNode().id();
+    }
+
+    @Override
+    public CompletableFuture<Void> setRole(NodeId nodeId, DeviceId deviceId,
+                                           MastershipRole role) {
+        checkNotNull(nodeId, NODE_ID_NULL);
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        checkNotNull(role, ROLE_NULL);
+
+        CompletableFuture<MastershipEvent> eventFuture = null;
+
+        switch (role) {
+            case MASTER:
+                eventFuture = store.setMaster(networkId, nodeId, deviceId);
+                break;
+            case STANDBY:
+                eventFuture = store.setStandby(networkId, nodeId, deviceId);
+                break;
+            case NONE:
+                eventFuture = store.relinquishRole(networkId, nodeId, deviceId);
+                break;
+            default:
+                log.info("Unknown role; ignoring");
+                return CompletableFuture.completedFuture(null);
+        }
+
+        return eventFuture.thenAccept(this::post).thenApply(v -> null);
+    }
+
+    @Override
+    public MastershipRole getLocalRole(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+
+        return store.getRole(networkId, localNodeId, deviceId);
+    }
+
+    @Override
+    public CompletableFuture<MastershipRole> requestRoleFor(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+
+        final Timer.Context timer = startTimer(requestRoleTimer);
+        return store.requestRole(networkId, deviceId)
+                .whenComplete((result, error) -> stopTimer(timer));
+    }
+
+    @Override
+    public CompletableFuture<Void> relinquishMastership(DeviceId deviceId) {
+        return store.relinquishRole(networkId, localNodeId, deviceId)
+                .thenAccept(this::post)
+                .thenApply(v -> null);
+    }
+
+    @Override
+    public NodeId getMasterFor(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+
+        return store.getMaster(networkId, deviceId);
+    }
+
+    @Override
+    public RoleInfo getNodesFor(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+
+        return store.getNodes(networkId, deviceId);
+    }
+
+    @Override
+    public MastershipInfo getMastershipFor(DeviceId deviceId) {
+        checkNotNull(deviceId, DEVICE_ID_NULL);
+        return store.getMastership(networkId, deviceId);
+    }
+
+    @Override
+    public Set<DeviceId> getDevicesOf(NodeId nodeId) {
+        checkNotNull(nodeId, NODE_ID_NULL);
+
+        return store.getDevices(networkId, nodeId);
+    }
+
+    @Override
+    public MastershipTerm getMastershipTerm(DeviceId deviceId) {
+        return store.getTermFor(networkId, deviceId);
+    }
+
+    @Override
+    public MetricsService metricsService() {
+        //TODO: support metric service for virtual network
+        log.warn("Currently, virtual network does not support metric service.");
+        return null;
+    }
+
+    @Override
+    public void balanceRoles() {
+        //FIXME: More advanced logic for balancing virtual network roles.
+        List<ControllerNode> nodes = clusterService.getNodes().stream()
+                .filter(n -> clusterService.getState(n.id())
+                        .equals(ControllerNode.State.ACTIVE))
+                .collect(Collectors.toList());
+
+        nodes.sort(Comparator.comparing(ControllerNode::id));
+
+        //Pick a node using network Id,
+        NodeId masterNode = nodes.get((int) ((networkId.id() - 1) % nodes.size())).id();
+
+        List<CompletableFuture<Void>> setRoleFutures = Lists.newLinkedList();
+        for (VirtualDevice device : manager.getVirtualDevices(networkId)) {
+            setRoleFutures.add(setRole(masterNode, device.id(), MastershipRole.MASTER));
+        }
+
+        CompletableFuture<Void> balanceRolesFuture = CompletableFuture.allOf(
+                setRoleFutures.toArray(new CompletableFuture[setRoleFutures.size()]));
+
+        Futures.getUnchecked(balanceRolesFuture);
+    }
+
+    public class InternalDelegate implements MastershipStoreDelegate {
+        @Override
+        public void notify(MastershipEvent event) {
+            post(event);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMeterManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMeterManager.java
new file mode 100644
index 0000000..2cce14c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkMeterManager.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.TriConsumer;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkMeterStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualMeterProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualMeterProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.meter.DefaultMeter;
+import org.onosproject.net.meter.Meter;
+import org.onosproject.net.meter.MeterEvent;
+import org.onosproject.net.meter.MeterFailReason;
+import org.onosproject.net.meter.MeterFeatures;
+import org.onosproject.net.meter.MeterFeaturesKey;
+import org.onosproject.net.meter.MeterId;
+import org.onosproject.net.meter.MeterKey;
+import org.onosproject.net.meter.MeterListener;
+import org.onosproject.net.meter.MeterOperation;
+import org.onosproject.net.meter.MeterRequest;
+import org.onosproject.net.meter.MeterService;
+import org.onosproject.net.meter.MeterState;
+import org.onosproject.net.meter.MeterStoreDelegate;
+import org.onosproject.net.meter.MeterStoreResult;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.store.service.AtomicCounter;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+public class VirtualNetworkMeterManager
+        extends AbstractVirtualListenerManager<MeterEvent, MeterListener>
+        implements MeterService {
+
+    private static final String METERCOUNTERIDENTIFIER = "meter-id-counter-%s";
+    private final Logger log = getLogger(getClass());
+
+    protected StorageService coreStorageService;
+
+    protected VirtualNetworkMeterStore store;
+    private final MeterStoreDelegate storeDelegate = new InternalMeterStoreDelegate();
+
+    private VirtualProviderRegistryService providerRegistryService;
+    private InternalMeterProviderService innerProviderService;
+
+    private Map<DeviceId, AtomicCounter> meterIdCounters
+            = Maps.newConcurrentMap();
+
+    private TriConsumer<MeterRequest, MeterStoreResult, Throwable> onComplete;
+
+    /**
+     * Creates a new VirtualNetworkMeterManager object.
+     *
+     * @param manager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkMeterManager(VirtualNetworkService manager,
+                                      NetworkId networkId) {
+        super(manager, networkId, MeterEvent.class);
+
+        coreStorageService = serviceDirectory.get(StorageService.class);
+        providerRegistryService =
+                serviceDirectory.get(VirtualProviderRegistryService.class);
+
+        store = serviceDirectory.get(VirtualNetworkMeterStore.class);
+        store.setDelegate(networkId, this.storeDelegate);
+
+        innerProviderService = new InternalMeterProviderService();
+        providerRegistryService.registerProviderService(networkId(), innerProviderService);
+
+
+        onComplete = (request, result, error) -> {
+            request.context().ifPresent(c -> {
+                if (error != null) {
+                    c.onError(request, MeterFailReason.UNKNOWN);
+                } else {
+                    if (result.reason().isPresent()) {
+                        c.onError(request, result.reason().get());
+                    } else {
+                        c.onSuccess(request);
+                    }
+                }
+            });
+
+        };
+
+        log.info("Started");
+    }
+
+    @Override
+    public Meter submit(MeterRequest request) {
+
+        MeterId id = allocateMeterId(request.deviceId());
+
+        Meter.Builder mBuilder = DefaultMeter.builder()
+                .forDevice(request.deviceId())
+                .fromApp(request.appId())
+                .withBands(request.bands())
+                .withId(id)
+                .withUnit(request.unit());
+
+        if (request.isBurst()) {
+            mBuilder.burst();
+        }
+        DefaultMeter m = (DefaultMeter) mBuilder.build();
+        m.setState(MeterState.PENDING_ADD);
+        store.storeMeter(networkId(), m).whenComplete((result, error) ->
+                                                 onComplete.accept(request, result, error));
+        return m;
+    }
+
+    @Override
+    public void withdraw(MeterRequest request, MeterId meterId) {
+        Meter.Builder mBuilder = DefaultMeter.builder()
+                .forDevice(request.deviceId())
+                .fromApp(request.appId())
+                .withBands(request.bands())
+                .withId(meterId)
+                .withUnit(request.unit());
+
+        if (request.isBurst()) {
+            mBuilder.burst();
+        }
+
+        DefaultMeter m = (DefaultMeter) mBuilder.build();
+        m.setState(MeterState.PENDING_REMOVE);
+        store.deleteMeter(networkId(), m).whenComplete((result, error) ->
+                                                  onComplete.accept(request, result, error));
+    }
+
+    @Override
+    public Meter getMeter(DeviceId deviceId, MeterId id) {
+        MeterKey key = MeterKey.key(deviceId, id);
+        return store.getMeter(networkId(), key);
+    }
+
+    @Override
+    public Collection<Meter> getMeters(DeviceId deviceId) {
+        return store.getAllMeters(networkId()).stream()
+                .filter(m -> m.deviceId().equals(deviceId)).collect(Collectors.toList());
+    }
+
+    @Override
+    public Collection<Meter> getAllMeters() {
+        return store.getAllMeters(networkId());
+    }
+
+    private long queryMeters(DeviceId device) {
+        //FIXME: how to decide maximum number of meters per virtual device?
+        return 1;
+    }
+
+    private AtomicCounter allocateCounter(DeviceId deviceId) {
+        return coreStorageService
+                .getAtomicCounter(String.format(METERCOUNTERIDENTIFIER, deviceId));
+    }
+
+    public MeterId allocateMeterId(DeviceId deviceId) {
+        long maxMeters = store.getMaxMeters(networkId(), MeterFeaturesKey.key(deviceId));
+        if (maxMeters == 0L) {
+            // MeterFeatures couldn't be retrieved, trying with queryMeters
+            maxMeters = queryMeters(deviceId);
+        }
+
+        if (maxMeters == 0L) {
+            throw new IllegalStateException("Meters not supported by device " + deviceId);
+        }
+
+        final long mmeters = maxMeters;
+        long id = meterIdCounters.compute(deviceId, (k, v) -> {
+            if (v == null) {
+                return allocateCounter(k);
+            }
+            if (v.get() >= mmeters) {
+                throw new IllegalStateException("Maximum number of meters " +
+                                                        meterIdCounters.get(deviceId).get() +
+                                                        " reached for device " + deviceId +
+                                                        " virtual network " + networkId());
+            }
+            return v;
+        }).incrementAndGet();
+
+        return MeterId.meterId(id);
+    }
+
+    @Override
+    public void freeMeterId(DeviceId deviceId, MeterId meterId) {
+        // Do nothing
+    }
+
+    private class InternalMeterProviderService
+            extends AbstractVirtualProviderService<VirtualMeterProvider>
+            implements VirtualMeterProviderService {
+
+        /**
+         * Creates a provider service on behalf of the specified provider.
+         */
+        protected InternalMeterProviderService() {
+            Set<ProviderId> providerIds =
+                    providerRegistryService.getProvidersByService(this);
+            ProviderId providerId = providerIds.stream().findFirst().get();
+            VirtualMeterProvider provider = (VirtualMeterProvider)
+                    providerRegistryService.getProvider(providerId);
+            setProvider(provider);
+        }
+
+        @Override
+        public void meterOperationFailed(MeterOperation operation,
+                                         MeterFailReason reason) {
+            store.failedMeter(networkId(), operation, reason);
+        }
+
+        @Override
+        public void pushMeterMetrics(DeviceId deviceId, Collection<Meter> meterEntries) {
+            //FIXME: FOLLOWING CODE CANNOT BE TESTED UNTIL SOMETHING THAT
+            //FIXME: IMPLEMENTS METERS EXISTS
+            Map<Pair<DeviceId, MeterId>, Meter> storedMeterMap =
+                    store.getAllMeters(networkId()).stream()
+                    .collect(Collectors.toMap(m -> Pair.of(m.deviceId(), m.id()), Function.identity()));
+
+            meterEntries.stream()
+                    .filter(m -> storedMeterMap.remove(Pair.of(m.deviceId(), m.id())) != null)
+                    .forEach(m -> store.updateMeterState(networkId(), m));
+
+            storedMeterMap.values().forEach(m -> {
+                if (m.state() == MeterState.PENDING_ADD) {
+                    provider().performMeterOperation(networkId(), m.deviceId(),
+                                                     new MeterOperation(m,
+                                                                        MeterOperation.Type.MODIFY));
+                } else if (m.state() == MeterState.PENDING_REMOVE) {
+                    store.deleteMeterNow(networkId(), m);
+                }
+            });
+        }
+
+        @Override
+        public void pushMeterFeatures(DeviceId deviceId, MeterFeatures meterfeatures) {
+            store.storeMeterFeatures(networkId(), meterfeatures);
+        }
+
+        @Override
+        public void deleteMeterFeatures(DeviceId deviceId) {
+            store.deleteMeterFeatures(networkId(), deviceId);
+        }
+    }
+
+    private class InternalMeterStoreDelegate implements MeterStoreDelegate {
+
+        @Override
+        public void notify(MeterEvent event) {
+            DeviceId deviceId = event.subject().deviceId();
+            VirtualMeterProvider p = innerProviderService.provider();
+
+            switch (event.type()) {
+                case METER_ADD_REQ:
+                    p.performMeterOperation(networkId(), deviceId,
+                                            new MeterOperation(event.subject(),
+                                                               MeterOperation.Type.ADD));
+                    break;
+                case METER_REM_REQ:
+                    p.performMeterOperation(networkId(), deviceId,
+                                            new MeterOperation(event.subject(),
+                                                               MeterOperation.Type.REMOVE));
+                    break;
+                default:
+                    log.warn("Unknown meter event {}", event.type());
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPacketManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPacketManager.java
new file mode 100644
index 0000000..d1869ce
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPacketManager.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.incubator.net.virtual.AbstractVnetService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkPacketStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualPacketProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualPacketProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flowobjective.DefaultForwardingObjective;
+import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.ForwardingObjective;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.packet.DefaultPacketRequest;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketEvent;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketProcessorEntry;
+import org.onosproject.net.packet.PacketRequest;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.packet.PacketStoreDelegate;
+import org.onosproject.net.provider.ProviderId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class VirtualNetworkPacketManager extends AbstractVnetService
+        implements PacketService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private final VirtualNetworkService manager;
+
+    protected VirtualNetworkPacketStore store;
+    private final List<ProcessorEntry> processors = Lists.newCopyOnWriteArrayList();
+
+    private NodeId localNodeId;
+
+    private DeviceService deviceService;
+    private FlowObjectiveService objectiveService;
+
+    private VirtualProviderRegistryService providerRegistryService = null;
+
+    private InternalPacketProviderService providerService = null;
+
+    public VirtualNetworkPacketManager(VirtualNetworkService virtualNetworkManager,
+                                       NetworkId networkId) {
+        super(virtualNetworkManager, networkId);
+        this.manager = virtualNetworkManager;
+
+        //Set node id as same as the node hosting virtual manager
+        ClusterService clusterService = serviceDirectory.get(ClusterService.class);
+        this.localNodeId = clusterService.getLocalNode().id();
+
+        this.store = serviceDirectory.get(VirtualNetworkPacketStore.class);
+        this.store.setDelegate(networkId(), new InternalStoreDelegate());
+
+        this.deviceService = manager.get(networkId(), DeviceService.class);
+        this.objectiveService = manager.get(networkId(), FlowObjectiveService.class);
+
+        providerRegistryService =
+                serviceDirectory.get(VirtualProviderRegistryService.class);
+        providerService = new InternalPacketProviderService();
+        providerRegistryService.registerProviderService(networkId(), providerService);
+    }
+
+    @Override
+    public void addProcessor(PacketProcessor processor, int priority) {
+        ProcessorEntry entry = new ProcessorEntry(processor, priority);
+
+        // Insert the new processor according to its priority.
+        int i = 0;
+        for (; i < processors.size(); i++) {
+            if (priority < processors.get(i).priority()) {
+                break;
+            }
+        }
+        processors.add(i, entry);
+    }
+
+    @Override
+    public void removeProcessor(PacketProcessor processor) {
+        // Remove the processor entry.
+        for (int i = 0; i < processors.size(); i++) {
+            if (processors.get(i).processor() == processor) {
+                processors.remove(i);
+                break;
+            }
+        }
+    }
+
+    @Override
+    public List<PacketProcessorEntry> getProcessors() {
+        return ImmutableList.copyOf(processors);
+    }
+
+    @Override
+    public void requestPackets(TrafficSelector selector, PacketPriority priority, ApplicationId appId) {
+        PacketRequest request = new DefaultPacketRequest(selector, priority, appId,
+                                                         localNodeId, Optional.empty());
+        store.requestPackets(networkId(), request);
+    }
+
+    @Override
+    public void requestPackets(TrafficSelector selector, PacketPriority priority,
+                               ApplicationId appId, Optional<DeviceId> deviceId) {
+        PacketRequest request =
+                new DefaultPacketRequest(selector, priority, appId,
+                                         localNodeId, deviceId);
+
+        store.requestPackets(networkId(), request);
+    }
+
+    @Override
+    public void cancelPackets(TrafficSelector selector, PacketPriority priority, ApplicationId appId) {
+        PacketRequest request = new DefaultPacketRequest(selector, priority, appId,
+                                                         localNodeId, Optional.empty());
+        store.cancelPackets(networkId(), request);
+    }
+
+    @Override
+    public void cancelPackets(TrafficSelector selector, PacketPriority priority,
+                              ApplicationId appId, Optional<DeviceId> deviceId) {
+        PacketRequest request = new DefaultPacketRequest(selector, priority,
+                                                         appId, localNodeId,
+                                                         deviceId);
+        store.cancelPackets(networkId(), request);
+    }
+
+    @Override
+    public List<PacketRequest> getRequests() {
+        return store.existingRequests(networkId());
+    }
+
+    @Override
+    public void emit(OutboundPacket packet) {
+        store.emit(networkId(), packet);
+    }
+
+    /**
+     * Personalized packet provider service issued to the supplied provider.
+     */
+    private class InternalPacketProviderService
+            extends AbstractVirtualProviderService<VirtualPacketProvider>
+            implements VirtualPacketProviderService {
+
+        protected InternalPacketProviderService() {
+            super();
+
+            Set<ProviderId> providerIds =
+                    providerRegistryService.getProvidersByService(this);
+            ProviderId providerId = providerIds.stream().findFirst().get();
+            VirtualPacketProvider provider = (VirtualPacketProvider)
+                    providerRegistryService.getProvider(providerId);
+            setProvider(provider);
+        }
+
+        @Override
+        public void processPacket(PacketContext context) {
+            // TODO filter packets sent to processors based on registrations
+            for (ProcessorEntry entry : processors) {
+                try {
+                    long start = System.nanoTime();
+                    entry.processor().process(context);
+                    entry.addNanos(System.nanoTime() - start);
+                } catch (Exception e) {
+                    log.warn("Packet processor {} threw an exception", entry.processor(), e);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Entity for tracking stats for a packet processor.
+     */
+    private class ProcessorEntry implements PacketProcessorEntry {
+        private final PacketProcessor processor;
+        private final int priority;
+        private long invocations = 0;
+        private long nanos = 0;
+
+        public ProcessorEntry(PacketProcessor processor, int priority) {
+            this.processor = processor;
+            this.priority = priority;
+        }
+
+        @Override
+        public PacketProcessor processor() {
+            return processor;
+        }
+
+        @Override
+        public int priority() {
+            return priority;
+        }
+
+        @Override
+        public long invocations() {
+            return invocations;
+        }
+
+        @Override
+        public long totalNanos() {
+            return nanos;
+        }
+
+        @Override
+        public long averageNanos() {
+            return invocations > 0 ? nanos / invocations : 0;
+        }
+
+        void addNanos(long nanos) {
+            this.nanos += nanos;
+            this.invocations++;
+        }
+    }
+
+    private void localEmit(NetworkId networkId, OutboundPacket packet) {
+        Device device = deviceService.getDevice(packet.sendThrough());
+        if (device == null) {
+            return;
+        }
+        VirtualPacketProvider packetProvider = providerService.provider();
+
+        if (packetProvider != null) {
+            packetProvider.emit(networkId, packet);
+        }
+    }
+
+    /**
+     * Internal callback from the packet store.
+     */
+    protected class InternalStoreDelegate implements PacketStoreDelegate {
+        @Override
+        public void notify(PacketEvent event) {
+            localEmit(networkId(), event.subject());
+        }
+
+        @Override
+        public void requestPackets(PacketRequest request) {
+            DeviceId deviceid = request.deviceId().orElse(null);
+
+            if (deviceid != null) {
+                pushRule(deviceService.getDevice(deviceid), request);
+            } else {
+                pushToAllDevices(request);
+            }
+        }
+
+        @Override
+        public void cancelPackets(PacketRequest request) {
+            DeviceId deviceid = request.deviceId().orElse(null);
+
+            if (deviceid != null) {
+                removeRule(deviceService.getDevice(deviceid), request);
+            } else {
+                removeFromAllDevices(request);
+            }
+        }
+    }
+
+    /**
+     * Pushes packet intercept flow rules to the device.
+     *
+     * @param device  the device to push the rules to
+     * @param request the packet request
+     */
+    private void pushRule(Device device, PacketRequest request) {
+        if (!device.type().equals(Device.Type.VIRTUAL)) {
+            return;
+        }
+
+        ForwardingObjective forwarding = createBuilder(request)
+                .add(new ObjectiveContext() {
+                    @Override
+                    public void onError(Objective objective, ObjectiveError error) {
+                        log.warn("Failed to install packet request {} to {}: {}",
+                                 request, device.id(), error);
+                    }
+                });
+
+        objectiveService.forward(device.id(), forwarding);
+    }
+
+    /**
+     * Removes packet intercept flow rules from the device.
+     *
+     * @param device  the device to remove the rules deom
+     * @param request the packet request
+     */
+    private void removeRule(Device device, PacketRequest request) {
+        if (!device.type().equals(Device.Type.VIRTUAL)) {
+            return;
+        }
+        ForwardingObjective forwarding = createBuilder(request)
+                .remove(new ObjectiveContext() {
+                    @Override
+                    public void onError(Objective objective, ObjectiveError error) {
+                        log.warn("Failed to withdraw packet request {} from {}: {}",
+                                 request, device.id(), error);
+                    }
+                });
+        objectiveService.forward(device.id(), forwarding);
+    }
+
+    /**
+     * Pushes a packet request flow rule to all devices.
+     *
+     * @param request the packet request
+     */
+    private void pushToAllDevices(PacketRequest request) {
+        log.debug("Pushing packet request {} to all devices", request);
+        for (Device device : deviceService.getDevices()) {
+            pushRule(device, request);
+        }
+    }
+
+    /**
+     * Removes packet request flow rule from all devices.
+     *
+     * @param request the packet request
+     */
+    private void removeFromAllDevices(PacketRequest request) {
+        deviceService.getAvailableDevices().forEach(d -> removeRule(d, request));
+    }
+
+    private DefaultForwardingObjective.Builder createBuilder(PacketRequest request) {
+        return DefaultForwardingObjective.builder()
+                .withPriority(request.priority().priorityValue())
+                .withSelector(request.selector())
+                .fromApp(request.appId())
+                .withFlag(ForwardingObjective.Flag.VERSATILE)
+                .withTreatment(DefaultTrafficTreatment.builder().punt().build())
+                .makePermanent();
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPathManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPathManager.java
new file mode 100644
index 0000000..de0dffa
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkPathManager.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VnetService;
+import org.onosproject.net.DisjointPath;
+import org.onosproject.net.ElementId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.topology.LinkWeigher;
+import org.onosproject.net.topology.PathService;
+import org.onosproject.net.topology.AbstractPathService;
+import org.onosproject.net.topology.TopologyService;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Path service implementation built on the virtual network service.
+ */
+public class VirtualNetworkPathManager
+        extends AbstractPathService
+        implements PathService, VnetService {
+
+    private final NetworkId networkId;
+
+    /**
+     * Creates a new virtual network path service object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+
+    public VirtualNetworkPathManager(VirtualNetworkService virtualNetworkManager,
+                                     NetworkId networkId) {
+        this.networkId = networkId;
+
+        topologyService = virtualNetworkManager.get(networkId(), TopologyService.class);
+        hostService = virtualNetworkManager.get(networkId(), HostService.class);
+    }
+
+    @Override
+    public Set<Path> getPaths(ElementId src, ElementId dst) {
+        return super.getPaths(src, dst, (LinkWeigher) null);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(ElementId src, ElementId dst) {
+        return getDisjointPaths(src, dst, (LinkWeigher) null);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(ElementId src, ElementId dst,
+                                              Map<Link, Object> riskProfile) {
+        return getDisjointPaths(src, dst, (LinkWeigher) null, riskProfile);
+    }
+
+    @Override
+    public NetworkId networkId() {
+        return this.networkId;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkTopologyManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkTopologyManager.java
new file mode 100644
index 0000000..aad3ac4
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/VirtualNetworkTopologyManager.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.impl;
+
+import org.onosproject.common.DefaultTopology;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.event.AbstractVirtualListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.DisjointPath;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.topology.ClusterId;
+import org.onosproject.net.topology.DefaultGraphDescription;
+import org.onosproject.net.topology.LinkWeigher;
+import org.onosproject.net.topology.Topology;
+import org.onosproject.net.topology.TopologyCluster;
+import org.onosproject.net.topology.TopologyEvent;
+import org.onosproject.net.topology.TopologyGraph;
+import org.onosproject.net.topology.TopologyListener;
+import org.onosproject.net.topology.TopologyService;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.incubator.net.virtual.DefaultVirtualLink.PID;
+
+/**
+ * Topology service implementation built on the virtual network service.
+ */
+public class VirtualNetworkTopologyManager
+        extends AbstractVirtualListenerManager<TopologyEvent, TopologyListener>
+        implements TopologyService {
+
+    private static final String TOPOLOGY_NULL = "Topology cannot be null";
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String CLUSTER_ID_NULL = "Cluster ID cannot be null";
+    private static final String CLUSTER_NULL = "Topology cluster cannot be null";
+    private static final String CONNECTION_POINT_NULL = "Connection point cannot be null";
+    private static final String LINK_WEIGHT_NULL = "Link weight cannot be null";
+
+    /**
+     * Creates a new VirtualNetworkTopologyService object.
+     *
+     * @param virtualNetworkManager virtual network manager service
+     * @param networkId a virtual network identifier
+     */
+    public VirtualNetworkTopologyManager(VirtualNetworkService virtualNetworkManager,
+                                         NetworkId networkId) {
+        super(virtualNetworkManager, networkId, TopologyEvent.class);
+    }
+
+    @Override
+    public Topology currentTopology() {
+        Iterable<Device> devices = manager.getVirtualDevices(networkId())
+                .stream()
+                .collect(Collectors.toSet());
+        Iterable<Link> links = manager.getVirtualLinks(networkId())
+                .stream()
+                .collect(Collectors.toSet());
+
+        DefaultGraphDescription graph =
+                new DefaultGraphDescription(System.nanoTime(),
+                                            System.currentTimeMillis(),
+                                            devices, links);
+        return new DefaultTopology(PID, graph);
+    }
+
+    @Override
+    public boolean isLatest(Topology topology) {
+        Topology currentTopology = currentTopology();
+        return defaultTopology(topology).getGraph()
+                .equals(defaultTopology(currentTopology).getGraph());
+    }
+
+    @Override
+    public TopologyGraph getGraph(Topology topology) {
+        return defaultTopology(topology).getGraph();
+    }
+
+    // Validates the specified topology and returns it as a default
+    private DefaultTopology defaultTopology(Topology topology) {
+        checkNotNull(topology, TOPOLOGY_NULL);
+        checkArgument(topology instanceof DefaultTopology,
+                      "Topology class %s not supported", topology.getClass());
+        return (DefaultTopology) topology;
+    }
+
+    @Override
+    public Set<TopologyCluster> getClusters(Topology topology) {
+        return defaultTopology(topology).getClusters();
+    }
+
+    @Override
+    public TopologyCluster getCluster(Topology topology, ClusterId clusterId) {
+        checkNotNull(clusterId, CLUSTER_ID_NULL);
+        return defaultTopology(topology).getCluster(clusterId);
+    }
+
+    @Override
+    public Set<DeviceId> getClusterDevices(Topology topology, TopologyCluster cluster) {
+        checkNotNull(cluster, CLUSTER_NULL);
+        return defaultTopology(topology).getClusterDevices(cluster);
+    }
+
+    @Override
+    public Set<Link> getClusterLinks(Topology topology, TopologyCluster cluster) {
+        checkNotNull(cluster, CLUSTER_NULL);
+        return defaultTopology(topology).getClusterLinks(cluster);
+    }
+
+    @Override
+    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        return defaultTopology(topology).getPaths(src, dst);
+    }
+
+    @Override
+    public Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst,
+                              LinkWeigher weigher) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        checkNotNull(weigher, LINK_WEIGHT_NULL);
+        return defaultTopology(topology).getPaths(src, dst, weigher);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(Topology topology, DeviceId src,
+                                              DeviceId dst) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        return defaultTopology(topology).getDisjointPaths(src, dst);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(Topology topology, DeviceId src,
+                                              DeviceId dst,
+                                              LinkWeigher weigher) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        checkNotNull(weigher, LINK_WEIGHT_NULL);
+        return defaultTopology(topology).getDisjointPaths(src, dst, weigher);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(Topology topology, DeviceId src,
+                                              DeviceId dst,
+                                              Map<Link, Object> riskProfile) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        return defaultTopology(topology).getDisjointPaths(src, dst, riskProfile);
+    }
+
+    @Override
+    public Set<DisjointPath> getDisjointPaths(Topology topology, DeviceId src,
+                                              DeviceId dst,
+                                              LinkWeigher weigher,
+                                              Map<Link, Object> riskProfile) {
+        checkNotNull(src, DEVICE_ID_NULL);
+        checkNotNull(dst, DEVICE_ID_NULL);
+        checkNotNull(weigher, LINK_WEIGHT_NULL);
+        return defaultTopology(topology).getDisjointPaths(src, dst, weigher,
+                riskProfile);
+    }
+
+    @Override
+    public boolean isInfrastructure(Topology topology, ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECTION_POINT_NULL);
+        return defaultTopology(topology).isInfrastructure(connectPoint);
+    }
+
+    @Override
+    public boolean isBroadcastPoint(Topology topology, ConnectPoint connectPoint) {
+        checkNotNull(connectPoint, CONNECTION_POINT_NULL);
+        return defaultTopology(topology).isBroadcastPoint(connectPoint);
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentAccumulator.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentAccumulator.java
new file mode 100644
index 0000000..0328383
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentAccumulator.java
@@ -0,0 +1,82 @@
+/*
+ * 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.impl.intent;
+
+import com.google.common.collect.Maps;
+import org.onlab.util.AbstractAccumulator;
+import org.onosproject.net.intent.IntentBatchDelegate;
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.Key;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+
+/**
+ * An accumulator for building batches of intent operations for virtual network.
+ * Only one batch should be in process per instance at a time.
+ */
+public class VirtualIntentAccumulator extends AbstractAccumulator<IntentData> {
+    private static final int DEFAULT_MAX_EVENTS = 1000;
+    private static final int DEFAULT_MAX_IDLE_MS = 10;
+    private static final int DEFAULT_MAX_BATCH_MS = 50;
+
+    // FIXME: Replace with a system-wide timer instance;
+    // TODO: Convert to use HashedWheelTimer or produce a variant of that; then decide which we want to adopt
+    private static final Timer TIMER = new Timer("virtual-intent-op-batching");
+
+    private final IntentBatchDelegate delegate;
+
+    private volatile boolean ready;
+
+    /**
+     * Creates an intent operation accumulator.
+     *
+     * @param delegate the intent batch delegate
+     */
+    public VirtualIntentAccumulator(IntentBatchDelegate delegate) {
+        super(TIMER, DEFAULT_MAX_EVENTS, DEFAULT_MAX_BATCH_MS, DEFAULT_MAX_IDLE_MS);
+        this.delegate = delegate;
+        // Assume that the delegate is ready for work at the start
+        ready = true; //TODO validate the assumption that delegate is ready
+    }
+
+    @Override
+    public void processItems(List<IntentData> items) {
+        ready = false;
+        delegate.execute(reduce(items));
+    }
+
+    private Collection<IntentData> reduce(List<IntentData> ops) {
+        Map<Key, IntentData> map = Maps.newHashMap();
+        for (IntentData op : ops) {
+            map.put(op.key(), op);
+        }
+        //TODO check the version... or maybe store will handle this.
+        return map.values();
+    }
+
+    @Override
+    public boolean isReady() {
+        return ready;
+    }
+
+    public void ready() {
+        ready = true;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentCompilerRegistry.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentCompilerRegistry.java
new file mode 100644
index 0000000..f922c22
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentCompilerRegistry.java
@@ -0,0 +1,170 @@
+/*
+ * 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.impl.intent;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.intent.VirtualIntentCompiler;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentException;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public final class VirtualIntentCompilerRegistry {
+    private final ConcurrentMap<Class<? extends Intent>,
+                VirtualIntentCompiler<? extends Intent>> compilers = new ConcurrentHashMap<>();
+
+    // non-instantiable (except for our Singleton)
+    private VirtualIntentCompilerRegistry() {
+
+    }
+
+    public static VirtualIntentCompilerRegistry getInstance() {
+        return SingletonHelper.INSTANCE;
+    }
+
+    /**
+     * Registers the specified compiler for the given intent class.
+     *
+     * @param cls      intent class
+     * @param compiler intent compiler
+     * @param <T>      the type of intent
+     */
+    public <T extends Intent> void registerCompiler(Class<T> cls,
+                                                    VirtualIntentCompiler<T> compiler) {
+        compilers.put(cls, compiler);
+    }
+
+    /**
+     * Unregisters the compiler for the specified intent class.
+     *
+     * @param cls intent class
+     * @param <T> the type of intent
+     */
+    public <T extends Intent> void unregisterCompiler(Class<T> cls) {
+        compilers.remove(cls);
+    }
+
+    /**
+     * Returns immutable set of bindings of currently registered intent compilers.
+     *
+     * @return the set of compiler bindings
+     */
+    public Map<Class<? extends Intent>, VirtualIntentCompiler<? extends Intent>> getCompilers() {
+        return ImmutableMap.copyOf(compilers);
+    }
+
+    /**
+     * Compiles an intent recursively.
+     *
+     * @param networkId network identifier
+     * @param intent intent
+     * @param previousInstallables previous intent installables
+     * @return result of compilation
+     */
+    public List<Intent> compile(NetworkId networkId,
+                         Intent intent, List<Intent> previousInstallables) {
+        if (intent.isInstallable()) {
+            return ImmutableList.of(intent);
+        }
+
+        // FIXME: get previous resources
+        List<Intent> installables = new ArrayList<>();
+        Queue<Intent> compileQueue = new LinkedList<>();
+        compileQueue.add(intent);
+
+        Intent compiling;
+        while ((compiling = compileQueue.poll()) != null) {
+            registerSubclassCompilerIfNeeded(compiling);
+
+            List<Intent> compiled = getCompiler(compiling)
+                    .compile(networkId, compiling, previousInstallables);
+
+            compiled.forEach(i -> {
+                if (i.isInstallable()) {
+                    installables.add(i);
+                } else {
+                    compileQueue.add(i);
+                }
+            });
+        }
+        return installables;
+    }
+
+    /**
+     * Returns the corresponding intent compiler to the specified intent.
+     *
+     * @param intent intent
+     * @param <T>    the type of intent
+     * @return intent compiler corresponding to the specified intent
+     */
+    private <T extends Intent> VirtualIntentCompiler<T> getCompiler(T intent) {
+        @SuppressWarnings("unchecked")
+        VirtualIntentCompiler<T> compiler =
+                (VirtualIntentCompiler<T>) compilers.get(intent.getClass());
+        if (compiler == null) {
+            throw new IntentException("no compiler for class " + intent.getClass());
+        }
+        return compiler;
+    }
+
+    /**
+     * Registers an intent compiler of the specified intent if an intent compiler
+     * for the intent is not registered. This method traverses the class hierarchy of
+     * the intent. Once an intent compiler for a parent type is found, this method
+     * registers the found intent compiler.
+     *
+     * @param intent intent
+     */
+    private void registerSubclassCompilerIfNeeded(Intent intent) {
+        if (!compilers.containsKey(intent.getClass())) {
+            Class<?> cls = intent.getClass();
+            while (cls != Object.class) {
+                // As long as we're within the Intent class descendants
+                if (Intent.class.isAssignableFrom(cls)) {
+                    VirtualIntentCompiler<?> compiler = compilers.get(cls);
+                    if (compiler != null) {
+                        compilers.put(intent.getClass(), compiler);
+                        return;
+                    }
+                }
+                cls = cls.getSuperclass();
+            }
+        }
+    }
+
+    /**
+     * Prevents object instantiation from external.
+     */
+    private static final class SingletonHelper {
+        private static final String ILLEGAL_ACCESS_MSG =
+                "Should not instantiate this class.";
+        private static final VirtualIntentCompilerRegistry INSTANCE =
+                new VirtualIntentCompilerRegistry();
+
+        private SingletonHelper() {
+            throw new IllegalAccessError(ILLEGAL_ACCESS_MSG);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallCoordinator.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallCoordinator.java
new file mode 100644
index 0000000..4a09250
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallCoordinator.java
@@ -0,0 +1,226 @@
+/*
+ * 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.impl.intent;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntentStore;
+import org.onosproject.incubator.net.virtual.impl.VirtualNetworkIntentManager;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.IntentInstallationContext;
+import org.onosproject.net.intent.IntentInstaller;
+import org.onosproject.net.intent.IntentOperationContext;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.onosproject.net.intent.IntentState.*;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of IntentInstallCoordinator for virtual network.
+ */
+public class VirtualIntentInstallCoordinator {
+    private static final String INSTALLER_NOT_FOUND = "Intent installer not found, Intent: {}";
+    private final Logger log = getLogger(VirtualNetworkIntentManager.class);
+
+    NetworkId networkId;
+    private VirtualIntentInstallerRegistry installerRegistry;
+    private VirtualNetworkIntentStore intentStore;
+
+    /**
+     * Creates an InstallCoordinator.
+     *
+     * @param networkId virtual network identifier
+     * @param installerRegistry the installer registry
+     * @param intentStore the Intent store
+     */
+    public VirtualIntentInstallCoordinator(NetworkId networkId,
+                                           VirtualIntentInstallerRegistry installerRegistry,
+                                           VirtualNetworkIntentStore intentStore) {
+        this.networkId = networkId;
+        this.installerRegistry = installerRegistry;
+        this.intentStore = intentStore;
+    }
+
+    /**
+     * Applies Intent data to be uninstalled and to be installed.
+     *
+     * @param toUninstall Intent data to be uninstalled
+     * @param toInstall Intent data to be installed
+     */
+    public void installIntents(Optional<IntentData> toUninstall,
+                               Optional<IntentData> toInstall) {
+        // If no any Intents to be uninstalled or installed, ignore it.
+        if (!toUninstall.isPresent() && !toInstall.isPresent()) {
+            return;
+        }
+
+        // Classify installable Intents to different installers.
+        ArrayListMultimap<IntentInstaller, Intent> uninstallInstallers;
+        ArrayListMultimap<IntentInstaller, Intent> installInstallers;
+        Set<IntentInstaller> allInstallers = Sets.newHashSet();
+
+        if (toUninstall.isPresent()) {
+            uninstallInstallers = getInstallers(toUninstall.get());
+            allInstallers.addAll(uninstallInstallers.keySet());
+        } else {
+            uninstallInstallers = ArrayListMultimap.create();
+        }
+
+        if (toInstall.isPresent()) {
+            installInstallers = getInstallers(toInstall.get());
+            allInstallers.addAll(installInstallers.keySet());
+        } else {
+            installInstallers = ArrayListMultimap.create();
+        }
+
+        // Generates an installation context for the high level Intent.
+        IntentInstallationContext installationContext =
+                new IntentInstallationContext(toUninstall.orElse(null), toInstall.orElse(null));
+
+        //Generates different operation context for different installable Intents.
+        Map<IntentInstaller, IntentOperationContext> contexts = Maps.newHashMap();
+        allInstallers.forEach(installer -> {
+            List<Intent> intentsToUninstall = uninstallInstallers.get(installer);
+            List<Intent> intentsToInstall = installInstallers.get(installer);
+
+            // Connect context to high level installation context
+            IntentOperationContext context =
+                    new IntentOperationContext(intentsToUninstall, intentsToInstall,
+                                               installationContext);
+            installationContext.addPendingContext(context);
+            contexts.put(installer, context);
+        });
+
+        // Apply contexts to installers
+        contexts.forEach((installer, context) -> {
+            installer.apply(context);
+        });
+    }
+
+    /**
+     * Generates a mapping for installable Intents to installers.
+     *
+     * @param intentData the Intent data which contains installable Intents
+     * @return the mapping for installable Intents to installers
+     */
+    private ArrayListMultimap<IntentInstaller, Intent> getInstallers(IntentData intentData) {
+        ArrayListMultimap<IntentInstaller, Intent> intentInstallers = ArrayListMultimap.create();
+        intentData.installables().forEach(intent -> {
+            IntentInstaller installer = installerRegistry.getInstaller(intent.getClass());
+            if (installer != null) {
+                intentInstallers.put(installer, intent);
+            } else {
+                log.warn(INSTALLER_NOT_FOUND, intent);
+            }
+        });
+        return intentInstallers;
+    }
+
+    /**
+     * Handles success operation context.
+     *
+     * @param context the operation context
+     */
+    public void success(IntentOperationContext context) {
+        IntentInstallationContext intentInstallationContext =
+                context.intentInstallationContext();
+        intentInstallationContext.removePendingContext(context);
+
+        if (intentInstallationContext.isPendingContextsEmpty()) {
+            finish(intentInstallationContext);
+        }
+    }
+
+    /**
+     * Handles failed operation context.
+     *
+     * @param context the operation context
+     */
+    public void failed(IntentOperationContext context) {
+        IntentInstallationContext intentInstallationContext =
+                context.intentInstallationContext();
+        intentInstallationContext.addErrorContext(context);
+        intentInstallationContext.removePendingContext(context);
+
+        if (intentInstallationContext.isPendingContextsEmpty()) {
+            finish(intentInstallationContext);
+        }
+    }
+
+    /**
+     * Completed the installation context and update the Intent store.
+     *
+     * @param intentInstallationContext the installation context
+     */
+    private void finish(IntentInstallationContext intentInstallationContext) {
+        Set<IntentOperationContext> errCtxs = intentInstallationContext.errorContexts();
+        Optional<IntentData> toUninstall = intentInstallationContext.toUninstall();
+        Optional<IntentData> toInstall = intentInstallationContext.toInstall();
+
+        // Intent install success
+        if (errCtxs == null || errCtxs.isEmpty()) {
+            if (toInstall.isPresent()) {
+                IntentData installData = toInstall.get();
+                log.debug("Completed installing: {}", installData.key());
+                installData = new IntentData(installData, installData.installables());
+                installData.setState(INSTALLED);
+                intentStore.write(networkId, installData);
+            } else if (toUninstall.isPresent()) {
+                IntentData uninstallData = toUninstall.get();
+                uninstallData = new IntentData(uninstallData, Collections.emptyList());
+                log.debug("Completed withdrawing: {}", uninstallData.key());
+                switch (uninstallData.request()) {
+                    case INSTALL_REQ:
+                        log.warn("{} was requested to withdraw during installation?",
+                                 uninstallData.intent());
+                        uninstallData.setState(FAILED);
+                        break;
+                    case WITHDRAW_REQ:
+                    default: //TODO "default" case should not happen
+                        uninstallData.setState(WITHDRAWN);
+                        break;
+                }
+                // Intent has been withdrawn; we can clear the installables
+                intentStore.write(networkId, uninstallData);
+            }
+        } else {
+            // if toInstall was cause of error, then recompile (manage/increment counter, when exceeded -> CORRUPT)
+            if (toInstall.isPresent()) {
+                IntentData installData = toInstall.get();
+                installData.setState(CORRUPT);
+                installData.incrementErrorCount();
+                intentStore.write(networkId, installData);
+            }
+            // if toUninstall was cause of error, then CORRUPT (another job will clean this up)
+            if (toUninstall.isPresent()) {
+                IntentData uninstallData = toUninstall.get();
+                uninstallData.setState(CORRUPT);
+                uninstallData.incrementErrorCount();
+                intentStore.write(networkId, uninstallData);
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallerRegistry.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallerRegistry.java
new file mode 100644
index 0000000..a0e6448
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentInstallerRegistry.java
@@ -0,0 +1,96 @@
+/*
+ * 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.impl.intent;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentInstaller;
+
+import java.util.Map;
+
+/**
+ * The local registry for Intent installer for virtual networks.
+ */
+public final class VirtualIntentInstallerRegistry {
+    private final Map<Class<? extends Intent>,
+                IntentInstaller<? extends Intent>> installers;
+
+    // non-instantiable (except for our Singleton)
+    private VirtualIntentInstallerRegistry() {
+        installers = Maps.newConcurrentMap();
+    }
+
+    public static VirtualIntentInstallerRegistry getInstance() {
+        return SingletonHelper.INSTANCE;
+    }
+
+    /**
+     * Registers the specific installer for the given intent class.
+     *
+     * @param cls intent class
+     * @param installer intent installer
+     * @param <T> the type of intent
+     */
+    public <T extends Intent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer) {
+        installers.put(cls, installer);
+    }
+
+    /**
+     * Unregisters the installer for the specific intent class.
+     *
+     * @param cls intent class
+     * @param <T> the type of intent
+     */
+    public <T extends Intent> void unregisterInstaller(Class<T> cls) {
+        installers.remove(cls);
+    }
+
+    /**
+     * Returns immutable set of binding of currently registered intent installers.
+     *
+     * @return the set of installer bindings
+     */
+    public Map<Class<? extends Intent>, IntentInstaller<? extends Intent>> getInstallers() {
+        return ImmutableMap.copyOf(installers);
+    }
+
+    /**
+     * Get an Intent installer by given Intent type.
+     *
+     * @param cls the Intent type
+     * @param <T> the Intent type
+     * @return the Intent installer of the Intent type if exists; null otherwise
+     */
+    public <T extends Intent> IntentInstaller<T> getInstaller(Class<T> cls) {
+        return (IntentInstaller<T>) installers.get(cls);
+    }
+
+    /**
+     * Prevents object instantiation from external.
+     */
+    private static final class SingletonHelper {
+        private static final String ILLEGAL_ACCESS_MSG =
+                "Should not instantiate this class.";
+        private static final VirtualIntentInstallerRegistry INSTANCE =
+                new VirtualIntentInstallerRegistry();
+
+        private SingletonHelper() {
+            throw new IllegalAccessError(ILLEGAL_ACCESS_MSG);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentProcessor.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentProcessor.java
new file mode 100644
index 0000000..a7719f9
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentProcessor.java
@@ -0,0 +1,52 @@
+/*
+ * 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.impl.intent;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentData;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A collection of methods to process an intent for virtual networks.
+ *
+ * This interface is public, but intended to be used only by IntentManager and
+ * IntentProcessPhase subclasses stored under phase package.
+ */
+public interface VirtualIntentProcessor {
+    /**
+     * Compiles an intent recursively.
+     *
+     * @param networkId virtual network identifier
+     * @param intent intent
+     * @param previousInstallables previous intent installables
+     * @return result of compilation
+     */
+    List<Intent> compile(NetworkId networkId, Intent intent, List<Intent> previousInstallables);
+
+    /**
+     * Applies intents.
+     *
+     * @param networkId virtual network identifier
+     * @param toUninstall Intent data describing flows to uninstall.
+     * @param toInstall Intent data describing flows to install.
+     */
+    void apply(NetworkId networkId, Optional<IntentData> toUninstall,
+               Optional<IntentData> toInstall);
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentSkipped.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentSkipped.java
new file mode 100644
index 0000000..df7f451
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/VirtualIntentSkipped.java
@@ -0,0 +1,48 @@
+/*
+ * 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.impl.intent;
+
+import org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualFinalIntentProcessPhase;
+import org.onosproject.net.intent.IntentData;
+
+/**
+ * Represents a phase where an intent is not compiled for a virtual network.
+ * This should be used if a new version of the intent will immediately override
+ * this one.
+ */
+public final class VirtualIntentSkipped extends VirtualFinalIntentProcessPhase {
+
+    private static final VirtualIntentSkipped SINGLETON = new VirtualIntentSkipped();
+
+    /**
+     * Returns a shared skipped phase.
+     *
+     * @return skipped phase
+     */
+    public static VirtualIntentSkipped getPhase() {
+        return SINGLETON;
+    }
+
+    // Prevent object construction; use getPhase()
+    private VirtualIntentSkipped() {
+    }
+
+    @Override
+    public IntentData data() {
+        return null;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/package-info.java
new file mode 100644
index 0000000..c6758ae
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Core subsystem for tracking high-level intents for treatment of selected
+ * network traffic for virtual networks.
+ */
+package org.onosproject.incubator.net.virtual.impl.intent;
\ No newline at end of file
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualFinalIntentProcessPhase.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualFinalIntentProcessPhase.java
new file mode 100644
index 0000000..978c95c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualFinalIntentProcessPhase.java
@@ -0,0 +1,46 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.net.intent.IntentData;
+
+import java.util.Optional;
+
+/**
+ * Represents a final phase of processing an intent for virtual networks.
+ */
+public abstract class VirtualFinalIntentProcessPhase
+        implements VirtualIntentProcessPhase {
+
+    @Override
+    public final Optional<VirtualIntentProcessPhase> execute() {
+        preExecute();
+        return Optional.empty();
+    }
+
+    /**
+     * Executes operations that must take place before the phase starts.
+     */
+    protected void preExecute() {}
+
+    /**
+     * Returns the IntentData object being acted on by this phase.
+     *
+     * @return intent data object for the phase
+     */
+    public abstract IntentData data();
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentCompiling.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentCompiling.java
new file mode 100644
index 0000000..639c04c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentCompiling.java
@@ -0,0 +1,81 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.IntentException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Represents a phase where an intent is being compiled or recompiled
+ * for virtual networks.
+ */
+public class VirtualIntentCompiling implements VirtualIntentProcessPhase {
+    private static final Logger log = LoggerFactory.getLogger(VirtualIntentCompiling.class);
+
+    private final NetworkId networkId;
+    private final VirtualIntentProcessor processor;
+    private final IntentData data;
+    private final Optional<IntentData> stored;
+
+    /**
+     * Creates a intent recompiling phase.
+     *
+     * @param networkId virtual network identifier
+     * @param processor intent processor that does work for recompiling
+     * @param data      intent data containing an intent to be recompiled
+     * @param stored    intent data stored in the store
+     */
+    VirtualIntentCompiling(NetworkId networkId, VirtualIntentProcessor processor,
+                           IntentData data, Optional<IntentData> stored) {
+        this.networkId = checkNotNull(networkId);
+        this.processor = checkNotNull(processor);
+        this.data = checkNotNull(data);
+        this.stored = checkNotNull(stored);
+    }
+
+    @Override
+    public Optional<VirtualIntentProcessPhase> execute() {
+        try {
+            List<Intent> compiled = processor
+                    .compile(networkId, data.intent(),
+                             //TODO consider passing an optional here in the future
+                             stored.map(IntentData::installables).orElse(null));
+            return Optional.of(new VirtualIntentInstalling(networkId, processor,
+                                                           IntentData.compiled(data, compiled), stored));
+        } catch (IntentException e) {
+            log.warn("Unable to compile intent {} due to:", data.intent(), e);
+            if (stored.filter(x -> !x.installables().isEmpty()).isPresent()) {
+                // removing orphaned flows and deallocating resources
+                return Optional.of(
+                        new VirtualIntentWithdrawing(networkId, processor,
+                                                     new IntentData(data, stored.get().installables())));
+            } else {
+                return Optional.of(new VirtualIntentFailed(data));
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentFailed.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentFailed.java
new file mode 100644
index 0000000..dff1d13
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentFailed.java
@@ -0,0 +1,44 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.net.intent.IntentData;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.intent.IntentState.FAILED;
+
+/**
+ * Represents a phase where the compile has failed.
+ */
+public class VirtualIntentFailed extends VirtualFinalIntentProcessPhase {
+
+    private final IntentData data;
+
+    /**
+     * Create an instance with the specified data.
+     *
+     * @param data intentData
+     */
+    VirtualIntentFailed(IntentData data) {
+        this.data = IntentData.nextState(checkNotNull(data), FAILED);
+    }
+
+    @Override
+    public IntentData data() {
+        return data;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstallRequest.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstallRequest.java
new file mode 100644
index 0000000..08bfec7
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstallRequest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.IntentData;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualIntentProcessPhase.transferErrorCount;
+
+/**
+ * Represents a phase where intent installation has been requested
+ * for a virtual network.
+ */
+final class VirtualIntentInstallRequest implements VirtualIntentProcessPhase {
+
+    private final NetworkId networkId;
+    private final VirtualIntentProcessor processor;
+    private final IntentData data;
+    private final Optional<IntentData> stored;
+
+    /**
+     * Creates an install request phase.
+     *
+     * @param networkId virtual network identifier
+     * @param processor  intent processor to be passed to intent process phases
+     *                   generated after this phase
+     * @param intentData intent data to be processed
+     * @param stored     intent data stored in the store
+     */
+    VirtualIntentInstallRequest(NetworkId networkId, VirtualIntentProcessor processor,
+                                IntentData intentData, Optional<IntentData> stored) {
+        this.networkId = checkNotNull(networkId);
+        this.processor = checkNotNull(processor);
+        this.data = checkNotNull(intentData);
+        this.stored = checkNotNull(stored);
+    }
+
+    @Override
+    public Optional<VirtualIntentProcessPhase> execute() {
+        transferErrorCount(data, stored);
+
+        return Optional.of(new VirtualIntentCompiling(networkId, processor, data, stored));
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstalling.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstalling.java
new file mode 100644
index 0000000..cd8a185
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentInstalling.java
@@ -0,0 +1,66 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.IntentData;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.intent.IntentState.INSTALLING;
+
+/**
+ * Represents a phase where an intent is being installed for a virtual network.
+ */
+//FIXME: better way to implement intent phase and processing for virtual networks?
+public class VirtualIntentInstalling extends VirtualFinalIntentProcessPhase {
+
+    private final NetworkId networkId;
+    private final VirtualIntentProcessor processor;
+    private final IntentData data;
+    private final Optional<IntentData> stored;
+
+    /**
+     * Create an installing phase.
+     *
+     * @param networkId virtual network identifier
+     * @param processor intent processor that does work for installing
+     * @param data      intent data containing an intent to be installed
+     * @param stored    intent data already stored
+     */
+    VirtualIntentInstalling(NetworkId networkId, VirtualIntentProcessor processor,
+                            IntentData data,
+                            Optional<IntentData> stored) {
+        this.networkId = checkNotNull(networkId);
+        this.processor = checkNotNull(processor);
+        this.data = checkNotNull(data);
+        this.stored = checkNotNull(stored);
+        this.data.setState(INSTALLING);
+    }
+
+    @Override
+    public void preExecute() {
+        processor.apply(networkId, stored, Optional.of(data));
+    }
+
+    @Override
+    public IntentData data() {
+        return data;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentProcessPhase.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentProcessPhase.java
new file mode 100644
index 0000000..99fab54
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentProcessPhase.java
@@ -0,0 +1,87 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.IntentData;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Represents a phase of processing an intent.
+ */
+public interface VirtualIntentProcessPhase {
+    /**
+     * Execute the procedure represented by the instance
+     * and generates the next update instance.
+     *
+     * @return next update
+     */
+    Optional<VirtualIntentProcessPhase> execute();
+
+    /**
+     * Create a starting intent process phase according to intent data this class holds.
+     *
+     * @param networkId virtual network identifier
+     * @param processor intent processor to be passed to intent process phases
+     *                  generated while this instance is working
+     * @param data intent data to be processed
+     * @param current intent date that is stored in the store
+     * @return starting intent process phase
+     */
+    static VirtualIntentProcessPhase newInitialPhase(NetworkId networkId,
+                                                     VirtualIntentProcessor processor,
+                                                     IntentData data, IntentData current) {
+        switch (data.request()) {
+            case INSTALL_REQ:
+                return new VirtualIntentInstallRequest(networkId, processor, data,
+                                                       Optional.ofNullable(current));
+            case WITHDRAW_REQ:
+                return new VirtualIntentWithdrawRequest(networkId, processor, data,
+                                                        Optional.ofNullable(current));
+            case PURGE_REQ:
+                return new VirtualIntentPurgeRequest(data, Optional.ofNullable(current));
+            default:
+                // illegal state
+                return new VirtualIntentFailed(data);
+        }
+    }
+
+    static VirtualFinalIntentProcessPhase process(VirtualIntentProcessPhase initial) {
+        Optional<VirtualIntentProcessPhase> currentPhase = Optional.of(initial);
+        VirtualIntentProcessPhase previousPhase = initial;
+
+        while (currentPhase.isPresent()) {
+            previousPhase = currentPhase.get();
+            currentPhase = previousPhase.execute();
+        }
+        return (VirtualFinalIntentProcessPhase) previousPhase;
+    }
+
+    static void transferErrorCount(IntentData data, Optional<IntentData> stored) {
+        stored.ifPresent(storedData -> {
+            if (Objects.equals(data.intent(), storedData.intent()) &&
+                    Objects.equals(data.request(), storedData.request())) {
+                data.setErrorCount(storedData.errorCount());
+            } else {
+                data.setErrorCount(0);
+            }
+        });
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentPurgeRequest.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentPurgeRequest.java
new file mode 100644
index 0000000..a6f29ea
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentPurgeRequest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.IntentState;
+import org.slf4j.Logger;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Represents a phase of requesting a purge of an intent for a virtual network.
+ * Note: The purge will only succeed if the intent is FAILED or WITHDRAWN.
+ */
+final class VirtualIntentPurgeRequest extends VirtualFinalIntentProcessPhase {
+    private static final Logger log = getLogger(VirtualIntentPurgeRequest.class);
+
+    private final IntentData data;
+    protected final Optional<IntentData> stored;
+
+    VirtualIntentPurgeRequest(IntentData intentData, Optional<IntentData> stored) {
+        this.data = checkNotNull(intentData);
+        this.stored = checkNotNull(stored);
+    }
+
+    private boolean shouldAcceptPurge() {
+        if (!stored.isPresent()) {
+            log.info("Purge for intent {}, but intent is not present",
+                     data.key());
+            return true;
+        }
+
+        IntentData storedData = stored.get();
+        if (storedData.state() == IntentState.WITHDRAWN
+                || storedData.state() == IntentState.FAILED) {
+            return true;
+        }
+        log.info("Purge for intent {} is rejected because intent state is {}",
+                 data.key(), storedData.state());
+        return false;
+    }
+
+    @Override
+    public IntentData data() {
+        if (shouldAcceptPurge()) {
+            return data;
+        } else {
+            return stored.get();
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawRequest.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawRequest.java
new file mode 100644
index 0000000..ce8dfc9
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawRequest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.IntentData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.incubator.net.virtual.impl.intent.phase.VirtualIntentProcessPhase.transferErrorCount;
+
+/**
+ * Represents a phase of requesting a withdraw of an intent for a virtual network.
+ */
+final class VirtualIntentWithdrawRequest implements VirtualIntentProcessPhase {
+    private static final Logger log = LoggerFactory.getLogger(VirtualIntentWithdrawRequest.class);
+
+    private final NetworkId networkId;
+    private final VirtualIntentProcessor processor;
+    private final IntentData data;
+    private final Optional<IntentData> stored;
+
+    /**
+     * Creates a withdraw request phase.
+     *
+     * @param networkId virtual network identifier
+     * @param processor  intent processor to be passed to intent process phases
+     *                   generated after this phase
+     * @param intentData intent data to be processed
+     * @param stored     intent data stored in the store
+     */
+    VirtualIntentWithdrawRequest(NetworkId networkId, VirtualIntentProcessor processor,
+                                 IntentData intentData, Optional<IntentData> stored) {
+        this.networkId = checkNotNull(networkId);
+        this.processor = checkNotNull(processor);
+        this.data = checkNotNull(intentData);
+        this.stored = checkNotNull(stored);
+    }
+
+    @Override
+    public Optional<VirtualIntentProcessPhase> execute() {
+        //TODO perhaps we want to validate that the pending and current are the
+        // same version i.e. they are the same
+        // Note: this call is not just the symmetric version of submit
+
+        transferErrorCount(data, stored);
+
+        if (!stored.isPresent() || stored.get().installables().isEmpty()) {
+            switch (data.request()) {
+                case INSTALL_REQ:
+                    // illegal state?
+                    log.warn("{} was requested to withdraw during installation?", data.intent());
+                    return Optional.of(new VirtualIntentFailed(data));
+                case WITHDRAW_REQ:
+                default: //TODO "default" case should not happen
+                    return Optional.of(new VirtualIntentWithdrawn(data));
+            }
+        }
+
+        return Optional.of(new VirtualIntentWithdrawing(networkId, processor,
+                                                        new IntentData(data, stored.get().installables())));
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawing.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawing.java
new file mode 100644
index 0000000..f7a2867
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawing.java
@@ -0,0 +1,61 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.impl.intent.VirtualIntentProcessor;
+import org.onosproject.net.intent.IntentData;
+
+import java.util.Optional;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.intent.IntentState.WITHDRAWING;
+
+/**
+ * Represents a phase where an intent is withdrawing.
+ */
+final class VirtualIntentWithdrawing extends VirtualFinalIntentProcessPhase {
+
+    private final NetworkId networkId;
+    private final VirtualIntentProcessor processor;
+    private final IntentData data;
+
+    /**
+     * Creates a withdrawing phase.
+     *
+     * @param networkId virtual network identifier
+     * @param processor intent processor that does work for withdrawing
+     * @param data      intent data containing an intent to be withdrawn
+     */
+    VirtualIntentWithdrawing(NetworkId networkId, VirtualIntentProcessor processor,
+                             IntentData data) {
+        this.networkId = checkNotNull(networkId);
+        this.processor = checkNotNull(processor);
+        this.data = checkNotNull(data);
+        this.data.setState(WITHDRAWING);
+    }
+
+    @Override
+    protected void preExecute() {
+        processor.apply(networkId, Optional.of(data), Optional.empty());
+    }
+
+    @Override
+    public IntentData data() {
+        return data;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawn.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawn.java
new file mode 100644
index 0000000..7e592c2
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/VirtualIntentWithdrawn.java
@@ -0,0 +1,44 @@
+/*
+ * 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.impl.intent.phase;
+
+import org.onosproject.net.intent.IntentData;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.intent.IntentState.WITHDRAWN;
+
+/**
+ * Represents a phase where an intent has been withdrawn for a virtual network.
+ */
+final class VirtualIntentWithdrawn extends VirtualFinalIntentProcessPhase {
+
+    private final IntentData data;
+
+    /**
+     * Create a withdrawn phase.
+     *
+     * @param data intent data containing an intent to be withdrawn
+     */
+    VirtualIntentWithdrawn(IntentData data) {
+        this.data = IntentData.nextState(checkNotNull(data), WITHDRAWN);
+    }
+
+    @Override
+    public IntentData data() {
+        return data;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/package-info.java
new file mode 100644
index 0000000..5fac168
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/intent/phase/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Implementations of various intent processing phases for virtual networks.
+ */
+package org.onosproject.incubator.net.virtual.impl.intent.phase;
\ No newline at end of file
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/package-info.java
new file mode 100644
index 0000000..b0750eb
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Implementation of the virtual network subsystem.
+ */
+package org.onosproject.incubator.net.virtual.impl;
\ No newline at end of file
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualFlowRuleProvider.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualFlowRuleProvider.java
new file mode 100644
index 0000000..436e1d4
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualFlowRuleProvider.java
@@ -0,0 +1,792 @@
+/*
+ * 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.impl.provider;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProvider;
+import org.onosproject.incubator.net.virtual.provider.InternalRoutingAlgorithm;
+import org.onosproject.incubator.net.virtual.provider.VirtualFlowRuleProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualFlowRuleProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.BatchOperationEntry;
+import org.onosproject.net.flow.CompletedBatchOperation;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.DefaultFlowRule;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleEvent;
+import org.onosproject.net.flow.FlowRuleListener;
+import org.onosproject.net.flow.FlowRuleOperations;
+import org.onosproject.net.flow.FlowRuleOperationsContext;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.criteria.Criterion;
+import org.onosproject.net.flow.criteria.PortCriterion;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.topology.TopologyService;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableSet.copyOf;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provider that translate virtual flow rules into physical rules.
+ * Current implementation is based on FlowRules.
+ * This virtualize and de-virtualize virtual flow rules into physical flow rules.
+ * {@link org.onosproject.net.flow.FlowRule}
+ */
+@Component(service = VirtualFlowRuleProvider.class)
+public class DefaultVirtualFlowRuleProvider extends AbstractVirtualProvider
+        implements VirtualFlowRuleProvider {
+
+    private static final String APP_ID_STR = "org.onosproject.virtual.vnet-flow_";
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected TopologyService topologyService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualNetworkService vnService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected FlowRuleService flowRuleService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualProviderRegistryService providerRegistryService;
+
+    private InternalRoutingAlgorithm internalRoutingAlgorithm;
+    private InternalVirtualFlowRuleManager frm;
+    private ApplicationId appId;
+    private FlowRuleListener flowRuleListener;
+
+    /**
+     * Creates a provider with the identifier.
+     */
+    public DefaultVirtualFlowRuleProvider() {
+        super(new ProviderId("vnet-flow", "org.onosproject.virtual.vnet-flow"));
+    }
+
+    @Activate
+    public void activate() {
+        appId = coreService.registerApplication(APP_ID_STR);
+
+        providerRegistryService.registerProvider(this);
+
+        flowRuleListener = new InternalFlowRuleListener();
+        flowRuleService.addListener(flowRuleListener);
+
+        internalRoutingAlgorithm = new DefaultInternalRoutingAlgorithm();
+        frm = new InternalVirtualFlowRuleManager();
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        flowRuleService.removeListener(flowRuleListener);
+        flowRuleService.removeFlowRulesById(appId);
+        providerRegistryService.unregisterProvider(this);
+        log.info("Stopped");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+    }
+
+    @Override
+    public void applyFlowRule(NetworkId networkId, FlowRule... flowRules) {
+        for (FlowRule flowRule : flowRules) {
+            devirtualize(networkId, flowRule).forEach(
+                    r -> flowRuleService.applyFlowRules(r));
+        }
+    }
+
+    @Override
+    public void removeFlowRule(NetworkId networkId, FlowRule... flowRules) {
+        for (FlowRule flowRule : flowRules) {
+            devirtualize(networkId, flowRule).forEach(
+                    r -> flowRuleService.removeFlowRules(r));
+        }
+    }
+
+    @Override
+    public void executeBatch(NetworkId networkId, FlowRuleBatchOperation batch) {
+        checkNotNull(batch);
+
+        for (FlowRuleBatchEntry fop : batch.getOperations()) {
+            FlowRuleOperations.Builder builder = FlowRuleOperations.builder();
+
+            switch (fop.operator()) {
+                case ADD:
+                    devirtualize(networkId, fop.target()).forEach(builder::add);
+                    break;
+                case REMOVE:
+                    devirtualize(networkId, fop.target()).forEach(builder::remove);
+                    break;
+                case MODIFY:
+                    devirtualize(networkId, fop.target()).forEach(builder::modify);
+                    break;
+                default:
+                    break;
+            }
+
+            flowRuleService.apply(builder.build(new FlowRuleOperationsContext() {
+                @Override
+                public void onSuccess(FlowRuleOperations ops) {
+                    CompletedBatchOperation status =
+                            new CompletedBatchOperation(true,
+                                                        Sets.newConcurrentHashSet(),
+                                                        batch.deviceId());
+
+                    VirtualFlowRuleProviderService providerService =
+                            (VirtualFlowRuleProviderService) providerRegistryService
+                                    .getProviderService(networkId,
+                                                        VirtualFlowRuleProvider.class);
+                    providerService.batchOperationCompleted(batch.id(), status);
+                }
+
+                @Override
+                public void onError(FlowRuleOperations ops) {
+                    Set<FlowRule> failures = ImmutableSet.copyOf(
+                            Lists.transform(batch.getOperations(),
+                                            BatchOperationEntry::target));
+
+                    CompletedBatchOperation status =
+                            new CompletedBatchOperation(false,
+                                                        failures,
+                                                        batch.deviceId());
+
+                    VirtualFlowRuleProviderService providerService =
+                            (VirtualFlowRuleProviderService) providerRegistryService
+                                    .getProviderService(networkId,
+                                                        VirtualFlowRuleProvider.class);
+                    providerService.batchOperationCompleted(batch.id(), status);
+                }
+            }));
+        }
+    }
+
+    public void setEmbeddingAlgorithm(InternalRoutingAlgorithm
+                                              internalRoutingAlgorithm) {
+        this.internalRoutingAlgorithm = internalRoutingAlgorithm;
+    }
+
+    /**
+     * Translate the requested physical flow rules into virtual flow rules.
+     *
+     * @param flowRule A virtual flow rule to be translated
+     * @return A flow rule for a specific virtual network
+     */
+    private FlowRule virtualizeFlowRule(FlowRule flowRule) {
+
+        FlowRule storedrule = frm.getVirtualRule(flowRule);
+
+        if (flowRule.reason() == FlowRule.FlowRemoveReason.NO_REASON) {
+            return storedrule;
+        } else {
+            return DefaultFlowRule.builder()
+                    .withReason(flowRule.reason())
+                    .withPriority(storedrule.priority())
+                    .forDevice(storedrule.deviceId())
+                    .forTable(storedrule.tableId())
+                    .fromApp(new DefaultApplicationId(storedrule.appId(), null))
+                    .withIdleTimeout(storedrule.timeout())
+                    .withHardTimeout(storedrule.hardTimeout())
+                    .withSelector(storedrule.selector())
+                    .withTreatment(storedrule.treatment())
+                    .build();
+        }
+    }
+
+    private FlowEntry virtualize(FlowEntry flowEntry) {
+        FlowRule vRule = virtualizeFlowRule(flowEntry);
+
+        if (vRule == null) {
+            return null;
+        }
+
+        FlowEntry vEntry = new DefaultFlowEntry(vRule, flowEntry.state(),
+                                                flowEntry.life(),
+                                                flowEntry.packets(),
+                                                flowEntry.bytes());
+        return vEntry;
+    }
+
+    /**
+     * Translate the requested virtual flow rules into physical flow rules.
+     * The translation could be one to many.
+     *
+     * @param flowRule A flow rule from underlying data plane to be translated
+     * @return A set of flow rules for physical network
+     */
+    private Set<FlowRule> devirtualize(NetworkId networkId, FlowRule flowRule) {
+
+        Set<FlowRule> outRules = new HashSet<>();
+
+        Set<ConnectPoint> ingressPoints = extractIngressPoints(networkId,
+                                                               flowRule.deviceId(),
+                                                               flowRule.selector());
+
+        ConnectPoint egressPoint = extractEgressPoints(networkId,
+                                                         flowRule.deviceId(),
+                                                         flowRule.treatment());
+
+        if (egressPoint == null) {
+            return outRules;
+        }
+
+        TrafficSelector.Builder commonSelectorBuilder
+                = DefaultTrafficSelector.builder();
+        flowRule.selector().criteria().stream()
+                .filter(c -> c.type() != Criterion.Type.IN_PORT)
+                .forEach(c -> commonSelectorBuilder.add(c));
+        TrafficSelector commonSelector = commonSelectorBuilder.build();
+
+        TrafficTreatment.Builder commonTreatmentBuilder
+                = DefaultTrafficTreatment.builder();
+        flowRule.treatment().allInstructions().stream()
+                .filter(i -> i.type() != Instruction.Type.OUTPUT)
+                .forEach(i -> commonTreatmentBuilder.add(i));
+        TrafficTreatment commonTreatment = commonTreatmentBuilder.build();
+
+        for (ConnectPoint ingressPoint : ingressPoints) {
+            if (egressPoint.port() == PortNumber.FLOOD) {
+                Set<ConnectPoint> outPoints = vnService
+                        .getVirtualPorts(networkId, flowRule.deviceId())
+                        .stream()
+                        .map(VirtualPort::realizedBy)
+                        .filter(p -> !p.equals(ingressPoint))
+                        .collect(Collectors.toSet());
+
+                for (ConnectPoint outPoint : outPoints) {
+                    outRules.addAll(generateRules(networkId, ingressPoint, outPoint,
+                                                  commonSelector, commonTreatment, flowRule));
+                }
+            } else {
+                outRules.addAll(generateRules(networkId, ingressPoint, egressPoint,
+                                              commonSelector, commonTreatment, flowRule));
+            }
+        }
+
+        return outRules;
+    }
+
+    /**
+     * Extract ingress connect points of the physical network
+     * from the requested traffic selector.
+     *
+     * @param networkId the virtual network identifier
+     * @param deviceId the virtual device identifier
+     * @param selector the traffic selector to extract ingress point
+     * @return the set of ingress connect points of the physical network
+     */
+    private Set<ConnectPoint> extractIngressPoints(NetworkId networkId,
+                                                   DeviceId deviceId,
+                                                   TrafficSelector selector) {
+
+        Set<ConnectPoint> ingressPoints = new HashSet<>();
+
+        Set<VirtualPort> vPorts = vnService
+                .getVirtualPorts(networkId, deviceId);
+
+        PortCriterion portCriterion = ((PortCriterion) selector
+                .getCriterion(Criterion.Type.IN_PORT));
+
+        if (portCriterion != null) {
+            PortNumber vInPortNum = portCriterion.port();
+
+            Optional<ConnectPoint> optionalCp =  vPorts.stream()
+                    .filter(v -> v.number().equals(vInPortNum))
+                    .map(VirtualPort::realizedBy).findFirst();
+            if (!optionalCp.isPresent()) {
+                log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                         vInPortNum, networkId, deviceId);
+                return ingressPoints;
+            }
+
+            ingressPoints.add(optionalCp.get());
+        } else {
+            for (VirtualPort vPort : vPorts) {
+                if (vPort.realizedBy() != null) {
+                    ingressPoints.add(vPort.realizedBy());
+                } else {
+                    log.warn("Port {} is not realized yet, in Network {}, " +
+                                     "Device {}",
+                             vPort, networkId, deviceId);
+                }
+            }
+        }
+
+        return ingressPoints;
+    }
+
+    /**
+     * Extract egress connect point of the physical network
+     * from the requested traffic treatment.
+     *
+     * @param networkId the virtual network identifier
+     * @param deviceId the virtual device identifier
+     * @param treatment the traffic treatment to extract ingress point
+     * @return the egress connect point of the physical network
+     */
+    private ConnectPoint extractEgressPoints(NetworkId networkId,
+                                                  DeviceId deviceId,
+                                                  TrafficTreatment treatment) {
+
+        Set<VirtualPort> vPorts = vnService
+                .getVirtualPorts(networkId, deviceId);
+
+        PortNumber vOutPortNum = treatment.allInstructions().stream()
+                .filter(i -> i.type() == Instruction.Type.OUTPUT)
+                .map(i -> ((Instructions.OutputInstruction) i).port())
+                .findFirst().get();
+
+        Optional<ConnectPoint> optionalCpOut = vPorts.stream()
+                .filter(v -> v.number().equals(vOutPortNum))
+                .map(VirtualPort::realizedBy)
+                .findFirst();
+
+        if (!optionalCpOut.isPresent()) {
+            if (vOutPortNum.isLogical()) {
+                return new ConnectPoint(DeviceId.deviceId("vNet"), vOutPortNum);
+            }
+
+            log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                     vOutPortNum, networkId, deviceId);
+            return null;
+        }
+
+        return optionalCpOut.get();
+    }
+
+
+    /**
+     * Generates the corresponding flow rules for the physical network.
+     *
+     * @param networkId The virtual network identifier
+     * @param ingressPoint The ingress point of the physical network
+     * @param egressPoint The egress point of the physical network
+     * @param commonSelector A common traffic selector between the virtual
+     *                       and physical flow rules
+     * @param commonTreatment A common traffic treatment between the virtual
+     *                        and physical flow rules
+     * @param flowRule The virtual flow rule to be translated
+     * @return A set of flow rules for the physical network
+     */
+    private Set<FlowRule> generateRules(NetworkId networkId,
+                                        ConnectPoint ingressPoint,
+                                        ConnectPoint egressPoint,
+                                        TrafficSelector commonSelector,
+                                        TrafficTreatment commonTreatment,
+                                        FlowRule flowRule) {
+
+        if (ingressPoint.deviceId().equals(egressPoint.deviceId()) ||
+                egressPoint.port().isLogical()) {
+            return generateRuleForSingle(networkId, ingressPoint, egressPoint,
+                                         commonSelector, commonTreatment, flowRule);
+        } else {
+            return generateRuleForMulti(networkId, ingressPoint, egressPoint,
+                                         commonSelector, commonTreatment, flowRule);
+        }
+    }
+
+    /**
+     * Generate physical rules when a virtual flow rule can be handled inside
+     * a single physical switch.
+     *
+     * @param networkId The virtual network identifier
+     * @param ingressPoint The ingress point of the physical network
+     * @param egressPoint The egress point of the physical network
+     * @param commonSelector A common traffic selector between the virtual
+     *                       and physical flow rules
+     * @param commonTreatment A common traffic treatment between the virtual
+     *                        and physical flow rules
+     * @param flowRule The virtual flow rule to be translated
+     * @return A set of flow rules for the physical network
+     */
+    private Set<FlowRule> generateRuleForSingle(NetworkId networkId,
+            ConnectPoint ingressPoint,
+            ConnectPoint egressPoint,
+            TrafficSelector commonSelector,
+            TrafficTreatment commonTreatment,
+            FlowRule flowRule) {
+
+        Set<FlowRule> outRules = new HashSet<>();
+
+        TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector
+                .builder(commonSelector)
+                .matchInPort(ingressPoint.port());
+
+        TrafficTreatment.Builder treatmentBuilder = DefaultTrafficTreatment
+                .builder(commonTreatment)
+                .setOutput(egressPoint.port());
+
+        FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                .fromApp(vnService.getVirtualNetworkApplicationId(networkId))
+                .forDevice(ingressPoint.deviceId())
+                .withSelector(selectorBuilder.build())
+                .withTreatment(treatmentBuilder.build())
+                .withIdleTimeout(flowRule.timeout())
+                .withPriority(flowRule.priority());
+
+        FlowRule rule = ruleBuilder.build();
+        frm.addIngressRule(flowRule, rule, networkId);
+        outRules.add(rule);
+
+        return outRules;
+    }
+
+    /**
+     * Generate physical rules when a virtual flow rule can be handled with
+     * multiple physical switches.
+     *
+     * @param networkId The virtual network identifier
+     * @param ingressPoint The ingress point of the physical network
+     * @param egressPoint The egress point of the physical network
+     * @param commonSelector A common traffic selector between the virtual
+     *                       and physical flow rules
+     * @param commonTreatment A common traffic treatment between the virtual
+     *                        and physical flow rules
+     * @param flowRule The virtual flow rule to be translated
+     * @return A set of flow rules for the physical network
+     */
+    private Set<FlowRule> generateRuleForMulti(NetworkId networkId,
+                                                ConnectPoint ingressPoint,
+                                                ConnectPoint egressPoint,
+                                                TrafficSelector commonSelector,
+                                                TrafficTreatment commonTreatment,
+                                                FlowRule flowRule) {
+        Set<FlowRule> outRules = new HashSet<>();
+
+        Path internalPath = internalRoutingAlgorithm
+                .findPath(ingressPoint, egressPoint);
+        checkNotNull(internalPath, "No path between " +
+                ingressPoint.toString() + " " + egressPoint.toString());
+
+        ConnectPoint outCp = internalPath.links().get(0).src();
+
+        //ingress point of tunnel
+        TrafficSelector.Builder selectorBuilder =
+                DefaultTrafficSelector.builder(commonSelector);
+        selectorBuilder.matchInPort(ingressPoint.port());
+
+        TrafficTreatment.Builder treatmentBuilder =
+                DefaultTrafficTreatment.builder(commonTreatment);
+        //TODO: add the logic to check host location
+        treatmentBuilder.pushVlan()
+                .setVlanId(VlanId.vlanId(networkId.id().shortValue()));
+        treatmentBuilder.setOutput(outCp.port());
+
+        FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
+                .fromApp(vnService.getVirtualNetworkApplicationId(networkId))
+                .forDevice(ingressPoint.deviceId())
+                .withSelector(selectorBuilder.build())
+                .withIdleTimeout(flowRule.timeout())
+                .withTreatment(treatmentBuilder.build())
+                .withPriority(flowRule.priority());
+
+        FlowRule rule = ruleBuilder.build();
+        frm.addIngressRule(flowRule, rule, networkId);
+        outRules.add(rule);
+
+        //routing inside tunnel
+        ConnectPoint inCp = internalPath.links().get(0).dst();
+
+        if (internalPath.links().size() > 1) {
+            for (Link l : internalPath.links()
+                    .subList(1, internalPath.links().size())) {
+
+                outCp = l.src();
+
+                selectorBuilder = DefaultTrafficSelector
+                        .builder(commonSelector)
+                        .matchVlanId(VlanId.vlanId(networkId.id().shortValue()))
+                        .matchInPort(inCp.port());
+
+                treatmentBuilder = DefaultTrafficTreatment
+                        .builder(commonTreatment)
+                        .setOutput(outCp.port());
+
+                ruleBuilder = DefaultFlowRule.builder()
+                        .fromApp(vnService.getVirtualNetworkApplicationId(networkId))
+                        .forDevice(inCp.deviceId())
+                        .withSelector(selectorBuilder.build())
+                        .withTreatment(treatmentBuilder.build())
+                        .withIdleTimeout(flowRule.timeout())
+                        .withPriority(flowRule.priority());
+
+                outRules.add(ruleBuilder.build());
+                inCp = l.dst();
+            }
+        }
+
+        //egress point of tunnel
+        selectorBuilder = DefaultTrafficSelector.builder(commonSelector)
+                .matchVlanId(VlanId.vlanId(networkId.id().shortValue()))
+                .matchInPort(inCp.port());
+
+        treatmentBuilder = DefaultTrafficTreatment.builder(commonTreatment)
+                .popVlan()
+                .setOutput(egressPoint.port());
+
+        ruleBuilder = DefaultFlowRule.builder()
+                .fromApp(appId)
+                .forDevice(egressPoint.deviceId())
+                .withSelector(selectorBuilder.build())
+                .withTreatment(treatmentBuilder.build())
+                .withIdleTimeout(flowRule.timeout())
+                .withPriority(flowRule.priority());
+
+        outRules.add(ruleBuilder.build());
+
+        return outRules;
+    }
+
+    private class InternalFlowRuleListener implements FlowRuleListener {
+        @Override
+        public void event(FlowRuleEvent event) {
+            if ((event.type() == FlowRuleEvent.Type.RULE_ADDED) ||
+                    (event.type() == FlowRuleEvent.Type.RULE_UPDATED)) {
+                if (frm.isVirtualIngressRule(event.subject())) {
+                    NetworkId networkId = frm.getVirtualNetworkId(event.subject());
+                    FlowEntry vEntry = getVirtualFlowEntry(event.subject());
+
+                    if (vEntry == null) {
+                        return;
+                    }
+
+                    frm.addOrUpdateFlowEntry(networkId, vEntry.deviceId(), vEntry);
+
+                    VirtualFlowRuleProviderService providerService =
+                            (VirtualFlowRuleProviderService) providerRegistryService
+                                    .getProviderService(networkId,
+                                                        VirtualFlowRuleProvider.class);
+
+                    ImmutableList.Builder<FlowEntry> builder = ImmutableList.builder();
+                    builder.addAll(frm.getFlowEntries(networkId, vEntry.deviceId()));
+
+                    providerService.pushFlowMetrics(vEntry.deviceId(), builder.build());
+                }
+            } else if (event.type() == FlowRuleEvent.Type.RULE_REMOVED) {
+                if (frm.isVirtualIngressRule(event.subject())) {
+                    //FIXME confirm all physical rules are removed
+                    NetworkId networkId = frm.getVirtualNetworkId(event.subject());
+                    FlowEntry vEntry = getVirtualFlowEntry(event.subject());
+
+                    if (vEntry == null) {
+                        return;
+                    }
+
+                    frm.removeFlowEntry(networkId, vEntry.deviceId(), vEntry);
+                    frm.removeFlowRule(networkId, vEntry.deviceId(), vEntry);
+
+                    VirtualFlowRuleProviderService providerService =
+                            (VirtualFlowRuleProviderService) providerRegistryService
+                                    .getProviderService(networkId,
+                                                        VirtualFlowRuleProvider.class);
+                    providerService.flowRemoved(vEntry);
+                }
+            }
+        }
+
+        private FlowEntry getVirtualFlowEntry(FlowRule rule) {
+            FlowEntry entry = null;
+            for (FlowEntry fe :
+                    flowRuleService.getFlowEntries(rule.deviceId())) {
+                if (rule.exactMatch(fe)) {
+                    entry = fe;
+                }
+            }
+
+            if (entry != null) {
+                return virtualize(entry);
+            } else  {
+                return virtualize(new DefaultFlowEntry(rule,
+                                                       FlowEntry.FlowEntryState.PENDING_REMOVE));
+            }
+        }
+    }
+
+    private class InternalVirtualFlowRuleManager {
+        /** <Virtual Network ID, Virtual Device ID, Virtual Flow Rules>.*/
+        final Table<NetworkId, DeviceId, Set<FlowRule>> flowRuleTable
+                = HashBasedTable.create();
+
+        /** <Virtual Network ID, Virtual Device ID, Virtual Flow Entries>.*/
+        final Table<NetworkId, DeviceId, Set<FlowEntry>> flowEntryTable
+                = HashBasedTable.create();
+
+        /** <Physical Flow Rule, Virtual Network ID>.*/
+        final Map<FlowRule, NetworkId> ingressRuleMap = Maps.newHashMap();
+
+        /** <Physical Flow Rule, Virtual Virtual Flow Rule>.*/
+        final Map<FlowRule, FlowRule> virtualizationMap = Maps.newHashMap();
+
+        private Iterable<FlowRule> getFlowRules(NetworkId networkId,
+                                                DeviceId deviceId) {
+            return flowRuleTable.get(networkId, deviceId);
+        }
+
+        private Iterable<FlowEntry> getFlowEntries(NetworkId networkId,
+                                                   DeviceId deviceId) {
+            return flowEntryTable.get(networkId, deviceId);
+        }
+
+        private void addFlowRule(NetworkId networkId, DeviceId deviceId,
+                                 FlowRule flowRule) {
+            Set<FlowRule> set = flowRuleTable.get(networkId, deviceId);
+            if (set == null) {
+                set = Sets.newHashSet();
+                flowRuleTable.put(networkId, deviceId, set);
+            }
+            set.add(flowRule);
+        }
+
+        private void removeFlowRule(NetworkId networkId, DeviceId deviceId,
+                                    FlowRule flowRule) {
+            Set<FlowRule> set = flowRuleTable.get(networkId, deviceId);
+            if (set == null) {
+                return;
+            }
+            set.remove(flowRule);
+        }
+
+        private void addOrUpdateFlowEntry(NetworkId networkId, DeviceId deviceId,
+                                  FlowEntry flowEntry) {
+            Set<FlowEntry> set = flowEntryTable.get(networkId, deviceId);
+            if (set == null) {
+                set = Sets.newConcurrentHashSet();
+                flowEntryTable.put(networkId, deviceId, set);
+            }
+
+            //Replace old entry with new one
+            set.stream().filter(fe -> fe.exactMatch(flowEntry))
+                    .forEach(set::remove);
+            set.add(flowEntry);
+        }
+
+        private void removeFlowEntry(NetworkId networkId, DeviceId deviceId,
+                                     FlowEntry flowEntry) {
+            Set<FlowEntry> set = flowEntryTable.get(networkId, deviceId);
+            if (set == null) {
+                return;
+            }
+            set.remove(flowEntry);
+        }
+
+        private void addIngressRule(FlowRule virtualRule, FlowRule physicalRule,
+                                    NetworkId networkId) {
+            ingressRuleMap.put(physicalRule, networkId);
+            virtualizationMap.put(physicalRule, virtualRule);
+        }
+
+        private FlowRule getVirtualRule(FlowRule physicalRule) {
+                return virtualizationMap.get(physicalRule);
+        }
+
+        private void removeIngressRule(FlowRule physicalRule) {
+            ingressRuleMap.remove(physicalRule);
+            virtualizationMap.remove(physicalRule);
+        }
+
+        private Set<FlowRule> getAllPhysicalRule() {
+            return copyOf(virtualizationMap.keySet());
+        }
+
+        private NetworkId getVirtualNetworkId(FlowRule physicalRule) {
+            return ingressRuleMap.get(physicalRule);
+        }
+
+        /**
+         * Test the rule is the ingress rule for virtual rules.
+         *
+         * @param flowRule A flow rule from underlying data plane to be translated
+         * @return True when the rule is for ingress point for a virtual switch
+         */
+        private boolean isVirtualIngressRule(FlowRule flowRule) {
+            return ingressRuleMap.containsKey(flowRule);
+        }
+    }
+
+    private class DefaultInternalRoutingAlgorithm
+            implements InternalRoutingAlgorithm {
+
+        @Override
+        public Path findPath(ConnectPoint src, ConnectPoint dst) {
+            Set<Path> paths =
+                    topologyService.getPaths(topologyService.currentTopology(),
+                                             src.deviceId(),
+                                             dst.deviceId());
+
+            if (paths.isEmpty()) {
+                return null;
+            }
+
+            //TODO the logic find the best path
+            return (Path) paths.toArray()[0];
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualGroupProvider.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualGroupProvider.java
new file mode 100644
index 0000000..7cc86cc
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualGroupProvider.java
@@ -0,0 +1,128 @@
+/*
+ * 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.impl.provider;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualGroupProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupListener;
+import org.onosproject.net.group.GroupOperation;
+import org.onosproject.net.group.GroupOperations;
+import org.onosproject.net.group.GroupService;
+import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provider to handle Group for virtual network.
+ */
+@Component(service = VirtualGroupProvider.class)
+public class DefaultVirtualGroupProvider extends AbstractVirtualProvider
+        implements VirtualGroupProvider {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualProviderRegistryService providerRegistryService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected GroupService groupService;
+
+    private InternalGroupEventListener internalGroupEventListener;
+
+    /**
+     * Creates a virtual provider with the supplied identifier.
+     */
+    public DefaultVirtualGroupProvider() {
+        super(new ProviderId("vnet-group", "org.onosproject.virtual.of-group"));
+    }
+
+    @Activate
+    public void activate() {
+        providerRegistryService.registerProvider(this);
+
+        internalGroupEventListener = new InternalGroupEventListener();
+        groupService.addListener(internalGroupEventListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        groupService.removeListener(internalGroupEventListener);
+        providerRegistryService.unregisterProvider(this);
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+    }
+
+    @Override
+    public void performGroupOperation(NetworkId networkId, DeviceId deviceId, GroupOperations groupOps) {
+        for (GroupOperation groupOperation: groupOps.operations()) {
+            switch (groupOperation.opType()) {
+                case ADD:
+                    //TODO: devirtualize + groupAdd
+                    log.info("Group Add is not supported, yet");
+                    break;
+                case MODIFY:
+                    //TODO: devirtualize + groupMod
+                    log.info("Group Modify is not supported, yet");
+                    break;
+                case DELETE:
+                    //TODO: devirtualize + groupDel
+                    log.info("Group Delete is not supported, yet");
+                    break;
+                default:
+                    log.error("Unsupported Group operation");
+                    return;
+            }
+        }
+    }
+
+    private class InternalGroupEventListener implements GroupListener {
+        @Override
+        public void event(GroupEvent event) {
+            switch (event.type()) {
+                //TODO: virtualize + notify to virtual provider service
+                case GROUP_ADD_REQUESTED:
+                case GROUP_UPDATE_REQUESTED:
+                case GROUP_REMOVE_REQUESTED:
+                case GROUP_ADDED:
+                case GROUP_UPDATED:
+                case GROUP_REMOVED:
+                case GROUP_ADD_FAILED:
+                case GROUP_UPDATE_FAILED:
+                case GROUP_REMOVE_FAILED:
+                case GROUP_BUCKET_FAILOVER:
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualMeterProvider.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualMeterProvider.java
new file mode 100644
index 0000000..26462ad
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualMeterProvider.java
@@ -0,0 +1,264 @@
+/*
+ * 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.impl.provider;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalCause;
+import com.google.common.cache.RemovalNotification;
+import org.onosproject.core.IdGenerator;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualMeterProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualMeterProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.meter.Meter;
+import org.onosproject.net.meter.MeterEvent;
+import org.onosproject.net.meter.MeterFailReason;
+import org.onosproject.net.meter.MeterListener;
+import org.onosproject.net.meter.MeterOperation;
+import org.onosproject.net.meter.MeterOperations;
+import org.onosproject.net.meter.MeterService;
+import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Provider to handle meters for virtual networks.
+ */
+@Component(service = VirtualMeterProvider.class)
+public class DefaultVirtualMeterProvider extends AbstractVirtualProvider
+        implements VirtualMeterProvider {
+
+    private final Logger log = getLogger(getClass());
+
+    static final long TIMEOUT = 30;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualProviderRegistryService providerRegistryService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected MeterService meterService;
+
+    private MeterListener internalMeterListener;
+    private Cache<Long, VirtualMeterOperation> pendingOperations;
+    private IdGenerator idGenerator;
+
+    @Activate
+    public void activate() {
+        providerRegistryService.registerProvider(this);
+        internalMeterListener = new InternalMeterListener();
+
+        idGenerator = getIdGenerator();
+
+        pendingOperations = CacheBuilder.newBuilder()
+                .expireAfterWrite(TIMEOUT, TimeUnit.SECONDS)
+                .removalListener(
+                        (RemovalNotification<Long, VirtualMeterOperation>
+                                 notification) -> {
+                    if (notification.getCause() == RemovalCause.EXPIRED) {
+                        NetworkId networkId = notification.getValue().networkId();
+                        MeterOperation op = notification.getValue().operation();
+
+                        VirtualMeterProviderService providerService =
+                                (VirtualMeterProviderService) providerRegistryService
+                                        .getProviderService(networkId,
+                                                            VirtualMeterProvider.class);
+
+                        providerService.meterOperationFailed(op,
+                                                             MeterFailReason.TIMEOUT);
+                    }
+                }).build();
+
+        meterService.addListener(internalMeterListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        meterService.removeListener(internalMeterListener);
+        providerRegistryService.unregisterProvider(this);
+    }
+
+    /**
+     * Creates a provider with the identifier.
+     */
+    public DefaultVirtualMeterProvider() {
+        super(new ProviderId("vnet-meter",
+                             "org.onosproject.virtual.vnet-meter"));
+    }
+
+    @Override
+    public void performMeterOperation(NetworkId networkId, DeviceId deviceId,
+                                      MeterOperations meterOps) {
+        meterOps.operations().forEach(op -> performOperation(networkId, deviceId, op));
+    }
+
+    @Override
+    public void performMeterOperation(NetworkId networkId, DeviceId deviceId,
+                                      MeterOperation meterOp) {
+        performOperation(networkId, deviceId, meterOp);
+    }
+
+    private void performOperation(NetworkId networkId, DeviceId deviceId,
+                                  MeterOperation op) {
+
+        VirtualMeterOperation vOp = new VirtualMeterOperation(networkId, op);
+        pendingOperations.put(idGenerator.getNewId(), vOp);
+
+        switch (op.type()) {
+            case ADD:
+                //TODO: devirtualize + submit
+                break;
+            case REMOVE:
+                //TODO: devirtualize + withdraw
+                break;
+            case MODIFY:
+                //TODO: devitualize + withdraw and submit
+                break;
+            default:
+                log.warn("Unknown Meter command {}; not sending anything",
+                         op.type());
+                VirtualMeterProviderService providerService =
+                        (VirtualMeterProviderService) providerRegistryService
+                                .getProviderService(networkId,
+                                                    VirtualMeterProvider.class);
+                providerService.meterOperationFailed(op,
+                                                     MeterFailReason.UNKNOWN_COMMAND);
+        }
+
+    }
+
+    /**
+     * De-virtualizes a meter operation.
+     * It takes a virtual meter operation, and translate it to a physical meter operation.
+     *
+     * @param networkId a virtual network identifier
+     * @param deviceId a virtual network device identifier
+     * @param meterOps a meter operation to be de-virtualized
+     * @return de-virtualized meter operation
+     */
+    private VirtualMeterOperation devirtualize(NetworkId networkId,
+                                      DeviceId deviceId,
+                                      MeterOperation meterOps) {
+        return null;
+    }
+
+    /**
+     * Virtualizes meter.
+     * This translates meter events for virtual networks before delivering them.
+     *
+     * @param meter
+     * @return
+     */
+    private Meter virtualize(Meter meter) {
+        return  null;
+    }
+
+
+    private class InternalMeterListener implements MeterListener {
+        @Override
+        public void event(MeterEvent event) {
+            //TODO: virtualize + notify event to meter provider service
+            //Is it enough to enable virtual network provider?
+            switch (event.type()) {
+                case METER_ADD_REQ:
+                    break;
+                case METER_REM_REQ:
+                    break;
+                case METER_ADDED:
+                    break;
+                case METER_REMOVED:
+                    break;
+                default:
+                    log.warn("Unknown meter event {}", event.type());
+            }
+        }
+    }
+
+    /**
+     * A class to hold a network identifier and a meter operation.
+     * This class is designed to be used only in virtual network meter provider.
+     */
+    private final class VirtualMeterOperation {
+        private NetworkId networkId;
+        private MeterOperation op;
+
+        private VirtualMeterOperation(NetworkId networkId, MeterOperation op) {
+            this.networkId = networkId;
+            this.op = op;
+        }
+
+        private NetworkId networkId() {
+            return networkId;
+        }
+
+        private MeterOperation operation() {
+            return this.op;
+        }
+    }
+
+    /**
+     * A class to hold a network identifier and a meter.
+     * This class is designed to be used in only virtual network meter provider.
+     */
+    private final class VirtualMeter {
+        private NetworkId networkId;
+        private Meter meter;
+
+        private VirtualMeter(NetworkId networkId, Meter meter) {
+            this.networkId = networkId;
+            this.meter = meter;
+        }
+
+        private NetworkId networkId() {
+            return this.networkId;
+        }
+
+        private Meter meter() {
+            return this.meter;
+        }
+    }
+
+    /**
+     * Id generator for virtual meters to guarantee the uniqueness of its identifier
+     * among multiple virtual network meters.
+     *
+     * @return an ID generator
+     */
+    private IdGenerator getIdGenerator() {
+        return new IdGenerator() {
+            private AtomicLong counter = new AtomicLong(0);
+
+            @Override
+            public long getNewId() {
+                return counter.getAndIncrement();
+            }
+        };
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualNetworkProvider.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualNetworkProvider.java
new file mode 100644
index 0000000..3860dee
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualNetworkProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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.impl.provider;
+
+import org.onosproject.incubator.net.virtual.DefaultVirtualLink;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProviderRegistry;
+import org.onosproject.incubator.net.virtual.provider.VirtualNetworkProviderService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Link;
+import org.onosproject.net.Path;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.provider.AbstractProvider;
+import org.onosproject.net.topology.Topology;
+import org.onosproject.net.topology.TopologyCluster;
+import org.onosproject.net.topology.TopologyEvent;
+import org.onosproject.net.topology.TopologyListener;
+import org.onosproject.net.topology.TopologyService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Virtual network topology provider.
+ */
+@Component(service = VirtualNetworkProvider.class)
+public class DefaultVirtualNetworkProvider
+        extends AbstractProvider implements VirtualNetworkProvider {
+
+    private final Logger log = getLogger(DefaultVirtualNetworkProvider.class);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualNetworkProviderRegistry providerRegistry;
+
+    private VirtualNetworkProviderService providerService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected TopologyService topologyService;
+
+    protected TopologyListener topologyListener = new InternalTopologyListener();
+
+    private ExecutorService executor;
+
+    /**
+     * Default constructor.
+     */
+    public DefaultVirtualNetworkProvider() {
+        super(DefaultVirtualLink.PID);
+    }
+
+    @Activate
+    public void activate() {
+        executor = newSingleThreadExecutor(groupedThreads("onos/vnet", "provider", log));
+        providerService = providerRegistry.register(this);
+        topologyService.addListener(topologyListener);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        topologyService.removeListener(topologyListener);
+        executor.shutdownNow();
+        executor = null;
+        providerRegistry.unregister(this);
+        providerService = null;
+        log.info("Stopped");
+    }
+
+    @Override
+    public boolean isTraversable(ConnectPoint src, ConnectPoint dst) {
+        final boolean[] foundSrc = new boolean[1];
+        final boolean[] foundDst = new boolean[1];
+        Topology topology = topologyService.currentTopology();
+        Set<Path> paths = topologyService.getPaths(topology, src.deviceId(), dst.deviceId());
+        paths.forEach(path -> {
+            foundDst[0] = false;
+            foundSrc[0] = false;
+            // Traverse the links in each path to determine if both the src and dst connection
+            // point are in the path, if so then this src/dst pair are traversable.
+            path.links().forEach(link -> {
+                if (link.src().equals(src)) {
+                    foundSrc[0] = true;
+                }
+                if (link.dst().equals(dst)) {
+                    foundDst[0] = true;
+                }
+            });
+            if (foundSrc[0] && foundDst[0]) {
+                return;
+            }
+        });
+        return foundSrc[0] && foundDst[0];
+    }
+
+    /**
+     * Returns a set of set of interconnected connect points in the default topology.
+     * The inner set represents the interconnected connect points, and the outerset
+     * represents separate clusters.
+     *
+     * @param topology the default topology
+     * @return set of set of interconnected connect points.
+     */
+    public Set<Set<ConnectPoint>> getConnectPoints(Topology topology) {
+        Set<Set<ConnectPoint>> clusters = new HashSet<>();
+        Set<TopologyCluster> topologyClusters = topologyService.getClusters(topology);
+        topologyClusters.forEach(topologyCluster -> {
+            Set<ConnectPoint> connectPointSet = new HashSet<>();
+            Set<Link> clusterLinks =
+                    topologyService.getClusterLinks(topology, topologyCluster);
+            clusterLinks.forEach(link -> {
+                connectPointSet.add(link.src());
+                connectPointSet.add(link.dst());
+            });
+            if (!connectPointSet.isEmpty()) {
+                clusters.add(connectPointSet);
+            }
+        });
+        return clusters;
+    }
+
+    /**
+     * Topology event listener.
+     */
+    private class InternalTopologyListener implements TopologyListener {
+        @Override
+        public void event(TopologyEvent event) {
+            // Perform processing off the listener thread.
+            executor.submit(() -> providerService
+                    .topologyChanged(getConnectPoints(event.subject())));
+        }
+
+        @Override
+        public boolean isRelevant(TopologyEvent event) {
+            return event.type() == TopologyEvent.Type.TOPOLOGY_CHANGED &&
+                    event.reasons().stream().anyMatch(reason -> reason instanceof LinkEvent);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketContext.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketContext.java
new file mode 100644
index 0000000..014f436
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketContext.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.impl.provider;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualPacketContext;
+import org.onosproject.net.packet.DefaultPacketContext;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+
+/**
+ *Default implementation of a virtual packet context.
+ */
+public class DefaultVirtualPacketContext extends DefaultPacketContext
+        implements VirtualPacketContext {
+
+    private NetworkId networkId;
+    private DefaultVirtualPacketProvider dvpp;
+
+    /**
+     * Creates a new packet context.
+     *
+     * @param time   creation time
+     * @param inPkt  inbound packet
+     * @param outPkt outbound packet
+     * @param block  whether the context is blocked or not
+     * @param networkId virtual network ID where this context is handled
+     * @param dvpp  pointer to default virtual packet provider
+     */
+
+    protected DefaultVirtualPacketContext(long time, InboundPacket inPkt,
+                                          OutboundPacket outPkt, boolean block,
+                                          NetworkId networkId,
+                                          DefaultVirtualPacketProvider dvpp) {
+        super(time, inPkt, outPkt, block);
+
+        this.networkId = networkId;
+        this.dvpp = dvpp;
+    }
+
+    @Override
+    public void send() {
+        if (!this.block()) {
+            dvpp.send(this);
+        }
+    }
+
+    @Override
+    public NetworkId networkId() {
+        return networkId;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketProvider.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketProvider.java
new file mode 100644
index 0000000..9048954
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/DefaultVirtualPacketProvider.java
@@ -0,0 +1,421 @@
+/*
+ * 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.impl.provider;
+
+import com.google.common.collect.Sets;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.onlab.packet.Ethernet;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkEvent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkListener;
+import org.onosproject.incubator.net.virtual.VirtualPacketContext;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.incubator.net.virtual.provider.AbstractVirtualProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualPacketProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualPacketProviderService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
+import org.onosproject.net.packet.DefaultInboundPacket;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+
+import java.nio.ByteBuffer;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+@Component(service = VirtualPacketProvider.class)
+public class DefaultVirtualPacketProvider extends AbstractVirtualProvider
+        implements VirtualPacketProvider {
+
+    private static final int PACKET_PROCESSOR_PRIORITY = 1;
+    private static final PacketPriority VIRTUAL_PACKET_PRIORITY = PacketPriority.REACTIVE;
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualNetworkAdminService vnaService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualProviderRegistryService providerRegistryService;
+
+    private final VirtualNetworkListener virtualNetListener = new InternalVirtualNetworkListener();
+
+    private InternalPacketProcessor processor = null;
+
+    private Set<NetworkId> networkIdSet = Sets.newConcurrentHashSet();
+
+    private ApplicationId appId;
+
+    /**
+     * Creates a provider with the supplied identifier.
+     */
+    public DefaultVirtualPacketProvider() {
+        super(new ProviderId("virtual-packet", "org.onosproject.virtual.virtual-packet"));
+    }
+
+    @Activate
+    public void activate() {
+        appId = coreService.registerApplication("org.onosproject.virtual.virtual-packet");
+        providerRegistryService.registerProvider(this);
+        vnaService.addListener(virtualNetListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+
+        providerRegistryService.unregisterProvider(this);
+        vnaService.removeListener(virtualNetListener);
+
+        log.info("Stopped");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+    }
+
+
+    @Override
+    public void emit(NetworkId networkId, OutboundPacket packet) {
+       devirtualize(networkId, packet)
+               .forEach(outboundPacket -> packetService.emit(outboundPacket));
+    }
+
+    /**
+     * Just for test.
+     */
+    protected void startPacketHandling() {
+        processor = new InternalPacketProcessor();
+        packetService.addProcessor(processor, PACKET_PROCESSOR_PRIORITY);
+    }
+
+    /**
+     * Send the outbound packet of a virtual context.
+     * This method is designed to support Context's send() method that invoked
+     * by applications.
+     * See {@link org.onosproject.net.packet.PacketContext}
+     *
+     * @param virtualPacketContext virtual packet context
+     */
+    protected void send(VirtualPacketContext virtualPacketContext) {
+        devirtualizeContext(virtualPacketContext)
+                .forEach(outboundPacket -> packetService.emit(outboundPacket));
+    }
+
+    /**
+     * Translate the requested physical PacketContext into a virtual PacketContext.
+     * See {@link org.onosproject.net.packet.PacketContext}
+     *
+     * @param context A physical PacketContext be translated
+     * @return A translated virtual PacketContext
+     */
+    private VirtualPacketContext virtualize(PacketContext context) {
+
+        VirtualPort vPort = getMappedVirtualPort(context.inPacket().receivedFrom());
+
+        if (vPort != null) {
+            ConnectPoint cp = new ConnectPoint(vPort.element().id(),
+                                               vPort.number());
+
+            Ethernet eth = context.inPacket().parsed();
+            eth.setVlanID(Ethernet.VLAN_UNTAGGED);
+
+            InboundPacket inPacket =
+                    new DefaultInboundPacket(cp, eth,
+                                             ByteBuffer.wrap(eth.serialize()));
+
+            DefaultOutboundPacket outPkt =
+                    new DefaultOutboundPacket(cp.deviceId(),
+                                              DefaultTrafficTreatment.builder().build(),
+                                              ByteBuffer.wrap(eth.serialize()));
+
+            VirtualPacketContext vContext =
+                    new DefaultVirtualPacketContext(context.time(), inPacket, outPkt,
+                                             false, vPort.networkId(),
+                                             this);
+
+            return vContext;
+        } else {
+            return null;
+        }
+
+    }
+
+    /**
+     * Find the corresponding virtual port with the physical port.
+     *
+     * @param cp the connect point for the physical network
+     * @return a virtual port
+     */
+    private VirtualPort getMappedVirtualPort(ConnectPoint cp) {
+        Set<TenantId> tIds = vnaService.getTenantIds();
+
+        Set<VirtualNetwork> vNetworks = new HashSet<>();
+        tIds.forEach(tid -> vNetworks.addAll(vnaService.getVirtualNetworks(tid)));
+
+        for (VirtualNetwork vNet : vNetworks) {
+            Set<VirtualDevice> vDevices = vnaService.getVirtualDevices(vNet.id());
+
+            Set<VirtualPort> vPorts = new HashSet<>();
+            vDevices.forEach(dev -> vPorts
+                    .addAll(vnaService.getVirtualPorts(dev.networkId(), dev.id())));
+
+            VirtualPort vPort = vPorts.stream()
+                    .filter(vp -> vp.realizedBy().equals(cp))
+                    .findFirst().orElse(null);
+
+            if (vPort != null) {
+                return vPort;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Translate the requested virtual outbound packet into
+     * a set of physical OutboundPacket.
+     * See {@link org.onosproject.net.packet.OutboundPacket}
+     *
+     * @param packet an OutboundPacket to be translated
+     * @return a set of de-virtualized (physical) OutboundPacket
+     */
+    private Set<OutboundPacket> devirtualize(NetworkId networkId, OutboundPacket packet) {
+        Set<OutboundPacket> outboundPackets = new HashSet<>();
+        Set<VirtualPort> vPorts = vnaService
+                .getVirtualPorts(networkId, packet.sendThrough());
+
+        TrafficTreatment.Builder commonTreatmentBuilder
+                = DefaultTrafficTreatment.builder();
+        packet.treatment().allInstructions().stream()
+                .filter(i -> i.type() != Instruction.Type.OUTPUT)
+                .forEach(i -> commonTreatmentBuilder.add(i));
+        TrafficTreatment commonTreatment = commonTreatmentBuilder.build();
+
+        PortNumber vOutPortNum = packet.treatment().allInstructions().stream()
+                .filter(i -> i.type() == Instruction.Type.OUTPUT)
+                .map(i -> ((Instructions.OutputInstruction) i).port())
+                .findFirst().get();
+
+        if (!vOutPortNum.isLogical()) {
+            Optional<ConnectPoint> optionalCpOut = vPorts.stream()
+                    .filter(v -> v.number().equals(vOutPortNum))
+                    .map(v -> v.realizedBy())
+                    .findFirst();
+            if (!optionalCpOut.isPresent()) {
+                log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                        vOutPortNum, networkId, packet.sendThrough());
+                return outboundPackets;
+            }
+            ConnectPoint egressPoint = optionalCpOut.get();
+
+            TrafficTreatment treatment = DefaultTrafficTreatment
+                    .builder(commonTreatment)
+                    .setOutput(egressPoint.port()).build();
+
+            OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                    egressPoint.deviceId(), treatment, packet.data());
+            outboundPackets.add(outboundPacket);
+        } else {
+            if (vOutPortNum == PortNumber.FLOOD) {
+                for (VirtualPort outPort : vPorts) {
+                    ConnectPoint cpOut = outPort.realizedBy();
+                    if (cpOut != null) {
+                        TrafficTreatment treatment = DefaultTrafficTreatment
+                                .builder(commonTreatment)
+                                .setOutput(cpOut.port()).build();
+                        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                                cpOut.deviceId(), treatment, packet.data());
+                        outboundPackets.add(outboundPacket);
+                    } else {
+                        log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                                outPort.number(), networkId, packet.sendThrough());
+                    }
+                }
+            }
+        }
+
+        return outboundPackets;
+    }
+
+    /**
+     * Translate the requested virtual packet context into
+     * a set of physical outbound packets.
+     *
+     * @param context A handled virtual packet context
+     */
+    private Set<OutboundPacket> devirtualizeContext(VirtualPacketContext context) {
+
+        Set<OutboundPacket> outboundPackets = new HashSet<>();
+
+        NetworkId networkId = context.networkId();
+        TrafficTreatment vTreatment = context.treatmentBuilder().build();
+        DeviceId sendThrough = context.outPacket().sendThrough();
+
+        Set<VirtualPort> vPorts = vnaService
+                .getVirtualPorts(networkId, sendThrough);
+
+        PortNumber vOutPortNum = vTreatment.allInstructions().stream()
+                .filter(i -> i.type() == Instruction.Type.OUTPUT)
+                .map(i -> ((Instructions.OutputInstruction) i).port())
+                .findFirst().get();
+
+        TrafficTreatment.Builder commonTreatmentBuilder
+                = DefaultTrafficTreatment.builder();
+        vTreatment.allInstructions().stream()
+                .filter(i -> i.type() != Instruction.Type.OUTPUT)
+                .forEach(i -> commonTreatmentBuilder.add(i));
+        TrafficTreatment commonTreatment = commonTreatmentBuilder.build();
+
+        if (!vOutPortNum.isLogical()) {
+            Optional<ConnectPoint> optionalCpOut = vPorts.stream()
+                    .filter(v -> v.number().equals(vOutPortNum))
+                    .map(v -> v.realizedBy())
+                    .findFirst();
+            if (!optionalCpOut.isPresent()) {
+                log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                        vOutPortNum, networkId, sendThrough);
+                return outboundPackets;
+            }
+            ConnectPoint egressPoint = optionalCpOut.get();
+
+            TrafficTreatment treatment = DefaultTrafficTreatment
+                    .builder(commonTreatment)
+                    .setOutput(egressPoint.port()).build();
+
+            OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                    egressPoint.deviceId(), treatment, context.outPacket().data());
+            outboundPackets.add(outboundPacket);
+        } else {
+            if (vOutPortNum == PortNumber.FLOOD) {
+                Set<VirtualPort> outPorts = vPorts.stream()
+                        .filter(vp -> !vp.number().isLogical())
+                        .filter(vp -> vp.number() !=
+                                context.inPacket().receivedFrom().port())
+                        .collect(Collectors.toSet());
+
+                for (VirtualPort outPort : outPorts) {
+                    ConnectPoint cpOut = outPort.realizedBy();
+                    if (cpOut != null) {
+                        TrafficTreatment treatment = DefaultTrafficTreatment
+                                .builder(commonTreatment)
+                                .setOutput(cpOut.port()).build();
+                        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                                cpOut.deviceId(), treatment, context.outPacket().data());
+                        outboundPackets.add(outboundPacket);
+                    } else {
+                        log.warn("Port {} is not realized yet, in Network {}, Device {}",
+                                outPort.number(), networkId, sendThrough);
+                    }
+                }
+            }
+        }
+        return outboundPackets;
+    }
+
+    private final class InternalPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            if (context.isHandled()) {
+                return;
+            }
+            VirtualPacketContext vContexts = virtualize(context);
+
+            if (vContexts == null) {
+                return;
+            }
+
+            VirtualPacketProviderService service =
+                    (VirtualPacketProviderService) providerRegistryService
+                            .getProviderService(vContexts.networkId(),
+                                                VirtualPacketProvider.class);
+            if (service != null) {
+                service.processPacket(vContexts);
+            }
+        }
+    }
+
+    private class InternalVirtualNetworkListener implements VirtualNetworkListener {
+
+        @Override
+        public void event(VirtualNetworkEvent event) {
+            switch (event.type()) {
+                case NETWORK_ADDED:
+                    if (networkIdSet.isEmpty()) {
+                        processor = new InternalPacketProcessor();
+                        packetService.addProcessor(processor, PACKET_PROCESSOR_PRIORITY);
+                        log.info("Packet processor {} for virtual network is added.", processor.getClass().getName());
+                    }
+                    networkIdSet.add(event.subject());
+                    break;
+
+                case NETWORK_REMOVED:
+                    networkIdSet.remove(event.subject());
+                    if (networkIdSet.isEmpty()) {
+                        packetService.removeProcessor(processor);
+                        log.info("Packet processor {} for virtual network is removed.", processor.getClass().getName());
+                        processor = null;
+                    }
+                    break;
+
+                default:
+                    // do nothing
+                    break;
+            }
+        }
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/VirtualProviderManager.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/VirtualProviderManager.java
new file mode 100644
index 0000000..ebd87ea
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/VirtualProviderManager.java
@@ -0,0 +1,168 @@
+/*
+ * 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.impl.provider;
+
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.provider.VirtualProvider;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderRegistryService;
+import org.onosproject.incubator.net.virtual.provider.VirtualProviderService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.provider.ProviderId;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Implementation of the virtual provider registry and providerService registry service.
+ */
+@Component(service = VirtualProviderRegistryService.class)
+public class VirtualProviderManager implements VirtualProviderRegistryService {
+
+    private final Map<ProviderId, VirtualProvider> providers = new HashMap<>();
+    private final Map<ProviderId, VirtualProviderService> servicesWithProvider = new HashMap<>();
+    private final Map<String, VirtualProvider> providersByScheme = new HashMap<>();
+    private final Map<NetworkId, Set<VirtualProviderService>> servicesByNetwork = new HashMap<>();
+    private static Logger log = LoggerFactory.getLogger(VirtualProviderManager.class);
+
+    @Override
+    public synchronized void registerProvider(VirtualProvider virtualProvider) {
+        checkNotNull(virtualProvider, "Provider cannot be null");
+        checkState(!providers.containsKey(virtualProvider.id()),
+                   "Provider %s already registered", virtualProvider.id());
+
+        // If the provider is a primary one, check for a conflict.
+        ProviderId pid = virtualProvider.id();
+        checkState(pid.isAncillary() || !providersByScheme.containsKey(pid.scheme()),
+                   "A primary provider with id %s is already registered",
+                   providersByScheme.get(pid.scheme()));
+
+        providers.put(virtualProvider.id(), virtualProvider);
+
+        // Register the provider by URI scheme only if it is not ancillary.
+        if (!pid.isAncillary()) {
+            providersByScheme.put(pid.scheme(), virtualProvider);
+        }
+    }
+
+    @Override
+    public synchronized void unregisterProvider(VirtualProvider virtualProvider) {
+        checkNotNull(virtualProvider, "Provider cannot be null");
+
+        //TODO: invalidate provider services which subscribe the provider
+        providers.remove(virtualProvider.id());
+
+        if (!virtualProvider.id().isAncillary()) {
+            providersByScheme.remove(virtualProvider.id().scheme());
+        }
+    }
+
+    @Override
+    public synchronized void
+    registerProviderService(NetworkId networkId,
+                            VirtualProviderService virtualProviderService) {
+        Set<VirtualProviderService> services =
+                servicesByNetwork.computeIfAbsent(networkId, k -> new HashSet<>());
+
+        services.add(virtualProviderService);
+    }
+
+    @Override
+    public synchronized void
+    unregisterProviderService(NetworkId networkId,
+                              VirtualProviderService virtualProviderService) {
+        Set<VirtualProviderService> services = servicesByNetwork.get(networkId);
+
+        if (services != null) {
+            services.remove(virtualProviderService);
+        }
+    }
+
+    @Override
+    public synchronized Set<ProviderId> getProviders() {
+        return ImmutableSet.copyOf(providers.keySet());
+    }
+
+    @Override
+    public Set<ProviderId> getProvidersByService(VirtualProviderService
+                                                             virtualProviderService) {
+        Class clazz = getProviderClass(virtualProviderService);
+
+        return ImmutableSet.copyOf(providers.values().stream()
+                                           .filter(clazz::isInstance)
+                                           .map(VirtualProvider::id)
+                                           .collect(Collectors.toSet()));
+    }
+
+    @Override
+    public synchronized VirtualProvider getProvider(ProviderId providerId) {
+        return providers.get(providerId);
+    }
+
+    @Override
+    public synchronized VirtualProvider getProvider(DeviceId deviceId) {
+        return providersByScheme.get(deviceId.uri().getScheme());
+    }
+
+    @Override
+    public synchronized VirtualProvider getProvider(String scheme) {
+        return providersByScheme.get(scheme);
+    }
+
+    @Override
+    public synchronized VirtualProviderService
+    getProviderService(NetworkId networkId, Class<? extends VirtualProvider> providerClass) {
+        Set<VirtualProviderService> services = servicesByNetwork.get(networkId);
+
+        if (services == null) {
+            return null;
+        }
+
+        return services.stream()
+                .filter(s -> getProviderClass(s).equals(providerClass))
+                .findFirst().orElse(null);
+    }
+
+    /**
+     * Returns the class type of parameter type.
+     * More specifically, it returns the class type of provider service's provider type.
+     *
+     * @param service a virtual provider service
+     * @return the class type of provider service of the service
+     */
+    private Class getProviderClass(VirtualProviderService service) {
+       String className = service.getClass().getGenericSuperclass().getTypeName();
+       String pramType = className.split("<")[1].split(">")[0];
+
+        try {
+            return Class.forName(pramType);
+        } catch (ClassNotFoundException e) {
+            log.warn("getProviderClass()", e);
+        }
+
+        return null;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/package-info.java
new file mode 100644
index 0000000..1d9646f
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/impl/provider/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.
+ */
+
+/**
+ * Network virtualization provider implementations.
+ */
+package org.onosproject.incubator.net.virtual.impl.provider;
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/TenantWebResource.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/TenantWebResource.java
new file mode 100644
index 0000000..768c3b9
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/TenantWebResource.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.util.ItemNotFoundException;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkAdminService;
+import org.onosproject.rest.AbstractWebResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.onlab.util.Tools.readTreeFromStream;
+
+/**
+ * Query and manage tenants of virtual networks.
+ */
+@Path("tenants")
+public class TenantWebResource extends AbstractWebResource {
+
+    private static final String MISSING_TENANTID = "Missing tenant identifier";
+    private static final String TENANTID_NOT_FOUND = "Tenant identifier not found";
+    private static final String INVALID_TENANTID = "Invalid tenant identifier ";
+
+    @Context
+    private UriInfo uriInfo;
+
+    private final VirtualNetworkAdminService vnetAdminService = get(VirtualNetworkAdminService.class);
+
+    /**
+     * Returns all tenant identifiers.
+     *
+     * @return 200 OK with set of tenant identifiers
+     * @onos.rsModel TenantIds
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getVirtualNetworkTenantIds() {
+        Iterable<TenantId> tenantIds = vnetAdminService.getTenantIds();
+        return ok(encodeArray(TenantId.class, "tenants", tenantIds)).build();
+    }
+
+    /**
+     * Creates a tenant with the given tenant identifier.
+     *
+     * @param stream TenantId JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel TenantId
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response addTenantId(InputStream stream) {
+        try {
+            final TenantId tid = getTenantIdFromJsonStream(stream);
+            vnetAdminService.registerTenantId(tid);
+            final TenantId resultTid = getExistingTenantId(vnetAdminService, tid);
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("tenants")
+                    .path(resultTid.id());
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the specified tenant with the specified tenant identifier.
+     *
+     * @param tenantId tenant identifier
+     * @return 204 NO CONTENT
+     */
+    @DELETE
+    @Path("{tenantId}")
+    public Response removeTenantId(@PathParam("tenantId") String tenantId) {
+        final TenantId tid = TenantId.tenantId(tenantId);
+        final TenantId existingTid = getExistingTenantId(vnetAdminService, tid);
+        vnetAdminService.unregisterTenantId(existingTid);
+        return Response.noContent().build();
+    }
+
+    /**
+     * Gets the tenant identifier from the JSON stream.
+     *
+     * @param stream TenantId JSON stream
+     * @return TenantId
+     * @throws IOException if unable to parse the request
+     */
+    private TenantId getTenantIdFromJsonStream(InputStream stream) throws IOException {
+        ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+        JsonNode specifiedTenantId = jsonTree.get("id");
+
+        if (specifiedTenantId == null) {
+            throw new IllegalArgumentException(MISSING_TENANTID);
+        }
+        return TenantId.tenantId(specifiedTenantId.asText());
+    }
+
+    /**
+     * Get the matching tenant identifier from existing tenant identifiers in system.
+     *
+     * @param vnetAdminSvc virtual network administration service
+     * @param tidIn        tenant identifier
+     * @return TenantId
+     */
+    protected static TenantId getExistingTenantId(VirtualNetworkAdminService vnetAdminSvc,
+                                                TenantId tidIn) {
+        return vnetAdminSvc
+                .getTenantIds()
+                .stream()
+                .filter(tenantId -> tenantId.equals(tidIn))
+                .findFirst()
+                .orElseThrow(() -> new ItemNotFoundException(TENANTID_NOT_FOUND));
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebApplication.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebApplication.java
new file mode 100644
index 0000000..4a80dbe
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebApplication.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.rest;
+
+import org.onlab.rest.AbstractWebApplication;
+
+import java.util.Set;
+
+/**
+ * Virtual network REST APIs web application.
+ */
+public class VirtualNetworkWebApplication extends AbstractWebApplication {
+    @Override
+    public Set<Class<?>> getClasses() {
+        return getClasses(
+                TenantWebResource.class,
+                VirtualNetworkWebResource.class
+        );
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebResource.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebResource.java
new file mode 100644
index 0000000..e8696c3
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/VirtualNetworkWebResource.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualLink;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+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.rest.AbstractWebResource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onlab.util.Tools.readTreeFromStream;
+
+/**
+ * Query and Manage Virtual Network elements.
+ */
+@Path("vnets")
+public class VirtualNetworkWebResource extends AbstractWebResource {
+
+    private static final String MISSING_FIELD = "Missing ";
+    private static final String INVALID_FIELD = "Invalid ";
+
+    private final VirtualNetworkAdminService vnetAdminService = get(VirtualNetworkAdminService.class);
+    private final VirtualNetworkService vnetService = get(VirtualNetworkService.class);
+
+    @Context
+    private UriInfo uriInfo;
+
+    // VirtualNetwork
+
+    /**
+     * Returns all virtual networks.
+     *
+     * @return 200 OK with set of virtual networks
+     * @onos.rsModel VirtualNetworks
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getVirtualNetworks() {
+        Set<TenantId> tenantIds = vnetAdminService.getTenantIds();
+        List<VirtualNetwork> allVnets = tenantIds.stream()
+                .map(tenantId -> vnetService.getVirtualNetworks(tenantId))
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
+        return ok(encodeArray(VirtualNetwork.class, "vnets", allVnets)).build();
+    }
+
+    /**
+     * Returns the virtual networks with the specified tenant identifier.
+     *
+     * @param tenantId tenant identifier
+     * @return 200 OK with a virtual network, 404 not found
+     * @onos.rsModel VirtualNetworks
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{tenantId}")
+    public Response getVirtualNetworkById(@PathParam("tenantId") String tenantId) {
+        final TenantId existingTid = TenantWebResource.getExistingTenantId(vnetAdminService,
+                                                                           TenantId.tenantId(tenantId));
+        Set<VirtualNetwork> vnets = vnetService.getVirtualNetworks(existingTid);
+        return ok(encodeArray(VirtualNetwork.class, "vnets", vnets)).build();
+    }
+
+    /**
+     * Creates a virtual network from the JSON input stream.
+     *
+     * @param stream tenant identifier JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel TenantId
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createVirtualNetwork(InputStream stream) {
+        try {
+            final TenantId tid = TenantId.tenantId(getFromJsonStream(stream, "id").asText());
+            VirtualNetwork newVnet = vnetAdminService.createVirtualNetwork(tid);
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("vnets")
+                    .path(newVnet.id().toString());
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the virtual network with the specified network identifier.
+     *
+     * @param networkId network identifier
+     * @return 204 NO CONTENT
+     */
+    @DELETE
+    @Path("{networkId}")
+    public Response removeVirtualNetwork(@PathParam("networkId") long networkId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        vnetAdminService.removeVirtualNetwork(nid);
+        return Response.noContent().build();
+    }
+
+    // VirtualDevice
+
+    /**
+     * Returns all virtual network devices in a virtual network.
+     *
+     * @param networkId network identifier
+     * @return 200 OK with set of virtual devices, 404 not found
+     * @onos.rsModel VirtualDevices
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{networkId}/devices")
+    public Response getVirtualDevices(@PathParam("networkId") long networkId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        Set<VirtualDevice> vdevs = vnetService.getVirtualDevices(nid);
+        return ok(encodeArray(VirtualDevice.class, "devices", vdevs)).build();
+    }
+
+    /**
+     * Creates a virtual device from the JSON input stream.
+     *
+     * @param networkId network identifier
+     * @param stream    virtual device JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel VirtualDevice
+     */
+    @POST
+    @Path("{networkId}/devices")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createVirtualDevice(@PathParam("networkId") long networkId,
+                                        InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+            final VirtualDevice vdevReq = codec(VirtualDevice.class).decode(jsonTree, this);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            if (specifiedNetworkId == null || specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            final VirtualDevice vdevRes = vnetAdminService.createVirtualDevice(vdevReq.networkId(),
+                                                                               vdevReq.id());
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("vnets").path(specifiedNetworkId.asText())
+                    .path("devices").path(vdevRes.id().toString());
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the virtual network device from the virtual network.
+     *
+     * @param networkId network identifier
+     * @param deviceId  device identifier
+     * @return 204 NO CONTENT
+     */
+    @DELETE
+    @Path("{networkId}/devices/{deviceId}")
+    public Response removeVirtualDevice(@PathParam("networkId") long networkId,
+                                        @PathParam("deviceId") String deviceId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        DeviceId did = DeviceId.deviceId(deviceId);
+        vnetAdminService.removeVirtualDevice(nid, did);
+        return Response.noContent().build();
+    }
+
+    // VirtualPort
+
+    /**
+     * Returns all virtual network ports in a virtual device in a virtual network.
+     *
+     * @param networkId network identifier
+     * @param deviceId  virtual device identifier
+     * @return 200 OK with set of virtual ports, 404 not found
+     * @onos.rsModel VirtualPorts
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{networkId}/devices/{deviceId}/ports")
+    public Response getVirtualPorts(@PathParam("networkId") long networkId,
+                                    @PathParam("deviceId") String deviceId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        Iterable<VirtualPort> vports = vnetService.getVirtualPorts(nid, DeviceId.deviceId(deviceId));
+        return ok(encodeArray(VirtualPort.class, "ports", vports)).build();
+    }
+
+    /**
+     * Creates a virtual network port in a virtual device in a virtual network.
+     *
+     * @param networkId    network identifier
+     * @param virtDeviceId virtual device identifier
+     * @param stream       virtual port JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel VirtualPort
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{networkId}/devices/{deviceId}/ports")
+    public Response createVirtualPort(@PathParam("networkId") long networkId,
+                                      @PathParam("deviceId") String virtDeviceId,
+                                      InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+//            final VirtualPort vportReq = codec(VirtualPort.class).decode(jsonTree, this);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            JsonNode specifiedDeviceId = jsonTree.get("deviceId");
+            if (specifiedNetworkId == null || specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            if (specifiedDeviceId == null || !specifiedDeviceId.asText().equals(virtDeviceId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "deviceId");
+            }
+            JsonNode specifiedPortNum = jsonTree.get("portNum");
+            JsonNode specifiedPhysDeviceId = jsonTree.get("physDeviceId");
+            JsonNode specifiedPhysPortNum = jsonTree.get("physPortNum");
+            final NetworkId nid = NetworkId.networkId(networkId);
+            DeviceId vdevId = DeviceId.deviceId(virtDeviceId);
+
+            ConnectPoint realizedBy = new ConnectPoint(DeviceId.deviceId(specifiedPhysDeviceId.asText()),
+                                              PortNumber.portNumber(specifiedPhysPortNum.asText()));
+            VirtualPort vport = vnetAdminService.createVirtualPort(nid, vdevId,
+                                    PortNumber.portNumber(specifiedPortNum.asText()), realizedBy);
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("vnets").path(specifiedNetworkId.asText())
+                    .path("devices").path(specifiedDeviceId.asText())
+                    .path("ports").path(vport.number().toString());
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the virtual network port from the virtual device in a virtual network.
+     *
+     * @param networkId network identifier
+     * @param deviceId  virtual device identifier
+     * @param portNum   virtual port number
+     * @return 204 NO CONTENT
+     */
+    @DELETE
+    @Path("{networkId}/devices/{deviceId}/ports/{portNum}")
+    public Response removeVirtualPort(@PathParam("networkId") long networkId,
+                                      @PathParam("deviceId") String deviceId,
+                                      @PathParam("portNum") long portNum) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        vnetAdminService.removeVirtualPort(nid, DeviceId.deviceId(deviceId),
+                                           PortNumber.portNumber(portNum));
+        return Response.noContent().build();
+    }
+
+    // VirtualLink
+
+    /**
+     * Returns all virtual network links in a virtual network.
+     *
+     * @param networkId network identifier
+     * @return 200 OK with set of virtual network links
+     * @onos.rsModel VirtualLinks
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{networkId}/links")
+    public Response getVirtualLinks(@PathParam("networkId") long networkId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        Set<VirtualLink> vlinks = vnetService.getVirtualLinks(nid);
+        return ok(encodeArray(VirtualLink.class, "links", vlinks)).build();
+    }
+
+    /**
+     * Creates a virtual network link from the JSON input stream.
+     *
+     * @param networkId network identifier
+     * @param stream    virtual link JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel VirtualLink
+     */
+    @POST
+    @Path("{networkId}/links")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createVirtualLink(@PathParam("networkId") long networkId,
+                                      InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            if (specifiedNetworkId == null || specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            final VirtualLink vlinkReq = codec(VirtualLink.class).decode(jsonTree, this);
+            vnetAdminService.createVirtualLink(vlinkReq.networkId(),
+                                               vlinkReq.src(), vlinkReq.dst());
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("vnets").path(specifiedNetworkId.asText())
+                    .path("links");
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the virtual network link from the JSON input stream.
+     *
+     * @param networkId network identifier
+     * @param stream    virtual link JSON stream
+     * @return 204 NO CONTENT
+     * @onos.rsModel VirtualLink
+     */
+    @DELETE
+    @Path("{networkId}/links")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response removeVirtualLink(@PathParam("networkId") long networkId,
+                                      InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            if (specifiedNetworkId != null &&
+                    specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            final VirtualLink vlinkReq = codec(VirtualLink.class).decode(jsonTree, this);
+            vnetAdminService.removeVirtualLink(vlinkReq.networkId(),
+                                               vlinkReq.src(), vlinkReq.dst());
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.noContent().build();
+    }
+
+    /**
+     * Returns all virtual network hosts in a virtual network.
+     *
+     * @param networkId network identifier
+     * @return 200 OK with set of virtual network hosts
+     * @onos.rsModel VirtualHosts
+     */
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{networkId}/hosts")
+    public Response getVirtualHosts(@PathParam("networkId") long networkId) {
+        NetworkId nid = NetworkId.networkId(networkId);
+        Set<VirtualHost> vhosts = vnetService.getVirtualHosts(nid);
+        return ok(encodeArray(VirtualHost.class, "hosts", vhosts)).build();
+    }
+
+    /**
+     * Creates a virtual network host from the JSON input stream.
+     *
+     * @param networkId network identifier
+     * @param stream    virtual host JSON stream
+     * @return status of the request - CREATED if the JSON is correct,
+     * BAD_REQUEST if the JSON is invalid
+     * @onos.rsModel VirtualHostPut
+     */
+    @POST
+    @Path("{networkId}/hosts")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response createVirtualHost(@PathParam("networkId") long networkId,
+                                      InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            if (specifiedNetworkId == null || specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            final VirtualHost vhostReq = codec(VirtualHost.class).decode(jsonTree, this);
+            vnetAdminService.createVirtualHost(vhostReq.networkId(), vhostReq.id(),
+                                               vhostReq.mac(), vhostReq.vlan(),
+                                               vhostReq.location(), vhostReq.ipAddresses());
+            UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+                    .path("vnets").path(specifiedNetworkId.asText())
+                    .path("hosts");
+            return Response
+                    .created(locationBuilder.build())
+                    .build();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Removes the virtual network host from the JSON input stream.
+     *
+     * @param networkId network identifier
+     * @param stream    virtual host JSON stream
+     * @return 204 NO CONTENT
+     * @onos.rsModel VirtualHost
+     */
+    @DELETE
+    @Path("{networkId}/hosts")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response removeVirtualHost(@PathParam("networkId") long networkId,
+                                      InputStream stream) {
+        try {
+            ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+            JsonNode specifiedNetworkId = jsonTree.get("networkId");
+            if (specifiedNetworkId != null &&
+                    specifiedNetworkId.asLong() != (networkId)) {
+                throw new IllegalArgumentException(INVALID_FIELD + "networkId");
+            }
+            final VirtualHost vhostReq = codec(VirtualHost.class).decode(jsonTree, this);
+            vnetAdminService.removeVirtualHost(vhostReq.networkId(), vhostReq.id());
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        return Response.noContent().build();
+    }
+
+    /**
+     * Get the tenant identifier from the JSON stream.
+     *
+     * @param stream        TenantId JSON stream
+     * @param jsonFieldName field name
+     * @return JsonNode
+     * @throws IOException if unable to parse the request
+     */
+    private JsonNode getFromJsonStream(InputStream stream, String jsonFieldName) throws IOException {
+        ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+        JsonNode jsonNode = jsonTree.get(jsonFieldName);
+
+        if (jsonNode == null) {
+            throw new IllegalArgumentException(MISSING_FIELD + jsonFieldName);
+        }
+        return jsonNode;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/package-info.java
new file mode 100644
index 0000000..0b8a711
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/rest/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * REST API of the virtual network subsystem.
+ */
+package org.onosproject.incubator.net.virtual.rest;
\ No newline at end of file
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/AbstractVirtualStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/AbstractVirtualStore.java
new file mode 100644
index 0000000..73f5bdb
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/AbstractVirtualStore.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.store.impl;
+
+import com.google.common.collect.Maps;
+import org.onosproject.event.Event;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualStore;
+import org.onosproject.store.StoreDelegate;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Base implementation of a virtual store.
+ */
+public class AbstractVirtualStore<E extends Event, D extends StoreDelegate<E>>
+        implements VirtualStore<E, D> {
+
+    protected Map<NetworkId, D> delegateMap = Maps.newConcurrentMap();
+
+    @Override
+    public void setDelegate(NetworkId networkId, D delegate) {
+        checkState(delegateMap.get(networkId) == null
+                           || delegateMap.get(networkId) == delegate,
+                   "Store delegate already set");
+
+        delegateMap.putIfAbsent(networkId, delegate);
+    }
+
+    @Override
+    public void unsetDelegate(NetworkId networkId, D delegate) {
+        if (delegateMap.get(networkId) == delegate) {
+            delegateMap.remove(networkId, delegate);
+        }
+    }
+
+    @Override
+    public boolean hasDelegate(NetworkId networkId) {
+        return delegateMap.get(networkId) != null;
+    }
+
+    /**
+     * Notifies the delegate with the specified event.
+     *
+     * @param networkId a virtual network identifier
+     * @param event event to delegate
+     */
+    protected void notifyDelegate(NetworkId networkId, E event) {
+        if (delegateMap.get(networkId) != null) {
+            delegateMap.get(networkId).notify(event);
+        }
+    }
+
+    /**
+     * Notifies the delegate with the specified list of events.
+     *
+     * @param networkId a virtual network identifier
+     * @param events list of events to delegate
+     */
+    protected void notifyDelegate(NetworkId networkId, List<E> events) {
+        for (E event: events) {
+            notifyDelegate(networkId, event);
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/ConsistentVirtualDeviceMastershipStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/ConsistentVirtualDeviceMastershipStore.java
new file mode 100644
index 0000000..408ce9c
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/ConsistentVirtualDeviceMastershipStore.java
@@ -0,0 +1,467 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.Leadership;
+import org.onosproject.cluster.LeadershipAdminService;
+import org.onosproject.cluster.LeadershipEvent;
+import org.onosproject.cluster.LeadershipEventListener;
+import org.onosproject.cluster.LeadershipService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.cluster.RoleInfo;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkMastershipStore;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipInfo;
+import org.onosproject.mastership.MastershipStoreDelegate;
+import org.onosproject.mastership.MastershipTerm;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
+import org.onosproject.store.cluster.messaging.MessageSubject;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.Serializer;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.mastership.MastershipEvent.Type.BACKUPS_CHANGED;
+import static org.onosproject.mastership.MastershipEvent.Type.MASTER_CHANGED;
+import static org.onosproject.mastership.MastershipEvent.Type.SUSPENDED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+@Component(immediate = true, enabled = false, service = VirtualNetworkMastershipStore.class)
+public class ConsistentVirtualDeviceMastershipStore
+        extends AbstractVirtualStore<MastershipEvent, MastershipStoreDelegate>
+        implements VirtualNetworkMastershipStore {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LeadershipService leadershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LeadershipAdminService leadershipAdminService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterCommunicationService clusterCommunicator;
+
+    private NodeId localNodeId;
+
+    private static final MessageSubject ROLE_RELINQUISH_SUBJECT =
+            new MessageSubject("virtual-mastership-store-device-role-relinquish");
+
+    private static final Pattern DEVICE_MASTERSHIP_TOPIC_PATTERN =
+            Pattern.compile("vnet:(.*),device:(.*)");
+
+    private ExecutorService eventHandler;
+    private ExecutorService messageHandlingExecutor;
+    private ScheduledExecutorService transferExecutor;
+    private final LeadershipEventListener leadershipEventListener =
+            new InternalDeviceMastershipEventListener();
+
+    private static final String NODE_ID_NULL = "Node ID cannot be null";
+    private static final String NETWORK_ID_NULL = "Network ID cannot be null";
+    private static final String DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final int WAIT_BEFORE_MASTERSHIP_HANDOFF_MILLIS = 3000;
+
+    public static final Serializer SERIALIZER = Serializer.using(
+            KryoNamespace.newBuilder()
+                    .register(KryoNamespaces.API)
+                    .register(MastershipRole.class)
+                    .register(MastershipEvent.class)
+                    .register(MastershipEvent.Type.class)
+                    .register(VirtualDeviceId.class)
+                    .build("VirtualMastershipStore"));
+
+    @Activate
+    public void activate() {
+        eventHandler = Executors.newSingleThreadExecutor(
+                groupedThreads("onos/store/virtual/mastership", "event-handler", log));
+
+        messageHandlingExecutor =
+                Executors.newSingleThreadExecutor(
+                        groupedThreads("onos/store/virtual/mastership", "message-handler", log));
+        transferExecutor =
+                Executors.newSingleThreadScheduledExecutor(
+                        groupedThreads("onos/store/virtual/mastership", "mastership-transfer-executor", log));
+        clusterCommunicator.addSubscriber(ROLE_RELINQUISH_SUBJECT,
+                                          SERIALIZER::decode,
+                                          this::relinquishLocalRole,
+                                          SERIALIZER::encode,
+                                          messageHandlingExecutor);
+        localNodeId = clusterService.getLocalNode().id();
+        leadershipService.addListener(leadershipEventListener);
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        clusterCommunicator.removeSubscriber(ROLE_RELINQUISH_SUBJECT);
+        leadershipService.removeListener(leadershipEventListener);
+        messageHandlingExecutor.shutdown();
+        transferExecutor.shutdown();
+        eventHandler.shutdown();
+        log.info("Stopped");
+    }
+
+    @Override
+    public CompletableFuture<MastershipRole> requestRole(NetworkId networkId,
+                                                         DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        Leadership leadership = leadershipService.runForLeadership(leadershipTopic);
+        return CompletableFuture
+                .completedFuture(localNodeId.equals(leadership.leaderNodeId()) ?
+                                         MastershipRole.MASTER : MastershipRole.STANDBY);
+    }
+
+    @Override
+    public MastershipRole getRole(NetworkId networkId, NodeId nodeId, DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(nodeId != null, NODE_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        Leadership leadership = leadershipService.getLeadership(leadershipTopic);
+        NodeId leader = leadership == null ? null : leadership.leaderNodeId();
+        List<NodeId> candidates = leadership == null ?
+                ImmutableList.of() : ImmutableList.copyOf(leadership.candidates());
+        return Objects.equal(nodeId, leader) ?
+                MastershipRole.MASTER : candidates.contains(nodeId) ?
+                MastershipRole.STANDBY : MastershipRole.NONE;
+    }
+
+    @Override
+    public NodeId getMaster(NetworkId networkId, DeviceId deviceId) {
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        return leadershipService.getLeader(createDeviceMastershipTopic(networkId, deviceId));
+    }
+
+    @Override
+    public RoleInfo getNodes(NetworkId networkId, DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+        Leadership leadership = leadershipService.getLeadership(createDeviceMastershipTopic(networkId, deviceId));
+        return new RoleInfo(leadership.leaderNodeId(), leadership.candidates());
+    }
+
+    @Override
+    public MastershipInfo getMastership(NetworkId networkId, DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+        Leadership leadership = leadershipService.getLeadership(createDeviceMastershipTopic(networkId, deviceId));
+        return buildMastershipFromLeadership(leadership);
+    }
+
+    @Override
+    public Set<DeviceId> getDevices(NetworkId networkId, NodeId nodeId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(nodeId != null, NODE_ID_NULL);
+
+        // FIXME This result contains REMOVED device.
+        // MastershipService cannot listen to DeviceEvent to GC removed topic,
+        // since DeviceManager depend on it.
+        // Reference count, etc. at LeadershipService layer?
+        return leadershipService
+                .ownedTopics(nodeId)
+                .stream()
+                .filter(this::isVirtualMastershipTopic)
+                .map(this::extractDeviceIdFromTopic)
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public CompletableFuture<MastershipEvent> setMaster(NetworkId networkId,
+                                                        NodeId nodeId, DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(nodeId != null, NODE_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        if (leadershipAdminService.promoteToTopOfCandidateList(leadershipTopic, nodeId)) {
+            transferExecutor.schedule(() -> leadershipAdminService.transferLeadership(leadershipTopic, nodeId),
+                                      WAIT_BEFORE_MASTERSHIP_HANDOFF_MILLIS, TimeUnit.MILLISECONDS);
+        }
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public MastershipTerm getTermFor(NetworkId networkId, DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        Leadership leadership = leadershipService.getLeadership(leadershipTopic);
+        return leadership != null && leadership.leaderNodeId() != null ?
+                MastershipTerm.of(leadership.leaderNodeId(),
+                                  leadership.leader().term()) : null;
+    }
+
+    @Override
+    public CompletableFuture<MastershipEvent> setStandby(NetworkId networkId,
+                                                         NodeId nodeId,
+                                                         DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(nodeId != null, NODE_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        NodeId currentMaster = getMaster(networkId, deviceId);
+        if (!nodeId.equals(currentMaster)) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        List<NodeId> candidates = leadershipService.getCandidates(leadershipTopic);
+
+        NodeId newMaster = candidates.stream()
+                .filter(candidate -> !Objects.equal(nodeId, candidate))
+                .findFirst()
+                .orElse(null);
+        log.info("Transitioning to role {} for {}. Next master: {}",
+                 newMaster != null ? MastershipRole.STANDBY : MastershipRole.NONE,
+                 deviceId, newMaster);
+
+        if (newMaster != null) {
+            return setMaster(networkId, newMaster, deviceId);
+        }
+        return relinquishRole(networkId, nodeId, deviceId);
+    }
+
+    @Override
+    public CompletableFuture<MastershipEvent> relinquishRole(NetworkId networkId,
+                                                             NodeId nodeId,
+                                                             DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(nodeId != null, NODE_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        if (nodeId.equals(localNodeId)) {
+            return relinquishLocalRoleByNetwork(networkId, deviceId);
+        }
+
+        log.debug("Forwarding request to relinquish "
+                          + "role for vnet {} device {} to {}", deviceId, nodeId);
+        return clusterCommunicator.sendAndReceive(
+                new VirtualDeviceId(networkId, deviceId),
+                ROLE_RELINQUISH_SUBJECT,
+                SERIALIZER::encode,
+                SERIALIZER::decode,
+                nodeId);
+    }
+
+    private CompletableFuture<MastershipEvent> relinquishLocalRoleByNetwork(NetworkId networkId,
+                                                                   DeviceId deviceId) {
+        checkArgument(networkId != null, NETWORK_ID_NULL);
+        checkArgument(deviceId != null, DEVICE_ID_NULL);
+
+        String leadershipTopic = createDeviceMastershipTopic(networkId, deviceId);
+        if (!leadershipService.getCandidates(leadershipTopic).contains(localNodeId)) {
+            return CompletableFuture.completedFuture(null);
+        }
+        MastershipEvent.Type eventType = localNodeId.equals(leadershipService.getLeader(leadershipTopic)) ?
+                MastershipEvent.Type.MASTER_CHANGED : MastershipEvent.Type.BACKUPS_CHANGED;
+        leadershipService.withdraw(leadershipTopic);
+        return CompletableFuture.completedFuture(
+            new MastershipEvent(eventType, deviceId, getMastership(networkId, deviceId)));
+    }
+
+    private CompletableFuture<MastershipEvent>
+    relinquishLocalRole(VirtualDeviceId virtualDeviceId) {
+        return relinquishLocalRoleByNetwork(virtualDeviceId.networkId,
+                                            virtualDeviceId.deviceId);
+    }
+
+    @Override
+    public void relinquishAllRole(NetworkId networkId, NodeId nodeId) {
+        // Noop. LeadershipService already takes care of detecting and purging stale locks.
+    }
+
+    private MastershipInfo buildMastershipFromLeadership(Leadership leadership) {
+        ImmutableMap.Builder<NodeId, MastershipRole> builder = ImmutableMap.builder();
+        if (leadership.leaderNodeId() != null) {
+            builder.put(leadership.leaderNodeId(), MastershipRole.MASTER);
+        }
+        leadership.candidates().forEach(nodeId -> builder.put(nodeId, MastershipRole.STANDBY));
+        clusterService.getNodes().stream()
+            .filter(node -> !leadership.candidates().contains(node.id()))
+            .forEach(node -> builder.put(node.id(), MastershipRole.NONE));
+
+        return new MastershipInfo(
+            leadership.leader() != null ? leadership.leader().term() : 0,
+            leadership.leader() != null
+                ? Optional.of(leadership.leader().nodeId())
+                : Optional.empty(),
+            builder.build());
+    }
+
+    private class InternalDeviceMastershipEventListener
+            implements LeadershipEventListener {
+
+        @Override
+        public boolean isRelevant(LeadershipEvent event) {
+            Leadership leadership = event.subject();
+            return isVirtualMastershipTopic(leadership.topic());
+        }
+
+        @Override
+        public void event(LeadershipEvent event) {
+            eventHandler.execute(() -> handleEvent(event));
+        }
+
+        private void handleEvent(LeadershipEvent event) {
+            Leadership leadership = event.subject();
+
+            NetworkId networkId = extractNetworkIdFromTopic(leadership.topic());
+            DeviceId deviceId = extractDeviceIdFromTopic(leadership.topic());
+            MastershipInfo mastershipInfo = event.type() != LeadershipEvent.Type.SERVICE_DISRUPTED
+                ? buildMastershipFromLeadership(event.subject())
+                : new MastershipInfo();
+
+            switch (event.type()) {
+                case LEADER_AND_CANDIDATES_CHANGED:
+                    notifyDelegate(networkId, new MastershipEvent(BACKUPS_CHANGED, deviceId, mastershipInfo));
+                    notifyDelegate(networkId, new MastershipEvent(MASTER_CHANGED, deviceId, mastershipInfo));
+                    break;
+                case LEADER_CHANGED:
+                    notifyDelegate(networkId, new MastershipEvent(MASTER_CHANGED, deviceId, mastershipInfo));
+                    break;
+                case CANDIDATES_CHANGED:
+                    notifyDelegate(networkId, new MastershipEvent(BACKUPS_CHANGED, deviceId, mastershipInfo));
+                    break;
+                case SERVICE_DISRUPTED:
+                    notifyDelegate(networkId, new MastershipEvent(SUSPENDED, deviceId, mastershipInfo));
+                    break;
+                case SERVICE_RESTORED:
+                    // Do nothing, wait for updates from peers
+                    break;
+                default:
+            }
+        }
+    }
+
+    private String createDeviceMastershipTopic(NetworkId networkId, DeviceId deviceId) {
+        return String.format("vnet:%s,device:%s", networkId.toString(), deviceId.toString());
+    }
+
+    /**
+     * Returns the virtual network identifier extracted from the topic.
+     *
+     * @param topic topic to extract virtual network identifier
+     * @return an extracted virtual network identifier
+     * @throws IllegalArgumentException the topic not match with the pattern
+     * used for virtual network mastership store
+     */
+    private NetworkId extractNetworkIdFromTopic(String topic) {
+        Matcher m = DEVICE_MASTERSHIP_TOPIC_PATTERN.matcher(topic);
+        if (m.matches()) {
+            return NetworkId.networkId(Long.getLong(m.group(1)));
+        } else {
+            throw new IllegalArgumentException("Invalid virtual mastership topic: "
+                                                       + topic);
+        }
+    }
+
+    /**
+     * Returns the device identifier extracted from the topic.
+     *
+     * @param topic topic to extract device identifier
+     * @return an extracted virtual device identifier
+     * @throws IllegalArgumentException the topic not match with the pattern
+     * used for virtual network mastership store
+     */
+    private DeviceId extractDeviceIdFromTopic(String topic) {
+        Matcher m = DEVICE_MASTERSHIP_TOPIC_PATTERN.matcher(topic);
+        if (m.matches()) {
+            return DeviceId.deviceId(m.group(2));
+        } else {
+            throw new IllegalArgumentException("Invalid virtual mastership topic: "
+                                                       + topic);
+        }
+    }
+
+    /**
+     * Returns whether the topic is matched with virtual mastership store topic.
+     *
+     * @param topic topic to match
+     * @return True when the topic matched with virtual network mastership store
+     */
+    private boolean isVirtualMastershipTopic(String topic) {
+        Matcher m = DEVICE_MASTERSHIP_TOPIC_PATTERN.matcher(topic);
+        return m.matches();
+    }
+
+    /**
+     * A wrapper class used for the communication service.
+     */
+    private class VirtualDeviceId {
+        NetworkId networkId;
+        DeviceId deviceId;
+
+        public VirtualDeviceId(NetworkId networkId, DeviceId deviceId) {
+            this.networkId = networkId;
+            this.deviceId = deviceId;
+        }
+
+        public int hashCode() {
+            return Objects.hashCode(networkId, deviceId);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof VirtualDeviceId) {
+                final VirtualDeviceId that = (VirtualDeviceId) obj;
+                return this.getClass() == that.getClass() &&
+                        Objects.equal(this.networkId, that.networkId) &&
+                        Objects.equal(this.deviceId, that.deviceId);
+            }
+            return false;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowObjectiveStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowObjectiveStore.java
new file mode 100644
index 0000000..5bcfabc
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowObjectiveStore.java
@@ -0,0 +1,78 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.Maps;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowObjectiveStore;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+
+import java.util.concurrent.ConcurrentMap;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Distributed flow objective store for virtual network.
+ */
+@Component(immediate = true, enabled = false, service = VirtualNetworkFlowObjectiveStore.class)
+public class DistributedVirtualFlowObjectiveStore
+        extends SimpleVirtualFlowObjectiveStore
+        implements VirtualNetworkFlowObjectiveStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private ConsistentMap<NetworkId, ConcurrentMap<Integer, byte[]>> nextGroupsMap;
+    private static final String VNET_FLOW_OBJ_GROUP_MAP_NAME =
+            "onos-networkId-flowobjective-groups";
+    private static final String VNET_FLOW_OBJ_GROUP_MAP_FRIENDLYNAME =
+            "DistributedVirtualFlowObjectiveStore";
+
+    @Override
+    protected void initNextGroupsMap() {
+        nextGroupsMap = storageService.<NetworkId, ConcurrentMap<Integer, byte[]>>consistentMapBuilder()
+                .withName(VNET_FLOW_OBJ_GROUP_MAP_NAME)
+                .withSerializer(Serializer.using(
+                        new KryoNamespace.Builder()
+                                .register(KryoNamespaces.API)
+                                .register(NetworkId.class)
+                                .build(VNET_FLOW_OBJ_GROUP_MAP_FRIENDLYNAME)))
+                .build();
+
+    }
+
+    @Override
+    protected ConcurrentMap<Integer, byte[]> getNextGroups(NetworkId networkId) {
+        nextGroupsMap.computeIfAbsent(networkId, n -> {
+            log.debug("getNextGroups - creating new ConcurrentMap");
+            return Maps.newConcurrentMap();
+        });
+
+        return nextGroupsMap.get(networkId).value();
+    }
+
+    @Override
+    protected void updateNextGroupsMap(NetworkId networkId, ConcurrentMap<Integer,
+            byte[]> nextGroups) {
+        nextGroupsMap.put(networkId, nextGroups);
+    }
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowRuleStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowRuleStore.java
new file mode 100644
index 0000000..8722c92
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualFlowRuleStore.java
@@ -0,0 +1,922 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import org.onlab.util.KryoNamespace;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.IdGenerator;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowRuleStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.store.impl.primitives.VirtualDeviceId;
+import org.onosproject.incubator.net.virtual.store.impl.primitives.VirtualFlowEntry;
+import org.onosproject.incubator.net.virtual.store.impl.primitives.VirtualFlowRule;
+import org.onosproject.incubator.net.virtual.store.impl.primitives.VirtualFlowRuleBatchEvent;
+import org.onosproject.incubator.net.virtual.store.impl.primitives.VirtualFlowRuleBatchOperation;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.BatchOperationEntry;
+import org.onosproject.net.flow.CompletedBatchOperation;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleEvent;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.FlowRuleStoreDelegate;
+import org.onosproject.net.flow.StoredFlowEntry;
+import org.onosproject.net.flow.TableStatisticsEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEvent;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchRequest;
+import org.onosproject.store.Timestamp;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
+import org.onosproject.store.cluster.messaging.ClusterMessage;
+import org.onosproject.store.cluster.messaging.ClusterMessageHandler;
+import org.onosproject.store.cluster.messaging.MessageSubject;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onlab.util.Tools.get;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.incubator.net.virtual.store.impl.OsgiPropertyConstants.*;
+import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Manages inventory of flow rules using a distributed state management protocol
+ * for virtual networks.
+ */
+//TODO: support backup and persistent mechanism
+@Component(immediate = true, enabled = false, service = VirtualNetworkFlowRuleStore.class,
+        property = {
+                MESSAGE_HANDLER_THREAD_POOL_SIZE + ":Integer=" + MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT,
+                BACKUP_PERIOD_MILLIS + ":Integer=" + BACKUP_PERIOD_MILLIS_DEFAULT,
+                PERSISTENCE_ENABLED + ":Boolean=" + PERSISTENCE_ENABLED_DEFAULT,
+        })
+
+public class DistributedVirtualFlowRuleStore
+        extends AbstractVirtualStore<FlowRuleBatchEvent, FlowRuleStoreDelegate>
+        implements VirtualNetworkFlowRuleStore {
+
+    private final Logger log = getLogger(getClass());
+
+    //TODO: confirm this working fine with multiple thread more than 1
+    private static final long FLOW_RULE_STORE_TIMEOUT_MILLIS = 5000;
+
+    private static final String FLOW_OP_TOPIC = "virtual-flow-ops-ids";
+
+    // MessageSubjects used by DistributedVirtualFlowRuleStore peer-peer communication.
+    private static final MessageSubject APPLY_BATCH_FLOWS
+            = new MessageSubject("virtual-peer-forward-apply-batch");
+    private static final MessageSubject GET_FLOW_ENTRY
+            = new MessageSubject("virtual-peer-forward-get-flow-entry");
+    private static final MessageSubject GET_DEVICE_FLOW_ENTRIES
+            = new MessageSubject("virtual-peer-forward-get-device-flow-entries");
+    private static final MessageSubject REMOVE_FLOW_ENTRY
+            = new MessageSubject("virtual-peer-forward-remove-flow-entry");
+    private static final MessageSubject REMOTE_APPLY_COMPLETED
+            = new MessageSubject("virtual-peer-apply-completed");
+
+    /** Number of threads in the message handler pool. */
+    private int msgHandlerThreadPoolSize = MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT;
+
+    /** Delay in ms between successive backup runs. */
+    private int backupPeriod = BACKUP_PERIOD_MILLIS_DEFAULT;
+
+    /** Indicates whether or not changes in the flow table should be persisted to disk.. */
+    private boolean persistenceEnabled = PERSISTENCE_ENABLED_DEFAULT;
+
+    private InternalFlowTable flowTable = new InternalFlowTable();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterCommunicationService clusterCommunicator;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService configService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VirtualNetworkService vnaService;
+
+    private Map<Long, NodeId> pendingResponses = Maps.newConcurrentMap();
+    private ExecutorService messageHandlingExecutor;
+    private ExecutorService eventHandler;
+
+    private EventuallyConsistentMap<NetworkId, Map<DeviceId, List<TableStatisticsEntry>>> deviceTableStats;
+    private final EventuallyConsistentMapListener<NetworkId, Map<DeviceId, List<TableStatisticsEntry>>>
+            tableStatsListener = new InternalTableStatsListener();
+
+
+    protected final Serializer serializer = Serializer.using(KryoNamespace.newBuilder()
+                                                                     .register(KryoNamespaces.API)
+                                                                     .register(NetworkId.class)
+                                                                     .register(VirtualFlowRuleBatchOperation.class)
+                                                                     .register(VirtualFlowRuleBatchEvent.class)
+                                                                     .build());
+
+    protected final KryoNamespace.Builder serializerBuilder = KryoNamespace.newBuilder()
+            .register(KryoNamespaces.API)
+            .register(MastershipBasedTimestamp.class);
+
+    private IdGenerator idGenerator;
+    private NodeId local;
+
+
+    @Activate
+    public void activate(ComponentContext context) {
+        configService.registerProperties(getClass());
+
+        idGenerator = coreService.getIdGenerator(FLOW_OP_TOPIC);
+
+        local = clusterService.getLocalNode().id();
+
+        eventHandler = Executors.newSingleThreadExecutor(
+                groupedThreads("onos/virtual-flow", "event-handler", log));
+        messageHandlingExecutor = Executors.newFixedThreadPool(
+                msgHandlerThreadPoolSize, groupedThreads("onos/store/virtual-flow", "message-handlers", log));
+
+        registerMessageHandlers(messageHandlingExecutor);
+
+        deviceTableStats = storageService
+                .<NetworkId, Map<DeviceId, List<TableStatisticsEntry>>>eventuallyConsistentMapBuilder()
+                .withName("onos-virtual-flow-table-stats")
+                .withSerializer(serializerBuilder)
+                .withAntiEntropyPeriod(5, TimeUnit.SECONDS)
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .withTombstonesDisabled()
+                .build();
+        deviceTableStats.addListener(tableStatsListener);
+
+        logConfig("Started");
+    }
+
+    @Deactivate
+    public void deactivate(ComponentContext context) {
+        configService.unregisterProperties(getClass(), false);
+        unregisterMessageHandlers();
+        deviceTableStats.removeListener(tableStatsListener);
+        deviceTableStats.destroy();
+        eventHandler.shutdownNow();
+        messageHandlingExecutor.shutdownNow();
+        log.info("Stopped");
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Modified
+    public void modified(ComponentContext context) {
+        if (context == null) {
+            logConfig("Default config");
+            return;
+        }
+
+        Dictionary properties = context.getProperties();
+        int newPoolSize;
+        int newBackupPeriod;
+        try {
+            String s = get(properties, MESSAGE_HANDLER_THREAD_POOL_SIZE);
+            newPoolSize = isNullOrEmpty(s) ? msgHandlerThreadPoolSize : Integer.parseInt(s.trim());
+
+            s = get(properties, BACKUP_PERIOD_MILLIS);
+            newBackupPeriod = isNullOrEmpty(s) ? backupPeriod : Integer.parseInt(s.trim());
+        } catch (NumberFormatException | ClassCastException e) {
+            newPoolSize = MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT;
+            newBackupPeriod = BACKUP_PERIOD_MILLIS_DEFAULT;
+        }
+
+        boolean restartBackupTask = false;
+
+        if (newBackupPeriod != backupPeriod) {
+            backupPeriod = newBackupPeriod;
+            restartBackupTask = true;
+        }
+        if (restartBackupTask) {
+            log.warn("Currently, backup tasks are not supported.");
+        }
+        if (newPoolSize != msgHandlerThreadPoolSize) {
+            msgHandlerThreadPoolSize = newPoolSize;
+            ExecutorService oldMsgHandler = messageHandlingExecutor;
+            messageHandlingExecutor = Executors.newFixedThreadPool(
+                    msgHandlerThreadPoolSize, groupedThreads("onos/store/virtual-flow", "message-handlers", log));
+
+            // replace previously registered handlers.
+            registerMessageHandlers(messageHandlingExecutor);
+            oldMsgHandler.shutdown();
+        }
+
+        logConfig("Reconfigured");
+    }
+
+    @Override
+    public int getFlowRuleCount(NetworkId networkId) {
+        AtomicInteger sum = new AtomicInteger(0);
+        DeviceService deviceService = vnaService.get(networkId, DeviceService.class);
+        deviceService.getDevices()
+                .forEach(device -> sum.addAndGet(
+                        Iterables.size(getFlowEntries(networkId, device.id()))));
+        return sum.get();
+    }
+
+    @Override
+    public FlowEntry getFlowEntry(NetworkId networkId, FlowRule rule) {
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(rule.deviceId());
+
+        if (master == null) {
+            log.debug("Failed to getFlowEntry: No master for {}, vnet {}",
+                      rule.deviceId(), networkId);
+            return null;
+        }
+
+        if (Objects.equals(local, master)) {
+            return flowTable.getFlowEntry(networkId, rule);
+        }
+
+        log.trace("Forwarding getFlowEntry to {}, which is the primary (master) " +
+                          "for device {}, vnet {}",
+                  master, rule.deviceId(), networkId);
+
+        VirtualFlowRule vRule = new VirtualFlowRule(networkId, rule);
+
+        return Tools.futureGetOrElse(clusterCommunicator.sendAndReceive(vRule,
+                                                                        GET_FLOW_ENTRY,
+                                                                        serializer::encode,
+                                                                        serializer::decode,
+                                                                        master),
+                                     FLOW_RULE_STORE_TIMEOUT_MILLIS,
+                                     TimeUnit.MILLISECONDS,
+                                     null);
+    }
+
+    @Override
+    public Iterable<FlowEntry> getFlowEntries(NetworkId networkId, DeviceId deviceId) {
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(deviceId);
+
+        if (master == null) {
+            log.debug("Failed to getFlowEntries: No master for {}, vnet {}", deviceId, networkId);
+            return Collections.emptyList();
+        }
+
+        if (Objects.equals(local, master)) {
+            return flowTable.getFlowEntries(networkId, deviceId);
+        }
+
+        log.trace("Forwarding getFlowEntries to {}, which is the primary (master) for device {}",
+                  master, deviceId);
+
+        return Tools.futureGetOrElse(
+                clusterCommunicator.sendAndReceive(deviceId,
+                                                   GET_DEVICE_FLOW_ENTRIES,
+                                                   serializer::encode,
+                                                   serializer::decode,
+                                                   master),
+                FLOW_RULE_STORE_TIMEOUT_MILLIS,
+                TimeUnit.MILLISECONDS,
+                Collections.emptyList());
+    }
+
+    @Override
+    public void storeBatch(NetworkId networkId, FlowRuleBatchOperation operation) {
+        if (operation.getOperations().isEmpty()) {
+            notifyDelegate(networkId, FlowRuleBatchEvent.completed(
+                    new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+                    new CompletedBatchOperation(true, Collections.emptySet(), operation.deviceId())));
+            return;
+        }
+
+        DeviceId deviceId = operation.deviceId();
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(deviceId);
+
+        if (master == null) {
+            log.warn("No master for {}, vnet {} : flows will be marked for removal", deviceId, networkId);
+
+            updateStoreInternal(networkId, operation);
+
+            notifyDelegate(networkId, FlowRuleBatchEvent.completed(
+                    new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+                    new CompletedBatchOperation(true, Collections.emptySet(), operation.deviceId())));
+            return;
+        }
+
+        if (Objects.equals(local, master)) {
+            storeBatchInternal(networkId, operation);
+            return;
+        }
+
+        log.trace("Forwarding storeBatch to {}, which is the primary (master) for device {}, vent {}",
+                  master, deviceId, networkId);
+
+        clusterCommunicator.unicast(new VirtualFlowRuleBatchOperation(networkId, operation),
+                                    APPLY_BATCH_FLOWS,
+                                    serializer::encode,
+                                    master)
+                .whenComplete((result, error) -> {
+                    if (error != null) {
+                        log.warn("Failed to storeBatch: {} to {}", operation, master, error);
+
+                        Set<FlowRule> allFailures = operation.getOperations()
+                                .stream()
+                                .map(BatchOperationEntry::target)
+                                .collect(Collectors.toSet());
+
+                        notifyDelegate(networkId, FlowRuleBatchEvent.completed(
+                                new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+                                new CompletedBatchOperation(false, allFailures, deviceId)));
+                    }
+                });
+    }
+
+    @Override
+    public void batchOperationComplete(NetworkId networkId, FlowRuleBatchEvent event) {
+        //FIXME: need a per device pending response
+        NodeId nodeId = pendingResponses.remove(event.subject().batchId());
+        if (nodeId == null) {
+            notifyDelegate(networkId, event);
+        } else {
+            // TODO check unicast return value
+            clusterCommunicator.unicast(new VirtualFlowRuleBatchEvent(networkId, event),
+                                        REMOTE_APPLY_COMPLETED, serializer::encode, nodeId);
+            //error log: log.warn("Failed to respond to peer for batch operation result");
+        }
+    }
+
+    @Override
+    public void deleteFlowRule(NetworkId networkId, FlowRule rule) {
+        storeBatch(networkId,
+                new FlowRuleBatchOperation(
+                        Collections.singletonList(
+                                new FlowRuleBatchEntry(
+                                        FlowRuleBatchEntry.FlowRuleOperation.REMOVE,
+                                        rule)), rule.deviceId(), idGenerator.getNewId()));
+    }
+
+    @Override
+    public FlowRuleEvent addOrUpdateFlowRule(NetworkId networkId, FlowEntry rule) {
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(rule.deviceId());
+        if (Objects.equals(local, master)) {
+            return addOrUpdateFlowRuleInternal(networkId, rule);
+        }
+
+        log.warn("Tried to update FlowRule {} state,"
+                         + " while the Node was not the master.", rule);
+        return null;
+    }
+
+    private FlowRuleEvent addOrUpdateFlowRuleInternal(NetworkId networkId, FlowEntry rule) {
+        // check if this new rule is an update to an existing entry
+        StoredFlowEntry stored = flowTable.getFlowEntry(networkId, rule);
+        if (stored != null) {
+            //FIXME modification of "stored" flow entry outside of flow table
+            stored.setBytes(rule.bytes());
+            stored.setLife(rule.life(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
+            stored.setLiveType(rule.liveType());
+            stored.setPackets(rule.packets());
+            stored.setLastSeen();
+            if (stored.state() == FlowEntry.FlowEntryState.PENDING_ADD) {
+                stored.setState(FlowEntry.FlowEntryState.ADDED);
+                return new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED, rule);
+            }
+            return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule);
+        }
+
+        // TODO: Confirm if this behavior is correct. See SimpleFlowRuleStore
+        // TODO: also update backup if the behavior is correct.
+        flowTable.add(networkId, rule);
+        return null;
+    }
+
+    @Override
+    public FlowRuleEvent removeFlowRule(NetworkId networkId, FlowEntry rule) {
+        final DeviceId deviceId = rule.deviceId();
+
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(deviceId);
+
+        if (Objects.equals(local, master)) {
+            // bypass and handle it locally
+            return removeFlowRuleInternal(new VirtualFlowEntry(networkId, rule));
+        }
+
+        if (master == null) {
+            log.warn("Failed to removeFlowRule: No master for {}", deviceId);
+            // TODO: revisit if this should be null (="no-op") or Exception
+            return null;
+        }
+
+        log.trace("Forwarding removeFlowRule to {}, which is the master for device {}",
+                  master, deviceId);
+
+        return Futures.getUnchecked(clusterCommunicator.sendAndReceive(
+                new VirtualFlowEntry(networkId, rule),
+                REMOVE_FLOW_ENTRY,
+                serializer::encode,
+                serializer::decode,
+                master));
+    }
+
+    @Override
+    public FlowRuleEvent pendingFlowRule(NetworkId networkId, FlowEntry rule) {
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        if (mastershipService.isLocalMaster(rule.deviceId())) {
+            StoredFlowEntry stored = flowTable.getFlowEntry(networkId, rule);
+            if (stored != null &&
+                    stored.state() != FlowEntry.FlowEntryState.PENDING_ADD) {
+                stored.setState(FlowEntry.FlowEntryState.PENDING_ADD);
+                return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void purgeFlowRules(NetworkId networkId) {
+        flowTable.purgeFlowRules(networkId);
+    }
+
+    @Override
+    public FlowRuleEvent updateTableStatistics(NetworkId networkId,
+                                               DeviceId deviceId,
+                                               List<TableStatisticsEntry> tableStats) {
+        if (deviceTableStats.get(networkId) == null) {
+            deviceTableStats.put(networkId, Maps.newConcurrentMap());
+        }
+        deviceTableStats.get(networkId).put(deviceId, tableStats);
+        return null;
+    }
+
+    @Override
+    public Iterable<TableStatisticsEntry> getTableStatistics(NetworkId networkId, DeviceId deviceId) {
+        MastershipService mastershipService =
+                vnaService.get(networkId, MastershipService.class);
+        NodeId master = mastershipService.getMasterFor(deviceId);
+
+        if (master == null) {
+            log.debug("Failed to getTableStats: No master for {}", deviceId);
+            return Collections.emptyList();
+        }
+
+        if (deviceTableStats.get(networkId) == null) {
+            deviceTableStats.put(networkId, Maps.newConcurrentMap());
+        }
+
+        List<TableStatisticsEntry> tableStats = deviceTableStats.get(networkId).get(deviceId);
+        if (tableStats == null) {
+            return Collections.emptyList();
+        }
+        return ImmutableList.copyOf(tableStats);
+    }
+
+    private void registerMessageHandlers(ExecutorService executor) {
+        clusterCommunicator.addSubscriber(APPLY_BATCH_FLOWS, new OnStoreBatch(), executor);
+        clusterCommunicator.<VirtualFlowRuleBatchEvent>addSubscriber(
+                REMOTE_APPLY_COMPLETED, serializer::decode,
+                this::notifyDelicateByNetwork, executor);
+        clusterCommunicator.addSubscriber(
+                GET_FLOW_ENTRY, serializer::decode, this::getFlowEntryByNetwork,
+                serializer::encode, executor);
+        clusterCommunicator.addSubscriber(
+                GET_DEVICE_FLOW_ENTRIES, serializer::decode,
+                this::getFlowEntriesByNetwork,
+                serializer::encode, executor);
+        clusterCommunicator.addSubscriber(
+                REMOVE_FLOW_ENTRY, serializer::decode, this::removeFlowRuleInternal,
+                serializer::encode, executor);
+    }
+
+    private void unregisterMessageHandlers() {
+        clusterCommunicator.removeSubscriber(REMOVE_FLOW_ENTRY);
+        clusterCommunicator.removeSubscriber(GET_DEVICE_FLOW_ENTRIES);
+        clusterCommunicator.removeSubscriber(GET_FLOW_ENTRY);
+        clusterCommunicator.removeSubscriber(APPLY_BATCH_FLOWS);
+        clusterCommunicator.removeSubscriber(REMOTE_APPLY_COMPLETED);
+    }
+
+
+    private void logConfig(String prefix) {
+        log.info("{} with msgHandlerPoolSize = {}; backupPeriod = {}",
+                 prefix, msgHandlerThreadPoolSize, backupPeriod);
+    }
+
+    private void storeBatchInternal(NetworkId networkId, FlowRuleBatchOperation operation) {
+
+        final DeviceId did = operation.deviceId();
+        //final Collection<FlowEntry> ft = flowTable.getFlowEntries(did);
+        Set<FlowRuleBatchEntry> currentOps = updateStoreInternal(networkId, operation);
+        if (currentOps.isEmpty()) {
+            batchOperationComplete(networkId, FlowRuleBatchEvent.completed(
+                    new FlowRuleBatchRequest(operation.id(), Collections.emptySet()),
+                    new CompletedBatchOperation(true, Collections.emptySet(), did)));
+            return;
+        }
+
+        //Confirm that flowrule service is created
+        vnaService.get(networkId, FlowRuleService.class);
+
+        notifyDelegate(networkId, FlowRuleBatchEvent.requested(new
+                                                            FlowRuleBatchRequest(operation.id(),
+                                                                                 currentOps), operation.deviceId()));
+    }
+
+    private Set<FlowRuleBatchEntry> updateStoreInternal(NetworkId networkId,
+                                                        FlowRuleBatchOperation operation) {
+        return operation.getOperations().stream().map(
+                op -> {
+                    StoredFlowEntry entry;
+                    switch (op.operator()) {
+                        case ADD:
+                            entry = new DefaultFlowEntry(op.target());
+                            // always add requested FlowRule
+                            // Note: 2 equal FlowEntry may have different treatment
+                            flowTable.remove(networkId, entry.deviceId(), entry);
+                            flowTable.add(networkId, entry);
+
+                            return op;
+                        case REMOVE:
+                            entry = flowTable.getFlowEntry(networkId, op.target());
+                            if (entry != null) {
+                                //FIXME modification of "stored" flow entry outside of flow table
+                                entry.setState(FlowEntry.FlowEntryState.PENDING_REMOVE);
+                                log.debug("Setting state of rule to pending remove: {}", entry);
+                                return op;
+                            }
+                            break;
+                        case MODIFY:
+                            //TODO: figure this out at some point
+                            break;
+                        default:
+                            log.warn("Unknown flow operation operator: {}", op.operator());
+                    }
+                    return null;
+                }
+        ).filter(Objects::nonNull).collect(Collectors.toSet());
+    }
+
+    private FlowRuleEvent removeFlowRuleInternal(VirtualFlowEntry rule) {
+        final DeviceId deviceId = rule.flowEntry().deviceId();
+        // This is where one could mark a rule as removed and still keep it in the store.
+        final FlowEntry removed = flowTable.remove(rule.networkId(), deviceId, rule.flowEntry());
+        // rule may be partial rule that is missing treatment, we should use rule from store instead
+        return removed != null ? new FlowRuleEvent(RULE_REMOVED, removed) : null;
+    }
+
+    private final class OnStoreBatch implements ClusterMessageHandler {
+
+        @Override
+        public void handle(final ClusterMessage message) {
+            VirtualFlowRuleBatchOperation vOperation = serializer.decode(message.payload());
+            log.debug("received batch request {}", vOperation);
+
+            FlowRuleBatchOperation operation = vOperation.operation();
+
+            final DeviceId deviceId = operation.deviceId();
+            MastershipService mastershipService =
+                    vnaService.get(vOperation.networkId(), MastershipService.class);
+            NodeId master = mastershipService.getMasterFor(deviceId);
+            if (!Objects.equals(local, master)) {
+                Set<FlowRule> failures = new HashSet<>(operation.size());
+                for (FlowRuleBatchEntry op : operation.getOperations()) {
+                    failures.add(op.target());
+                }
+                CompletedBatchOperation allFailed = new CompletedBatchOperation(false, failures, deviceId);
+                // This node is no longer the master, respond as all failed.
+                // TODO: we might want to wrap response in envelope
+                // to distinguish sw programming failure and hand over
+                // it make sense in the latter case to retry immediately.
+                message.respond(serializer.encode(allFailed));
+                return;
+            }
+
+            pendingResponses.put(operation.id(), message.sender());
+            storeBatchInternal(vOperation.networkId(), operation);
+        }
+    }
+
+    /**
+     * Returns flow rule entry using virtual flow rule.
+     *
+     * @param rule an encapsulated flow rule to be queried
+     */
+    private FlowEntry getFlowEntryByNetwork(VirtualFlowRule rule) {
+        return flowTable.getFlowEntry(rule.networkId(), rule.rule());
+    }
+
+    /**
+     * returns flow entries using virtual device id.
+     *
+     * @param deviceId an encapsulated virtual device id
+     * @return a set of flow entries
+     */
+    private Set<FlowEntry> getFlowEntriesByNetwork(VirtualDeviceId deviceId) {
+        return flowTable.getFlowEntries(deviceId.networkId(), deviceId.deviceId());
+    }
+
+    /**
+     * span out Flow Rule Batch event according to virtual network id.
+     *
+     * @param event a event to be span out
+     */
+    private void notifyDelicateByNetwork(VirtualFlowRuleBatchEvent event) {
+        batchOperationComplete(event.networkId(), event.event());
+    }
+
+    private class InternalFlowTable {
+        //TODO replace the Map<V,V> with ExtendedSet
+        //TODO: support backup mechanism
+        private final Map<NetworkId, Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>>
+                flowEntriesMap = Maps.newConcurrentMap();
+        private final Map<NetworkId, Map<DeviceId, Long>> lastUpdateTimesMap = Maps.newConcurrentMap();
+
+        private Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
+        getFlowEntriesByNetwork(NetworkId networkId) {
+            return flowEntriesMap.computeIfAbsent(networkId, k -> Maps.newConcurrentMap());
+        }
+
+        private Map<DeviceId, Long> getLastUpdateTimesByNetwork(NetworkId networkId) {
+            return lastUpdateTimesMap.computeIfAbsent(networkId, k -> Maps.newConcurrentMap());
+        }
+
+        /**
+         * Returns the flow table for specified device.
+         *
+         * @param networkId virtual network identifier
+         * @param deviceId identifier of the device
+         * @return Map representing Flow Table of given device.
+         */
+        private Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>
+        getFlowTable(NetworkId networkId, DeviceId deviceId) {
+            Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
+                    flowEntries = getFlowEntriesByNetwork(networkId);
+            if (persistenceEnabled) {
+                //TODO: support persistent
+                log.warn("Persistent is not supported");
+                return null;
+            } else {
+                return flowEntries.computeIfAbsent(deviceId, id -> Maps.newConcurrentMap());
+            }
+        }
+
+        private Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>
+        getFlowTableCopy(NetworkId networkId, DeviceId deviceId) {
+
+            Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
+                    flowEntries = getFlowEntriesByNetwork(networkId);
+            Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>> copy = Maps.newHashMap();
+
+            if (persistenceEnabled) {
+                //TODO: support persistent
+                log.warn("Persistent is not supported");
+                return null;
+            } else {
+                flowEntries.computeIfAbsent(deviceId, id -> Maps.newConcurrentMap()).forEach((k, v) -> {
+                    copy.put(k, Maps.newHashMap(v));
+                });
+                return copy;
+            }
+        }
+
+        private Map<StoredFlowEntry, StoredFlowEntry>
+        getFlowEntriesInternal(NetworkId networkId, DeviceId deviceId, FlowId flowId) {
+
+            return getFlowTable(networkId, deviceId)
+                    .computeIfAbsent(flowId, id -> Maps.newConcurrentMap());
+        }
+
+        private StoredFlowEntry getFlowEntryInternal(NetworkId networkId, FlowRule rule) {
+
+            return getFlowEntriesInternal(networkId, rule.deviceId(), rule.id()).get(rule);
+        }
+
+        private Set<FlowEntry> getFlowEntriesInternal(NetworkId networkId, DeviceId deviceId) {
+
+            return getFlowTable(networkId, deviceId).values().stream()
+                    .flatMap(m -> m.values().stream())
+                    .collect(Collectors.toSet());
+        }
+
+        public StoredFlowEntry getFlowEntry(NetworkId networkId, FlowRule rule) {
+            return getFlowEntryInternal(networkId, rule);
+        }
+
+        public Set<FlowEntry> getFlowEntries(NetworkId networkId, DeviceId deviceId) {
+
+            return getFlowEntriesInternal(networkId, deviceId);
+        }
+
+        public void add(NetworkId networkId, FlowEntry rule) {
+            Map<DeviceId, Long> lastUpdateTimes = getLastUpdateTimesByNetwork(networkId);
+
+            getFlowEntriesInternal(networkId, rule.deviceId(), rule.id())
+                    .compute((StoredFlowEntry) rule, (k, stored) -> {
+                        //TODO compare stored and rule timestamps
+                        //TODO the key is not updated
+                        return (StoredFlowEntry) rule;
+                    });
+            lastUpdateTimes.put(rule.deviceId(), System.currentTimeMillis());
+        }
+
+        public FlowEntry remove(NetworkId networkId, DeviceId deviceId, FlowEntry rule) {
+            final AtomicReference<FlowEntry> removedRule = new AtomicReference<>();
+            Map<DeviceId, Long> lastUpdateTimes = getLastUpdateTimesByNetwork(networkId);
+
+            getFlowEntriesInternal(networkId, rule.deviceId(), rule.id())
+                    .computeIfPresent((StoredFlowEntry) rule, (k, stored) -> {
+                        if (rule instanceof DefaultFlowEntry) {
+                            DefaultFlowEntry toRemove = (DefaultFlowEntry) rule;
+                            if (stored instanceof DefaultFlowEntry) {
+                                DefaultFlowEntry storedEntry = (DefaultFlowEntry) stored;
+                                if (toRemove.created() < storedEntry.created()) {
+                                    log.debug("Trying to remove more recent flow entry {} (stored: {})",
+                                              toRemove, stored);
+                                    // the key is not updated, removedRule remains null
+                                    return stored;
+                                }
+                            }
+                        }
+                        removedRule.set(stored);
+                        return null;
+                    });
+
+            if (removedRule.get() != null) {
+                lastUpdateTimes.put(deviceId, System.currentTimeMillis());
+                return removedRule.get();
+            } else {
+                return null;
+            }
+        }
+
+        public void purgeFlowRule(NetworkId networkId, DeviceId deviceId) {
+            Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
+                    flowEntries = getFlowEntriesByNetwork(networkId);
+            flowEntries.remove(deviceId);
+        }
+
+        public void purgeFlowRules(NetworkId networkId) {
+            Map<DeviceId, Map<FlowId, Map<StoredFlowEntry, StoredFlowEntry>>>
+                    flowEntries = getFlowEntriesByNetwork(networkId);
+            flowEntries.clear();
+        }
+    }
+
+    private class InternalTableStatsListener
+            implements EventuallyConsistentMapListener<NetworkId, Map<DeviceId, List<TableStatisticsEntry>>> {
+
+        @Override
+        public void event(EventuallyConsistentMapEvent<NetworkId, Map<DeviceId,
+                List<TableStatisticsEntry>>> event) {
+            //TODO: Generate an event to listeners (do we need?)
+        }
+    }
+
+    public final class MastershipBasedTimestamp implements Timestamp {
+
+        private final long termNumber;
+        private final long sequenceNumber;
+
+        /**
+         * Default constructor for serialization.
+         */
+        protected MastershipBasedTimestamp() {
+            this.termNumber = -1;
+            this.sequenceNumber = -1;
+        }
+
+        /**
+         * Default version tuple.
+         *
+         * @param termNumber the mastership termNumber
+         * @param sequenceNumber  the sequenceNumber number within the termNumber
+         */
+        public MastershipBasedTimestamp(long termNumber, long sequenceNumber) {
+            this.termNumber = termNumber;
+            this.sequenceNumber = sequenceNumber;
+        }
+
+        @Override
+        public int compareTo(Timestamp o) {
+            checkArgument(o instanceof MastershipBasedTimestamp,
+                          "Must be MastershipBasedTimestamp", o);
+            MastershipBasedTimestamp that = (MastershipBasedTimestamp) o;
+
+            return ComparisonChain.start()
+                    .compare(this.termNumber, that.termNumber)
+                    .compare(this.sequenceNumber, that.sequenceNumber)
+                    .result();
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(termNumber, sequenceNumber);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof MastershipBasedTimestamp)) {
+                return false;
+            }
+            MastershipBasedTimestamp that = (MastershipBasedTimestamp) obj;
+            return Objects.equals(this.termNumber, that.termNumber) &&
+                    Objects.equals(this.sequenceNumber, that.sequenceNumber);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(getClass())
+                    .add("termNumber", termNumber)
+                    .add("sequenceNumber", sequenceNumber)
+                    .toString();
+        }
+
+        /**
+         * Returns the termNumber.
+         *
+         * @return termNumber
+         */
+        public long termNumber() {
+            return termNumber;
+        }
+
+        /**
+         * Returns the sequenceNumber number.
+         *
+         * @return sequenceNumber
+         */
+        public long sequenceNumber() {
+            return sequenceNumber;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualNetworkStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualNetworkStore.java
new file mode 100644
index 0000000..b756c62
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualNetworkStore.java
@@ -0,0 +1,954 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.incubator.net.virtual.store.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.IdGenerator;
+import org.onosproject.incubator.net.tunnel.TunnelId;
+import org.onosproject.incubator.net.virtual.DefaultVirtualDevice;
+import org.onosproject.incubator.net.virtual.DefaultVirtualHost;
+import org.onosproject.incubator.net.virtual.DefaultVirtualLink;
+import org.onosproject.incubator.net.virtual.DefaultVirtualNetwork;
+import org.onosproject.incubator.net.virtual.DefaultVirtualPort;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.TenantId;
+import org.onosproject.incubator.net.virtual.VirtualDevice;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.incubator.net.virtual.VirtualLink;
+import org.onosproject.incubator.net.virtual.VirtualNetwork;
+import org.onosproject.incubator.net.virtual.VirtualNetworkEvent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntent;
+import org.onosproject.incubator.net.virtual.VirtualNetworkService;
+import org.onosproject.incubator.net.virtual.VirtualNetworkStore;
+import org.onosproject.incubator.net.virtual.VirtualNetworkStoreDelegate;
+import org.onosproject.incubator.net.virtual.VirtualPort;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.Key;
+import org.onosproject.store.AbstractStore;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.DistributedSet;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.SetEvent;
+import org.onosproject.store.service.SetEventListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the virtual network store.
+ */
+@Component(immediate = true, service = VirtualNetworkStore.class)
+public class DistributedVirtualNetworkStore
+        extends AbstractStore<VirtualNetworkEvent, VirtualNetworkStoreDelegate>
+        implements VirtualNetworkStore {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    private IdGenerator idGenerator;
+
+    // Track tenants by ID
+    private DistributedSet<TenantId> tenantIdSet;
+
+    // Listener for tenant events
+    private final SetEventListener<TenantId> setListener = new InternalSetListener();
+
+    // Track virtual networks by network Id
+    private ConsistentMap<NetworkId, VirtualNetwork> networkIdVirtualNetworkConsistentMap;
+    private Map<NetworkId, VirtualNetwork> networkIdVirtualNetworkMap;
+
+    // Listener for virtual network events
+    private final MapEventListener<NetworkId, VirtualNetwork> virtualNetworkMapListener =
+            new InternalMapListener<>((mapEventType, virtualNetwork) -> {
+                VirtualNetworkEvent.Type eventType =
+                    mapEventType.equals(MapEvent.Type.INSERT)
+                            ? VirtualNetworkEvent.Type.NETWORK_ADDED :
+                    mapEventType.equals(MapEvent.Type.UPDATE)
+                            ? VirtualNetworkEvent.Type.NETWORK_UPDATED :
+                    mapEventType.equals(MapEvent.Type.REMOVE)
+                            ? VirtualNetworkEvent.Type.NETWORK_REMOVED : null;
+                return eventType == null ? null : new VirtualNetworkEvent(eventType, virtualNetwork.id());
+            });
+
+    // Listener for virtual device events
+    private final MapEventListener<VirtualDeviceId, VirtualDevice> virtualDeviceMapListener =
+            new InternalMapListener<>((mapEventType, virtualDevice) -> {
+                VirtualNetworkEvent.Type eventType =
+                        mapEventType.equals(MapEvent.Type.INSERT)
+                                ? VirtualNetworkEvent.Type.VIRTUAL_DEVICE_ADDED :
+                        mapEventType.equals(MapEvent.Type.UPDATE)
+                                ? VirtualNetworkEvent.Type.VIRTUAL_DEVICE_UPDATED :
+                        mapEventType.equals(MapEvent.Type.REMOVE)
+                                ? VirtualNetworkEvent.Type.VIRTUAL_DEVICE_REMOVED : null;
+                return eventType == null ? null :
+                        new VirtualNetworkEvent(eventType, virtualDevice.networkId(), virtualDevice);
+            });
+
+    // Track virtual network IDs by tenant Id
+    private ConsistentMap<TenantId, Set<NetworkId>> tenantIdNetworkIdSetConsistentMap;
+    private Map<TenantId, Set<NetworkId>> tenantIdNetworkIdSetMap;
+
+    // Track virtual devices by device Id
+    private ConsistentMap<VirtualDeviceId, VirtualDevice> deviceIdVirtualDeviceConsistentMap;
+    private Map<VirtualDeviceId, VirtualDevice> deviceIdVirtualDeviceMap;
+
+    // Track device IDs by network Id
+    private ConsistentMap<NetworkId, Set<DeviceId>> networkIdDeviceIdSetConsistentMap;
+    private Map<NetworkId, Set<DeviceId>> networkIdDeviceIdSetMap;
+
+    // Track virtual hosts by host Id
+    private ConsistentMap<HostId, VirtualHost> hostIdVirtualHostConsistentMap;
+    private Map<HostId, VirtualHost> hostIdVirtualHostMap;
+
+    // Track host IDs by network Id
+    private ConsistentMap<NetworkId, Set<HostId>> networkIdHostIdSetConsistentMap;
+    private Map<NetworkId, Set<HostId>> networkIdHostIdSetMap;
+
+    // Track virtual links by network Id
+    private ConsistentMap<NetworkId, Set<VirtualLink>> networkIdVirtualLinkSetConsistentMap;
+    private Map<NetworkId, Set<VirtualLink>> networkIdVirtualLinkSetMap;
+
+    // Track virtual ports by network Id
+    private ConsistentMap<NetworkId, Set<VirtualPort>> networkIdVirtualPortSetConsistentMap;
+    private Map<NetworkId, Set<VirtualPort>> networkIdVirtualPortSetMap;
+
+    // Track intent ID to TunnelIds
+    private ConsistentMap<Key, Set<TunnelId>> intentKeyTunnelIdSetConsistentMap;
+    private Map<Key, Set<TunnelId>> intentKeyTunnelIdSetMap;
+
+    private static final Serializer SERIALIZER = Serializer
+            .using(new KryoNamespace.Builder().register(KryoNamespaces.API)
+                           .register(TenantId.class)
+                           .register(NetworkId.class)
+                           .register(VirtualNetwork.class)
+                           .register(DefaultVirtualNetwork.class)
+                           .register(VirtualDevice.class)
+                           .register(VirtualDeviceId.class)
+                           .register(DefaultVirtualDevice.class)
+                           .register(VirtualHost.class)
+                           .register(DefaultVirtualHost.class)
+                           .register(VirtualLink.class)
+                           .register(DefaultVirtualLink.class)
+                           .register(VirtualPort.class)
+                           .register(DefaultVirtualPort.class)
+                           .register(Device.class)
+                           .register(TunnelId.class)
+                           .register(VirtualNetworkIntent.class)
+                           .register(WallClockTimestamp.class)
+                           .nextId(KryoNamespaces.BEGIN_USER_CUSTOM_ID)
+                           .build());
+
+    /**
+     * Distributed network store service activate method.
+     */
+    @Activate
+    public void activate() {
+        idGenerator = coreService.getIdGenerator(VirtualNetworkService.VIRTUAL_NETWORK_TOPIC);
+
+        tenantIdSet = storageService.<TenantId>setBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-tenantId")
+                .withRelaxedReadConsistency()
+                .build()
+                .asDistributedSet();
+        tenantIdSet.addListener(setListener);
+
+        networkIdVirtualNetworkConsistentMap = storageService.<NetworkId, VirtualNetwork>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-networkId-virtualnetwork")
+                .withRelaxedReadConsistency()
+                .build();
+        networkIdVirtualNetworkConsistentMap.addListener(virtualNetworkMapListener);
+        networkIdVirtualNetworkMap = networkIdVirtualNetworkConsistentMap.asJavaMap();
+
+        tenantIdNetworkIdSetConsistentMap = storageService.<TenantId, Set<NetworkId>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-tenantId-networkIds")
+                .withRelaxedReadConsistency()
+                .build();
+        tenantIdNetworkIdSetMap = tenantIdNetworkIdSetConsistentMap.asJavaMap();
+
+        deviceIdVirtualDeviceConsistentMap = storageService.<VirtualDeviceId, VirtualDevice>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-deviceId-virtualdevice")
+                .withRelaxedReadConsistency()
+                .build();
+        deviceIdVirtualDeviceConsistentMap.addListener(virtualDeviceMapListener);
+        deviceIdVirtualDeviceMap = deviceIdVirtualDeviceConsistentMap.asJavaMap();
+
+        networkIdDeviceIdSetConsistentMap = storageService.<NetworkId, Set<DeviceId>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-networkId-deviceIds")
+                .withRelaxedReadConsistency()
+                .build();
+        networkIdDeviceIdSetMap = networkIdDeviceIdSetConsistentMap.asJavaMap();
+
+        hostIdVirtualHostConsistentMap = storageService.<HostId, VirtualHost>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-hostId-virtualhost")
+                .withRelaxedReadConsistency()
+                .build();
+        hostIdVirtualHostMap = hostIdVirtualHostConsistentMap.asJavaMap();
+
+        networkIdHostIdSetConsistentMap = storageService.<NetworkId, Set<HostId>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-networkId-hostIds")
+                .withRelaxedReadConsistency()
+                .build();
+        networkIdHostIdSetMap = networkIdHostIdSetConsistentMap.asJavaMap();
+
+        networkIdVirtualLinkSetConsistentMap = storageService.<NetworkId, Set<VirtualLink>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-networkId-virtuallinks")
+                .withRelaxedReadConsistency()
+                .build();
+        networkIdVirtualLinkSetMap = networkIdVirtualLinkSetConsistentMap.asJavaMap();
+
+        networkIdVirtualPortSetConsistentMap = storageService.<NetworkId, Set<VirtualPort>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-networkId-virtualports")
+                .withRelaxedReadConsistency()
+                .build();
+        networkIdVirtualPortSetMap = networkIdVirtualPortSetConsistentMap.asJavaMap();
+
+        intentKeyTunnelIdSetConsistentMap = storageService.<Key, Set<TunnelId>>consistentMapBuilder()
+                .withSerializer(SERIALIZER)
+                .withName("onos-intentKey-tunnelIds")
+                .withRelaxedReadConsistency()
+                .build();
+        intentKeyTunnelIdSetMap = intentKeyTunnelIdSetConsistentMap.asJavaMap();
+
+        log.info("Started");
+    }
+
+    /**
+     * Distributed network store service deactivate method.
+     */
+    @Deactivate
+    public void deactivate() {
+        tenantIdSet.removeListener(setListener);
+        networkIdVirtualNetworkConsistentMap.removeListener(virtualNetworkMapListener);
+        deviceIdVirtualDeviceConsistentMap.removeListener(virtualDeviceMapListener);
+        log.info("Stopped");
+    }
+
+    @Override
+    public void addTenantId(TenantId tenantId) {
+        tenantIdSet.add(tenantId);
+    }
+
+    @Override
+    public void removeTenantId(TenantId tenantId) {
+        //Remove all the virtual networks of this tenant
+        Set<VirtualNetwork> networkIdSet = getNetworks(tenantId);
+        if (networkIdSet != null) {
+            networkIdSet.forEach(virtualNetwork -> removeNetwork(virtualNetwork.id()));
+        }
+
+        tenantIdSet.remove(tenantId);
+    }
+
+    @Override
+    public Set<TenantId> getTenantIds() {
+        return ImmutableSet.copyOf(tenantIdSet);
+    }
+
+    @Override
+    public VirtualNetwork addNetwork(TenantId tenantId) {
+
+        checkState(tenantIdSet.contains(tenantId), "The tenant has not been registered. " + tenantId.id());
+        VirtualNetwork virtualNetwork = new DefaultVirtualNetwork(genNetworkId(), tenantId);
+        //TODO update both maps in one transaction.
+        networkIdVirtualNetworkMap.put(virtualNetwork.id(), virtualNetwork);
+
+        Set<NetworkId> networkIdSet = tenantIdNetworkIdSetMap.get(tenantId);
+        if (networkIdSet == null) {
+            networkIdSet = new HashSet<>();
+        }
+        networkIdSet.add(virtualNetwork.id());
+        tenantIdNetworkIdSetMap.put(tenantId, networkIdSet);
+
+        return virtualNetwork;
+    }
+
+    /**
+     * Returns a new network identifier from a virtual network block of identifiers.
+     *
+     * @return NetworkId network identifier
+     */
+    private NetworkId genNetworkId() {
+        NetworkId networkId;
+        do {
+            networkId = NetworkId.networkId(idGenerator.getNewId());
+        } while (!networkId.isVirtualNetworkId());
+
+        return networkId;
+    }
+
+    @Override
+    public void removeNetwork(NetworkId networkId) {
+        // Make sure that the virtual network exists before attempting to remove it.
+        checkState(networkExists(networkId), "The network does not exist.");
+
+        //Remove all the devices of this network
+        Set<VirtualDevice> deviceSet = getDevices(networkId);
+        if (deviceSet != null) {
+            deviceSet.forEach(virtualDevice -> removeDevice(networkId, virtualDevice.id()));
+        }
+        //TODO update both maps in one transaction.
+
+        VirtualNetwork virtualNetwork = networkIdVirtualNetworkMap.remove(networkId);
+        if (virtualNetwork == null) {
+            return;
+        }
+        TenantId tenantId = virtualNetwork.tenantId();
+
+        Set<NetworkId> networkIdSet = new HashSet<>();
+        tenantIdNetworkIdSetMap.get(tenantId).forEach(networkId1 -> {
+            if (networkId1.id().equals(networkId.id())) {
+                networkIdSet.add(networkId1);
+            }
+        });
+
+        tenantIdNetworkIdSetMap.compute(virtualNetwork.tenantId(), (id, existingNetworkIds) -> {
+            if (existingNetworkIds == null || existingNetworkIds.isEmpty()) {
+                return new HashSet<>();
+            } else {
+                return new HashSet<>(Sets.difference(existingNetworkIds, networkIdSet));
+            }
+        });
+    }
+
+    /**
+     * Returns if the network identifier exists.
+     *
+     * @param networkId network identifier
+     * @return true if the network identifier exists, false otherwise.
+     */
+    private boolean networkExists(NetworkId networkId) {
+        checkNotNull(networkId, "The network identifier cannot be null.");
+        return (networkIdVirtualNetworkMap.containsKey(networkId));
+    }
+
+    @Override
+    public VirtualDevice addDevice(NetworkId networkId, DeviceId deviceId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+
+        Set<DeviceId> deviceIdSet = networkIdDeviceIdSetMap.get(networkId);
+        if (deviceIdSet == null) {
+            deviceIdSet = new HashSet<>();
+        }
+
+        checkState(!deviceIdSet.contains(deviceId), "The device already exists.");
+
+        VirtualDevice virtualDevice = new DefaultVirtualDevice(networkId, deviceId);
+        //TODO update both maps in one transaction.
+        deviceIdVirtualDeviceMap.put(new VirtualDeviceId(networkId, deviceId), virtualDevice);
+        deviceIdSet.add(deviceId);
+        networkIdDeviceIdSetMap.put(networkId, deviceIdSet);
+        return virtualDevice;
+    }
+
+    @Override
+    public void removeDevice(NetworkId networkId, DeviceId deviceId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        //Remove all the virtual ports of the this device
+        Set<VirtualPort> virtualPorts = getPorts(networkId, deviceId);
+        if (virtualPorts != null) {
+            virtualPorts.forEach(virtualPort -> removePort(networkId, deviceId, virtualPort.number()));
+        }
+        //TODO update both maps in one transaction.
+
+        Set<DeviceId> deviceIdSet = new HashSet<>();
+        networkIdDeviceIdSetMap.get(networkId).forEach(deviceId1 -> {
+            if (deviceId1.equals(deviceId)) {
+                deviceIdSet.add(deviceId1);
+            }
+        });
+
+        if (!deviceIdSet.isEmpty()) {
+            networkIdDeviceIdSetMap.compute(networkId, (id, existingDeviceIds) -> {
+                if (existingDeviceIds == null || existingDeviceIds.isEmpty()) {
+                    return new HashSet<>();
+                } else {
+                    return new HashSet<>(Sets.difference(existingDeviceIds, deviceIdSet));
+                }
+            });
+
+            deviceIdVirtualDeviceMap.remove(new VirtualDeviceId(networkId, deviceId));
+        }
+    }
+
+    @Override
+    public VirtualHost addHost(NetworkId networkId, HostId hostId, MacAddress mac,
+                               VlanId vlan, HostLocation location, Set<IpAddress> ips) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        checkState(virtualPortExists(networkId, location.deviceId(), location.port()),
+                "The virtual port has not been created.");
+        Set<HostId> hostIdSet = networkIdHostIdSetMap.get(networkId);
+        if (hostIdSet == null) {
+            hostIdSet = new HashSet<>();
+        }
+        VirtualHost virtualhost = new DefaultVirtualHost(networkId, hostId, mac, vlan, location, ips);
+        //TODO update both maps in one transaction.
+        hostIdVirtualHostMap.put(hostId, virtualhost);
+        hostIdSet.add(hostId);
+        networkIdHostIdSetMap.put(networkId, hostIdSet);
+        return virtualhost;
+    }
+
+    @Override
+    public void removeHost(NetworkId networkId, HostId hostId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        //TODO update both maps in one transaction.
+
+        Set<HostId> hostIdSet = new HashSet<>();
+        networkIdHostIdSetMap.get(networkId).forEach(hostId1 -> {
+            if (hostId1.equals(hostId)) {
+                hostIdSet.add(hostId1);
+            }
+        });
+
+        networkIdHostIdSetMap.compute(networkId, (id, existingHostIds) -> {
+            if (existingHostIds == null || existingHostIds.isEmpty()) {
+                return new HashSet<>();
+            } else {
+                return new HashSet<>(Sets.difference(existingHostIds, hostIdSet));
+            }
+        });
+
+        hostIdVirtualHostMap.remove(hostId);
+    }
+
+    /**
+     * Returns if the given virtual port exists.
+     *
+     * @param networkId network identifier
+     * @param deviceId virtual device Id
+     * @param portNumber virtual port number
+     * @return true if the virtual port exists, false otherwise.
+     */
+    private boolean virtualPortExists(NetworkId networkId, DeviceId deviceId, PortNumber portNumber) {
+        Set<VirtualPort> virtualPortSet = networkIdVirtualPortSetMap.get(networkId);
+        if (virtualPortSet != null) {
+            return virtualPortSet.stream().anyMatch(
+                    p -> p.element().id().equals(deviceId) &&
+                            p.number().equals(portNumber));
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public VirtualLink addLink(NetworkId networkId, ConnectPoint src, ConnectPoint dst,
+                               Link.State state, TunnelId realizedBy) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        checkState(virtualPortExists(networkId, src.deviceId(), src.port()),
+                "The source virtual port has not been added.");
+        checkState(virtualPortExists(networkId, dst.deviceId(), dst.port()),
+                "The destination virtual port has not been added.");
+        Set<VirtualLink> virtualLinkSet = networkIdVirtualLinkSetMap.get(networkId);
+        if (virtualLinkSet == null) {
+            virtualLinkSet = new HashSet<>();
+        }
+
+        // validate that the link does not already exist in this network
+        checkState(getLink(networkId, src, dst) == null,
+                "The virtual link already exists");
+        checkState(getLink(networkId, src, null) == null,
+                "The source connection point has been used by another link");
+        checkState(getLink(networkId, null, dst) == null,
+                "The destination connection point has been used by another link");
+
+        VirtualLink virtualLink = DefaultVirtualLink.builder()
+                .networkId(networkId)
+                .src(src)
+                .dst(dst)
+                .state(state)
+                .tunnelId(realizedBy)
+                .build();
+
+        virtualLinkSet.add(virtualLink);
+        networkIdVirtualLinkSetMap.put(networkId, virtualLinkSet);
+        return virtualLink;
+    }
+
+    @Override
+    public void updateLink(VirtualLink virtualLink, TunnelId tunnelId, Link.State state) {
+        checkState(networkExists(virtualLink.networkId()), "The network has not been added.");
+        Set<VirtualLink> virtualLinkSet = networkIdVirtualLinkSetMap.get(virtualLink.networkId());
+        if (virtualLinkSet == null) {
+            virtualLinkSet = new HashSet<>();
+            networkIdVirtualLinkSetMap.put(virtualLink.networkId(), virtualLinkSet);
+            log.warn("The updated virtual link {} has not been added", virtualLink);
+            return;
+        }
+        if (!virtualLinkSet.remove(virtualLink)) {
+            log.warn("The updated virtual link {} does not exist", virtualLink);
+            return;
+        }
+
+        VirtualLink newVirtualLink = DefaultVirtualLink.builder()
+                .networkId(virtualLink.networkId())
+                .src(virtualLink.src())
+                .dst(virtualLink.dst())
+                .tunnelId(tunnelId)
+                .state(state)
+                .build();
+
+        virtualLinkSet.add(newVirtualLink);
+        networkIdVirtualLinkSetMap.put(newVirtualLink.networkId(), virtualLinkSet);
+    }
+
+    @Override
+    public VirtualLink removeLink(NetworkId networkId, ConnectPoint src, ConnectPoint dst) {
+        checkState(networkExists(networkId), "The network has not been added.");
+
+        final VirtualLink virtualLink = getLink(networkId, src, dst);
+        if (virtualLink == null) {
+            log.warn("The removed virtual link between {} and {} does not exist", src, dst);
+            return null;
+        }
+        Set<VirtualLink> virtualLinkSet = new HashSet<>();
+        virtualLinkSet.add(virtualLink);
+
+        networkIdVirtualLinkSetMap.compute(networkId, (id, existingVirtualLinks) -> {
+            if (existingVirtualLinks == null || existingVirtualLinks.isEmpty()) {
+                return new HashSet<>();
+            } else {
+                return new HashSet<>(Sets.difference(existingVirtualLinks, virtualLinkSet));
+            }
+        });
+        return virtualLink;
+    }
+
+    @Override
+    public VirtualPort addPort(NetworkId networkId, DeviceId deviceId,
+                               PortNumber portNumber, ConnectPoint realizedBy) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        Set<VirtualPort> virtualPortSet = networkIdVirtualPortSetMap.get(networkId);
+
+        if (virtualPortSet == null) {
+            virtualPortSet = new HashSet<>();
+        }
+
+        VirtualDevice device = deviceIdVirtualDeviceMap.get(new VirtualDeviceId(networkId, deviceId));
+        checkNotNull(device, "The device has not been created for deviceId: " + deviceId);
+
+        checkState(!virtualPortExists(networkId, deviceId, portNumber),
+                "The requested Port Number has been added.");
+
+        VirtualPort virtualPort = new DefaultVirtualPort(networkId, device,
+                                                         portNumber, realizedBy);
+        virtualPortSet.add(virtualPort);
+        networkIdVirtualPortSetMap.put(networkId, virtualPortSet);
+        notifyDelegate(new VirtualNetworkEvent(VirtualNetworkEvent.Type.VIRTUAL_PORT_ADDED,
+                                               networkId, device, virtualPort));
+        return virtualPort;
+    }
+
+    @Override
+    public void bindPort(NetworkId networkId, DeviceId deviceId,
+                         PortNumber portNumber, ConnectPoint realizedBy) {
+
+        Set<VirtualPort> virtualPortSet = networkIdVirtualPortSetMap
+                .get(networkId);
+
+        Optional<VirtualPort> virtualPortOptional = virtualPortSet.stream().filter(
+                p -> p.element().id().equals(deviceId) &&
+                        p.number().equals(portNumber)).findFirst();
+        checkState(virtualPortOptional.isPresent(), "The virtual port has not been added.");
+
+        VirtualDevice device = deviceIdVirtualDeviceMap.get(new VirtualDeviceId(networkId, deviceId));
+        checkNotNull(device, "The device has not been created for deviceId: "
+                + deviceId);
+
+        VirtualPort vPort = virtualPortOptional.get();
+        virtualPortSet.remove(vPort);
+        vPort = new DefaultVirtualPort(networkId, device, portNumber, realizedBy);
+        virtualPortSet.add(vPort);
+        networkIdVirtualPortSetMap.put(networkId, virtualPortSet);
+        notifyDelegate(new VirtualNetworkEvent(VirtualNetworkEvent.Type.VIRTUAL_PORT_UPDATED,
+                                               networkId, device, vPort));
+    }
+
+    @Override
+    public void updatePortState(NetworkId networkId, DeviceId deviceId,
+                                PortNumber portNumber, boolean isEnabled) {
+        checkState(networkExists(networkId), "No network with NetworkId %s exists.", networkId);
+
+        VirtualDevice device = deviceIdVirtualDeviceMap.get(new VirtualDeviceId(networkId, deviceId));
+        checkNotNull(device, "No device %s exists in NetworkId: %s", deviceId, networkId);
+
+        Set<VirtualPort> virtualPortSet = networkIdVirtualPortSetMap.get(networkId);
+        checkNotNull(virtualPortSet, "No port has been created for NetworkId: %s", networkId);
+
+        Optional<VirtualPort> virtualPortOptional = virtualPortSet.stream().filter(
+                p -> p.element().id().equals(deviceId) &&
+                        p.number().equals(portNumber)).findFirst();
+        checkState(virtualPortOptional.isPresent(), "The virtual port has not been added.");
+
+        VirtualPort oldPort = virtualPortOptional.get();
+        if (oldPort.isEnabled() == isEnabled) {
+            log.debug("No change in port state - port not updated");
+            return;
+        }
+        VirtualPort newPort = new DefaultVirtualPort(networkId, device, portNumber, isEnabled,
+                oldPort.realizedBy());
+        virtualPortSet.remove(oldPort);
+        virtualPortSet.add(newPort);
+        networkIdVirtualPortSetMap.put(networkId, virtualPortSet);
+        notifyDelegate(new VirtualNetworkEvent(VirtualNetworkEvent.Type.VIRTUAL_PORT_UPDATED,
+                                               networkId, device, newPort));
+        log.debug("port state changed from {} to {}", oldPort.isEnabled(), isEnabled);
+    }
+
+    @Override
+    public void removePort(NetworkId networkId, DeviceId deviceId, PortNumber portNumber) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        VirtualDevice device = deviceIdVirtualDeviceMap.get(new VirtualDeviceId(networkId, deviceId));
+        checkNotNull(device, "The device has not been created for deviceId: "
+                + deviceId);
+
+        if (networkIdVirtualPortSetMap.get(networkId) == null) {
+            log.warn("No port has been created for NetworkId: {}", networkId);
+            return;
+        }
+
+        Set<VirtualPort> virtualPortSet = new HashSet<>();
+        networkIdVirtualPortSetMap.get(networkId).forEach(port -> {
+            if (port.element().id().equals(deviceId) && port.number().equals(portNumber)) {
+                virtualPortSet.add(port);
+            }
+        });
+
+        if (!virtualPortSet.isEmpty()) {
+            AtomicBoolean portRemoved = new AtomicBoolean(false);
+            networkIdVirtualPortSetMap.compute(networkId, (id, existingVirtualPorts) -> {
+                if (existingVirtualPorts == null || existingVirtualPorts.isEmpty()) {
+                    return new HashSet<>();
+                } else {
+                    portRemoved.set(true);
+                    return new HashSet<>(Sets.difference(existingVirtualPorts, virtualPortSet));
+                }
+            });
+            if (portRemoved.get()) {
+                virtualPortSet.forEach(virtualPort -> notifyDelegate(
+                        new VirtualNetworkEvent(VirtualNetworkEvent.Type.VIRTUAL_PORT_REMOVED,
+                                                networkId, device, virtualPort)
+                ));
+
+                //Remove all the virtual links connected to this virtual port
+                Set<VirtualLink> existingVirtualLinks = networkIdVirtualLinkSetMap.get(networkId);
+                if (existingVirtualLinks != null && !existingVirtualLinks.isEmpty()) {
+                    Set<VirtualLink> virtualLinkSet = new HashSet<>();
+                    ConnectPoint cp = new ConnectPoint(deviceId, portNumber);
+                    existingVirtualLinks.forEach(virtualLink -> {
+                        if (virtualLink.src().equals(cp) || virtualLink.dst().equals(cp)) {
+                            virtualLinkSet.add(virtualLink);
+                        }
+                    });
+                    virtualLinkSet.forEach(virtualLink ->
+                            removeLink(networkId, virtualLink.src(), virtualLink.dst()));
+                }
+
+                //Remove all the hosts connected to this virtual port
+                Set<HostId> hostIdSet = new HashSet<>();
+                hostIdVirtualHostMap.forEach((hostId, virtualHost) -> {
+                    if (virtualHost.location().deviceId().equals(deviceId) &&
+                            virtualHost.location().port().equals(portNumber)) {
+                        hostIdSet.add(hostId);
+                    }
+                });
+                hostIdSet.forEach(hostId -> removeHost(networkId, hostId));
+            }
+        }
+    }
+
+    @Override
+    public Set<VirtualNetwork> getNetworks(TenantId tenantId) {
+        Set<NetworkId> networkIdSet = tenantIdNetworkIdSetMap.get(tenantId);
+        Set<VirtualNetwork> virtualNetworkSet = new HashSet<>();
+        if (networkIdSet != null) {
+            networkIdSet.forEach(networkId -> {
+                if (networkIdVirtualNetworkMap.get(networkId) != null) {
+                    virtualNetworkSet.add(networkIdVirtualNetworkMap.get(networkId));
+                }
+            });
+        }
+        return ImmutableSet.copyOf(virtualNetworkSet);
+    }
+
+    @Override
+    public VirtualNetwork getNetwork(NetworkId networkId) {
+        return networkIdVirtualNetworkMap.get(networkId);
+    }
+
+    @Override
+    public Set<VirtualDevice> getDevices(NetworkId networkId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        Set<DeviceId> deviceIdSet = networkIdDeviceIdSetMap.get(networkId);
+        Set<VirtualDevice> virtualDeviceSet = new HashSet<>();
+        if (deviceIdSet != null) {
+            deviceIdSet.forEach(deviceId -> virtualDeviceSet.add(
+                    deviceIdVirtualDeviceMap.get(new VirtualDeviceId(networkId, deviceId))));
+        }
+        return ImmutableSet.copyOf(virtualDeviceSet);
+    }
+
+    @Override
+    public Set<VirtualHost> getHosts(NetworkId networkId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        Set<HostId> hostIdSet = networkIdHostIdSetMap.get(networkId);
+        Set<VirtualHost> virtualHostSet = new HashSet<>();
+        if (hostIdSet != null) {
+            hostIdSet.forEach(hostId -> virtualHostSet.add(hostIdVirtualHostMap.get(hostId)));
+        }
+        return ImmutableSet.copyOf(virtualHostSet);
+    }
+
+    @Override
+    public Set<VirtualLink> getLinks(NetworkId networkId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        Set<VirtualLink> virtualLinkSet = networkIdVirtualLinkSetMap.get(networkId);
+        if (virtualLinkSet == null) {
+            virtualLinkSet = new HashSet<>();
+        }
+        return ImmutableSet.copyOf(virtualLinkSet);
+    }
+
+    @Override
+    public VirtualLink getLink(NetworkId networkId, ConnectPoint src, ConnectPoint dst) {
+        Set<VirtualLink> virtualLinkSet = networkIdVirtualLinkSetMap.get(networkId);
+        if (virtualLinkSet == null) {
+            return null;
+        }
+
+        VirtualLink virtualLink = null;
+        for (VirtualLink link : virtualLinkSet) {
+            if (src == null && link.dst().equals(dst)) {
+                virtualLink = link;
+                break;
+            } else if (dst == null && link.src().equals(src)) {
+                virtualLink = link;
+                break;
+            } else if (link.src().equals(src) && link.dst().equals(dst)) {
+                virtualLink = link;
+                break;
+            }
+        }
+        return virtualLink;
+    }
+
+    @Override
+    public Set<VirtualPort> getPorts(NetworkId networkId, DeviceId deviceId) {
+        checkState(networkExists(networkId), "The network has not been added.");
+        Set<VirtualPort> virtualPortSet = networkIdVirtualPortSetMap.get(networkId);
+        if (virtualPortSet == null) {
+            virtualPortSet = new HashSet<>();
+        }
+
+        if (deviceId == null) {
+            return ImmutableSet.copyOf(virtualPortSet);
+        }
+
+        Set<VirtualPort> portSet = new HashSet<>();
+        virtualPortSet.forEach(virtualPort -> {
+            if (virtualPort.element().id().equals(deviceId)) {
+                portSet.add(virtualPort);
+            }
+        });
+        return ImmutableSet.copyOf(portSet);
+    }
+
+    @Override
+    public void addTunnelId(Intent intent, TunnelId tunnelId) {
+        // Add the tunnelId to the intent key set map
+        Set<TunnelId> tunnelIdSet = intentKeyTunnelIdSetMap.remove(intent.key());
+        if (tunnelIdSet == null) {
+            tunnelIdSet = new HashSet<>();
+        }
+        tunnelIdSet.add(tunnelId);
+        intentKeyTunnelIdSetMap.put(intent.key(), tunnelIdSet);
+    }
+
+    @Override
+    public Set<TunnelId> getTunnelIds(Intent intent) {
+        Set<TunnelId> tunnelIdSet = intentKeyTunnelIdSetMap.get(intent.key());
+        return tunnelIdSet == null ? new HashSet<TunnelId>() : ImmutableSet.copyOf(tunnelIdSet);
+    }
+
+    @Override
+    public void removeTunnelId(Intent intent, TunnelId tunnelId) {
+        Set<TunnelId> tunnelIdSet = new HashSet<>();
+        intentKeyTunnelIdSetMap.get(intent.key()).forEach(tunnelId1 -> {
+            if (tunnelId1.equals(tunnelId)) {
+                tunnelIdSet.add(tunnelId);
+            }
+        });
+
+        if (!tunnelIdSet.isEmpty()) {
+            intentKeyTunnelIdSetMap.compute(intent.key(), (key, existingTunnelIds) -> {
+                if (existingTunnelIds == null || existingTunnelIds.isEmpty()) {
+                    return new HashSet<>();
+                } else {
+                    return new HashSet<>(Sets.difference(existingTunnelIds, tunnelIdSet));
+                }
+            });
+        }
+    }
+
+    /**
+     * Listener class to map listener set events to the virtual network events.
+     */
+    private class InternalSetListener implements SetEventListener<TenantId> {
+        @Override
+        public void event(SetEvent<TenantId> event) {
+            VirtualNetworkEvent.Type type = null;
+            switch (event.type()) {
+                case ADD:
+                    type = VirtualNetworkEvent.Type.TENANT_REGISTERED;
+                    break;
+                case REMOVE:
+                    type = VirtualNetworkEvent.Type.TENANT_UNREGISTERED;
+                    break;
+                default:
+                    log.error("Unsupported event type: " + event.type());
+            }
+            notifyDelegate(new VirtualNetworkEvent(type, null));
+        }
+    }
+
+    /**
+     * Listener class to map listener map events to the virtual network events.
+     */
+    private class InternalMapListener<K, V> implements MapEventListener<K, V> {
+
+        private final BiFunction<MapEvent.Type, V, VirtualNetworkEvent> createEvent;
+
+        InternalMapListener(BiFunction<MapEvent.Type, V, VirtualNetworkEvent> createEvent) {
+            this.createEvent = createEvent;
+        }
+
+        @Override
+        public void event(MapEvent<K, V> event) {
+            checkNotNull(event.key());
+            VirtualNetworkEvent vnetEvent = null;
+            switch (event.type()) {
+                case INSERT:
+                    vnetEvent = createEvent.apply(event.type(), event.newValue().value());
+                    break;
+                case UPDATE:
+                    if ((event.oldValue().value() != null) && (event.newValue().value() == null)) {
+                        vnetEvent = createEvent.apply(MapEvent.Type.REMOVE, event.oldValue().value());
+                    } else {
+                        vnetEvent = createEvent.apply(event.type(), event.newValue().value());
+                    }
+                    break;
+                case REMOVE:
+                    if (event.oldValue() != null) {
+                        vnetEvent = createEvent.apply(event.type(), event.oldValue().value());
+                    }
+                    break;
+                default:
+                    log.error("Unsupported event type: " + event.type());
+            }
+            if (vnetEvent != null) {
+                notifyDelegate(vnetEvent);
+            }
+        }
+    }
+
+    /**
+     * A wrapper class to isolate device id from other virtual networks.
+     */
+
+    private static class VirtualDeviceId {
+
+        NetworkId networkId;
+        DeviceId deviceId;
+
+        public VirtualDeviceId(NetworkId networkId, DeviceId deviceId) {
+            this.networkId = networkId;
+            this.deviceId = deviceId;
+        }
+
+        public NetworkId getNetworkId() {
+            return networkId;
+        }
+
+        public DeviceId getDeviceId() {
+            return deviceId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(networkId, deviceId);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (obj instanceof VirtualDeviceId) {
+                VirtualDeviceId that = (VirtualDeviceId) obj;
+                return this.deviceId.equals(that.deviceId) &&
+                        this.networkId.equals(that.networkId);
+            }
+            return false;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualPacketStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualPacketStore.java
new file mode 100644
index 0000000..16636a5
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/DistributedVirtualPacketStore.java
@@ -0,0 +1,413 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkPacketStore;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketEvent;
+import org.onosproject.net.packet.PacketPriority;
+import org.onosproject.net.packet.PacketRequest;
+import org.onosproject.net.packet.PacketStoreDelegate;
+import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
+import org.onosproject.store.cluster.messaging.MessageSubject;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static org.onlab.util.Tools.get;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.onosproject.incubator.net.virtual.store.impl.OsgiPropertyConstants.MESSAGE_HANDLER_THREAD_POOL_SIZE;
+import static org.onosproject.incubator.net.virtual.store.impl.OsgiPropertyConstants.MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT;
+import static org.onosproject.net.packet.PacketEvent.Type.EMIT;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Distributed virtual packet store implementation allowing packets to be sent to
+ * remote instances.  Implementation is based on DistributedPacketStore class.
+ */
+@Component(immediate = true, enabled = false, service = VirtualNetworkPacketStore.class,
+        property = {
+                 MESSAGE_HANDLER_THREAD_POOL_SIZE + ":Integer=" + MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT,
+        })
+public class DistributedVirtualPacketStore
+        extends AbstractVirtualStore<PacketEvent, PacketStoreDelegate>
+        implements VirtualNetworkPacketStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private static final String FORMAT = "Setting: messageHandlerThreadPoolSize={}";
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterCommunicationService communicationService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService cfgService;
+
+    private PacketRequestTracker tracker;
+
+    private static final MessageSubject PACKET_OUT_SUBJECT =
+            new MessageSubject("virtual-packet-out");
+
+    private static final Serializer SERIALIZER = Serializer.using(KryoNamespaces.API);
+
+    private ExecutorService messageHandlingExecutor;
+
+    /** Size of thread pool to assign message handler. */
+    private static int messageHandlerThreadPoolSize = MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT;
+
+    @Activate
+    public void activate(ComponentContext context) {
+        cfgService.registerProperties(getClass());
+
+        modified(context);
+
+        messageHandlingExecutor = Executors.newFixedThreadPool(
+                messageHandlerThreadPoolSize,
+                groupedThreads("onos/store/packet", "message-handlers", log));
+
+        communicationService.<OutboundPacketWrapper>addSubscriber(PACKET_OUT_SUBJECT,
+                SERIALIZER::decode,
+                packetWrapper -> notifyDelegate(packetWrapper.networkId,
+                                                new PacketEvent(EMIT,
+                                                                packetWrapper.outboundPacket)),
+                messageHandlingExecutor);
+
+        tracker = new PacketRequestTracker();
+
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        cfgService.unregisterProperties(getClass(), false);
+        communicationService.removeSubscriber(PACKET_OUT_SUBJECT);
+        messageHandlingExecutor.shutdown();
+        tracker = null;
+        log.info("Stopped");
+    }
+
+    @Modified
+    public void  modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+
+        int newMessageHandlerThreadPoolSize;
+
+        try {
+            String s = get(properties, MESSAGE_HANDLER_THREAD_POOL_SIZE);
+
+            newMessageHandlerThreadPoolSize =
+                    isNullOrEmpty(s) ? messageHandlerThreadPoolSize : Integer.parseInt(s.trim());
+
+        } catch (NumberFormatException e) {
+            log.warn(e.getMessage());
+            newMessageHandlerThreadPoolSize = messageHandlerThreadPoolSize;
+        }
+
+        // Any change in the following parameters implies thread pool restart
+        if (newMessageHandlerThreadPoolSize != messageHandlerThreadPoolSize) {
+            setMessageHandlerThreadPoolSize(newMessageHandlerThreadPoolSize);
+            restartMessageHandlerThreadPool();
+        }
+
+        log.info(FORMAT, messageHandlerThreadPoolSize);
+    }
+
+    @Override
+    public void emit(NetworkId networkId, OutboundPacket packet) {
+        NodeId myId = clusterService.getLocalNode().id();
+        // TODO revive this when there is MastershipService support for virtual devices
+//        NodeId master = mastershipService.getMasterFor(packet.sendThrough());
+//
+//        if (master == null) {
+//            log.warn("No master found for {}", packet.sendThrough());
+//            return;
+//        }
+//
+//        log.debug("master {} found for {}", myId, packet.sendThrough());
+//        if (myId.equals(master)) {
+//            notifyDelegate(networkId, new PacketEvent(EMIT, packet));
+//            return;
+//        }
+//
+//        communicationService.unicast(packet, PACKET_OUT_SUBJECT, SERIALIZER::encode, master)
+//                            .whenComplete((r, error) -> {
+//                                if (error != null) {
+//                                    log.warn("Failed to send packet-out to {}", master, error);
+//                                }
+//                            });
+    }
+
+    @Override
+    public void requestPackets(NetworkId networkId, PacketRequest request) {
+        tracker.add(networkId, request);
+
+    }
+
+    @Override
+    public void cancelPackets(NetworkId networkId, PacketRequest request) {
+        tracker.remove(networkId, request);
+    }
+
+    @Override
+    public List<PacketRequest> existingRequests(NetworkId networkId) {
+        return tracker.requests(networkId);
+    }
+
+    private final class PacketRequestTracker {
+
+        private ConsistentMap<NetworkId, Map<RequestKey, Set<PacketRequest>>> distRequests;
+        private Map<NetworkId, Map<RequestKey, Set<PacketRequest>>> requests;
+
+        private PacketRequestTracker() {
+            distRequests = storageService.<NetworkId, Map<RequestKey, Set<PacketRequest>>>consistentMapBuilder()
+                    .withName("onos-virtual-packet-requests")
+                    .withSerializer(Serializer.using(KryoNamespace.newBuilder()
+                            .register(KryoNamespaces.API)
+                            .register(RequestKey.class)
+                            .register(NetworkId.class)
+                            .build()))
+                    .build();
+            requests = distRequests.asJavaMap();
+        }
+
+        private void add(NetworkId networkId, PacketRequest request) {
+            AtomicBoolean firstRequest = addInternal(networkId, request);
+            PacketStoreDelegate delegate = delegateMap.get(networkId);
+            if (firstRequest.get() && delegate != null) {
+                // The instance that makes the first request will push to all devices
+                delegate.requestPackets(request);
+            }
+        }
+
+        private AtomicBoolean addInternal(NetworkId networkId, PacketRequest request) {
+            AtomicBoolean firstRequest = new AtomicBoolean(false);
+            AtomicBoolean changed = new AtomicBoolean(true);
+            Map<RequestKey, Set<PacketRequest>> requestsForNetwork = getMap(networkId);
+            requestsForNetwork.compute(key(request), (s, existingRequests) -> {
+                // Reset to false just in case this is a retry due to
+                // ConcurrentModificationException
+                firstRequest.set(false);
+                if (existingRequests == null) {
+                    firstRequest.set(true);
+                    return ImmutableSet.of(request);
+                } else if (!existingRequests.contains(request)) {
+                    firstRequest.set(true);
+                    return ImmutableSet.<PacketRequest>builder()
+                                       .addAll(existingRequests)
+                                       .add(request)
+                                       .build();
+                } else {
+                    changed.set(false);
+                    return existingRequests;
+                }
+            });
+            if (changed.get()) {
+                requests.put(networkId, requestsForNetwork);
+            }
+            return firstRequest;
+        }
+
+        private void remove(NetworkId networkId, PacketRequest request) {
+            AtomicBoolean removedLast = removeInternal(networkId, request);
+            PacketStoreDelegate delegate = delegateMap.get(networkId);
+            if (removedLast.get() && delegate != null) {
+                // The instance that removes the last request will remove from all devices
+                delegate.cancelPackets(request);
+            }
+        }
+
+        private AtomicBoolean removeInternal(NetworkId networkId, PacketRequest request) {
+            AtomicBoolean removedLast = new AtomicBoolean(false);
+            AtomicBoolean changed = new AtomicBoolean(true);
+            Map<RequestKey, Set<PacketRequest>> requestsForNetwork = getMap(networkId);
+            requestsForNetwork.computeIfPresent(key(request), (s, existingRequests) -> {
+                // Reset to false just in case this is a retry due to
+                // ConcurrentModificationException
+                removedLast.set(false);
+                if (existingRequests.contains(request)) {
+                    Set<PacketRequest> newRequests = Sets.newHashSet(existingRequests);
+                    newRequests.remove(request);
+                    if (newRequests.size() > 0) {
+                        return ImmutableSet.copyOf(newRequests);
+                    } else {
+                        removedLast.set(true);
+                        return null;
+                    }
+                } else {
+                    changed.set(false);
+                    return existingRequests;
+                }
+            });
+            if (changed.get()) {
+                requests.put(networkId, requestsForNetwork);
+            }
+            return removedLast;
+        }
+
+        private List<PacketRequest> requests(NetworkId networkId) {
+            Map<RequestKey, Set<PacketRequest>> requestsForNetwork = getMap(networkId);
+            List<PacketRequest> list = Lists.newArrayList();
+            requestsForNetwork.values().forEach(v -> list.addAll(v));
+            list.sort((o1, o2) -> o1.priority().priorityValue() - o2.priority().priorityValue());
+            return list;
+        }
+
+        /*
+         * Gets PacketRequests for specified networkId.
+         */
+        private Map<RequestKey, Set<PacketRequest>> getMap(NetworkId networkId) {
+            return requests.computeIfAbsent(networkId, networkId1 -> {
+                        log.debug("Creating new map for {}", networkId1);
+                        Map newMap = Maps.newHashMap();
+                        return newMap;
+                    });
+        }
+    }
+
+    /**
+     * Creates a new request key from a packet request.
+     *
+     * @param request packet request
+     * @return request key
+     */
+    private static RequestKey key(PacketRequest request) {
+        return new RequestKey(request.selector(), request.priority());
+    }
+
+    /**
+     * Key of a packet request.
+     */
+    private static final class RequestKey {
+        private final TrafficSelector selector;
+        private final PacketPriority priority;
+
+        private RequestKey(TrafficSelector selector, PacketPriority priority) {
+            this.selector = selector;
+            this.priority = priority;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(selector, priority);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+
+            if (!(other instanceof RequestKey)) {
+                return false;
+            }
+
+            RequestKey that = (RequestKey) other;
+
+            return Objects.equals(selector, that.selector) &&
+                    Objects.equals(priority, that.priority);
+        }
+    }
+
+    private static OutboundPacketWrapper wrapper(NetworkId networkId, OutboundPacket outboundPacket) {
+        return new OutboundPacketWrapper(networkId, outboundPacket);
+    }
+
+    /*
+     * OutboundPacket in
+     */
+    private static final class OutboundPacketWrapper {
+        private NetworkId networkId;
+        private OutboundPacket outboundPacket;
+
+        private OutboundPacketWrapper(NetworkId networkId, OutboundPacket outboundPacket) {
+            this.networkId = networkId;
+            this.outboundPacket = outboundPacket;
+        }
+
+    }
+
+    /**
+     * Sets thread pool size of message handler.
+     *
+     * @param poolSize
+     */
+    private void setMessageHandlerThreadPoolSize(int poolSize) {
+        checkArgument(poolSize >= 0, "Message handler pool size must be 0 or more");
+        messageHandlerThreadPoolSize = poolSize;
+    }
+
+    /**
+     * Restarts thread pool of message handler.
+     */
+    private void restartMessageHandlerThreadPool() {
+        ExecutorService prevExecutor = messageHandlingExecutor;
+        messageHandlingExecutor = newFixedThreadPool(getMessageHandlerThreadPoolSize(),
+                                                     groupedThreads("DistPktStore", "messageHandling-%d", log));
+        prevExecutor.shutdown();
+    }
+
+    /**
+     * Gets current thread pool size of message handler.
+     *
+     * @return messageHandlerThreadPoolSize
+     */
+    private int getMessageHandlerThreadPoolSize() {
+        return messageHandlerThreadPoolSize;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/MeterData.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/MeterData.java
new file mode 100644
index 0000000..c014505
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/MeterData.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.incubator.net.virtual.store.impl;
+
+import org.onosproject.cluster.NodeId;
+import org.onosproject.net.meter.Meter;
+import org.onosproject.net.meter.MeterFailReason;
+
+import java.util.Optional;
+
+/**
+ * A class representing the meter information stored in the meter store.
+ */
+public class MeterData {
+
+    private final Meter meter;
+    private final Optional<MeterFailReason> reason;
+    private final NodeId origin;
+
+    public MeterData(Meter meter, MeterFailReason reason, NodeId origin) {
+        this.meter = meter;
+        this.reason = Optional.ofNullable(reason);
+        this.origin = origin;
+    }
+
+    public Meter meter() {
+        return meter;
+    }
+
+    public Optional<MeterFailReason> reason() {
+        return this.reason;
+    }
+
+    public NodeId origin() {
+        return this.origin;
+    }
+
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/OsgiPropertyConstants.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/OsgiPropertyConstants.java
new file mode 100644
index 0000000..39bc5b0
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/OsgiPropertyConstants.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.incubator.net.virtual.store.impl;
+
+/**
+ * Constants for default values of configurable properties.
+ */
+public final class OsgiPropertyConstants {
+
+    private OsgiPropertyConstants() {}
+
+    public static final String MESSAGE_HANDLER_THREAD_POOL_SIZE = "messageHandlerThreadPoolSize";
+    public static final int MESSAGE_HANDLER_THREAD_POOL_SIZE_DEFAULT = 4;
+
+    public static final String BACKUP_PERIOD_MILLIS = "backupPeriod";
+    public static final int BACKUP_PERIOD_MILLIS_DEFAULT = 2000;
+
+    public static final String PERSISTENCE_ENABLED = "persistenceEnabled";
+    public static final boolean PERSISTENCE_ENABLED_DEFAULT = false;
+
+    public static final String PENDING_FUTURE_TIMEOUT_MINUTES = "pendingFutureTimeoutMinutes";
+    public static final int PENDING_FUTURE_TIMEOUT_MINUTES_DEFAULT = 5;
+
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowObjectiveStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowObjectiveStore.java
new file mode 100644
index 0000000..4ef268e
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowObjectiveStore.java
@@ -0,0 +1,172 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.Maps;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowObjectiveStore;
+import org.onosproject.net.behaviour.DefaultNextGroup;
+import org.onosproject.net.behaviour.NextGroup;
+import org.onosproject.net.flowobjective.FlowObjectiveStoreDelegate;
+import org.onosproject.net.flowobjective.ObjectiveEvent;
+import org.onosproject.store.service.AtomicCounter;
+import org.onosproject.store.service.StorageService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import static org.onlab.util.Tools.groupedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Single instance implementation of store to manage
+ * the inventory of created next groups for virtual network.
+ */
+@Component(immediate = true, service = VirtualNetworkFlowObjectiveStore.class)
+public class SimpleVirtualFlowObjectiveStore
+        extends AbstractVirtualStore<ObjectiveEvent, FlowObjectiveStoreDelegate>
+        implements VirtualNetworkFlowObjectiveStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private ConcurrentMap<NetworkId, ConcurrentMap<Integer, byte[]>> nextGroupsMap;
+
+    private AtomicCounter nextIds;
+
+    // event queue to separate map-listener threads from event-handler threads (tpool)
+    private BlockingQueue<VirtualObjectiveEvent> eventQ;
+    private ExecutorService tpool;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    @Activate
+    public void activate() {
+        tpool = Executors.newFixedThreadPool(4, groupedThreads("onos/virtual/flobj-notifier", "%d", log));
+        eventQ = new LinkedBlockingQueue<>();
+        tpool.execute(new FlowObjectiveNotifier());
+
+        initNextGroupsMap();
+
+        nextIds = storageService.getAtomicCounter("next-objective-counter");
+        log.info("Started");
+    }
+
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    protected void initNextGroupsMap() {
+        nextGroupsMap = Maps.newConcurrentMap();
+    }
+
+    protected void updateNextGroupsMap(NetworkId networkId,
+                                       ConcurrentMap<Integer, byte[]> nextGroups) {
+    }
+
+    protected ConcurrentMap<Integer, byte[]> getNextGroups(NetworkId networkId) {
+        nextGroupsMap.computeIfAbsent(networkId, n -> Maps.newConcurrentMap());
+        return nextGroupsMap.get(networkId);
+    }
+
+    @Override
+    public void putNextGroup(NetworkId networkId, Integer nextId, NextGroup group) {
+        ConcurrentMap<Integer, byte[]> nextGroups = getNextGroups(networkId);
+        nextGroups.put(nextId, group.data());
+        updateNextGroupsMap(networkId, nextGroups);
+
+        eventQ.add(new VirtualObjectiveEvent(networkId, ObjectiveEvent.Type.ADD, nextId));
+    }
+
+    @Override
+    public NextGroup getNextGroup(NetworkId networkId, Integer nextId) {
+        ConcurrentMap<Integer, byte[]> nextGroups = getNextGroups(networkId);
+        byte[] groupData = nextGroups.get(nextId);
+        if (groupData != null) {
+            return new DefaultNextGroup(groupData);
+        }
+        return null;
+    }
+
+    @Override
+    public NextGroup removeNextGroup(NetworkId networkId, Integer nextId) {
+        ConcurrentMap<Integer, byte[]> nextGroups = getNextGroups(networkId);
+        byte[] nextGroup = nextGroups.remove(nextId);
+        updateNextGroupsMap(networkId, nextGroups);
+
+        eventQ.add(new VirtualObjectiveEvent(networkId, ObjectiveEvent.Type.REMOVE, nextId));
+
+        return new DefaultNextGroup(nextGroup);
+    }
+
+    @Override
+    public Map<Integer, NextGroup> getAllGroups(NetworkId networkId) {
+        ConcurrentMap<Integer, byte[]> nextGroups = getNextGroups(networkId);
+
+        Map<Integer, NextGroup> nextGroupMappings = new HashMap<>();
+        for (int key : nextGroups.keySet()) {
+            NextGroup nextGroup = getNextGroup(networkId, key);
+            if (nextGroup != null) {
+                nextGroupMappings.put(key, nextGroup);
+            }
+        }
+        return nextGroupMappings;
+    }
+
+    @Override
+    public int allocateNextId(NetworkId networkId) {
+        return (int) nextIds.incrementAndGet();
+    }
+
+    private class FlowObjectiveNotifier implements Runnable {
+        @Override
+        public void run() {
+            try {
+                while (!Thread.currentThread().isInterrupted()) {
+                    VirtualObjectiveEvent vEvent = eventQ.take();
+                    notifyDelegate(vEvent.networkId(), vEvent);
+                }
+            } catch (InterruptedException ex) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    private class VirtualObjectiveEvent extends ObjectiveEvent {
+        NetworkId networkId;
+
+        public VirtualObjectiveEvent(NetworkId networkId, Type type,
+                                     Integer objective) {
+            super(type, objective);
+            this.networkId = networkId;
+        }
+
+        NetworkId networkId() {
+            return networkId;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowRuleStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowRuleStore.java
new file mode 100644
index 0000000..1fc5cde
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualFlowRuleStore.java
@@ -0,0 +1,411 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.SettableFuture;
+import org.onlab.util.Tools;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkFlowRuleStore;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.flow.CompletedBatchOperation;
+import org.onosproject.net.flow.DefaultFlowEntry;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowId;
+import org.onosproject.net.flow.FlowRule;
+import org.onosproject.net.flow.FlowRuleEvent;
+import org.onosproject.net.flow.FlowRuleStoreDelegate;
+import org.onosproject.net.flow.StoredFlowEntry;
+import org.onosproject.net.flow.TableStatisticsEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEntry;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEvent;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchRequest;
+import org.onosproject.store.service.StorageService;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.onosproject.incubator.net.virtual.store.impl.OsgiPropertyConstants.PENDING_FUTURE_TIMEOUT_MINUTES;
+import static org.onosproject.incubator.net.virtual.store.impl.OsgiPropertyConstants.PENDING_FUTURE_TIMEOUT_MINUTES_DEFAULT;
+import static org.onosproject.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the virtual network flow rule store to manage inventory of
+ * virtual flow rules using trivial in-memory implementation.
+ */
+//TODO: support distributed flowrule store for virtual networks
+
+@Component(immediate = true, service = VirtualNetworkFlowRuleStore.class,
+        property = {
+                 PENDING_FUTURE_TIMEOUT_MINUTES + ":Integer=" + PENDING_FUTURE_TIMEOUT_MINUTES_DEFAULT,
+        })
+public class SimpleVirtualFlowRuleStore
+        extends AbstractVirtualStore<FlowRuleBatchEvent, FlowRuleStoreDelegate>
+        implements VirtualNetworkFlowRuleStore {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    private final ConcurrentMap<NetworkId,
+            ConcurrentMap<DeviceId, ConcurrentMap<FlowId, List<StoredFlowEntry>>>>
+            flowEntries = new ConcurrentHashMap<>();
+
+
+    private final AtomicInteger localBatchIdGen = new AtomicInteger();
+
+    /** Expiration time after an entry is created that it should be automatically removed. */
+    private int pendingFutureTimeoutMinutes = PENDING_FUTURE_TIMEOUT_MINUTES_DEFAULT;
+
+    private Cache<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures =
+            CacheBuilder.newBuilder()
+                    .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES)
+                    .removalListener(new TimeoutFuture())
+                    .build();
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        flowEntries.clear();
+        log.info("Stopped");
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+
+        readComponentConfiguration(context);
+
+        // Reset Cache and copy all.
+        Cache<Integer, SettableFuture<CompletedBatchOperation>> prevFutures = pendingFutures;
+        pendingFutures = CacheBuilder.newBuilder()
+                .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES)
+                .removalListener(new TimeoutFuture())
+                .build();
+
+        pendingFutures.putAll(prevFutures.asMap());
+    }
+
+    /**
+     * Extracts properties from the component configuration context.
+     *
+     * @param context the component context
+     */
+    private void readComponentConfiguration(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+
+        Integer newPendingFutureTimeoutMinutes =
+                Tools.getIntegerProperty(properties, "pendingFutureTimeoutMinutes");
+        if (newPendingFutureTimeoutMinutes == null) {
+            pendingFutureTimeoutMinutes = PENDING_FUTURE_TIMEOUT_MINUTES_DEFAULT;
+            log.info("Pending future timeout is not configured, " +
+                             "using current value of {}", pendingFutureTimeoutMinutes);
+        } else {
+            pendingFutureTimeoutMinutes = newPendingFutureTimeoutMinutes;
+            log.info("Configured. Pending future timeout is configured to {}",
+                     pendingFutureTimeoutMinutes);
+        }
+    }
+
+    @Override
+    public int getFlowRuleCount(NetworkId networkId) {
+        int sum = 0;
+
+        if (flowEntries.get(networkId) == null) {
+            return 0;
+        }
+
+        for (ConcurrentMap<FlowId, List<StoredFlowEntry>> ft :
+                flowEntries.get(networkId).values()) {
+            for (List<StoredFlowEntry> fes : ft.values()) {
+                sum += fes.size();
+            }
+        }
+        return sum;
+    }
+
+    @Override
+    public FlowEntry getFlowEntry(NetworkId networkId, FlowRule rule) {
+        return getFlowEntryInternal(networkId, rule.deviceId(), rule);
+    }
+
+    @Override
+    public Iterable<FlowEntry> getFlowEntries(NetworkId networkId, DeviceId deviceId) {
+        return FluentIterable.from(getFlowTable(networkId, deviceId).values())
+                .transformAndConcat(Collections::unmodifiableList);
+    }
+
+    private void storeFlowRule(NetworkId networkId, FlowRule rule) {
+        storeFlowRuleInternal(networkId, rule);
+    }
+
+    @Override
+    public void storeBatch(NetworkId networkId, FlowRuleBatchOperation batchOperation) {
+        List<FlowRuleBatchEntry> toAdd = new ArrayList<>();
+        List<FlowRuleBatchEntry> toRemove = new ArrayList<>();
+
+        for (FlowRuleBatchEntry entry : batchOperation.getOperations()) {
+            final FlowRule flowRule = entry.target();
+            if (entry.operator().equals(FlowRuleBatchEntry.FlowRuleOperation.ADD)) {
+                if (!getFlowEntries(networkId, flowRule.deviceId(),
+                                    flowRule.id()).contains(flowRule)) {
+                    storeFlowRule(networkId, flowRule);
+                    toAdd.add(entry);
+                }
+            } else if (entry.operator().equals(FlowRuleBatchEntry.FlowRuleOperation.REMOVE)) {
+                if (getFlowEntries(networkId, flowRule.deviceId(), flowRule.id()).contains(flowRule)) {
+                    deleteFlowRule(networkId, flowRule);
+                    toRemove.add(entry);
+                }
+            } else {
+                throw new UnsupportedOperationException("Unsupported operation type");
+            }
+        }
+
+        if (toAdd.isEmpty() && toRemove.isEmpty()) {
+            notifyDelegate(networkId, FlowRuleBatchEvent.completed(
+                    new FlowRuleBatchRequest(batchOperation.id(), Collections.emptySet()),
+                    new CompletedBatchOperation(true, Collections.emptySet(),
+                                                batchOperation.deviceId())));
+            return;
+        }
+
+        SettableFuture<CompletedBatchOperation> r = SettableFuture.create();
+        final int futureId = localBatchIdGen.incrementAndGet();
+
+        pendingFutures.put(futureId, r);
+
+        toAdd.addAll(toRemove);
+        notifyDelegate(networkId, FlowRuleBatchEvent.requested(
+                new FlowRuleBatchRequest(batchOperation.id(),
+                                         Sets.newHashSet(toAdd)), batchOperation.deviceId()));
+
+    }
+
+    @Override
+    public void batchOperationComplete(NetworkId networkId, FlowRuleBatchEvent event) {
+        final Long batchId = event.subject().batchId();
+        SettableFuture<CompletedBatchOperation> future
+                = pendingFutures.getIfPresent(batchId);
+        if (future != null) {
+            future.set(event.result());
+            pendingFutures.invalidate(batchId);
+        }
+        notifyDelegate(networkId, event);
+    }
+
+    @Override
+    public void deleteFlowRule(NetworkId networkId, FlowRule rule) {
+        List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id());
+
+        synchronized (entries) {
+            for (StoredFlowEntry entry : entries) {
+                if (entry.equals(rule)) {
+                    synchronized (entry) {
+                        entry.setState(FlowEntry.FlowEntryState.PENDING_REMOVE);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public FlowRuleEvent addOrUpdateFlowRule(NetworkId networkId, FlowEntry rule) {
+        // check if this new rule is an update to an existing entry
+        List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id());
+        synchronized (entries) {
+            for (StoredFlowEntry stored : entries) {
+                if (stored.equals(rule)) {
+                    synchronized (stored) {
+                        //FIXME modification of "stored" flow entry outside of flow table
+                        stored.setBytes(rule.bytes());
+                        stored.setLife(rule.life());
+                        stored.setPackets(rule.packets());
+                        if (stored.state() == FlowEntry.FlowEntryState.PENDING_ADD) {
+                            stored.setState(FlowEntry.FlowEntryState.ADDED);
+                            // TODO: Do we need to change `rule` state?
+                            return new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED, rule);
+                        }
+                        return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule);
+                    }
+                }
+            }
+        }
+
+        // should not reach here
+        // storeFlowRule was expected to be called
+        log.error("FlowRule was not found in store {} to update", rule);
+
+        return null;
+    }
+
+    @Override
+    public FlowRuleEvent removeFlowRule(NetworkId networkId, FlowEntry rule) {
+        // This is where one could mark a rule as removed and still keep it in the store.
+        final DeviceId did = rule.deviceId();
+
+        List<StoredFlowEntry> entries = getFlowEntries(networkId, did, rule.id());
+        synchronized (entries) {
+            if (entries.remove(rule)) {
+                return new FlowRuleEvent(RULE_REMOVED, rule);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public FlowRuleEvent pendingFlowRule(NetworkId networkId, FlowEntry rule) {
+        List<StoredFlowEntry> entries = getFlowEntries(networkId, rule.deviceId(), rule.id());
+        synchronized (entries) {
+            for (StoredFlowEntry entry : entries) {
+                if (entry.equals(rule) &&
+                        entry.state() != FlowEntry.FlowEntryState.PENDING_ADD) {
+                    synchronized (entry) {
+                        entry.setState(FlowEntry.FlowEntryState.PENDING_ADD);
+                        return new FlowRuleEvent(FlowRuleEvent.Type.RULE_UPDATED, rule);
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void purgeFlowRule(NetworkId networkId, DeviceId deviceId) {
+        flowEntries.get(networkId).remove(deviceId);
+    }
+
+    @Override
+    public void purgeFlowRules(NetworkId networkId) {
+        flowEntries.get(networkId).clear();
+    }
+
+    @Override
+    public FlowRuleEvent
+    updateTableStatistics(NetworkId networkId, DeviceId deviceId, List<TableStatisticsEntry> tableStats) {
+        //TODO: Table operations are not supported yet
+        return null;
+    }
+
+    @Override
+    public Iterable<TableStatisticsEntry>
+    getTableStatistics(NetworkId networkId, DeviceId deviceId) {
+        //TODO: Table operations are not supported yet
+        return null;
+    }
+
+    /**
+     * Returns the flow table for specified device.
+     *
+     * @param networkId identifier of the virtual network
+     * @param deviceId identifier of the virtual device
+     * @return Map representing Flow Table of given device.
+     */
+    private ConcurrentMap<FlowId, List<StoredFlowEntry>>
+    getFlowTable(NetworkId networkId, DeviceId deviceId) {
+        return flowEntries
+                .computeIfAbsent(networkId, n -> new ConcurrentHashMap<>())
+                .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
+    }
+
+    private List<StoredFlowEntry>
+    getFlowEntries(NetworkId networkId, DeviceId deviceId, FlowId flowId) {
+        final ConcurrentMap<FlowId, List<StoredFlowEntry>> flowTable
+                = getFlowTable(networkId, deviceId);
+
+        List<StoredFlowEntry> r = flowTable.get(flowId);
+        if (r == null) {
+            final List<StoredFlowEntry> concurrentlyAdded;
+            r = new CopyOnWriteArrayList<>();
+            concurrentlyAdded = flowTable.putIfAbsent(flowId, r);
+            if (concurrentlyAdded != null) {
+                return concurrentlyAdded;
+            }
+        }
+        return r;
+    }
+
+    private FlowEntry
+    getFlowEntryInternal(NetworkId networkId, DeviceId deviceId, FlowRule rule) {
+        List<StoredFlowEntry> fes = getFlowEntries(networkId, deviceId, rule.id());
+        for (StoredFlowEntry fe : fes) {
+            if (fe.equals(rule)) {
+                return fe;
+            }
+        }
+        return null;
+    }
+
+    private void storeFlowRuleInternal(NetworkId networkId, FlowRule rule) {
+        StoredFlowEntry f = new DefaultFlowEntry(rule);
+        final DeviceId did = f.deviceId();
+        final FlowId fid = f.id();
+        List<StoredFlowEntry> existing = getFlowEntries(networkId, did, fid);
+        synchronized (existing) {
+            for (StoredFlowEntry fe : existing) {
+                if (fe.equals(rule)) {
+                    // was already there? ignore
+                    return;
+                }
+            }
+            // new flow rule added
+            existing.add(f);
+        }
+    }
+
+    private static final class TimeoutFuture
+            implements RemovalListener<Integer, SettableFuture<CompletedBatchOperation>> {
+        @Override
+        public void onRemoval(RemovalNotification<Integer,
+                SettableFuture<CompletedBatchOperation>> notification) {
+            // wrapping in ExecutionException to support Future.get
+            if (notification.wasEvicted()) {
+                notification.getValue()
+                        .setException(new ExecutionException("Timed out",
+                                                             new TimeoutException()));
+            }
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualGroupStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualGroupStore.java
new file mode 100644
index 0000000..3c696ce
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualGroupStore.java
@@ -0,0 +1,764 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Sets;
+import org.onosproject.core.GroupId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkGroupStore;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.group.DefaultGroup;
+import org.onosproject.net.group.DefaultGroupDescription;
+import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
+import org.onosproject.net.group.GroupDescription;
+import org.onosproject.net.group.GroupEvent;
+import org.onosproject.net.group.GroupKey;
+import org.onosproject.net.group.GroupOperation;
+import org.onosproject.net.group.GroupStoreDelegate;
+import org.onosproject.net.group.StoredGroupBucketEntry;
+import org.onosproject.net.group.StoredGroupEntry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Manages inventory of virtual group entries using trivial in-memory implementation.
+ */
+@Component(immediate = true, service = VirtualNetworkGroupStore.class)
+public class SimpleVirtualGroupStore
+        extends AbstractVirtualStore<GroupEvent, GroupStoreDelegate>
+        implements VirtualNetworkGroupStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private final int dummyId = 0xffffffff;
+    private final GroupId dummyGroupId = new GroupId(dummyId);
+
+    // inner Map is per device group table
+    private final ConcurrentMap<NetworkId,
+            ConcurrentMap<DeviceId, ConcurrentMap<GroupKey, StoredGroupEntry>>>
+            groupEntriesByKey = new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<NetworkId,
+            ConcurrentMap<DeviceId, ConcurrentMap<GroupId, StoredGroupEntry>>>
+            groupEntriesById = new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<NetworkId,
+            ConcurrentMap<DeviceId, ConcurrentMap<GroupKey, StoredGroupEntry>>>
+            pendingGroupEntriesByKey = new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<NetworkId,
+            ConcurrentMap<DeviceId, ConcurrentMap<GroupId, Group>>>
+            extraneousGroupEntriesById = new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<NetworkId, HashMap<DeviceId, Boolean>>
+            deviceAuditStatus = new ConcurrentHashMap<>();
+
+    private final AtomicInteger groupIdGen = new AtomicInteger();
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        groupEntriesByKey.clear();
+        groupEntriesById.clear();
+        log.info("Stopped");
+    }
+
+    /**
+     * Returns the group key table for specified device.
+     *
+     * @param networkId identifier of the virtual network
+     * @param deviceId identifier of the device
+     * @return Map representing group key table of given device.
+     */
+    private ConcurrentMap<GroupKey, StoredGroupEntry>
+    getGroupKeyTable(NetworkId networkId, DeviceId deviceId) {
+        groupEntriesByKey.computeIfAbsent(networkId, n -> new ConcurrentHashMap<>());
+        return groupEntriesByKey.get(networkId)
+                .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
+    }
+
+    /**
+     * Returns the group id table for specified device.
+     *
+     * @param networkId identifier of the virtual network
+     * @param deviceId identifier of the device
+     * @return Map representing group key table of given device.
+     */
+    private ConcurrentMap<GroupId, StoredGroupEntry>
+    getGroupIdTable(NetworkId networkId, DeviceId deviceId) {
+        groupEntriesById.computeIfAbsent(networkId, n -> new ConcurrentHashMap<>());
+        return groupEntriesById.get(networkId)
+                .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
+    }
+
+    /**
+     * Returns the pending group key table for specified device.
+     *
+     * @param networkId identifier of the virtual network
+     * @param deviceId identifier of the device
+     * @return Map representing group key table of given device.
+     */
+    private ConcurrentMap<GroupKey, StoredGroupEntry>
+    getPendingGroupKeyTable(NetworkId networkId, DeviceId deviceId) {
+        pendingGroupEntriesByKey.computeIfAbsent(networkId, n -> new ConcurrentHashMap<>());
+        return pendingGroupEntriesByKey.get(networkId)
+                .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
+    }
+
+    /**
+     * Returns the extraneous group id table for specified device.
+     *
+     * @param networkId identifier of the virtual network
+     * @param deviceId identifier of the device
+     * @return Map representing group key table of given device.
+     */
+    private ConcurrentMap<GroupId, Group>
+    getExtraneousGroupIdTable(NetworkId networkId, DeviceId deviceId) {
+        extraneousGroupEntriesById.computeIfAbsent(networkId, n -> new ConcurrentHashMap<>());
+        return extraneousGroupEntriesById.get(networkId)
+                .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
+    }
+
+    @Override
+    public int getGroupCount(NetworkId networkId, DeviceId deviceId) {
+        return (groupEntriesByKey.get(networkId).get(deviceId) != null) ?
+                groupEntriesByKey.get(networkId).get(deviceId).size() : 0;
+    }
+
+    @Override
+    public Iterable<Group> getGroups(NetworkId networkId, DeviceId deviceId) {
+        // flatten and make iterator unmodifiable
+        return FluentIterable.from(getGroupKeyTable(networkId, deviceId).values())
+                .transform(input -> input);
+    }
+
+    @Override
+    public Group getGroup(NetworkId networkId, DeviceId deviceId, GroupKey appCookie) {
+        if (groupEntriesByKey.get(networkId) != null &&
+                groupEntriesByKey.get(networkId).get(deviceId) != null) {
+            return groupEntriesByKey.get(networkId).get(deviceId).get(appCookie);
+        }
+        return null;
+    }
+
+    @Override
+    public Group getGroup(NetworkId networkId, DeviceId deviceId, GroupId groupId) {
+        if (groupEntriesById.get(networkId) != null &&
+                groupEntriesById.get(networkId).get(deviceId) != null) {
+            return groupEntriesById.get(networkId).get(deviceId).get(groupId);
+        }
+        return null;
+    }
+
+    private int getFreeGroupIdValue(NetworkId networkId, DeviceId deviceId) {
+        int freeId = groupIdGen.incrementAndGet();
+
+        while (true) {
+            Group existing = null;
+            if (groupEntriesById.get(networkId) != null &&
+                    groupEntriesById.get(networkId).get(deviceId) != null) {
+                existing = groupEntriesById.get(networkId).get(deviceId)
+                                .get(new GroupId(freeId));
+            }
+
+            if (existing == null) {
+                if (extraneousGroupEntriesById.get(networkId) != null &&
+                        extraneousGroupEntriesById.get(networkId).get(deviceId) != null) {
+                    existing = extraneousGroupEntriesById.get(networkId).get(deviceId)
+                                    .get(new GroupId(freeId));
+                }
+            }
+
+            if (existing != null) {
+                freeId = groupIdGen.incrementAndGet();
+            } else {
+                break;
+            }
+        }
+        return freeId;
+    }
+
+    @Override
+    public void storeGroupDescription(NetworkId networkId, GroupDescription groupDesc) {
+        // Check if a group is existing with the same key
+        if (getGroup(networkId, groupDesc.deviceId(), groupDesc.appCookie()) != null) {
+            return;
+        }
+
+        if (deviceAuditStatus.get(networkId) == null ||
+                deviceAuditStatus.get(networkId).get(groupDesc.deviceId()) == null) {
+            // Device group audit has not completed yet
+            // Add this group description to pending group key table
+            // Create a group entry object with Dummy Group ID
+            StoredGroupEntry group = new DefaultGroup(dummyGroupId, groupDesc);
+            group.setState(Group.GroupState.WAITING_AUDIT_COMPLETE);
+            ConcurrentMap<GroupKey, StoredGroupEntry> pendingKeyTable =
+                    getPendingGroupKeyTable(networkId, groupDesc.deviceId());
+            pendingKeyTable.put(groupDesc.appCookie(), group);
+            return;
+        }
+
+        storeGroupDescriptionInternal(networkId, groupDesc);
+    }
+
+    private void storeGroupDescriptionInternal(NetworkId networkId,
+                                               GroupDescription groupDesc) {
+        // Check if a group is existing with the same key
+        if (getGroup(networkId, groupDesc.deviceId(), groupDesc.appCookie()) != null) {
+            return;
+        }
+
+        GroupId id = null;
+        if (groupDesc.givenGroupId() == null) {
+            // Get a new group identifier
+            id = new GroupId(getFreeGroupIdValue(networkId, groupDesc.deviceId()));
+        } else {
+            id = new GroupId(groupDesc.givenGroupId());
+        }
+        // Create a group entry object
+        StoredGroupEntry group = new DefaultGroup(id, groupDesc);
+        // Insert the newly created group entry into concurrent key and id maps
+        ConcurrentMap<GroupKey, StoredGroupEntry> keyTable =
+                getGroupKeyTable(networkId, groupDesc.deviceId());
+        keyTable.put(groupDesc.appCookie(), group);
+        ConcurrentMap<GroupId, StoredGroupEntry> idTable =
+                getGroupIdTable(networkId, groupDesc.deviceId());
+        idTable.put(id, group);
+        notifyDelegate(networkId, new GroupEvent(GroupEvent.Type.GROUP_ADD_REQUESTED,
+                                      group));
+    }
+
+    @Override
+    public void updateGroupDescription(NetworkId networkId, DeviceId deviceId,
+                                       GroupKey oldAppCookie, UpdateType type,
+                                       GroupBuckets newBuckets, GroupKey newAppCookie) {
+        // Check if a group is existing with the provided key
+        Group oldGroup = getGroup(networkId, deviceId, oldAppCookie);
+        if (oldGroup == null) {
+            return;
+        }
+
+        List<GroupBucket> newBucketList = getUpdatedBucketList(oldGroup,
+                                                               type,
+                                                               newBuckets);
+        if (newBucketList != null) {
+            // Create a new group object from the old group
+            GroupBuckets updatedBuckets = new GroupBuckets(newBucketList);
+            GroupKey newCookie = (newAppCookie != null) ? newAppCookie : oldAppCookie;
+            GroupDescription updatedGroupDesc = new DefaultGroupDescription(
+                    oldGroup.deviceId(),
+                    oldGroup.type(),
+                    updatedBuckets,
+                    newCookie,
+                    oldGroup.givenGroupId(),
+                    oldGroup.appId());
+            StoredGroupEntry newGroup = new DefaultGroup(oldGroup.id(),
+                                                         updatedGroupDesc);
+            newGroup.setState(Group.GroupState.PENDING_UPDATE);
+            newGroup.setLife(oldGroup.life());
+            newGroup.setPackets(oldGroup.packets());
+            newGroup.setBytes(oldGroup.bytes());
+
+            // Remove the old entry from maps and add new entry using new key
+            ConcurrentMap<GroupKey, StoredGroupEntry> keyTable =
+                    getGroupKeyTable(networkId, oldGroup.deviceId());
+            ConcurrentMap<GroupId, StoredGroupEntry> idTable =
+                    getGroupIdTable(networkId, oldGroup.deviceId());
+            keyTable.remove(oldGroup.appCookie());
+            idTable.remove(oldGroup.id());
+            keyTable.put(newGroup.appCookie(), newGroup);
+            idTable.put(newGroup.id(), newGroup);
+            notifyDelegate(networkId,
+                           new GroupEvent(GroupEvent.Type.GROUP_UPDATE_REQUESTED,
+                                          newGroup));
+        }
+
+    }
+
+    private List<GroupBucket> getUpdatedBucketList(Group oldGroup,
+                                                   UpdateType type,
+                                                   GroupBuckets buckets) {
+        if (type == UpdateType.SET) {
+            return buckets.buckets();
+        }
+
+        List<GroupBucket> oldBuckets = oldGroup.buckets().buckets();
+        List<GroupBucket> updatedBucketList = new ArrayList<>();
+        boolean groupDescUpdated = false;
+
+        if (type == UpdateType.ADD) {
+            List<GroupBucket> newBuckets = buckets.buckets();
+
+            // Add old buckets that will not be updated and check if any will be updated.
+            for (GroupBucket oldBucket : oldBuckets) {
+                int newBucketIndex = newBuckets.indexOf(oldBucket);
+
+                if (newBucketIndex != -1) {
+                    GroupBucket newBucket = newBuckets.get(newBucketIndex);
+                    if (!newBucket.hasSameParameters(oldBucket)) {
+                        // Bucket will be updated
+                        groupDescUpdated = true;
+                    }
+                } else {
+                    // Old bucket will remain the same - add it.
+                    updatedBucketList.add(oldBucket);
+                }
+            }
+
+            // Add all new buckets
+            updatedBucketList.addAll(newBuckets);
+            if (!oldBuckets.containsAll(newBuckets)) {
+                groupDescUpdated = true;
+            }
+
+        } else if (type == UpdateType.REMOVE) {
+            List<GroupBucket> bucketsToRemove = buckets.buckets();
+
+            // Check which old buckets should remain
+            for (GroupBucket oldBucket : oldBuckets) {
+                if (!bucketsToRemove.contains(oldBucket)) {
+                    updatedBucketList.add(oldBucket);
+                } else {
+                    groupDescUpdated = true;
+                }
+            }
+        }
+
+        if (groupDescUpdated) {
+            return updatedBucketList;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void deleteGroupDescription(NetworkId networkId, DeviceId deviceId,
+                                       GroupKey appCookie) {
+        // Check if a group is existing with the provided key
+        StoredGroupEntry existing = null;
+        if (groupEntriesByKey.get(networkId) != null &&
+                groupEntriesByKey.get(networkId).get(deviceId) != null) {
+            existing = groupEntriesByKey.get(networkId).get(deviceId).get(appCookie);
+        }
+
+        if (existing == null) {
+            return;
+        }
+
+        synchronized (existing) {
+            existing.setState(Group.GroupState.PENDING_DELETE);
+        }
+        notifyDelegate(networkId,
+                       new GroupEvent(GroupEvent.Type.GROUP_REMOVE_REQUESTED, existing));
+    }
+
+    @Override
+    public void addOrUpdateGroupEntry(NetworkId networkId, Group group) {
+        // check if this new entry is an update to an existing entry
+        StoredGroupEntry existing = null;
+
+        if (groupEntriesById.get(networkId) != null &&
+                groupEntriesById.get(networkId).get(group.deviceId()) != null) {
+            existing = groupEntriesById
+                    .get(networkId)
+                    .get(group.deviceId())
+                    .get(group.id());
+        }
+
+        GroupEvent event = null;
+
+        if (existing != null) {
+            synchronized (existing) {
+                for (GroupBucket bucket:group.buckets().buckets()) {
+                    Optional<GroupBucket> matchingBucket =
+                            existing.buckets().buckets()
+                                    .stream()
+                                    .filter((existingBucket) -> (existingBucket.equals(bucket)))
+                                    .findFirst();
+                    if (matchingBucket.isPresent()) {
+                        ((StoredGroupBucketEntry) matchingBucket.
+                                get()).setPackets(bucket.packets());
+                        ((StoredGroupBucketEntry) matchingBucket.
+                                get()).setBytes(bucket.bytes());
+                    } else {
+                        log.warn("addOrUpdateGroupEntry: No matching "
+                                         + "buckets to update stats");
+                    }
+                }
+                existing.setLife(group.life());
+                existing.setPackets(group.packets());
+                existing.setBytes(group.bytes());
+                if (existing.state() == Group.GroupState.PENDING_ADD) {
+                    existing.setState(Group.GroupState.ADDED);
+                    event = new GroupEvent(GroupEvent.Type.GROUP_ADDED, existing);
+                } else {
+                    if (existing.state() == Group.GroupState.PENDING_UPDATE) {
+                        existing.setState(Group.GroupState.ADDED);
+                    }
+                    event = new GroupEvent(GroupEvent.Type.GROUP_UPDATED, existing);
+                }
+            }
+        }
+
+        if (event != null) {
+            notifyDelegate(networkId, event);
+        }
+    }
+
+    @Override
+    public void removeGroupEntry(NetworkId networkId, Group group) {
+        StoredGroupEntry existing = null;
+        if (groupEntriesById.get(networkId) != null
+                && groupEntriesById.get(networkId).get(group.deviceId()) != null) {
+           existing = groupEntriesById
+                   .get(networkId).get(group.deviceId()).get(group.id());
+        }
+
+        if (existing != null) {
+            ConcurrentMap<GroupKey, StoredGroupEntry> keyTable =
+                    getGroupKeyTable(networkId, existing.deviceId());
+            ConcurrentMap<GroupId, StoredGroupEntry> idTable =
+                    getGroupIdTable(networkId, existing.deviceId());
+            idTable.remove(existing.id());
+            keyTable.remove(existing.appCookie());
+            notifyDelegate(networkId,
+                           new GroupEvent(GroupEvent.Type.GROUP_REMOVED, existing));
+        }
+    }
+
+    @Override
+    public void purgeGroupEntry(NetworkId networkId, DeviceId deviceId) {
+        if (groupEntriesById.get(networkId) != null) {
+            Set<Map.Entry<GroupId, StoredGroupEntry>> entryPendingRemove =
+                    groupEntriesById.get(networkId).get(deviceId).entrySet();
+            groupEntriesById.get(networkId).remove(deviceId);
+            groupEntriesByKey.get(networkId).remove(deviceId);
+
+            entryPendingRemove.forEach(entry -> {
+                notifyDelegate(networkId,
+                               new GroupEvent(GroupEvent.Type.GROUP_REMOVED,
+                                              entry.getValue()));
+            });
+        }
+    }
+
+    @Override
+    public void purgeGroupEntries(NetworkId networkId) {
+        if (groupEntriesById.get(networkId) != null) {
+            groupEntriesById.get((networkId)).values().forEach(groupEntries -> {
+                groupEntries.entrySet().forEach(entry -> {
+                    notifyDelegate(networkId,
+                                   new GroupEvent(GroupEvent.Type.GROUP_REMOVED,
+                                                  entry.getValue()));
+                });
+            });
+
+            groupEntriesById.get(networkId).clear();
+            groupEntriesByKey.get(networkId).clear();
+        }
+    }
+
+    @Override
+    public void addOrUpdateExtraneousGroupEntry(NetworkId networkId, Group group) {
+        ConcurrentMap<GroupId, Group> extraneousIdTable =
+                getExtraneousGroupIdTable(networkId, group.deviceId());
+        extraneousIdTable.put(group.id(), group);
+        // Check the reference counter
+        if (group.referenceCount() == 0) {
+            notifyDelegate(networkId,
+                           new GroupEvent(GroupEvent.Type.GROUP_REMOVE_REQUESTED, group));
+        }
+    }
+
+    @Override
+    public void removeExtraneousGroupEntry(NetworkId networkId, Group group) {
+        ConcurrentMap<GroupId, Group> extraneousIdTable =
+                getExtraneousGroupIdTable(networkId, group.deviceId());
+        extraneousIdTable.remove(group.id());
+    }
+
+    @Override
+    public Iterable<Group> getExtraneousGroups(NetworkId networkId, DeviceId deviceId) {
+        // flatten and make iterator unmodifiable
+        return FluentIterable.from(
+                getExtraneousGroupIdTable(networkId, deviceId).values());
+    }
+
+    @Override
+    public void deviceInitialAuditCompleted(NetworkId networkId, DeviceId deviceId,
+                                            boolean completed) {
+        deviceAuditStatus.computeIfAbsent(networkId, k -> new HashMap<>());
+
+        HashMap<DeviceId, Boolean> deviceAuditStatusByNetwork =
+                deviceAuditStatus.get(networkId);
+
+        synchronized (deviceAuditStatusByNetwork) {
+            if (completed) {
+                log.debug("deviceInitialAuditCompleted: AUDIT "
+                                  + "completed for device {}", deviceId);
+                deviceAuditStatusByNetwork.put(deviceId, true);
+                // Execute all pending group requests
+                ConcurrentMap<GroupKey, StoredGroupEntry> pendingGroupRequests =
+                        getPendingGroupKeyTable(networkId, deviceId);
+                for (Group group:pendingGroupRequests.values()) {
+                    GroupDescription tmp = new DefaultGroupDescription(
+                            group.deviceId(),
+                            group.type(),
+                            group.buckets(),
+                            group.appCookie(),
+                            group.givenGroupId(),
+                            group.appId());
+                    storeGroupDescriptionInternal(networkId, tmp);
+                }
+                getPendingGroupKeyTable(networkId, deviceId).clear();
+            } else {
+                if (deviceAuditStatusByNetwork.get(deviceId)) {
+                    log.debug("deviceInitialAuditCompleted: Clearing AUDIT "
+                                      + "status for device {}", deviceId);
+                    deviceAuditStatusByNetwork.put(deviceId, false);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean deviceInitialAuditStatus(NetworkId networkId, DeviceId deviceId) {
+        deviceAuditStatus.computeIfAbsent(networkId, k -> new HashMap<>());
+
+        HashMap<DeviceId, Boolean> deviceAuditStatusByNetwork =
+                deviceAuditStatus.get(networkId);
+
+        synchronized (deviceAuditStatusByNetwork) {
+            return (deviceAuditStatusByNetwork.get(deviceId) != null)
+                    ? deviceAuditStatusByNetwork.get(deviceId) : false;
+        }
+    }
+
+    @Override
+    public void groupOperationFailed(NetworkId networkId, DeviceId deviceId,
+                                     GroupOperation operation) {
+
+        StoredGroupEntry existing = null;
+        if (groupEntriesById.get(networkId) != null &&
+                groupEntriesById.get(networkId).get(deviceId) != null) {
+            existing = groupEntriesById.get(networkId).get(deviceId)
+                    .get(operation.groupId());
+        }
+
+        if (existing == null) {
+            log.warn("No group entry with ID {} found ", operation.groupId());
+            return;
+        }
+
+        switch (operation.opType()) {
+            case ADD:
+                notifyDelegate(networkId,
+                               new GroupEvent(GroupEvent.Type.GROUP_ADD_FAILED,
+                                              existing));
+                break;
+            case MODIFY:
+                notifyDelegate(networkId,
+                               new GroupEvent(GroupEvent.Type.GROUP_UPDATE_FAILED,
+                                              existing));
+                break;
+            case DELETE:
+                notifyDelegate(networkId,
+                               new GroupEvent(GroupEvent.Type.GROUP_REMOVE_FAILED,
+                                              existing));
+                break;
+            default:
+                log.warn("Unknown group operation type {}", operation.opType());
+        }
+
+        ConcurrentMap<GroupKey, StoredGroupEntry> keyTable =
+                getGroupKeyTable(networkId, existing.deviceId());
+        ConcurrentMap<GroupId, StoredGroupEntry> idTable =
+                getGroupIdTable(networkId, existing.deviceId());
+        idTable.remove(existing.id());
+        keyTable.remove(existing.appCookie());
+    }
+
+    @Override
+    public void pushGroupMetrics(NetworkId networkId, DeviceId deviceId,
+                                 Collection<Group> groupEntries) {
+        boolean deviceInitialAuditStatus =
+                deviceInitialAuditStatus(networkId, deviceId);
+        Set<Group> southboundGroupEntries =
+                Sets.newHashSet(groupEntries);
+        Set<Group> storedGroupEntries =
+                Sets.newHashSet(getGroups(networkId, deviceId));
+        Set<Group> extraneousStoredEntries =
+                Sets.newHashSet(getExtraneousGroups(networkId, deviceId));
+
+        if (log.isTraceEnabled()) {
+            log.trace("pushGroupMetrics: Displaying all ({}) "
+                              + "southboundGroupEntries for device {}",
+                      southboundGroupEntries.size(),
+                      deviceId);
+            for (Group group : southboundGroupEntries) {
+                log.trace("Group {} in device {}", group, deviceId);
+            }
+
+            log.trace("Displaying all ({}) stored group entries for device {}",
+                      storedGroupEntries.size(),
+                      deviceId);
+            for (Group group : storedGroupEntries) {
+                log.trace("Stored Group {} for device {}", group, deviceId);
+            }
+        }
+
+        for (Iterator<Group> it2 = southboundGroupEntries.iterator(); it2.hasNext();) {
+            Group group = it2.next();
+            if (storedGroupEntries.remove(group)) {
+                // we both have the group, let's update some info then.
+                log.trace("Group AUDIT: group {} exists "
+                                  + "in both planes for device {}",
+                          group.id(), deviceId);
+                groupAdded(networkId, group);
+                it2.remove();
+            }
+        }
+        for (Group group : southboundGroupEntries) {
+            if (getGroup(networkId, group.deviceId(), group.id()) != null) {
+                // There is a group existing with the same id
+                // It is possible that group update is
+                // in progress while we got a stale info from switch
+                if (!storedGroupEntries.remove(getGroup(
+                        networkId, group.deviceId(), group.id()))) {
+                    log.warn("Group AUDIT: Inconsistent state:"
+                                     + "Group exists in ID based table while "
+                                     + "not present in key based table");
+                }
+            } else {
+                // there are groups in the switch that aren't in the store
+                log.trace("Group AUDIT: extraneous group {} exists "
+                                  + "in data plane for device {}",
+                          group.id(), deviceId);
+                extraneousStoredEntries.remove(group);
+                extraneousGroup(networkId, group);
+            }
+        }
+        for (Group group : storedGroupEntries) {
+            // there are groups in the store that aren't in the switch
+            log.trace("Group AUDIT: group {} missing "
+                              + "in data plane for device {}",
+                      group.id(), deviceId);
+            groupMissing(networkId, group);
+        }
+        for (Group group : extraneousStoredEntries) {
+            // there are groups in the extraneous store that
+            // aren't in the switch
+            log.trace("Group AUDIT: clearing extransoeus group {} "
+                              + "from store for device {}",
+                      group.id(), deviceId);
+            removeExtraneousGroupEntry(networkId, group);
+        }
+
+        if (!deviceInitialAuditStatus) {
+            log.debug("Group AUDIT: Setting device {} initial "
+                              + "AUDIT completed", deviceId);
+            deviceInitialAuditCompleted(networkId, deviceId, true);
+        }
+    }
+
+    @Override
+    public void notifyOfFailovers(NetworkId networkId, Collection<Group> failoverGroups) {
+        List<GroupEvent> failoverEvents = new ArrayList<>();
+        failoverGroups.forEach(group -> {
+            if (group.type() == Group.Type.FAILOVER) {
+                failoverEvents.add(new GroupEvent(GroupEvent.Type.GROUP_BUCKET_FAILOVER, group));
+            }
+        });
+        notifyDelegate(networkId, failoverEvents);
+    }
+
+    private void groupMissing(NetworkId networkId, Group group) {
+        switch (group.state()) {
+            case PENDING_DELETE:
+                log.debug("Group {} delete confirmation from device {} " +
+                                  "of virtaual network {}",
+                          group, group.deviceId(), networkId);
+                removeGroupEntry(networkId, group);
+                break;
+            case ADDED:
+            case PENDING_ADD:
+            case PENDING_UPDATE:
+                log.debug("Group {} is in store but not on device {}",
+                          group, group.deviceId());
+                StoredGroupEntry existing = null;
+                if (groupEntriesById.get(networkId) != null &&
+                        groupEntriesById.get(networkId).get(group.deviceId()) != null) {
+
+                    existing = groupEntriesById.get(networkId)
+                            .get(group.deviceId()).get(group.id());
+                }
+                if (existing == null) {
+                    break;
+                }
+
+                log.trace("groupMissing: group "
+                                  + "entry {} in device {} moving "
+                                  + "from {} to PENDING_ADD",
+                          existing.id(),
+                          existing.deviceId(),
+                          existing.state());
+                existing.setState(Group.GroupState.PENDING_ADD);
+                notifyDelegate(networkId, new GroupEvent(GroupEvent.Type.GROUP_ADD_REQUESTED,
+                                              group));
+                break;
+            default:
+                log.debug("Virtual network {} : Group {} has not been installed.",
+                          networkId, group);
+                break;
+        }
+    }
+
+    private void extraneousGroup(NetworkId networkId, Group group) {
+        log.debug("Group {} is on device {} of virtual network{}, but not in store.",
+                  group, group.deviceId(), networkId);
+        addOrUpdateExtraneousGroupEntry(networkId, group);
+    }
+
+    private void groupAdded(NetworkId networkId, Group group) {
+        log.trace("Group {} Added or Updated in device {} of virtual network {}",
+                  group, group.deviceId(), networkId);
+        addOrUpdateGroupEntry(networkId, group);
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualIntentStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualIntentStore.java
new file mode 100644
index 0000000..15b6bcd
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualIntentStore.java
@@ -0,0 +1,268 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkIntentStore;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentData;
+import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentState;
+import org.onosproject.net.intent.IntentStoreDelegate;
+import org.onosproject.net.intent.Key;
+import org.onosproject.store.Timestamp;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.net.intent.IntentState.PURGE_REQ;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Simple single-instance implementation of the intent store for virtual networks.
+ */
+
+@Component(immediate = true, service = VirtualNetworkIntentStore.class)
+public class SimpleVirtualIntentStore
+        extends AbstractVirtualStore<IntentEvent, IntentStoreDelegate>
+        implements VirtualNetworkIntentStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private final Map<NetworkId, Map<Key, IntentData>> currentByNetwork =
+            Maps.newConcurrentMap();
+    private final Map<NetworkId, Map<Key, IntentData>> pendingByNetwork =
+            Maps.newConcurrentMap();
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+
+    @Override
+    public long getIntentCount(NetworkId networkId) {
+        return getCurrentMap(networkId).size();
+    }
+
+    @Override
+    public Iterable<Intent> getIntents(NetworkId networkId) {
+        return getCurrentMap(networkId).values().stream()
+                .map(IntentData::intent)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Iterable<IntentData> getIntentData(NetworkId networkId,
+                                              boolean localOnly, long olderThan) {
+        if (localOnly || olderThan > 0) {
+            long older = System.nanoTime() - olderThan * 1_000_000; //convert ms to ns
+            final SystemClockTimestamp time = new SystemClockTimestamp(older);
+            return getCurrentMap(networkId).values().stream()
+                    .filter(data -> data.version().isOlderThan(time) &&
+                            (!localOnly || isMaster(networkId, data.key())))
+                    .collect(Collectors.toList());
+        }
+        return Lists.newArrayList(getCurrentMap(networkId).values());
+    }
+
+    @Override
+    public IntentState getIntentState(NetworkId networkId, Key intentKey) {
+        IntentData data = getCurrentMap(networkId).get(intentKey);
+        return (data != null) ? data.state() : null;
+    }
+
+    @Override
+    public List<Intent> getInstallableIntents(NetworkId networkId, Key intentKey) {
+        IntentData data = getCurrentMap(networkId).get(intentKey);
+        if (data != null) {
+            return data.installables();
+        }
+        return null;
+    }
+
+    @Override
+    public void write(NetworkId networkId, IntentData newData) {
+        checkNotNull(newData);
+
+        synchronized (this) {
+            // TODO this could be refactored/cleaned up
+            IntentData currentData = getCurrentMap(networkId).get(newData.key());
+            IntentData pendingData = getPendingMap(networkId).get(newData.key());
+
+            if (IntentData.isUpdateAcceptable(currentData, newData)) {
+                if (pendingData != null) {
+                    if (pendingData.state() == PURGE_REQ) {
+                        getCurrentMap(networkId).remove(newData.key(), newData);
+                    } else {
+                        getCurrentMap(networkId).put(newData.key(), IntentData.copy(newData));
+                    }
+
+                    if (pendingData.version().compareTo(newData.version()) <= 0) {
+                        // pendingData version is less than or equal to newData's
+                        // Note: a new update for this key could be pending (it's version will be greater)
+                        getPendingMap(networkId).remove(newData.key());
+                    }
+                }
+                IntentEvent.getEvent(newData).ifPresent(e -> notifyDelegate(networkId, e));
+            }
+        }
+    }
+
+    @Override
+    public void batchWrite(NetworkId networkId, Iterable<IntentData> updates) {
+        for (IntentData data : updates) {
+            write(networkId, data);
+        }
+    }
+
+    @Override
+    public Intent getIntent(NetworkId networkId, Key key) {
+        IntentData data = getCurrentMap(networkId).get(key);
+        return (data != null) ? data.intent() : null;
+    }
+
+    @Override
+    public IntentData getIntentData(NetworkId networkId, Key key) {
+        IntentData currentData = getCurrentMap(networkId).get(key);
+        if (currentData == null) {
+            return null;
+        }
+        return IntentData.copy(currentData);
+    }
+
+    @Override
+    public void addPending(NetworkId networkId, IntentData data) {
+        if (data.version() == null) { // recompiled intents will already have a version
+            data = new IntentData(data.intent(), data.state(), new SystemClockTimestamp());
+        }
+        synchronized (this) {
+            IntentData existingData = getPendingMap(networkId).get(data.key());
+            if (existingData == null ||
+                    // existing version is strictly less than data's version
+                    // Note: if they are equal, we already have the update
+                    // TODO maybe we should still make this <= to be safe?
+                    existingData.version().compareTo(data.version()) < 0) {
+                getPendingMap(networkId).put(data.key(), data);
+
+                checkNotNull(delegateMap.get(networkId), "Store delegate is not set")
+                        .process(IntentData.copy(data));
+                IntentEvent.getEvent(data).ifPresent(e -> notifyDelegate(networkId, e));
+            } else {
+                log.debug("IntentData {} is older than existing: {}",
+                          data, existingData);
+            }
+            //TODO consider also checking the current map at this point
+        }
+    }
+
+    @Override
+    public boolean isMaster(NetworkId networkId, Key intentKey) {
+        return true;
+    }
+
+    @Override
+    public Iterable<Intent> getPending(NetworkId networkId) {
+        return getPendingMap(networkId).values().stream()
+                .map(IntentData::intent)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Iterable<IntentData> getPendingData(NetworkId networkId) {
+        return Lists.newArrayList(getPendingMap(networkId).values());
+    }
+
+    @Override
+    public IntentData getPendingData(NetworkId networkId, Key intentKey) {
+        return getPendingMap(networkId).get(intentKey);
+    }
+
+    @Override
+    public Iterable<IntentData> getPendingData(NetworkId networkId,
+                                               boolean localOnly, long olderThan) {
+        long older = System.nanoTime() - olderThan * 1_000_000; //convert ms to ns
+        final SystemClockTimestamp time = new SystemClockTimestamp(older);
+        return getPendingMap(networkId).values().stream()
+                .filter(data -> data.version().isOlderThan(time) &&
+                        (!localOnly || isMaster(networkId, data.key())))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Returns the current intent map for a specific virtual network.
+     *
+     * @param networkId a virtual network identifier
+     * @return the current map for the requested virtual network
+     */
+    private Map<Key, IntentData> getCurrentMap(NetworkId networkId) {
+        currentByNetwork.computeIfAbsent(networkId,
+                                   n -> Maps.newConcurrentMap());
+        return currentByNetwork.get(networkId);
+    }
+
+    /**
+     * Returns the pending intent map for a specific virtual network.
+     *
+     * @param networkId a virtual network identifier
+     * @return the pending intent map for the requested virtual network
+     */
+    private Map<Key, IntentData> getPendingMap(NetworkId networkId) {
+        pendingByNetwork.computeIfAbsent(networkId,
+                                   n -> Maps.newConcurrentMap());
+        return pendingByNetwork.get(networkId);
+    }
+
+    public class SystemClockTimestamp implements Timestamp {
+
+        private final long nanoTimestamp;
+
+        public SystemClockTimestamp() {
+            nanoTimestamp = System.nanoTime();
+        }
+
+        public SystemClockTimestamp(long timestamp) {
+            nanoTimestamp = timestamp;
+        }
+
+        @Override
+        public int compareTo(Timestamp o) {
+            checkArgument(o instanceof SystemClockTimestamp,
+                          "Must be SystemClockTimestamp", o);
+            SystemClockTimestamp that = (SystemClockTimestamp) o;
+
+            return ComparisonChain.start()
+                    .compare(this.nanoTimestamp, that.nanoTimestamp)
+                    .result();
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMastershipStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMastershipStore.java
new file mode 100644
index 0000000..68d8ed8
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMastershipStore.java
@@ -0,0 +1,538 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ClusterEventListener;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.DefaultControllerNode;
+import org.onosproject.cluster.Node;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.cluster.RoleInfo;
+import org.onosproject.core.Version;
+import org.onosproject.core.VersionService;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkMastershipStore;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipInfo;
+import org.onosproject.mastership.MastershipStoreDelegate;
+import org.onosproject.mastership.MastershipTerm;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.MastershipRole;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.onosproject.mastership.MastershipEvent.Type.BACKUPS_CHANGED;
+import static org.onosproject.mastership.MastershipEvent.Type.MASTER_CHANGED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the virtual network mastership store to manage inventory of
+ * mastership using trivial in-memory implementation.
+ */
+@Component(immediate = true, service = VirtualNetworkMastershipStore.class)
+public class SimpleVirtualMastershipStore
+        extends AbstractVirtualStore<MastershipEvent, MastershipStoreDelegate>
+        implements VirtualNetworkMastershipStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private static final int NOTHING = 0;
+    private static final int INIT = 1;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected VersionService versionService;
+
+    //devices mapped to their masters, to emulate multiple nodes
+    protected final Map<NetworkId, Map<DeviceId, NodeId>> masterMapByNetwork =
+            new HashMap<>();
+    //emulate backups with pile of nodes
+    protected final Map<NetworkId, Map<DeviceId, List<NodeId>>> backupsByNetwork =
+            new HashMap<>();
+    //terms
+    protected final Map<NetworkId, Map<DeviceId, AtomicInteger>> termMapByNetwork =
+            new HashMap<>();
+
+    @Activate
+    public void activate() {
+        if (clusterService == null) {
+            clusterService = createFakeClusterService();
+        }
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public CompletableFuture<MastershipRole> requestRole(NetworkId networkId,
+                                                         DeviceId deviceId) {
+        //query+possible reelection
+        NodeId node = clusterService.getLocalNode().id();
+        MastershipRole role = getRole(networkId, node, deviceId);
+
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+
+        switch (role) {
+            case MASTER:
+                return CompletableFuture.completedFuture(MastershipRole.MASTER);
+            case STANDBY:
+                if (getMaster(networkId, deviceId) == null) {
+                    // no master => become master
+                    masterMap.put(deviceId, node);
+                    incrementTerm(networkId, deviceId);
+                    // remove from backup list
+                    removeFromBackups(networkId, deviceId, node);
+                    notifyDelegate(networkId, new MastershipEvent(MASTER_CHANGED, deviceId,
+                        getMastership(networkId, deviceId)));
+                    return CompletableFuture.completedFuture(MastershipRole.MASTER);
+                }
+                return CompletableFuture.completedFuture(MastershipRole.STANDBY);
+            case NONE:
+                if (getMaster(networkId, deviceId) == null) {
+                    // no master => become master
+                    masterMap.put(deviceId, node);
+                    incrementTerm(networkId, deviceId);
+                    notifyDelegate(networkId, new MastershipEvent(MASTER_CHANGED, deviceId,
+                        getMastership(networkId, deviceId)));
+                    return CompletableFuture.completedFuture(MastershipRole.MASTER);
+                }
+                // add to backup list
+                if (addToBackup(networkId, deviceId, node)) {
+                    notifyDelegate(networkId, new MastershipEvent(BACKUPS_CHANGED, deviceId,
+                        getMastership(networkId, deviceId)));
+                }
+                return CompletableFuture.completedFuture(MastershipRole.STANDBY);
+            default:
+                log.warn("unknown Mastership Role {}", role);
+        }
+        return CompletableFuture.completedFuture(role);
+    }
+
+    @Override
+    public MastershipRole getRole(NetworkId networkId, NodeId nodeId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        //just query
+        NodeId current = masterMap.get(deviceId);
+        MastershipRole role;
+
+        if (current != null && current.equals(nodeId)) {
+            return MastershipRole.MASTER;
+        }
+
+        if (backups.getOrDefault(deviceId, Collections.emptyList()).contains(nodeId)) {
+            role = MastershipRole.STANDBY;
+        } else {
+            role = MastershipRole.NONE;
+        }
+        return role;
+    }
+
+    @Override
+    public NodeId getMaster(NetworkId networkId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        return masterMap.get(deviceId);
+    }
+
+    @Override
+    public RoleInfo getNodes(NetworkId networkId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        return new RoleInfo(masterMap.get(deviceId),
+                            backups.getOrDefault(deviceId, ImmutableList.of()));
+    }
+
+    @Override
+    public MastershipInfo getMastership(NetworkId networkId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        Map<DeviceId, AtomicInteger> termMap = getTermMap(networkId);
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+        ImmutableMap.Builder<NodeId, MastershipRole> roleBuilder = ImmutableMap.builder();
+        NodeId master = masterMap.get(deviceId);
+        if (master != null) {
+            roleBuilder.put(master, MastershipRole.MASTER);
+        }
+        backups.getOrDefault(deviceId, Collections.emptyList())
+            .forEach(nodeId -> roleBuilder.put(nodeId, MastershipRole.STANDBY));
+        clusterService.getNodes().stream()
+            .filter(node -> !masterMap.containsValue(node.id()))
+            .filter(node -> !backups.get(deviceId).contains(node.id()))
+            .forEach(node -> roleBuilder.put(node.id(), MastershipRole.NONE));
+        return new MastershipInfo(
+            termMap.getOrDefault(deviceId, new AtomicInteger(NOTHING)).get(),
+            Optional.ofNullable(master),
+            roleBuilder.build());
+    }
+
+    @Override
+    public Set<DeviceId> getDevices(NetworkId networkId, NodeId nodeId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+
+        Set<DeviceId> ids = new HashSet<>();
+        for (Map.Entry<DeviceId, NodeId> d : masterMap.entrySet()) {
+            if (Objects.equals(d.getValue(), nodeId)) {
+                ids.add(d.getKey());
+            }
+        }
+        return ids;
+    }
+
+    @Override
+    public synchronized CompletableFuture<MastershipEvent> setMaster(NetworkId networkId,
+                                                        NodeId nodeId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+
+        MastershipRole role = getRole(networkId, nodeId, deviceId);
+        switch (role) {
+            case MASTER:
+                // no-op
+                return CompletableFuture.completedFuture(null);
+            case STANDBY:
+            case NONE:
+                NodeId prevMaster = masterMap.put(deviceId, nodeId);
+                incrementTerm(networkId, deviceId);
+                removeFromBackups(networkId, deviceId, nodeId);
+                addToBackup(networkId, deviceId, prevMaster);
+                break;
+            default:
+                log.warn("unknown Mastership Role {}", role);
+                return null;
+        }
+
+        return CompletableFuture.completedFuture(
+                new MastershipEvent(MASTER_CHANGED, deviceId, getMastership(networkId, deviceId)));
+    }
+
+    @Override
+    public MastershipTerm getTermFor(NetworkId networkId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        Map<DeviceId, AtomicInteger> termMap = getTermMap(networkId);
+
+        if ((termMap.get(deviceId) == null)) {
+            return MastershipTerm.of(masterMap.get(deviceId), NOTHING);
+        }
+        return MastershipTerm.of(
+                masterMap.get(deviceId), termMap.get(deviceId).get());
+    }
+
+    @Override
+    public CompletableFuture<MastershipEvent> setStandby(NetworkId networkId,
+                                                         NodeId nodeId, DeviceId deviceId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+
+        MastershipRole role = getRole(networkId, nodeId, deviceId);
+        switch (role) {
+            case MASTER:
+                NodeId backup = reelect(networkId, deviceId, nodeId);
+                if (backup == null) {
+                    // no master alternative
+                    masterMap.remove(deviceId);
+                    // TODO: Should there be new event type for no MASTER?
+                    return CompletableFuture.completedFuture(
+                            new MastershipEvent(MASTER_CHANGED, deviceId,
+                                getMastership(networkId, deviceId)));
+                } else {
+                    NodeId prevMaster = masterMap.put(deviceId, backup);
+                    incrementTerm(networkId, deviceId);
+                    addToBackup(networkId, deviceId, prevMaster);
+                    return CompletableFuture.completedFuture(
+                            new MastershipEvent(MASTER_CHANGED, deviceId,
+                                getMastership(networkId, deviceId)));
+                }
+
+            case STANDBY:
+            case NONE:
+                boolean modified = addToBackup(networkId, deviceId, nodeId);
+                if (modified) {
+                    return CompletableFuture.completedFuture(
+                            new MastershipEvent(BACKUPS_CHANGED, deviceId,
+                                getMastership(networkId, deviceId)));
+                }
+                break;
+
+            default:
+                log.warn("unknown Mastership Role {}", role);
+        }
+        return null;
+    }
+
+
+    /**
+     * Dumbly selects next-available node that's not the current one.
+     * emulate leader election.
+     *
+     * @param networkId a virtual network identifier
+     * @param deviceId a virtual device identifier
+     * @param nodeId a nod identifier
+     * @return Next available node as a leader
+     */
+    private synchronized NodeId reelect(NetworkId networkId, DeviceId deviceId,
+                                        NodeId nodeId) {
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        List<NodeId> stbys = backups.getOrDefault(deviceId, Collections.emptyList());
+        NodeId backup = null;
+        for (NodeId n : stbys) {
+            if (!n.equals(nodeId)) {
+                backup = n;
+                break;
+            }
+        }
+        stbys.remove(backup);
+        return backup;
+    }
+
+    @Override
+    public synchronized CompletableFuture<MastershipEvent>
+    relinquishRole(NetworkId networkId, NodeId nodeId, DeviceId deviceId) {
+    Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+
+        MastershipRole role = getRole(networkId, nodeId, deviceId);
+        switch (role) {
+            case MASTER:
+                NodeId backup = reelect(networkId, deviceId, nodeId);
+                masterMap.put(deviceId, backup);
+                incrementTerm(networkId, deviceId);
+                return CompletableFuture.completedFuture(
+                        new MastershipEvent(MASTER_CHANGED, deviceId,
+                            getMastership(networkId, deviceId)));
+
+            case STANDBY:
+                if (removeFromBackups(networkId, deviceId, nodeId)) {
+                    return CompletableFuture.completedFuture(
+                            new MastershipEvent(BACKUPS_CHANGED, deviceId,
+                                getMastership(networkId, deviceId)));
+                }
+                break;
+
+            case NONE:
+                break;
+
+            default:
+                log.warn("unknown Mastership Role {}", role);
+        }
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public void relinquishAllRole(NetworkId networkId, NodeId nodeId) {
+        Map<DeviceId, NodeId> masterMap = getMasterMap(networkId);
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        List<CompletableFuture<MastershipEvent>> eventFutures = new ArrayList<>();
+        Set<DeviceId> toRelinquish = new HashSet<>();
+
+        masterMap.entrySet().stream()
+                .filter(entry -> nodeId.equals(entry.getValue()))
+                .forEach(entry -> toRelinquish.add(entry.getKey()));
+
+        backups.entrySet().stream()
+                .filter(entry -> entry.getValue().contains(nodeId))
+                .forEach(entry -> toRelinquish.add(entry.getKey()));
+
+        toRelinquish.forEach(deviceId -> eventFutures.add(
+                relinquishRole(networkId, nodeId, deviceId)));
+
+        eventFutures.forEach(future -> {
+            future.whenComplete((event, error) -> notifyDelegate(networkId, event));
+        });
+    }
+
+    /**
+     * Increase the term for a device, and store it.
+     *
+     * @param networkId a virtual network identifier
+     * @param deviceId a virtual device identifier
+     */
+    private synchronized void incrementTerm(NetworkId networkId, DeviceId deviceId) {
+        Map<DeviceId, AtomicInteger> termMap = getTermMap(networkId);
+
+        AtomicInteger term = termMap.getOrDefault(deviceId, new AtomicInteger(NOTHING));
+        term.incrementAndGet();
+        termMap.put(deviceId, term);
+    }
+
+    /**
+     * Remove backup node for a device.
+     *
+     * @param networkId a virtual network identifier
+     * @param deviceId a virtual device identifier
+     * @param nodeId a node identifier
+     * @return True if success
+     */
+    private synchronized boolean removeFromBackups(NetworkId networkId,
+                                                   DeviceId deviceId, NodeId nodeId) {
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        List<NodeId> stbys = backups.getOrDefault(deviceId, new ArrayList<>());
+        boolean modified = stbys.remove(nodeId);
+        backups.put(deviceId, stbys);
+        return modified;
+    }
+
+    /**
+     * add to backup if not there already, silently ignores null node.
+     *
+     * @param networkId a virtual network identifier
+     * @param deviceId a virtual device identifier
+     * @param nodeId a node identifier
+     * @return True if success
+     */
+    private synchronized boolean addToBackup(NetworkId networkId,
+                                             DeviceId deviceId, NodeId nodeId) {
+        Map<DeviceId, List<NodeId>> backups = getBackups(networkId);
+
+        boolean modified = false;
+        List<NodeId> stbys = backups.getOrDefault(deviceId, new ArrayList<>());
+        if (nodeId != null && !stbys.contains(nodeId)) {
+            stbys.add(nodeId);
+            backups.put(deviceId, stbys);
+            modified = true;
+        }
+        return modified;
+    }
+
+    /**
+     * Returns deviceId-master map for a specified virtual network.
+     *
+     * @param networkId a virtual network identifier
+     * @return DeviceId-master map of a given virtual network.
+     */
+    private Map<DeviceId, NodeId> getMasterMap(NetworkId networkId) {
+        return masterMapByNetwork.computeIfAbsent(networkId, k -> new HashMap<>());
+    }
+
+    /**
+     * Returns deviceId-backups map for a specified virtual network.
+     *
+     * @param networkId a virtual network identifier
+     * @return DeviceId-backups map of a given virtual network.
+     */
+    private Map<DeviceId, List<NodeId>> getBackups(NetworkId networkId) {
+        return backupsByNetwork.computeIfAbsent(networkId, k -> new HashMap<>());
+    }
+
+    /**
+     * Returns deviceId-terms map for a specified virtual network.
+     *
+     * @param networkId a virtual network identifier
+     * @return DeviceId-terms map of a given virtual network.
+     */
+    private Map<DeviceId, AtomicInteger> getTermMap(NetworkId networkId) {
+        return termMapByNetwork.computeIfAbsent(networkId, k -> new HashMap<>());
+    }
+
+    /**
+     * Returns a fake cluster service for a test purpose only.
+     *
+     * @return a fake cluster service
+     */
+    private ClusterService createFakeClusterService() {
+        // just for ease of unit test
+        final ControllerNode instance =
+                new DefaultControllerNode(new NodeId("local"),
+                                          IpAddress.valueOf("127.0.0.1"));
+
+        ClusterService faceClusterService = new ClusterService() {
+
+            private final Instant creationTime = Instant.now();
+
+            @Override
+            public ControllerNode getLocalNode() {
+                return instance;
+            }
+
+            @Override
+            public Set<ControllerNode> getNodes() {
+                return ImmutableSet.of(instance);
+            }
+
+            @Override
+            public Set<Node> getConsensusNodes() {
+                return ImmutableSet.of();
+            }
+
+            @Override
+            public ControllerNode getNode(NodeId nodeId) {
+                if (instance.id().equals(nodeId)) {
+                    return instance;
+                }
+                return null;
+            }
+
+            @Override
+            public ControllerNode.State getState(NodeId nodeId) {
+                if (instance.id().equals(nodeId)) {
+                    return ControllerNode.State.ACTIVE;
+                } else {
+                    return ControllerNode.State.INACTIVE;
+                }
+            }
+
+            @Override
+            public Version getVersion(NodeId nodeId) {
+                if (instance.id().equals(nodeId)) {
+                    return versionService.version();
+                }
+                return null;
+            }
+
+            @Override
+            public Instant getLastUpdatedInstant(NodeId nodeId) {
+                return creationTime;
+            }
+
+            @Override
+            public void addListener(ClusterEventListener listener) {
+            }
+
+            @Override
+            public void removeListener(ClusterEventListener listener) {
+            }
+        };
+        return faceClusterService;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMeterStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMeterStore.java
new file mode 100644
index 0000000..5add89d
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualMeterStore.java
@@ -0,0 +1,265 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Maps;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkMeterStore;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.meter.DefaultMeter;
+import org.onosproject.net.meter.Meter;
+import org.onosproject.net.meter.MeterEvent;
+import org.onosproject.net.meter.MeterFailReason;
+import org.onosproject.net.meter.MeterFeatures;
+import org.onosproject.net.meter.MeterFeaturesKey;
+import org.onosproject.net.meter.MeterKey;
+import org.onosproject.net.meter.MeterOperation;
+import org.onosproject.net.meter.MeterStoreDelegate;
+import org.onosproject.net.meter.MeterStoreResult;
+import org.onosproject.store.service.StorageException;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static org.onosproject.net.meter.MeterFailReason.TIMEOUT;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the virtual meter store for a single instance.
+ */
+//TODO: support distributed meter store for virtual networks
+@Component(immediate = true, service = VirtualNetworkMeterStore.class)
+public class SimpleVirtualMeterStore
+        extends AbstractVirtualStore<MeterEvent, MeterStoreDelegate>
+        implements VirtualNetworkMeterStore {
+
+        private Logger log = getLogger(getClass());
+
+        @Reference(cardinality = ReferenceCardinality.MANDATORY)
+        protected ClusterService clusterService;
+
+        private ConcurrentMap<NetworkId, ConcurrentMap<MeterKey, MeterData>> meterMap =
+                Maps.newConcurrentMap();
+
+        private NodeId local;
+
+        private ConcurrentMap<NetworkId, ConcurrentMap<MeterFeaturesKey, MeterFeatures>>
+                meterFeatureMap = Maps.newConcurrentMap();
+
+        private ConcurrentMap<NetworkId,
+                ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>>> futuresMap =
+                Maps.newConcurrentMap();
+
+        @Activate
+        public void activate() {
+            log.info("Started");
+            local = clusterService.getLocalNode().id();
+        }
+
+        @Deactivate
+        public void deactivate() {
+            log.info("Stopped");
+        }
+
+        private ConcurrentMap<MeterKey, MeterData> getMetersByNetwork(NetworkId networkId) {
+            meterMap.computeIfAbsent(networkId, m -> new ConcurrentHashMap<>());
+            return meterMap.get(networkId);
+        }
+
+        private ConcurrentMap<MeterFeaturesKey, MeterFeatures>
+        getMeterFeaturesByNetwork(NetworkId networkId) {
+            meterFeatureMap.computeIfAbsent(networkId, f -> new ConcurrentHashMap<>());
+            return meterFeatureMap.get(networkId);
+        }
+
+        private ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>>
+        getFuturesByNetwork(NetworkId networkId) {
+            futuresMap.computeIfAbsent(networkId, f -> new ConcurrentHashMap<>());
+            return futuresMap.get(networkId);
+        }
+
+        @Override
+        public CompletableFuture<MeterStoreResult> storeMeter(NetworkId networkId, Meter meter) {
+
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>> futures =
+                   getFuturesByNetwork(networkId);
+
+            CompletableFuture<MeterStoreResult> future = new CompletableFuture<>();
+            MeterKey key = MeterKey.key(meter.deviceId(), meter.id());
+            futures.put(key, future);
+            MeterData data = new MeterData(meter, null, local);
+
+            try {
+                    meters.put(key, data);
+            } catch (StorageException e) {
+                    future.completeExceptionally(e);
+            }
+
+            return future;
+        }
+
+        @Override
+        public CompletableFuture<MeterStoreResult> deleteMeter(NetworkId networkId, Meter meter) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>> futures =
+                    getFuturesByNetwork(networkId);
+
+            CompletableFuture<MeterStoreResult> future = new CompletableFuture<>();
+            MeterKey key = MeterKey.key(meter.deviceId(), meter.id());
+            futures.put(key, future);
+
+            MeterData data = new MeterData(meter, null, local);
+
+            // update the state of the meter. It will be pruned by observing
+            // that it has been removed from the dataplane.
+            try {
+                    if (meters.computeIfPresent(key, (k, v) -> data) == null) {
+                            future.complete(MeterStoreResult.success());
+                    }
+            } catch (StorageException e) {
+                    future.completeExceptionally(e);
+            }
+
+            return future;
+        }
+
+        @Override
+        public MeterStoreResult storeMeterFeatures(NetworkId networkId, MeterFeatures meterfeatures) {
+            ConcurrentMap<MeterFeaturesKey, MeterFeatures> meterFeatures
+                    = getMeterFeaturesByNetwork(networkId);
+
+            MeterStoreResult result = MeterStoreResult.success();
+            MeterFeaturesKey key = MeterFeaturesKey.key(meterfeatures.deviceId());
+            try {
+                    meterFeatures.putIfAbsent(key, meterfeatures);
+            } catch (StorageException e) {
+                    result = MeterStoreResult.fail(TIMEOUT);
+            }
+            return result;
+        }
+
+        @Override
+        public MeterStoreResult deleteMeterFeatures(NetworkId networkId, DeviceId deviceId) {
+            ConcurrentMap<MeterFeaturesKey, MeterFeatures> meterFeatures
+                    = getMeterFeaturesByNetwork(networkId);
+
+            MeterStoreResult result = MeterStoreResult.success();
+            MeterFeaturesKey key = MeterFeaturesKey.key(deviceId);
+            try {
+                    meterFeatures.remove(key);
+            } catch (StorageException e) {
+                    result = MeterStoreResult.fail(TIMEOUT);
+            }
+            return result;
+        }
+
+        @Override
+        public CompletableFuture<MeterStoreResult> updateMeter(NetworkId networkId, Meter meter) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+            ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>> futures =
+                    getFuturesByNetwork(networkId);
+
+            CompletableFuture<MeterStoreResult> future = new CompletableFuture<>();
+            MeterKey key = MeterKey.key(meter.deviceId(), meter.id());
+            futures.put(key, future);
+
+            MeterData data = new MeterData(meter, null, local);
+            try {
+                    if (meters.computeIfPresent(key, (k, v) -> data) == null) {
+                            future.complete(MeterStoreResult.fail(MeterFailReason.INVALID_METER));
+                    }
+            } catch (StorageException e) {
+                    future.completeExceptionally(e);
+            }
+            return future;
+        }
+
+        @Override
+        public void updateMeterState(NetworkId networkId, Meter meter) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            MeterKey key = MeterKey.key(meter.deviceId(), meter.id());
+            meters.computeIfPresent(key, (k, v) -> {
+                    DefaultMeter m = (DefaultMeter) v.meter();
+                    m.setState(meter.state());
+                    m.setProcessedPackets(meter.packetsSeen());
+                    m.setProcessedBytes(meter.bytesSeen());
+                    m.setLife(meter.life());
+                    // TODO: Prune if drops to zero.
+                    m.setReferenceCount(meter.referenceCount());
+                    return new MeterData(m, null, v.origin());
+            });
+        }
+
+        @Override
+        public Meter getMeter(NetworkId networkId, MeterKey key) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            MeterData data = meters.get(key);
+            return data == null ? null : data.meter();
+        }
+
+        @Override
+        public Collection<Meter> getAllMeters(NetworkId networkId) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            return Collections2.transform(meters.values(), MeterData::meter);
+        }
+
+        @Override
+        public void failedMeter(NetworkId networkId, MeterOperation op, MeterFailReason reason) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+
+            MeterKey key = MeterKey.key(op.meter().deviceId(), op.meter().id());
+            meters.computeIfPresent(key, (k, v) ->
+                    new MeterData(v.meter(), reason, v.origin()));
+        }
+
+        @Override
+        public void deleteMeterNow(NetworkId networkId, Meter m) {
+            ConcurrentMap<MeterKey, MeterData> meters = getMetersByNetwork(networkId);
+            ConcurrentMap<MeterKey, CompletableFuture<MeterStoreResult>> futures =
+                    getFuturesByNetwork(networkId);
+
+            MeterKey key = MeterKey.key(m.deviceId(), m.id());
+            futures.remove(key);
+            meters.remove(key);
+        }
+
+        @Override
+        public long getMaxMeters(NetworkId networkId, MeterFeaturesKey key) {
+            ConcurrentMap<MeterFeaturesKey, MeterFeatures> meterFeatures
+                    = getMeterFeaturesByNetwork(networkId);
+
+            MeterFeatures features = meterFeatures.get(key);
+            return features == null ? 0L : features.maxMeter();
+        }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualPacketStore.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualPacketStore.java
new file mode 100644
index 0000000..e0bb7c6
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/SimpleVirtualPacketStore.java
@@ -0,0 +1,125 @@
+/*
+ * 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.store.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualNetworkPacketStore;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketEvent;
+import org.onosproject.net.packet.PacketRequest;
+import org.onosproject.net.packet.PacketStoreDelegate;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Simple single instance implementation of the virtual packet store.
+ */
+//TODO: support distributed packet store for virtual networks
+
+@Component(immediate = true, service = VirtualNetworkPacketStore.class)
+public class SimpleVirtualPacketStore
+        extends AbstractVirtualStore<PacketEvent, PacketStoreDelegate>
+        implements VirtualNetworkPacketStore {
+
+    private final Logger log = getLogger(getClass());
+
+    private Map<NetworkId, Map<TrafficSelector, Set<PacketRequest>>> requests
+            = Maps.newConcurrentMap();
+
+    @Activate
+    public void activate() {
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public void emit(NetworkId networkId, OutboundPacket packet) {
+        notifyDelegate(networkId, new PacketEvent(PacketEvent.Type.EMIT, packet));
+    }
+
+    @Override
+    public void requestPackets(NetworkId networkId, PacketRequest request) {
+        requests.computeIfAbsent(networkId, k -> Maps.newConcurrentMap());
+
+        requests.get(networkId).compute(request.selector(), (s, existingRequests) -> {
+            if (existingRequests == null) {
+                if (hasDelegate(networkId)) {
+                    delegateMap.get(networkId).requestPackets(request);
+                }
+                return ImmutableSet.of(request);
+            } else if (!existingRequests.contains(request)) {
+                if (hasDelegate(networkId)) {
+                    delegateMap.get(networkId).requestPackets(request);
+                }
+                return ImmutableSet.<PacketRequest>builder()
+                        .addAll(existingRequests)
+                        .add(request)
+                        .build();
+            } else {
+                return existingRequests;
+            }
+        });
+    }
+
+    @Override
+    public void cancelPackets(NetworkId networkId, PacketRequest request) {
+        requests.get(networkId).computeIfPresent(request.selector(), (s, existingRequests) -> {
+            if (existingRequests.contains(request)) {
+                HashSet<PacketRequest> newRequests = Sets.newHashSet(existingRequests);
+                newRequests.remove(request);
+                if (hasDelegate(networkId)) {
+                    delegateMap.get(networkId).cancelPackets(request);
+                }
+                if (newRequests.size() > 0) {
+                    return ImmutableSet.copyOf(newRequests);
+                } else {
+                    return null;
+                }
+            } else {
+                return existingRequests;
+            }
+        });
+    }
+
+    @Override
+    public List<PacketRequest> existingRequests(NetworkId networkId) {
+        List<PacketRequest> list = Lists.newArrayList();
+        if (requests.get(networkId) != null) {
+            requests.get(networkId).values().forEach(list::addAll);
+            list.sort((o1, o2) -> o1.priority().priorityValue() - o2.priority().priorityValue());
+        }
+        return list;
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/package-info.java
new file mode 100644
index 0000000..99b5d82
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Implementation of virtual network stores.
+ */
+package org.onosproject.incubator.net.virtual.store.impl;
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualDeviceId.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualDeviceId.java
new file mode 100644
index 0000000..6a38661
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualDeviceId.java
@@ -0,0 +1,64 @@
+/*
+ * 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.store.impl.primitives;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.DeviceId;
+
+import java.util.Objects;
+
+/**
+ * A wrapper class to isolate device id from other virtual networks.
+ */
+public class VirtualDeviceId {
+
+    NetworkId networkId;
+    DeviceId deviceId;
+
+    public VirtualDeviceId(NetworkId networkId, DeviceId deviceId) {
+        this.networkId = networkId;
+        this.deviceId = deviceId;
+    }
+
+    public NetworkId networkId() {
+        return networkId;
+    }
+
+    public DeviceId deviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkId, deviceId);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof VirtualDeviceId) {
+            VirtualDeviceId that = (VirtualDeviceId) obj;
+            return this.deviceId.equals(that.deviceId) &&
+                    this.networkId.equals(that.networkId);
+        }
+        return false;
+    }
+}
+
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowEntry.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowEntry.java
new file mode 100644
index 0000000..2a00883
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowEntry.java
@@ -0,0 +1,63 @@
+/*
+ * 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.store.impl.primitives;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.flow.FlowEntry;
+
+import java.util.Objects;
+
+/**
+ * A wrapper class to encapsulate flow entry.
+ */
+public class VirtualFlowEntry {
+    NetworkId networkId;
+    FlowEntry flowEntry;
+
+    public VirtualFlowEntry(NetworkId networkId, FlowEntry flowEntry) {
+        this.networkId = networkId;
+        this.flowEntry = flowEntry;
+    }
+
+    public NetworkId networkId() {
+        return networkId;
+    }
+
+    public FlowEntry flowEntry() {
+        return flowEntry;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkId, flowEntry);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof VirtualFlowEntry) {
+            VirtualFlowEntry that = (VirtualFlowEntry) other;
+            return this.networkId.equals(that.networkId) &&
+                    this.flowEntry.equals(that.flowEntry);
+        } else {
+            return false;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRule.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRule.java
new file mode 100644
index 0000000..ea64904
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRule.java
@@ -0,0 +1,65 @@
+/*
+ * 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.store.impl.primitives;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.flow.FlowRule;
+
+import java.util.Objects;
+
+/**
+ * A wrapper class to encapsulate flow rule.
+ */
+public class VirtualFlowRule {
+    NetworkId networkId;
+    FlowRule rule;
+
+    public VirtualFlowRule(NetworkId networkId, FlowRule rule) {
+        this.networkId = networkId;
+        this.rule = rule;
+    }
+
+    public NetworkId networkId() {
+        return networkId;
+    }
+
+    public FlowRule rule() {
+        return rule;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkId, rule);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this ==  other) {
+            return true;
+        }
+
+        if (other instanceof VirtualFlowRule) {
+            VirtualFlowRule that = (VirtualFlowRule) other;
+            return this.networkId.equals(that.networkId) &&
+                    this.rule.equals(that.rule);
+        } else {
+            return false;
+        }
+    }
+}
+
+
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchEvent.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchEvent.java
new file mode 100644
index 0000000..d1b5d57
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchEvent.java
@@ -0,0 +1,65 @@
+/*
+ * 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.store.impl.primitives;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchEvent;
+
+import java.util.Objects;
+
+/**
+ * A wrapper class to encapsulate flow rule batch event.
+ */
+public class VirtualFlowRuleBatchEvent {
+    NetworkId networkId;
+    FlowRuleBatchEvent event;
+
+    public VirtualFlowRuleBatchEvent(NetworkId networkId, FlowRuleBatchEvent event) {
+        this.networkId = networkId;
+        this.event = event;
+    }
+
+    public NetworkId networkId() {
+        return networkId;
+    }
+
+    public FlowRuleBatchEvent event() {
+        return event;
+    }
+
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkId, event);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof VirtualFlowRuleBatchEvent) {
+            VirtualFlowRuleBatchEvent that = (VirtualFlowRuleBatchEvent) other;
+            return this.networkId.equals(that.networkId) &&
+                    this.event.equals(that.event);
+        } else {
+            return false;
+        }
+    }
+}
+
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchOperation.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchOperation.java
new file mode 100644
index 0000000..8db1801
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/VirtualFlowRuleBatchOperation.java
@@ -0,0 +1,64 @@
+/*
+ * 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.store.impl.primitives;
+
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.net.flow.oldbatch.FlowRuleBatchOperation;
+
+import java.util.Objects;
+
+/**
+ * A wrapper class to encapsulate flow rule batch operation.
+ */
+public class VirtualFlowRuleBatchOperation {
+    NetworkId networkId;
+    FlowRuleBatchOperation operation;
+
+    public VirtualFlowRuleBatchOperation(NetworkId networkId,
+                                         FlowRuleBatchOperation operation) {
+        this.networkId = networkId;
+        this.operation = operation;
+    }
+
+    public NetworkId networkId() {
+        return networkId;
+    }
+
+    public FlowRuleBatchOperation operation() {
+        return operation;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkId, operation);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this ==  other) {
+            return true;
+        }
+
+        if (other instanceof VirtualFlowRuleBatchOperation) {
+            VirtualFlowRuleBatchOperation that = (VirtualFlowRuleBatchOperation) other;
+            return this.networkId.equals(that.networkId) &&
+                    this.operation.equals(that.operation);
+        } else {
+            return false;
+        }
+    }
+}
diff --git a/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/package-info.java b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/package-info.java
new file mode 100644
index 0000000..ee0de54
--- /dev/null
+++ b/apps/virtual/app/src/main/java/org/onosproject/incubator/net/virtual/store/impl/primitives/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+/**
+ * Implementation of distributed virtual network store primitives.
+ */
+package org.onosproject.incubator.net.virtual.store.impl.primitives;