ONOS-2184 VirtualHost CLI and REST api's

Change-Id: If0ebe4268f3161a34223eca58e3f1bdbb8d0c9be
diff --git a/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostCreateCommand.java b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostCreateCommand.java
new file mode 100644
index 0000000..fc66626
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostCreateCommand.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.cli.net.vnet;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.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.
+ */
+@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)
+    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 execute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+
+        Set<IpAddress> hostIps = new HashSet<>();
+        if (hostIpStrings != null) {
+            Arrays.asList(hostIpStrings).stream().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/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostListCommand.java b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostListCommand.java
new file mode 100644
index 0000000..c5f6dac
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostListCommand.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.cli.net.vnet;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+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.
+ */
+@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)
+    Long networkId = null;
+
+    @Override
+    protected void execute() {
+        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/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostRemoveCommand.java b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostRemoveCommand.java
new file mode 100644
index 0000000..b2f3e99
--- /dev/null
+++ b/cli/src/main/java/org/onosproject/cli/net/vnet/VirtualHostRemoveCommand.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.cli.net.vnet;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+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.
+ */
+
+@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)
+    Long networkId = null;
+
+    @Argument(index = 1, name = "id", description = "Host ID",
+              required = true, multiValued = false)
+    String id = null;
+
+    @Override
+    protected void execute() {
+        VirtualNetworkAdminService service = get(VirtualNetworkAdminService.class);
+        service.removeVirtualHost(NetworkId.networkId(networkId), HostId.hostId(id));
+        print("Virtual host successfully removed.");
+    }
+}
diff --git a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
index 0681129..92da929 100644
--- a/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
+++ b/cli/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -674,6 +674,15 @@
         <command>
             <action class="org.onosproject.cli.net.vnet.VirtualPortRemoveCommand"/>
         </command>
+        <command>
+            <action class="org.onosproject.cli.net.vnet.VirtualHostListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.cli.net.vnet.VirtualHostCreateCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.cli.net.vnet.VirtualHostRemoveCommand"/>
+        </command>
     </command-bundle>
 
     <bean id="reviewAppNameCompleter" class="org.onosproject.cli.security.ReviewApplicationNameCompleter"/>
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
index 91ad4ff..ea2f339 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
@@ -30,6 +30,7 @@
 import org.onosproject.core.ApplicationId;
 import org.onosproject.incubator.net.virtual.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.VirtualPort;
@@ -141,6 +142,7 @@
         registerCodec(VirtualDevice.class, new VirtualDeviceCodec());
         registerCodec(VirtualPort.class, new VirtualPortCodec());
         registerCodec(VirtualLink.class, new VirtualLinkCodec());
