Support double-tagged host

Change-Id: Ie4041a0b5159e7a8b3a9ed82b55ce3c26b520a3b
diff --git a/core/api/src/main/java/org/onosproject/net/DefaultHost.java b/core/api/src/main/java/org/onosproject/net/DefaultHost.java
index b430020..ec0d9b5 100644
--- a/core/api/src/main/java/org/onosproject/net/DefaultHost.java
+++ b/core/api/src/main/java/org/onosproject/net/DefaultHost.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.net;
 
+import org.onlab.packet.EthType;
 import org.onosproject.net.provider.ProviderId;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
@@ -37,8 +38,11 @@
     private final VlanId vlan;
     private final Set<HostLocation> locations;
     private final Set<IpAddress> ips;
+    private final VlanId innerVlan;
+    private final EthType tpid;
     private final boolean configured;
 
+    // TODO consider moving this constructor to a builder pattern.
     /**
      * Creates an end-station host using the supplied information.
      *
@@ -78,24 +82,47 @@
     /**
      * Creates an end-station host using the supplied information.
      *
-     * @param providerId  provider identity
-     * @param id          host identifier
-     * @param mac         host MAC address
-     * @param vlan        host VLAN identifier
-     * @param locations   set of host locations
-     * @param ips         host IP addresses
+     * @param providerId provider identity
+     * @param id         host identifier
+     * @param mac        host MAC address
+     * @param vlan       host VLAN identifier
+     * @param locations  set of host locations
+     * @param ips        host IP addresses
      * @param configured  true if configured via NetworkConfiguration
      * @param annotations optional key/value annotations
      */
     public DefaultHost(ProviderId providerId, HostId id, MacAddress mac,
                        VlanId vlan, Set<HostLocation> locations, Set<IpAddress> ips,
                        boolean configured, Annotations... annotations) {
+        this(providerId, id, mac, vlan, locations, ips, VlanId.NONE,
+             EthType.EtherType.UNKNOWN.ethType(), configured, annotations);
+    }
+
+    /**
+     * Creates an end-station host using the supplied information.
+     *
+     * @param providerId  provider identity
+     * @param id          host identifier
+     * @param mac         host MAC address
+     * @param vlan        host VLAN identifier
+     * @param locations   set of host locations
+     * @param ips         host IP addresses
+     * @param innerVlan   host inner VLAN identifier
+     * @param tpid        outer TPID of a host
+     * @param configured  true if configured via NetworkConfiguration
+     * @param annotations optional key/value annotations
+     */
+    public DefaultHost(ProviderId providerId, HostId id, MacAddress mac, VlanId vlan,
+                       Set<HostLocation> locations, Set<IpAddress> ips, VlanId innerVlan,
+                       EthType tpid, boolean configured, Annotations... annotations) {
         super(providerId, id, annotations);
         this.mac = mac;
         this.vlan = vlan;
         this.locations = new HashSet<>(locations);
         this.ips = new HashSet<>(ips);
         this.configured = configured;
+        this.innerVlan = innerVlan;
+        this.tpid = tpid;
     }
 
     @Override
@@ -135,6 +162,16 @@
     }
 
     @Override
+    public VlanId innerVlan() {
+        return innerVlan;
+    }
+
+    @Override
+    public EthType tpid() {
+        return tpid;
+    }
+
+    @Override
     public boolean configured() {
         return configured;
     }
@@ -156,6 +193,8 @@
                     Objects.equals(this.vlan, other.vlan) &&
                     Objects.equals(this.locations, other.locations) &&
                     Objects.equals(this.ipAddresses(), other.ipAddresses()) &&
+                    Objects.equals(this.innerVlan, other.innerVlan) &&
+                    Objects.equals(this.tpid, other.tpid) &&
                     Objects.equals(this.annotations(), other.annotations());
         }
         return false;
@@ -171,6 +210,8 @@
                 .add("ipAddresses", ipAddresses())
                 .add("annotations", annotations())
                 .add("configured", configured())
+                .add("innerVlanId", innerVlan())
+                .add("outerTPID", tpid())
                 .toString();
     }
 
diff --git a/core/api/src/main/java/org/onosproject/net/Host.java b/core/api/src/main/java/org/onosproject/net/Host.java
index b47f843..1c2e50a 100644
--- a/core/api/src/main/java/org/onosproject/net/Host.java
+++ b/core/api/src/main/java/org/onosproject/net/Host.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.net;
 