+        registerCodec(VirtualHost.class, new VirtualHostCodec());
         registerCodec(MastershipTerm.class, new MastershipTermCodec());
         registerCodec(MastershipRole.class, new MastershipRoleCodec());
         registerCodec(RoleInfo.class, new RoleInfoCodec());
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/VirtualHostCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/VirtualHostCodec.java
new file mode 100644
index 0000000..7515f1c
--- /dev/null
+++ b/core/common/src/main/java/org/onosproject/codec/impl/VirtualHostCodec.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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.codec.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.incubator.net.virtual.DefaultVirtualHost;
+import org.onosproject.incubator.net.virtual.NetworkId;
+import org.onosproject.incubator.net.virtual.VirtualHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+
+/**
+ * Codec for the VirtualHost class.
+ */
+public class VirtualHostCodec extends JsonCodec<VirtualHost> {
+
+    // JSON field names
+    private static final String NETWORK_ID = "networkId";
+    private static final String HOST_ID = "id";
+    private static final String MAC_ADDRESS = "mac";
+    private static final String VLAN = "vlan";
+    private static final String IP_ADDRESSES = "ipAddresses";
+    private static final String HOST_LOCATION = "location";
+
+    private static final String NULL_OBJECT_MSG = "VirtualHost cannot be null";
+    private static final String MISSING_MEMBER_MSG = " member is required in VirtualHost";
+
+    @Override
+    public ObjectNode encode(VirtualHost vHost, CodecContext context) {
+        checkNotNull(vHost, NULL_OBJECT_MSG);
+
+        final JsonCodec<HostLocation> locationCodec =
+                context.codec(HostLocation.class);
+        final ObjectNode result = context.mapper().createObjectNode()
+                .put(NETWORK_ID, vHost.networkId().toString())
+                .put(HOST_ID, vHost.id().toString())
+                .put(MAC_ADDRESS, vHost.mac().toString())
+                .put(VLAN, vHost.vlan().toString());
+
+        final ArrayNode jsonIpAddresses = result.putArray(IP_ADDRESSES);
+        for (final IpAddress ipAddress : vHost.ipAddresses()) {
+            jsonIpAddresses.add(ipAddress.toString());
+        }
+        result.set(IP_ADDRESSES, jsonIpAddresses);
+        result.set(HOST_LOCATION, locationCodec.encode(vHost.location(), context));
+
+        return result;
+    }
+
+    @Override
+    public VirtualHost decode(ObjectNode json, CodecContext context) {
+        if (json == null || !json.isObject()) {
+            return null;
+        }
+
+        NetworkId nId = NetworkId.networkId(Long.parseLong(extractMember(NETWORK_ID, json)));
+        MacAddress mac = MacAddress.valueOf(json.get("mac").asText());
+        VlanId vlanId = VlanId.vlanId((short) json.get("vlan").asInt(VlanId.UNTAGGED));
+        JsonNode locationNode = json.get("location");
+        PortNumber portNumber = PortNumber.portNumber(locationNode.get("port").asText());
+        DeviceId deviceId = DeviceId.deviceId(locationNode.get("elementId").asText());
+        HostLocation hostLocation = new HostLocation(deviceId, portNumber, 0);
+        HostId id = HostId.hostId(mac, vlanId);
+
+        Iterator<JsonNode> ipStrings = json.get("ipAddresses").elements();
+        Set<IpAddress> ips = new HashSet<>();
+        while (ipStrings.hasNext()) {
+            ips.add(IpAddress.valueOf(ipStrings.next().asText()));
+        }
+
+        return new DefaultVirtualHost(nId, id, mac, vlanId, hostLocation, ips);
+    }
+
+    /**
+     * Extract member from JSON ObjectNode.
+     *
+     * @param key key for which value is needed
+     * @param json JSON ObjectNode
+     * @return member value
+     */
+    private String extractMember(String key, ObjectNode json) {
+        return nullIsIllegal(json.get(key), key + MISSING_MEMBER_MSG).asText();
+    }
+}
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/VirtualNetworkWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/VirtualNetworkWebResource.java
index aaedb98..dca328d 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/VirtualNetworkWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/VirtualNetworkWebResource.java
@@ -21,6 +21,7 @@
 import org.onosproject.incubator.net.virtual.NetworkId;
 import org.onosproject.incubator.net.virtual.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;
@@ -390,6 +391,87 @@
     }
 
     /**
+     * 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 = (ObjectNode) mapper().readTree(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 = (ObjectNode) mapper().readTree(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
diff --git a/web/api/src/main/resources/definitions/VirtualHost.json b/web/api/src/main/resources/definitions/VirtualHost.json
new file mode 100644
index 0000000..e27d148
--- /dev/null
+++ b/web/api/src/main/resources/definitions/VirtualHost.json
@@ -0,0 +1,60 @@
+{
+  "type": "object",
+  "title": "host",
+  "required": [
+    "networkId",
+    "id",
+    "mac",
+    "vlan",
+    "ipAddresses",
+    "location"
+  ],
+  "properties": {
+    "networkId": {
+      "type": "int64",
+      "description": "Network identifier",
+      "example": 3
+    },
+    "id": {
+      "type": "string",
+      "example": "46:E4:3C:A4:17:C8/-1"
+    },
+    "mac": {
+      "type": "string",
+      "example": "46:E4:3C:A4:17:C8"
+    },
+    "vlan": {
+      "type": "string",
+      "example": "-1"
+    },
+    "ipAddresses": {
+      "type": "array",
+      "xml": {
+        "name": "hosts",
+        "wrapped": true
+      },
+      "items": {
+        "type": "string",
+        "example": "127.0.0.1"
+      }
+    },
+    "location": {
+      "type": "object",
+      "title": "location",
+      "required": [
+        "elementId",
+        "port"
+      ],
+      "properties": {
+        "elementId": {
+          "type": "string",
+          "example": "of:0000000000000002"
+        },
+        "port": {
+          "type": "string",
+          "example": "3"
+        }
+      }
+    }
+  }
+}
diff --git a/web/api/src/main/resources/definitions/VirtualHostPut.json b/web/api/src/main/resources/definitions/VirtualHostPut.json
new file mode 100644
index 0000000..c0b8eba
--- /dev/null
+++ b/web/api/src/main/resources/definitions/VirtualHostPut.json
@@ -0,0 +1,55 @@
+{
+  "type": "object",
+  "title": "host",
+  "required": [
+    "networkId",
+    "mac",
+    "vlan",
+    "ipAddresses",
+    "location"
+  ],
+  "properties": {
+    "networkId": {
+      "type": "int64",
+      "description": "Network identifier",
+      "example": 3
+    },
+    "mac": {
+      "type": "string",
+      "example": "46:E4:3C:A4:17:C8"
+    },
+    "vlan": {
+      "type": "string",
+      "example": "-1"
+    },
+    "ipAddresses": {
+      "type": "array",
+      "xml": {
+        "name": "hosts",
+        "wrapped": true
+      },
+      "items": {
+        "type": "string",
+        "example": "127.0.0.1"
+      }
+    },
+    "location": {
+      "type": "object",
+      "title": "location",
+      "required": [
+        "elementId",
+        "port"
+      ],
+      "properties": {
+        "elementId": {
+          "type": "string",
+          "example": "of:0000000000000002"
+        },
+        "port": {
+          "type": "string",
+          "example": "3"
+        }
+      }
+    }
+  }
+}
diff --git a/web/api/src/main/resources/definitions/VirtualHosts.json b/web/api/src/main/resources/definitions/VirtualHosts.json
new file mode 100644
index 0000000..001867e
--- /dev/null
+++ b/web/api/src/main/resources/definitions/VirtualHosts.json
@@ -0,0 +1,76 @@
+{
+  "type": "object",
+  "title": "hosts",
+  "required": [
+    "hosts"
+  ],
+  "properties": {
+    "hosts": {
+      "type": "array",
+      "xml": {
+        "name": "hosts",
+        "wrapped": true
+      },
+      "items": {
+        "type": "object",
+        "title": "host",
+        "required": [
+          "networkId",
+          "id",
+          "mac",
+          "vlan",
+          "ipAddresses",
+          "location"
+        ],
+        "properties": {
+          "networkId": {
+            "type": "int64",
+            "description": "Network identifier",
+            "example": 3
+          },
+          "id": {
+            "type": "string",
+            "example": "46:E4:3C:A4:17:C8/-1"
+          },
+          "mac": {
+            "type": "string",
+            "example": "46:E4:3C:A4:17:C8"
+          },
+          "vlan": {
+            "type": "string",
+            "example": "-1"
+          },
+          "ipAddresses": {
+            "type": "array",
+            "xml": {
+              "name": "hosts",
+              "wrapped": true
+            },
+            "items": {
+              "type": "string",
+              "example": "127.0.0.1"
+            }
+          },
+          "location": {
+            "type": "object",
+            "title": "location",
+            "required": [
+              "elementId",
+              "port"
+            ],
+            "properties": {
+              "elementId": {
+                "type": "string",
+                "example": "of:0000000000000002"
+              },
+              "port": {
+                "type": "string",
+                "example": "3"
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/web/api/src/test/java/org/onosproject/rest/resources/VirtualNetworkWebResourceTest.java b/web/api/src/test/java/org/onosproject/rest/resources/VirtualNetworkWebResourceTest.java
index 7c00e55..720bd9a 100644
--- a/web/api/src/test/java/org/onosproject/rest/resources/VirtualNetworkWebResourceTest.java
+++ b/web/api/src/test/java/org/onosproject/rest/resources/VirtualNetworkWebResourceTest.java
@@ -21,6 +21,7 @@
 import com.eclipsesource.json.JsonObject;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import org.glassfish.jersey.client.ClientProperties;
 import org.hamcrest.Description;
 import org.hamcrest.Matchers;
@@ -29,16 +30,21 @@
 import org.junit.Test;
 import org.onlab.osgi.ServiceDirectory;
 import org.onlab.osgi.TestServiceDirectory;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onlab.rest.BaseResource;
 import org.onosproject.codec.CodecService;
 import org.onosproject.codec.impl.CodecManager;
 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.incubator.net.virtual.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;
@@ -50,6 +56,8 @@
 import org.onosproject.net.DefaultPort;
 import org.onosproject.net.Device;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
 import org.onosproject.net.NetTestTools;
 import org.onosproject.net.Port;
 import org.onosproject.net.PortNumber;
@@ -93,9 +101,8 @@
     private final VirtualNetworkService mockVnetService = createMock(VirtualNetworkService.class);
     private CodecManager codecService;
 
-    final HashSet<TenantId> tenantIdSet = new HashSet<>();
-    final HashSet<VirtualDevice> vdevSet = new HashSet<>();
-    final HashSet<VirtualPort> vportSet = new HashSet<>();
+    private final HashSet<VirtualDevice> vdevSet = new HashSet<>();
+    private final HashSet<VirtualPort> vportSet = new HashSet<>();
 
     private static final String ID = "networkId";
     private static final String TENANT_ID = "tenantId";
@@ -104,7 +111,6 @@
     private static final String PHYS_DEVICE_ID = "physDeviceId";
     private static final String PHYS_PORT_NUM = "physPortNum";
 
-    private final TenantId tenantId1 = TenantId.tenantId("TenantId1");
     private final TenantId tenantId2 = TenantId.tenantId("TenantId2");
     private final TenantId tenantId3 = TenantId.tenantId("TenantId3");
     private final TenantId tenantId4 = TenantId.tenantId("TenantId4");
@@ -128,11 +134,10 @@
 
     private final Device dev1 = NetTestTools.device("dev1");
     private final Device dev2 = NetTestTools.device("dev2");
-    private final Device dev21 = NetTestTools.device("dev21");
     private final Device dev22 = NetTestTools.device("dev22");
 
-    Port port1 = new DefaultPort(dev1, portNumber(1), true);
-    Port port2 = new DefaultPort(dev2, portNumber(2), true);
+    private final Port port1 = new DefaultPort(dev1, portNumber(1), true);
+    private final Port port2 = new DefaultPort(dev2, portNumber(2), true);
 
     private final VirtualPort vport22 = new DefaultVirtualPort(networkId3,
                                                                dev22, portNumber(22), port1);
@@ -156,6 +161,28 @@
             .dst(cp21)
             .build();
 
+    private final MacAddress mac1 = MacAddress.valueOf("00:11:00:00:00:01");
+    private final MacAddress mac2 = MacAddress.valueOf("00:22:00:00:00:02");
+    private final VlanId vlan1 = VlanId.vlanId((short) 11);
+    private final VlanId vlan2 = VlanId.vlanId((short) 22);
+    private final IpAddress ip1 = IpAddress.valueOf("10.0.0.1");
+    private final IpAddress ip2 = IpAddress.valueOf("10.0.0.2");
+    private final IpAddress ip3 = IpAddress.valueOf("10.0.0.3");
+
+    private final HostId hId1 = HostId.hostId(mac1, vlan1);
+    private final HostId hId2 = HostId.hostId(mac2, vlan2);
+    private final HostLocation loc1 = new HostLocation(devId1, portNumber(100), 123L);
+    private final HostLocation loc2 = new HostLocation(devId2, portNumber(200), 123L);
+    private final Set<IpAddress> ipSet1 = Sets.newHashSet(ip1, ip2);
+    private final Set<IpAddress> ipSet2 = Sets.newHashSet(ip1, ip3);
+    private final VirtualHost vhost1 = new DefaultVirtualHost(networkId1, hId1,
+                                                              mac1, vlan1, loc1, ipSet1);
+    private final VirtualHost vhost2 = new DefaultVirtualHost(networkId2, hId2,
+                                                              mac2, vlan2, loc2, ipSet2);
+
+
+
+
     /**
      * Sets up the global values for all the tests.
      */