+import org.onlab.packet.EthType;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
@@ -77,6 +78,24 @@
     default boolean configured() {
         return false;
     }
+
+    /**
+     * Returns the inner VLAN ID tied to this host.
+     *
+     * @return VLAN ID value; VlanId.NONE if only one VLAN ID is tied to this host
+     */
+    default VlanId innerVlan() {
+        return VlanId.NONE;
+    }
+
+    /**
+     * Returns the TPID of the outermost VLAN associated with this host.
+     *
+     * @return TPID of the outermost VLAN header
+     */
+    default EthType tpid() {
+        return EthType.EtherType.UNKNOWN.ethType();
+    }
     // TODO: explore capturing list of recent locations to aid in mobility
 
 }
diff --git a/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java b/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
index 3879257..17abc53 100644
--- a/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
+++ b/core/api/src/main/java/org/onosproject/net/config/basics/BasicHostConfig.java
@@ -17,7 +17,9 @@
 
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.EthType;
 import org.onlab.packet.IpAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.HostId;
 import org.onosproject.net.HostLocation;
@@ -25,6 +27,8 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 /**
  * Basic configuration for network end-station hosts.
  */
@@ -32,16 +36,29 @@
 
     private static final String IPS = "ips";
     private static final String LOCATIONS = "locations";
+    private static final String INNER_VLAN = "innerVlan";
+    private static final String OUTER_TPID = "outerTpid";
     private static final String DASH = "-";
 
     @Override
     public boolean isValid() {
         // locations is mandatory and must have at least one
         // ipAddresses can be absent, but if present must be valid
+        // innerVlan: 0 < innerVlan < VlanId.MAX_VLAN, if present
+        // outerTpid: either 0x8100 or 0x88a8, if present
+        if (!isIntegralNumber(object, INNER_VLAN, FieldPresence.OPTIONAL, 0, VlanId.MAX_VLAN)) {
+            return false;
+        }
+        checkArgument(!hasField(object, OUTER_TPID) ||
+                              (short) (Integer.decode(get(OUTER_TPID, "0")) & 0xFFFF) ==
+                              EthType.EtherType.QINQ.ethType().toShort() ||
+                              (short) (Integer.decode(get(OUTER_TPID, "0")) & 0xFFFF) ==
+                              EthType.EtherType.VLAN.ethType().toShort());
         this.locations();
         this.ipAddresses();
-        return hasOnlyFields(ALLOWED, NAME, LOC_TYPE, LATITUDE, LONGITUDE, ROLES,
-                             GRID_X, GRID_Y, UI_TYPE, RACK_ADDRESS, OWNER, IPS, LOCATIONS);
+        return hasOnlyFields(ALLOWED, NAME, LOC_TYPE, LATITUDE, LONGITUDE,
+                             GRID_Y, GRID_Y, UI_TYPE, RACK_ADDRESS, OWNER, IPS, LOCATIONS,
+                             INNER_VLAN, OUTER_TPID);
     }
 
     @Override
@@ -119,4 +136,26 @@
     public BasicHostConfig setIps(Set<IpAddress> ipAddresses) {
         return (BasicHostConfig) setOrClear(IPS, ipAddresses);
     }