@@ -177,15 +204,15 @@
      * Hamcrest matcher to check that a virtual network entity representation in JSON matches
      * the actual virtual network entity.
      */
-    public static class JsonObjectMatcher<T> extends TypeSafeMatcher<JsonObject> {
+    private static final class JsonObjectMatcher<T> extends TypeSafeMatcher<JsonObject> {
         private final T vnetEntity;
         private List<String> jsonFieldNames;
         private String reason = "";
         private BiFunction<T, String, String> getValue; // get vnetEntity's value
 
-        public JsonObjectMatcher(T vnetEntityValue,
-                                 List<String> jsonFieldNames1,
-                                 BiFunction<T, String, String> getValue1) {
+        private JsonObjectMatcher(T vnetEntityValue,
+                                  List<String> jsonFieldNames1,
+                                  BiFunction<T, String, String> getValue1) {
             vnetEntity = vnetEntityValue;
             jsonFieldNames = jsonFieldNames1;
             getValue = getValue1;
@@ -238,7 +265,7 @@
      * Hamcrest matcher to check that a virtual network entity is represented properly in a JSON
      * array of virtual network entities.
      */
-    public static class JsonArrayMatcher<T> extends TypeSafeMatcher<JsonArray> {
+    protected static class JsonArrayMatcher<T> extends TypeSafeMatcher<JsonArray> {
         private final T vnetEntity;
         private String reason = "";
         private Function<T, String> getKey; // gets vnetEntity's key
@@ -246,10 +273,10 @@
         private List<String> jsonFieldNames; // field/property names
         private BiFunction<T, String, String> getValue; // get vnetEntity's value
 
-        public JsonArrayMatcher(T vnetEntityValue, Function<T, String> getKey1,
-                                BiPredicate<T, JsonObject> checkKey1,
-                                List<String> jsonFieldNames1,
-                                BiFunction<T, String, String> getValue1) {
+        protected JsonArrayMatcher(T vnetEntityValue, Function<T, String> getKey1,
+                                   BiPredicate<T, JsonObject> checkKey1,
+                                   List<String> jsonFieldNames1,
+                                   BiFunction<T, String, String> getValue1) {
             vnetEntity = vnetEntityValue;
             getKey = getKey1;
             checkKey = checkKey1;
@@ -292,9 +319,9 @@
     /**
      * Array matcher for VirtualNetwork.
      */
-    public static class VnetJsonArrayMatcher extends JsonArrayMatcher<VirtualNetwork> {
+    private static final class VnetJsonArrayMatcher extends JsonArrayMatcher<VirtualNetwork> {
 
-        public VnetJsonArrayMatcher(VirtualNetwork vnetIn) {
+        private VnetJsonArrayMatcher(VirtualNetwork vnetIn) {
             super(vnetIn,
                   vnet -> "Virtual network " + vnet.id().toString(),
                   (vnet, jsonObject) -> jsonObject.get(ID).asString().equals(vnet.id().toString()),
@@ -571,9 +598,9 @@
     /**
      * Array matcher for VirtualDevice.
      */
-    public static class VdevJsonArrayMatcher extends JsonArrayMatcher<VirtualDevice> {
+    private static final class VdevJsonArrayMatcher extends JsonArrayMatcher<VirtualDevice> {
 
-        public VdevJsonArrayMatcher(VirtualDevice vdevIn) {
+        private VdevJsonArrayMatcher(VirtualDevice vdevIn) {
             super(vdevIn,
                   vdev -> "Virtual device " + vdev.networkId().toString()
                           + " " + vdev.id().toString(),
@@ -726,9 +753,9 @@
     /**
      * Array matcher for VirtualPort.
      */
-    public static class VportJsonArrayMatcher extends JsonArrayMatcher<VirtualPort> {
+    private static final class VportJsonArrayMatcher extends JsonArrayMatcher<VirtualPort> {
 
-        public VportJsonArrayMatcher(VirtualPort vportIn) {
+        private VportJsonArrayMatcher(VirtualPort vportIn) {
             super(vportIn,
                   vport -> "Virtual port " + vport.networkId().toString() + " "
                     + vport.element().id().toString() + " " + vport.number().toString(),
@@ -887,11 +914,11 @@
      * Hamcrest matcher to check that a virtual link representation in JSON matches
      * the actual virtual link.
      */
-    public static class VirtualLinkJsonMatcher extends LinksResourceTest.LinkJsonMatcher {
+    private static final class VirtualLinkJsonMatcher extends LinksResourceTest.LinkJsonMatcher {
         private final VirtualLink vlink;
         private String reason = "";
 
-        public VirtualLinkJsonMatcher(VirtualLink vlinkValue) {
+        private VirtualLinkJsonMatcher(VirtualLink vlinkValue) {
             super(vlinkValue);
             vlink = vlinkValue;
         }
@@ -931,18 +958,16 @@
      * Hamcrest matcher to check that a virtual link is represented properly in a JSON
      * array of links.
      */
-    private static class VirtualLinkJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
+    private static final class VirtualLinkJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
         private final VirtualLink vlink;
         private String reason = "";
 
-        public VirtualLinkJsonArrayMatcher(VirtualLink vlinkValue) {
+        private VirtualLinkJsonArrayMatcher(VirtualLink vlinkValue) {
             vlink = vlinkValue;
         }
 
         @Override
         public boolean matchesSafely(JsonArray json) {
-            final int expectedAttributes = 2;
-
             for (int jsonLinkIndex = 0; jsonLinkIndex < json.size();
                  jsonLinkIndex++) {
 
@@ -1037,4 +1062,207 @@
         assertThat(response.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT));
         verify(mockVnetAdminService);
     }
+
+    // Tests for Virtual Hosts
+
+    /**
+     * Tests the result of the REST API GET when there are no virtual hosts.
+     */
+    @Test
+    public void testGetVirtualHostsEmptyArray() {
+        NetworkId networkId = networkId4;
+        expect(mockVnetService.getVirtualHosts(networkId)).andReturn(ImmutableSet.of()).anyTimes();
+        replay(mockVnetService);
+
+        WebTarget wt = target();
+        String location = "vnets/" + networkId.toString() + "/hosts";
+        String response = wt.path(location).request().get(String.class);
+        assertThat(response, is("{\"hosts\":[]}"));
+
+        verify(mockVnetService);
+    }
+
+    /**
+     * Tests the result of the REST API GET when virtual hosts are defined.
+     */
+    @Test
+    public void testGetVirtualHostsArray() {
+        NetworkId networkId = networkId3;
+        final Set<VirtualHost> vhostSet = ImmutableSet.of(vhost1, vhost2);
+        expect(mockVnetService.getVirtualHosts(networkId)).andReturn(vhostSet).anyTimes();
+        replay(mockVnetService);
+
+        WebTarget wt = target();
+        String location = "vnets/" + networkId.toString() + "/hosts";
+        String response = wt.path(location).request().get(String.class);
+        assertThat(response, containsString("{\"hosts\":["));
+
+        final JsonObject result = Json.parse(response).asObject();
+        assertThat(result, notNullValue());
+
+        assertThat(result.names(), hasSize(1));
+        assertThat(result.names().get(0), is("hosts"));
+
+        final JsonArray vnetJsonArray = result.get("hosts").asArray();
+        assertThat(vnetJsonArray, notNullValue());
+        assertEquals("Virtual hosts array is not the correct size.",
+                     vhostSet.size(), vnetJsonArray.size());
+
+        vhostSet.forEach(vhost -> assertThat(vnetJsonArray, hasVhost(vhost)));
+
+        verify(mockVnetService);
+    }
+
+    /**
+     * Hamcrest matcher to check that a virtual host representation in JSON matches
+     * the actual virtual host.
+     */
+    private static final class VirtualHostJsonMatcher extends HostResourceTest.HostJsonMatcher {
+        private final VirtualHost vhost;
+        private String reason = "";
+
+        private VirtualHostJsonMatcher(VirtualHost vhostValue) {
+            super(vhostValue);
+            vhost = vhostValue;
+        }
+
+        @Override
+        public boolean matchesSafely(JsonObject jsonHost) {
+            if (!super.matchesSafely(jsonHost)) {
+                return false;
+            }
+            // check NetworkId
+            String jsonNetworkId = jsonHost.get(ID).asString();
+            String networkId = vhost.networkId().toString();
+            if (!jsonNetworkId.equals(networkId)) {
+                reason = ID + " was " + jsonNetworkId;
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(reason);
+        }
+    }
+
+    /**
+     * Factory to allocate a virtual host matcher.
+     *
+     * @param vhost virtual host object we are looking for
+     * @return matcher
+     */
+    private static VirtualHostJsonMatcher matchesVirtualHost(VirtualHost vhost) {
+        return new VirtualHostJsonMatcher(vhost);
+    }
+
+    /**
+     * Hamcrest matcher to check that a virtual host is represented properly in a JSON
+     * array of hosts.
+     */
+    private static final class VirtualHostJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
+        private final VirtualHost vhost;
+        private String reason = "";
+
+        private VirtualHostJsonArrayMatcher(VirtualHost vhostValue) {
+            vhost = vhostValue;
+        }
+
+        @Override
+        public boolean matchesSafely(JsonArray json) {
+            for (int jsonHostIndex = 0; jsonHostIndex < json.size();
+                 jsonHostIndex++) {
+
+                JsonObject jsonHost = json.get(jsonHostIndex).asObject();
+
+                if (matchesVirtualHost(vhost).matchesSafely(jsonHost)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(reason);
+        }
+    }
+
+    /**
+     * Factory to allocate a virtual host array matcher.
+     *
+     * @param vhost virtual host object we are looking for
+     * @return matcher
+     */
+    private VirtualHostJsonArrayMatcher hasVhost(VirtualHost vhost) {
+        return new VirtualHostJsonArrayMatcher(vhost);
+    }
+
+    /**
+     * Tests adding of new virtual host using POST via JSON stream.
+     */
+    @Test
+    public void testPostVirtualHost() {
+        NetworkId networkId = networkId3;
+        expect(mockVnetAdminService.createVirtualHost(networkId, hId1, mac1, vlan1, loc1, ipSet1))
+                .andReturn(vhost1);
+        replay(mockVnetAdminService);
+
+        WebTarget wt = target();
+        InputStream jsonStream = VirtualNetworkWebResourceTest.class
+                .getResourceAsStream("post-virtual-host.json");
+        String reqLocation = "vnets/" + networkId.toString() + "/hosts";
+        Response response = wt.path(reqLocation).request(MediaType.APPLICATION_JSON_TYPE)
+                .post(Entity.json(jsonStream));
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED));
+
+        String location = response.getLocation().getPath();
+        assertThat(location, Matchers.startsWith("/" + reqLocation));
+
+        verify(mockVnetAdminService);
+    }
+
+    /**
+     * Tests adding of a null virtual host using POST via JSON stream.
+     */
+    @Test
+    public void testPostVirtualHostNullJsonStream() {
+        NetworkId networkId = networkId3;
+        replay(mockVnetAdminService);
+
+        WebTarget wt = target();
+        try {
+            String reqLocation = "vnets/" + networkId.toString() + "/hosts";
+            wt.path(reqLocation)
+                    .request(MediaType.APPLICATION_JSON_TYPE)
+                    .post(Entity.json(null), String.class);
+            fail("POST of null virtual host did not throw an exception");
+        } catch (BadRequestException ex) {
+            assertThat(ex.getMessage(), containsString("HTTP 400 Bad Request"));
+        }
+
+        verify(mockVnetAdminService);
+    }
+
+    /**
+     * Tests removing a virtual host with DELETE request.
+     */
+    @Test
+    public void testDeleteVirtualHost() {
+        NetworkId networkId = networkId3;
+        mockVnetAdminService.removeVirtualHost(networkId, hId1);
+        expectLastCall();
+        replay(mockVnetAdminService);
+
+        WebTarget wt = target()
+                .property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
+        InputStream jsonStream = VirtualNetworkWebResourceTest.class
+                .getResourceAsStream("post-virtual-host.json");
+        String reqLocation = "vnets/" + networkId.toString() + "/hosts";
+        Response response = wt.path(reqLocation).request().method("DELETE", Entity.json(jsonStream));
+
+        assertThat(response.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT));
+        verify(mockVnetAdminService);
+    }
 }
diff --git a/web/api/src/test/resources/org/onosproject/rest/resources/post-virtual-host.json b/web/api/src/test/resources/org/onosproject/rest/resources/post-virtual-host.json
new file mode 100644
index 0000000..3621798
--- /dev/null
+++ b/web/api/src/test/resources/org/onosproject/rest/resources/post-virtual-host.json
@@ -0,0 +1,14 @@
+{
+  "networkId": "3",
+  "id": "00:11:00:00:00:01/11",
+  "mac": "00:11:00:00:00:01",
+  "vlan": "11",
+  "location": {
+    "elementId": "devid1",
+    "port": "100" 
+  },
+  "ipAddresses": [
+    "10.0.0.1",
+    "10.0.0.2"
+  ]
+}