-}
\ No newline at end of file
+
+    public VlanId innerVlan() {
+        String vlan = get(INNER_VLAN, null);
+        return vlan == null ? VlanId.NONE : VlanId.vlanId(Short.valueOf(vlan));
+    }
+
+    public BasicHostConfig setInnerVlan(VlanId vlanId) {
+        return (BasicHostConfig) setOrClear(INNER_VLAN, vlanId.toString());
+    }
+
+    public EthType outerTpid() {
+        short tpid = (short) (Integer.decode(get(OUTER_TPID, "0")) & 0xFFFF);
+        if (!(tpid == EthType.EtherType.VLAN.ethType().toShort() ||
+                tpid == EthType.EtherType.QINQ.ethType().toShort())) {
+            return EthType.EtherType.UNKNOWN.ethType();
+        }
+        return EthType.EtherType.lookup(tpid).ethType();
+    }
+
+    public BasicHostConfig setOuterTpid(EthType tpid) {
+        return (BasicHostConfig) setOrClear(OUTER_TPID, String.format("0x%04X", tpid.toShort()));
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/host/DefaultHostDescription.java b/core/api/src/main/java/org/onosproject/net/host/DefaultHostDescription.java
index e063c95..1992283 100644
--- a/core/api/src/main/java/org/onosproject/net/host/DefaultHostDescription.java
+++ b/core/api/src/main/java/org/onosproject/net/host/DefaultHostDescription.java
@@ -20,6 +20,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+import org.onlab.packet.EthType;
 import org.onosproject.net.AbstractDescription;
 import org.onosproject.net.HostLocation;
 import org.onosproject.net.SparseAnnotations;
@@ -42,6 +43,8 @@
     private final VlanId vlan;
     private final Set<HostLocation> locations;
     private final Set<IpAddress> ip;
+    private final VlanId innerVlan;
+    private final EthType tpid;
     private final boolean configured;
 
     /**
@@ -135,11 +138,32 @@
                                   Set<HostLocation> locations,
                                   Set<IpAddress> ip, boolean configured,
                                   SparseAnnotations... annotations) {
+        this(mac, vlan, locations, ip, VlanId.NONE, EthType.EtherType.UNKNOWN.ethType(),
+             configured, annotations);
+    }
+
+    /**
+     * Creates a host description using the supplied information.
+     *
+     * @param mac          host MAC address
+     * @param vlan         host VLAN identifier
+     * @param locations    host locations
+     * @param ip           host IP address
+     * @param innerVlan    host inner VLAN identifier
+     * @param tpid         outer TPID of a host
+     * @param configured   true if configured via NetworkConfiguration
+     * @param annotations  optional key/value annotations map
+     */
+    public DefaultHostDescription(MacAddress mac, VlanId vlan, Set<HostLocation> locations,
+                                  Set<IpAddress> ip, VlanId innerVlan, EthType tpid,
+                                  boolean configured, SparseAnnotations... annotations) {
         super(annotations);
         this.mac = mac;
         this.vlan = vlan;
         this.locations = new HashSet<>(locations);
         this.ip = new HashSet<>(ip);
+        this.innerVlan = innerVlan;
+        this.tpid = tpid;
         this.configured = configured;
     }
 
@@ -176,6 +200,16 @@
     }
 
     @Override
+    public VlanId innerVlan() {
+        return innerVlan;
+    }
+
+    @Override
+    public EthType tpid() {
+        return tpid;
+    }
+
+    @Override
     public String toString() {
         return toStringHelper(this)
                 .add("mac", mac)
@@ -183,6 +217,8 @@
                 .add("locations", locations)
                 .add("ipAddress", ip)
                 .add("configured", configured)
+                .add("innerVlanId", innerVlan)
+                .add("outerTPID", tpid)
                 .toString();
     }
 
@@ -201,7 +237,9 @@
             return Objects.equal(this.mac, that.mac)
                     && Objects.equal(this.vlan, that.vlan)
                     && Objects.equal(this.locations, that.locations)
-                    && Objects.equal(this.ip, that.ip);
+                    && Objects.equal(this.ip, that.ip)
+                    && Objects.equal(this.innerVlan, that.innerVlan)
+                    && Objects.equal(this.tpid, that.tpid);
         }
         return false;
     }
diff --git a/core/api/src/main/java/org/onosproject/net/host/HostDescription.java b/core/api/src/main/java/org/onosproject/net/host/HostDescription.java
index 7994599..543836e 100644
--- a/core/api/src/main/java/org/onosproject/net/host/HostDescription.java
+++ b/core/api/src/main/java/org/onosproject/net/host/HostDescription.java
@@ -22,6 +22,7 @@
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
+import org.onlab.packet.EthType;
 
 /**
  * Information describing host and its location.
@@ -43,6 +44,24 @@
     VlanId vlan();
 
     /**
+     * Returns the inner VLAN associated with this host.
+     *
+     * @return VLAN ID value; VlanId.NONE if only one VLAN ID is associated with this host
+     */
+    default VlanId innerVlan() {
+        return VlanId.NONE;
+    }
+
+    /**
+     * Returns the TPID of the outermost VLAN associated with this host.
+     *
+     * @return TPID of the outermost VLAN header
+     */
+    default EthType tpid() {
+        return EthType.EtherType.UNKNOWN.ethType();
+    }
+
+    /**
      * Returns the most recent location of the host on the network edge.
      *
      * @return the most recent host location