Merge branch 'master' of github.com:jclouds/jclouds into 1.5.x

* 'master' of github.com:jclouds/jclouds:
  minor volume test-related changes
  better exception message on key not found
  switched to buildView
  master is not accessible via getComputer
  Nova VolumeClient: adjusting attachment method names after review
  Issue 907: initial jenkins api
  Nova VolumeClient: improving javadocs
  Nova VolumeClient: improving javadocs
  Nova VolumeClient: improving javadocs
  Adding remaining VolumeClientExpectTest methods
  Adding CreateVolume and CreateSnapshot options and improving live tests accordingly
  Adding Volumes extension - first stage includes get/list volumes and list attachments
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java
index da9d650..9f5d32b 100644
--- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaAsyncClient.java
@@ -28,6 +28,7 @@
 import org.jclouds.openstack.nova.v1_1.extensions.KeyPairAsyncClient;
 import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupAsyncClient;
 import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageAsyncClient;
+import org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient;
 import org.jclouds.openstack.nova.v1_1.features.ExtensionAsyncClient;
 import org.jclouds.openstack.nova.v1_1.features.FlavorAsyncClient;
 import org.jclouds.openstack.nova.v1_1.features.ImageAsyncClient;
@@ -120,4 +121,10 @@
    Optional<SimpleTenantUsageAsyncClient> getSimpleTenantUsageExtensionForZone(
          @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone);
 
+   /**
+    * Provides asynchronous access to Volume features.
+    */
+   @Delegate
+   Optional<VolumeAsyncClient> getVolumeExtensionForZone(
+         @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone);
 }
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java
index d33e0ab..37dc81e 100644
--- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/NovaClient.java
@@ -30,6 +30,7 @@
 import org.jclouds.openstack.nova.v1_1.extensions.KeyPairClient;
 import org.jclouds.openstack.nova.v1_1.extensions.SecurityGroupClient;
 import org.jclouds.openstack.nova.v1_1.extensions.SimpleTenantUsageClient;
+import org.jclouds.openstack.nova.v1_1.extensions.VolumeClient;
 import org.jclouds.openstack.nova.v1_1.features.ExtensionClient;
 import org.jclouds.openstack.nova.v1_1.features.FlavorClient;
 import org.jclouds.openstack.nova.v1_1.features.ImageClient;
@@ -122,4 +123,12 @@
    Optional<SimpleTenantUsageClient> getSimpleTenantUsageExtensionForZone(
          @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone);
 
+
+   /**
+    * Provides synchronous access to Volume features.
+    */
+   @Delegate
+   Optional<VolumeClient> getVolumeExtensionForZone(
+         @EndpointParam(parser = ZoneToEndpoint.class) @Nullable String zone);
+
 }
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java
index fdb3b03..8a9a6e2 100644
--- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/config/NovaRestClientModule.java
@@ -73,6 +73,7 @@
          .put(KeyPairClient.class, KeyPairAsyncClient.class)
          .put(HostAdministrationClient.class, HostAdministrationAsyncClient.class)
          .put(SimpleTenantUsageClient.class, SimpleTenantUsageAsyncClient.class)
+         .put(VolumeClient.class, VolumeAsyncClient.class)
          .build();
 
    public NovaRestClientModule() {
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Volume.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Volume.java
new file mode 100644
index 0000000..ad581a6
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/Volume.java
@@ -0,0 +1,324 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * An Openstack Nova Volume
+ */
+public class Volume {
+
+   public static enum Status {
+      CREATING, AVAILABLE, IN_USE, DELETING, ERROR, UNRECOGNIZED;
+      public String value() {
+         return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, name());
+      }
+
+      @Override
+      public String toString() {
+         return value();
+      }
+
+      public static Status fromValue(String status) {
+         try {
+            return valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, checkNotNull(status, "status")));
+         } catch (IllegalArgumentException e) {
+            return UNRECOGNIZED;
+         }
+      }
+   }
+   
+   public static Builder<?> builder() {
+      return new ConcreteBuilder();
+   }
+
+   public Builder<?> toBuilder() {
+      return new ConcreteBuilder().fromVolume(this);
+   }
+
+   public static abstract class Builder<T extends Builder<T>>  {
+      protected abstract T self();
+
+      private String id;
+      private Status status;
+      private int size;
+      private String zone;
+      private Date created;
+      private Set<VolumeAttachment> attachments = Sets.newLinkedHashSet();
+      private String volumeType;
+      private String snapshotId;
+      private String name;
+      private String description;
+      private Map<String, String> metadata = Maps.newHashMap();
+
+      /** @see Volume#getId() */
+      public T id(String id) {
+         this.id = id;
+         return self();
+      }
+
+      /** @see Volume#getStatus() */
+      public T status(Status status) {
+         this.status = status;
+         return self();
+      }
+
+      /** @see Volume#getSize() */
+      public T size(int size) {
+         this.size = size;
+         return self();
+      }
+
+      /** @see Volume#getZone() */
+      public T zone(String zone) {
+         this.zone = zone;
+         return self();
+      }
+
+      /** @see Volume#getCreated() */
+      public T created(Date created) {
+         this.created = created;
+         return self();
+      }
+
+      /** @see Volume#getAttachments() */
+      public T attachments(Set<VolumeAttachment> attachments) {
+         this.attachments = attachments;
+         return self();
+      }
+      
+      /** @see Volume#getVolumeType() */
+      public T volumeType(String volumeType) {
+         this.volumeType = volumeType;
+         return self();
+      }
+
+      /** @see Volume#getSnapshotId() */
+      public T snapshotId(String snapshotId) {
+         this.snapshotId = snapshotId;
+         return self();
+      }
+      
+      /** @see Volume#getMetadata() */
+      public T metadata(Map<String, String> metadata) {
+         this.metadata = metadata;
+         return self();
+      }
+
+      /** @see Volume#getName() */
+      public T name(String name) {
+         this.name = name;
+         return self();
+      }
+
+      /** @see Volume#getDescription() */
+      public T description(String description) {
+         this.description = description;
+         return self();
+      }
+      
+      public Volume build() {
+         return new Volume(this);
+      }
+
+      public T fromVolume(Volume in) {
+         return this
+               .id(in.getId())
+               .status(in.getStatus())
+               .size(in.getSize())
+               .zone(in.getZone())
+               .created(in.getCreated())
+               .attachments(in.getAttachments())
+               .volumeType(in.getVolumeType())
+               .snapshotId(in.getSnapshotId())
+               .metadata(in.getMetadata())
+               ;
+      }
+
+   }
+
+   private static class ConcreteBuilder extends Builder<ConcreteBuilder> {
+      @Override
+      protected ConcreteBuilder self() {
+         return this;
+      }
+   }
+
+   private final String id;
+   private final Status status;
+   private final int size;
+   @SerializedName(value="availabilityZone")
+   private final String zone;
+   @SerializedName(value="createdAt")
+   private final Date created;
+   private final Set<VolumeAttachment> attachments;
+   private final String volumeType;   
+   private final String snapshotId;
+   @SerializedName(value="displayName")
+   private final String name;
+   @SerializedName(value="displayDescription")
+   private final String description;
+   private final Map<String, String> metadata;
+
+   protected Volume(Builder<?> builder) {
+      this.id = builder.id;
+      this.status = builder.status;
+      this.size = builder.size;
+      this.zone = builder.zone;
+      this.created = builder.created;
+      this.attachments = ImmutableSet.copyOf(checkNotNull(builder.attachments, "attachments"));
+      this.volumeType = builder.volumeType;
+      this.snapshotId = builder.snapshotId;
+      this.name = builder.name;
+      this.description = builder.description;
+      this.metadata = ImmutableMap.copyOf(checkNotNull(builder.metadata, "metadata"));
+   }
+
+   /**
+    * @return the id of this volume
+    */
+   public String getId() {
+      return this.id;
+   }
+
+   /**
+    * @return the status of this volume
+    */
+   public Status getStatus() {
+      return this.status;
+   }
+
+   /**
+    * @return the size in GB of this volume
+    */
+   public int getSize() {
+      return this.size;
+   }
+
+   /**
+    * @return the availabilityZone containing this volume
+    */
+   public String getZone() {
+      return this.zone;
+   }
+
+   /**
+    * @return the time this volume was created
+    */
+   public Date getCreated() {
+      return this.created;
+   }
+
+   /**
+    * @return the set of attachments (to Servers)
+    */
+   @Nullable
+   public Set<VolumeAttachment> getAttachments() {
+      return Collections.unmodifiableSet(this.attachments);
+   }
+
+   /**
+    * @return the type of this volume
+    */
+   @Nullable
+   public String getVolumeType() {
+      return this.volumeType;
+   }
+
+   @Nullable
+   public String getSnapshotId() {
+      return this.snapshotId;
+   }
+
+   /**
+    * @return the name of this volume - as displayed in the openstack console
+    */
+   @Nullable
+   public String getName() {
+      return this.name;
+   }
+
+   /**
+    * @return the description of this volume - as displayed in the openstack console
+    */
+   @Nullable
+   public String getDescription() {
+      return this.description;
+   }
+   
+   @Nullable
+   public Map<String, String> getMetadata() {
+      return Collections.unmodifiableMap(this.metadata);
+   }
+
+   // keeping fields short in eq/hashCode so that minor state differences don't affect collection membership
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(id, zone);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj)
+         return true;
+      if (obj == null || getClass() != obj.getClass())
+         return false;
+      Volume that = Volume.class.cast(obj);
+      return Objects.equal(this.id, that.id) && Objects.equal(this.zone, that.zone);
+   }
+
+   protected ToStringHelper string() {
+      return Objects.toStringHelper("")
+            .add("id", id)
+            .add("status", status)
+            .add("size", size)
+            .add("zone", zone)
+            .add("created", created)
+            .add("attachments", attachments)
+            .add("volumeType", volumeType)
+            .add("snapshotId", snapshotId)
+            .add("name", name)
+            .add("description", description)
+            .add("metadata", metadata)
+            ;
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java
new file mode 100644
index 0000000..bc33a0b
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeAttachment.java
@@ -0,0 +1,168 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+
+/**
+ * An Openstack Nova Volume Attachment (describes how Volumes are attached to Servers)
+ */
+public class VolumeAttachment {
+
+   public static Builder<?> builder() {
+      return new ConcreteBuilder();
+   }
+
+   public Builder<?> toBuilder() {
+      return new ConcreteBuilder().fromAttachment(this);
+   }
+
+   public static abstract class Builder<T extends Builder<T>>  {
+      protected abstract T self();
+
+      private String id;
+      private String volumeId;
+      private String serverId;
+      private String device;
+
+      /** @see VolumeAttachment#getId() */
+      public T id(String id) {
+         this.id = id;
+         return self();
+      }
+
+      /** @see VolumeAttachment#getVolumeId() */
+      public T volumeId(String volumeId) {
+         this.volumeId = volumeId;
+         return self();
+      }
+
+      /** @see VolumeAttachment#getServerId() */
+      public T serverId(String serverId) {
+         this.serverId = serverId;
+         return self();
+      }
+
+      /** @see VolumeAttachment#getDevice() */
+      public T device(String device) {
+         this.device = device;
+         return self();
+      }
+
+      public VolumeAttachment build() {
+         return new VolumeAttachment(this);
+      }
+
+      public T fromAttachment(VolumeAttachment in) {
+         return this
+               .id(in.getId())
+               .volumeId(in.getVolumeId())
+               .serverId(in.getServerId())
+               .device(in.getDevice())
+               ;
+      }
+
+   }
+
+   private static class ConcreteBuilder extends Builder<ConcreteBuilder> {
+      @Override
+      protected ConcreteBuilder self() {
+         return this;
+      }
+   }
+
+   private final String id;
+   private final String volumeId;
+   private final String serverId;
+   private final String device;
+
+   protected VolumeAttachment(Builder<?> builder) {
+      this.id = checkNotNull(builder.id, "id");
+      this.volumeId = checkNotNull(builder.volumeId, "volumeId");
+      this.serverId = builder.serverId;
+      this.device = builder.device;
+   }
+
+   /**
+    * @return the attachment id (typically the same as #getVolumeId())
+    */
+   public String getId() {
+      return this.id;
+   }
+
+   /**
+    * @return the id of the volume attached
+    */
+   public String getVolumeId() {
+      return this.volumeId;
+   }
+
+   /**
+    * @return the id of the server the volume is attached to
+    */
+   @Nullable
+   public String getServerId() {
+      return this.serverId;
+   }
+
+   /**
+    * @return the device name (e.g. "/dev/vdc")
+    */
+   @Nullable
+   public String getDevice() {
+      return this.device;
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(id, volumeId, serverId, device);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) return true;
+      if (obj == null || getClass() != obj.getClass()) return false;
+      VolumeAttachment that = VolumeAttachment.class.cast(obj);
+      return Objects.equal(this.id, that.id)
+            && Objects.equal(this.volumeId, that.volumeId)
+            && Objects.equal(this.serverId, that.serverId)
+            && Objects.equal(this.device, that.device)
+            ;
+   }
+
+   protected ToStringHelper string() {
+      return Objects.toStringHelper("")
+            .add("id", id)
+            .add("volumeId", volumeId)
+            .add("serverId", serverId)
+            .add("device", device)
+            ;
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java
new file mode 100644
index 0000000..d79d3d3
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/domain/VolumeSnapshot.java
@@ -0,0 +1,233 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Date;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * An Openstack Nova Volume Snapshot
+ */
+public class VolumeSnapshot {
+
+   public static Builder<?> builder() {
+      return new ConcreteBuilder();
+   }
+
+   public Builder<?> toBuilder() {
+      return new ConcreteBuilder().fromSnapshot(this);
+   }
+
+   public static abstract class Builder<T extends Builder<T>>  {
+      protected abstract T self();
+
+      private String id;
+      private String volumeId;
+      private Volume.Status status;
+      private int size;
+      private Date created;
+      private String name;
+      private String description;
+
+      /** @see VolumeSnapshot#getId() */
+      public T id(String id) {
+         this.id = id;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getVolumeId() */
+      public T volumeId(String volumeId) {
+         this.volumeId = volumeId;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getStatus() */
+      public T status(Volume.Status status) {
+         this.status = status;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getSize() */
+      public T size(int size) {
+         this.size = size;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getCreated() */
+      public T created(Date created) {
+         this.created = created;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getName() */
+      public T name(String name) {
+         this.name = name;
+         return self();
+      }
+
+      /** @see VolumeSnapshot#getDescription() */
+      public T description(String description) {
+         this.description = description;
+         return self();
+      }
+
+      public VolumeSnapshot build() {
+         return new VolumeSnapshot(this);
+      }
+
+      public T fromSnapshot(VolumeSnapshot in) {
+         return this
+               .id(in.getId())
+               .volumeId(in.getVolumeId())
+               .status(in.getStatus())
+               .size(in.getSize())
+               .created(in.getCreated())
+               .name(in.getName())
+               .description(in.getDescription())
+               ;
+      }
+
+   }
+
+   private static class ConcreteBuilder extends Builder<ConcreteBuilder> {
+      @Override
+      protected ConcreteBuilder self() {
+         return this;
+      }
+   }
+
+   private final String id;
+   private final String volumeId;
+   private final Volume.Status status;
+   private final int size;
+   @SerializedName(value="createdAt")
+   private final Date created;
+   @SerializedName(value="displayName")
+   private final String name;
+   @SerializedName(value="displayDescription")
+   private final String description;
+
+   protected VolumeSnapshot(Builder<?> builder) {
+      this.id = checkNotNull(builder.id, "id");
+      this.volumeId = checkNotNull(builder.volumeId, "volumeId");
+      this.status = checkNotNull(builder.status, "status");
+      this.size = builder.size;
+      this.created = builder.created;
+      this.name = builder.name;
+      this.description = builder.description;
+   }
+
+   /**
+    * @return the id of this snapshot
+    */
+   public String getId() {
+      return this.id;
+   }
+
+   /**
+    * @return the id of the Volume this snapshot was taken from
+    */
+   public String getVolumeId() {
+      return this.volumeId;
+   }
+
+   /**
+    * @return the status of this snapshot
+    */
+   public Volume.Status getStatus() {
+      return this.status;
+   }
+
+   /**
+    * @return the size in GB of the volume this snapshot was taken from
+    */
+   public int getSize() {
+      return this.size;
+   }
+
+   /**
+    * @return the data the snapshot was taken
+    */
+   @Nullable
+   public Date getCreated() {
+      return this.created;
+   }
+
+   /**
+    * @return the name of this snapshot - as displayed in the openstack console
+    */
+   @Nullable
+   public String getName() {
+      return this.name;
+   }
+
+
+   /**
+    * @return the description of this snapshot - as displayed in the openstack console
+    */
+   @Nullable
+   public String getDescription() {
+      return this.description;
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(id, volumeId, status, size, created, name, description);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) return true;
+      if (obj == null || getClass() != obj.getClass()) return false;
+      VolumeSnapshot that = VolumeSnapshot.class.cast(obj);
+      return Objects.equal(this.id, that.id)
+            && Objects.equal(this.volumeId, that.volumeId)
+            && Objects.equal(this.status, that.status)
+            && Objects.equal(this.size, that.size)
+            && Objects.equal(this.created, that.created)
+            && Objects.equal(this.name, that.name)
+            && Objects.equal(this.description, that.description)
+            ;
+   }
+
+   protected ToStringHelper string() {
+      return Objects.toStringHelper("")
+            .add("id", id)
+            .add("volumeId", volumeId)
+            .add("status", status)
+            .add("size", size)
+            .add("created", created)
+            .add("name", name)
+            .add("description", description)
+            ;
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java
new file mode 100644
index 0000000..97584b1
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeAsyncClient.java
@@ -0,0 +1,234 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.extensions;
+
+import java.util.Set;
+
+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.MediaType;
+
+import org.jclouds.openstack.filters.AuthenticateRequest;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot;
+import org.jclouds.openstack.nova.v1_1.domain.Volume;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions;
+import org.jclouds.openstack.services.Extension;
+import org.jclouds.openstack.services.ServiceType;
+import org.jclouds.rest.annotations.ExceptionParser;
+import org.jclouds.rest.annotations.MapBinder;
+import org.jclouds.rest.annotations.Payload;
+import org.jclouds.rest.annotations.PayloadParam;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.annotations.SkipEncoding;
+import org.jclouds.rest.functions.ReturnEmptySetOnNotFoundOr404;
+import org.jclouds.rest.functions.ReturnFalseOnNotFoundOr404;
+import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Provides synchronous access to Volumes.
+ * <p/>
+ * 
+ * @see org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient
+ * @author Adam Lowe
+ */
+@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.VOLUMES)
+@SkipEncoding({'/', '='})
+@RequestFilters(AuthenticateRequest.class)
+public interface VolumeAsyncClient {
+   /**
+    * Returns a summary list of volumes.
+    *
+    * @return the list of volumes
+    */
+   @GET
+   @Path("/os-volumes")
+   @SelectJson("volumes")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
+   ListenableFuture<Set<Volume>> listVolumes();
+
+   /**
+    * Returns a detailed list of volumes.
+    *
+    * @return the list of volumes.
+    */
+   @GET
+   @Path("/os-volumes/detail")
+   @SelectJson("volumes")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
+   ListenableFuture<Set<Volume>> listVolumesInDetail();
+
+   /**
+    * Return data about the given volume.
+    *
+    * @return details of a specific volume.
+    */
+   @GET
+   @Path("/os-volumes/{id}")
+   @SelectJson("volume")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   ListenableFuture<Volume> getVolume(@PathParam("id") String volumeId);
+
+   /**
+    * Creates a new volume
+    *
+    * @return the new Snapshot
+    */
+   @POST
+   @Path("/os-volumes")
+   @SelectJson("volume")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @Produces(MediaType.APPLICATION_JSON)
+   @MapBinder(CreateVolumeOptions.class)
+   ListenableFuture<Volume> createVolume(@PayloadParam("size") int sizeGB, CreateVolumeOptions... options);
+
+   /**
+    * Delete a volume.
+    *
+    * @return true if successful
+    */
+   @DELETE
+   @Path("/os-volumes/{id}")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnFalseOnNotFoundOr404.class)
+   ListenableFuture<Boolean> deleteVolume(@PathParam("id") String volumeId);
+   
+   /**
+    * List volume attachments for a given instance.
+    * 
+    * @return all Floating IPs
+    */
+   @GET
+   @Path("/servers/{server_id}/os-volume_attachments")
+   @SelectJson("volumeAttachments")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
+   ListenableFuture<Set<VolumeAttachment>> listAttachmentsOnServer(@PathParam("server_id") String serverId);
+
+   /**
+    * Get a specific attached volume.
+    * 
+    * @return data about the given volume attachment.
+    */
+   @GET
+   @Path("/servers/{server_id}/os-volume_attachments/{id}")
+   @SelectJson("volumeAttachment")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   ListenableFuture<VolumeAttachment> getAttachmentForVolumeOnServer(@PathParam("id") String volumeId,
+                                                                     @PathParam("server_id") String serverId);
+
+   /**
+    * Attach a volume to an instance
+    *
+    * @return the new Attachment
+    */
+   @POST
+   @Path("/servers/{server_id}/os-volume_attachments")
+   @SelectJson("volumeAttachment")
+   @Produces(MediaType.APPLICATION_JSON)
+   @Consumes(MediaType.APPLICATION_JSON)
+   @Payload("%7B\"volumeAttachment\":%7B\"volumeId\":\"{id}\",\"device\":\"{device}\"%7D%7D")
+   ListenableFuture<VolumeAttachment> attachVolumeToServerAsDevice(@PayloadParam("id") String volumeId,
+                                             @PathParam("server_id") String serverId, @PayloadParam("device") String device);
+
+   /**
+    * Detach a Volume from an instance.
+    * 
+    * @return true if successful
+    */
+   @DELETE
+   @Path("/servers/{server_id}/os-volume_attachments/{id}")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnFalseOnNotFoundOr404.class)
+   ListenableFuture<Boolean> detachVolumeFromServer(@PathParam("id") String volumeId, @PathParam("server_id") String serverId);
+
+   /**
+    * Returns a summary list of snapshots.
+    *
+    * @return the list of snapshots
+    */
+   @GET
+   @Path("/os-snapshots")
+   @SelectJson("snapshots")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
+   ListenableFuture<Set<VolumeSnapshot>> listSnapshots();
+
+   /**
+    * Returns a summary list of snapshots.
+    *
+    * @return the list of snapshots
+    */
+   @GET
+   @Path("/os-snapshots/detail")
+   @SelectJson("snapshots")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnEmptySetOnNotFoundOr404.class)
+   ListenableFuture<Set<VolumeSnapshot>> listSnapshotsInDetail();
+
+   /**
+    * Return data about the given snapshot.
+    *
+    * @return details of a specific snapshot.
+    */
+   @GET
+   @Path("/os-snapshots/{id}")
+   @SelectJson("snapshot")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   ListenableFuture<VolumeSnapshot> getSnapshot(@PathParam("id") String snapshotId);
+
+   /**
+    * Creates a new Snapshot
+    *
+    * @return the new Snapshot
+    */
+   @POST
+   @Path("/os-snapshots")
+   @SelectJson("snapshot")
+   @Produces(MediaType.APPLICATION_JSON)
+   @Consumes(MediaType.APPLICATION_JSON)
+   @MapBinder(CreateVolumeSnapshotOptions.class)
+   ListenableFuture<VolumeSnapshot> createSnapshot(@PayloadParam("volume_id") String volumeId, CreateVolumeSnapshotOptions... options);
+
+   /**
+    * Delete a snapshot.
+    *
+    * @return true if successful
+    */
+   @DELETE
+   @Path("/os-snapshots/{id}")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnFalseOnNotFoundOr404.class)
+   ListenableFuture<Boolean> deleteSnapshot(@PathParam("id") String snapshotId);
+   
+}
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java
new file mode 100644
index 0000000..3f40523
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClient.java
@@ -0,0 +1,141 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.extensions;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.concurrent.Timeout;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot;
+import org.jclouds.openstack.nova.v1_1.domain.Volume;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions;
+import org.jclouds.openstack.services.Extension;
+import org.jclouds.openstack.services.ServiceType;
+
+/**
+ * Provides synchronous access to Volumes.
+ * <p/>
+ * 
+ * @see org.jclouds.openstack.nova.v1_1.extensions.VolumeAsyncClient
+ * @author Adam Lowe
+ */
+@Extension(of = ServiceType.COMPUTE, namespace = ExtensionNamespaces.VOLUMES)
+@Timeout(duration = 180, timeUnit = TimeUnit.SECONDS)
+public interface VolumeClient {
+   /**
+    * Returns a summary list of snapshots.
+    *
+    * @return the list of snapshots
+    */
+   Set<Volume> listVolumes();
+
+   /**
+    * Returns a detailed list of volumes.
+    *
+    * @return the list of volumes.
+    */
+   Set<Volume> listVolumesInDetail();
+
+   /**
+    * Return data about the given volume.
+    *
+    * @return details of a specific snapshot.
+    */
+   Volume getVolume(String volumeId);
+
+   /**
+    * Creates a new Snapshot
+    *
+    * @return the new Snapshot
+    */
+   Volume createVolume(int sizeGB, CreateVolumeOptions... options);
+
+   /**
+    * Delete a snapshot.
+    *
+    * @return true if successful
+    */
+   Boolean deleteVolume(String volumeId);
+   
+   /**
+    * List volume attachments for a given instance.
+    * 
+    * @return all Floating IPs
+    */
+   Set<VolumeAttachment> listAttachmentsOnServer(String serverId);
+
+   /**
+    * Get a specific attached volume.
+    * 
+    * @return data about the given volume attachment.
+    */
+   VolumeAttachment getAttachmentForVolumeOnServer(String volumeId, String serverId);
+
+   /**
+    * Attach a volume to an instance
+    * 
+    * @return data about the new volume attachment
+    */
+   VolumeAttachment attachVolumeToServerAsDevice(String volumeId, String serverId, String device);
+
+   /**
+    * Detach a Volume from an instance.
+    * 
+    * @return true if successful
+    */
+   Boolean detachVolumeFromServer(String server_id, String volumeId);
+
+   /**
+    * Returns a summary list of snapshots.
+    *
+    * @return the list of snapshots
+    */
+   Set<VolumeSnapshot> listSnapshots();
+
+   /**
+    * Returns a summary list of snapshots.
+    *
+    * @return the list of snapshots
+    */
+   Set<VolumeSnapshot> listSnapshotsInDetail();
+
+   /**
+    * Return data about the given snapshot.
+    *
+    * @return details of a specific snapshot.
+    */
+   VolumeSnapshot getSnapshot(String snapshotId);
+
+   /**
+    * Creates a new Snapshot
+    *
+    * @return the new Snapshot
+    */
+   VolumeSnapshot createSnapshot(String volumeId, CreateVolumeSnapshotOptions... options);
+
+   /**
+    * Delete a snapshot.
+    *
+    * @return true if successful
+    */
+   Boolean deleteSnapshot(String snapshotId);
+   
+}
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java
index 5db7bb9..85406b5 100644
--- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/functions/PresentWhenExtensionAnnotationNamespaceEqualsAnyNamespaceInExtensionsSet.java
@@ -66,6 +66,8 @@
            URI.create("http://docs.openstack.org/compute/ext/os-simple-tenant-usage/api/v1.1"))
       .put(URI.create(ExtensionNamespaces.HOSTS),
            URI.create("http://docs.openstack.org/compute/ext/hosts/api/v1.1"))
+      .put(URI.create(ExtensionNamespaces.VOLUMES),
+           URI.create("http://docs.openstack.org/compute/ext/volumes/api/v1.1"))
       .build();
    
    @Inject
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/handlers/NovaErrorHandler.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/handlers/NovaErrorHandler.java
index ef2c150..c7cc8a2 100644
--- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/handlers/NovaErrorHandler.java
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/handlers/NovaErrorHandler.java
@@ -67,6 +67,9 @@
                exception = new ResourceNotFoundException(message, exception);
             }
             break;
+         case 413:
+            exception = new InsufficientResourcesException(message, exception);
+            break;
       }
       command.setException(exception);
    }
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java
new file mode 100644
index 0000000..e92f356
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeOptions.java
@@ -0,0 +1,224 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.options;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.jclouds.encryption.internal.Base64;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.openstack.nova.v1_1.domain.SecurityGroup;
+import org.jclouds.rest.MapBinder;
+import org.jclouds.rest.binders.BindToJsonPayload;
+import org.jclouds.util.Preconditions2;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
+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.gson.annotations.SerializedName;
+
+/**
+ * @author Adam Lowe
+ */
+public class CreateVolumeOptions implements MapBinder {
+   public static final CreateVolumeOptions NONE = new CreateVolumeOptions();
+
+   @Inject
+   private BindToJsonPayload jsonBinder;
+
+   private String name;
+   private String description;
+   private String volumeType;
+   private String availabilityZone;
+   private String snapshotId;
+   private Map<String, String> metadata = ImmutableMap.of();
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Map<String, String> postParams) {
+      Map<String, Object> image = Maps.newHashMap();
+      image.putAll(postParams);
+      if (name != null)
+         image.put("display_name", name);
+      if (description != null)
+         image.put("display_description", description);
+      if (!metadata.isEmpty())
+         image.put("metadata", metadata);
+      return jsonBinder.bindToRequest(request, ImmutableMap.of("volume", image));
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Object toBind) {
+      throw new IllegalStateException("CreateVolume is a POST operation");
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (!(object instanceof CreateVolumeOptions)) return false;
+      final CreateVolumeOptions other = CreateVolumeOptions.class.cast(object);
+      return equal(volumeType, other.volumeType) && equal(availabilityZone, other.availabilityZone) && equal(snapshotId, other.snapshotId)
+            && equal(name, other.name) && equal(description, other.description) && equal(metadata, other.metadata);
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(volumeType, availabilityZone, snapshotId, name, description, metadata);
+   }
+
+   protected ToStringHelper string() {
+      return toStringHelper("").add("volumeType", volumeType).add("availabilityZone", availabilityZone)
+            .add("snapshotId", snapshotId).add("name", name).add("description", description).add("metadata", metadata);
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+   /**
+    * Custom cloud server metadata can also be supplied at launch time. This
+    * metadata is stored in the API system where it is retrievable by querying
+    * the API for server status. The maximum size of the metadata key and value
+    * is each 255 bytes and the maximum number of key-value pairs that can be
+    * supplied per volume is 5.
+    */
+   public CreateVolumeOptions metadata(Map<String, String> metadata) {
+      checkNotNull(metadata, "metadata");
+      checkArgument(metadata.size() <= 5,
+            "you cannot have more then 5 metadata values.  You specified: " + metadata.size());
+      for (Entry<String, String> entry : metadata.entrySet()) {
+         checkArgument(
+               entry.getKey().getBytes().length < 255,
+               String.format("maximum length of metadata key is 255 bytes.  Key specified %s is %d bytes",
+                     entry.getKey(), entry.getKey().getBytes().length));
+         checkArgument(entry.getKey().getBytes().length < 255, String.format(
+               "maximum length of metadata value is 255 bytes.  Value specified for %s (%s) is %d bytes",
+               entry.getKey(), entry.getValue(), entry.getValue().getBytes().length));
+      }
+      this.metadata = ImmutableMap.copyOf(metadata);
+      return this;
+   }
+
+   public CreateVolumeOptions name(String name) {
+      this.name = name;
+      return this;
+   }
+
+   public CreateVolumeOptions description(String description) {
+      this.description = description;
+      return this;
+   }
+
+   public CreateVolumeOptions volumeType(String volumeType) {
+      this.volumeType = volumeType;
+      return this;
+   }
+
+   public CreateVolumeOptions availabilityZone(String availabilityZone) {
+      this.availabilityZone = availabilityZone;
+      return this;
+   }
+
+   public CreateVolumeOptions snapshotId(String snapshotId) {
+      this.snapshotId = snapshotId;
+      return this;
+   }
+
+   public String getVolumeType() {
+      return volumeType;
+   }
+
+   public String getAvailabilityZone() {
+      return availabilityZone;
+   }
+
+   public String getSnapshotId() {
+      return snapshotId;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public String getDescription() {
+      return description;
+   }
+
+   public Map<String, String> getMetadata() {
+      return metadata;
+   }
+
+   public static class Builder {
+      /**
+       * @see CreateVolumeOptions#getName()
+       */
+      public static CreateVolumeOptions name(String name) {
+         return new CreateVolumeOptions().name(name);
+      }
+      /**
+       * @see CreateVolumeOptions#getDescription()
+       */
+      public static CreateVolumeOptions description(String description) {
+         return new CreateVolumeOptions().description(description);
+      }
+
+      /**
+       * @see CreateVolumeOptions#getVolumeType()
+       */
+      public static CreateVolumeOptions volumeType(String volumeType) {
+         return new CreateVolumeOptions().volumeType(volumeType);
+      }
+
+      /**
+       * @see CreateVolumeOptions#getAvailabilityZone()
+       */
+      public static CreateVolumeOptions availabilityZone(String availabilityZone) {
+         return new CreateVolumeOptions().availabilityZone(availabilityZone);
+      }
+
+      /**
+       * @see CreateVolumeOptions#getSnapshotId()
+       */
+      public static CreateVolumeOptions snapshotId(String snapshotId) {
+         return new CreateVolumeOptions().snapshotId(snapshotId);
+      }
+
+      /**
+       * @see CreateVolumeOptions#getMetadata()
+       */
+      public static CreateVolumeOptions metadata(Map<String, String> metadata) {
+         return new CreateVolumeOptions().metadata(metadata);
+      }
+   }
+
+}
diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java
new file mode 100644
index 0000000..df9b628
--- /dev/null
+++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v1_1/options/CreateVolumeSnapshotOptions.java
@@ -0,0 +1,140 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.options;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.rest.MapBinder;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+/**
+ * @author Adam Lowe
+ */
+public class CreateVolumeSnapshotOptions implements MapBinder {
+   public static final CreateVolumeSnapshotOptions NONE = new CreateVolumeSnapshotOptions();
+
+   @Inject
+   private BindToJsonPayload jsonBinder;
+
+   private String name;
+   private String description;
+   private boolean force = false;
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Map<String, String> postParams) {
+      Map<String, String> data = Maps.newHashMap(postParams);
+      if (name != null)
+         data.put("display_name", name);
+      if (description != null)
+         data.put("display_description", description);
+      if (force)
+         data.put("force", "true");
+      return jsonBinder.bindToRequest(request, ImmutableMap.of("snapshot", data));
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Object toBind) {
+      throw new IllegalStateException("CreateSnapshot is a POST operation");
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (!(object instanceof CreateVolumeSnapshotOptions)) return false;
+      final CreateVolumeSnapshotOptions other = CreateVolumeSnapshotOptions.class.cast(object);
+      return equal(name, other.name) && equal(description, other.description);
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(name, description);
+   }
+
+   protected ToStringHelper string() {
+      return toStringHelper("").add("name", name).add("description", description);
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+   public CreateVolumeSnapshotOptions name(String name) {
+      this.name = name;
+      return this;
+   }
+
+   public CreateVolumeSnapshotOptions description(String description) {
+      this.description = description;
+      return this;
+   }
+
+   public CreateVolumeSnapshotOptions force() {
+      this.force = true;
+      return this;
+   }
+   
+   public String getName() {
+      return name;
+   }
+
+   public String getDescription() {
+      return description;
+   }
+
+   public boolean isForce() {
+      return force;
+   }
+
+   public static class Builder {
+      /**
+       * @see CreateVolumeSnapshotOptions#getName()
+       */
+      public static CreateVolumeSnapshotOptions name(String name) {
+         return new CreateVolumeSnapshotOptions().name(name);
+      }
+      /**
+       * @see CreateVolumeSnapshotOptions#getDescription()
+       */
+      public static CreateVolumeSnapshotOptions description(String description) {
+         return new CreateVolumeSnapshotOptions().description(description);
+      }
+
+      /**
+       * @see CreateVolumeSnapshotOptions#isForce()
+       */
+      public static CreateVolumeSnapshotOptions force() {
+         return new CreateVolumeSnapshotOptions().force();
+      }
+   }
+
+}
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/NovaErrorHandlerTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/NovaErrorHandlerTest.java
index ec82fd4..91dbf24 100644
--- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/NovaErrorHandlerTest.java
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/NovaErrorHandlerTest.java
@@ -76,7 +76,6 @@
                IllegalStateException.class);
    }
    
-   
    @Test
    public void test400MakesInsufficientResourcesExceptionOnQuotaExceeded() {
       assertCodeMakes(
@@ -87,6 +86,17 @@
                "{\"badRequest\": {\"message\": \"AddressLimitExceeded: Address quota exceeded. You cannot allocate any more addresses\", \"code\": 400}}",
                InsufficientResourcesException.class);
    }
+   
+   @Test
+   public void test413MakesInsufficientResourcesException() {
+      assertCodeMakes(
+               "POST",
+               URI.create("https://az-1.region-a.geo-1.compute.hpcloudsvc.com/v1.1/37936628937291/os-volumes"),
+               413,
+               "HTTP/1.1 413 Request Entity Too Large",
+               "{\"badRequest\": {\"message\": \"Volume quota exceeded. You cannot create a volume of size 1G\", \"code\": 413, \"retryAfter\": 0}}",
+               InsufficientResourcesException.class);
+   }
 
    @Test
    public void test404MakesResourceNotFoundException() {
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/FloatingIPClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/FloatingIPClientLiveTest.java
index b3262d3..d2bfb9d 100644
--- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/FloatingIPClientLiveTest.java
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/FloatingIPClientLiveTest.java
@@ -28,15 +28,11 @@
 import org.jclouds.openstack.nova.v1_1.domain.Address;
 import org.jclouds.openstack.nova.v1_1.domain.FloatingIP;
 import org.jclouds.openstack.nova.v1_1.domain.Server;
-import org.jclouds.openstack.nova.v1_1.domain.Server.Status;
-import org.jclouds.openstack.nova.v1_1.features.FlavorClient;
-import org.jclouds.openstack.nova.v1_1.features.ImageClient;
 import org.jclouds.openstack.nova.v1_1.features.ServerClient;
 import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientLiveTest;
 import org.testng.annotations.Test;
 
 import com.google.common.base.Optional;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 
 /**
@@ -110,8 +106,7 @@
             continue;
          FloatingIPClient client = clientOption.get();
          ServerClient serverClient = novaContext.getApi().getServerClientForZone(zoneId);
-         Server server = serverClient.createServer("test", imageIdForZone(zoneId), flavorRefForZone(zoneId));
-         blockUntilServerActive(server.getId(), serverClient);
+         Server server = createServerInZone(zoneId);
          FloatingIP floatingIP = client.allocate();
          assertNotNull(floatingIP);
          try {
@@ -124,25 +119,6 @@
       }
    }
 
-   private String imageIdForZone(String zoneId) {
-      ImageClient imageClient = novaContext.getApi().getImageClientForZone(zoneId);
-      return Iterables.getLast(imageClient.listImages()).getId();
-   }
-
-   private String flavorRefForZone(String zoneId) {
-      FlavorClient flavorClient = novaContext.getApi().getFlavorClientForZone(zoneId);
-      return Iterables.getLast(flavorClient.listFlavors()).getId();
-   }
-
-   private void blockUntilServerActive(String serverId, ServerClient client) throws InterruptedException {
-      Server currentDetails = null;
-      for (currentDetails = client.getServer(serverId); currentDetails.getStatus() != Status.ACTIVE; currentDetails = client
-            .getServer(serverId)) {
-         System.out.printf("blocking on status active%n%s%n", currentDetails);
-         Thread.sleep(5 * 1000);
-      }
-   }
-
    protected static void assertEventually(Runnable assertion) {
       long start = System.currentTimeMillis();
       AssertionError error = null;
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java
new file mode 100644
index 0000000..3349f0a
--- /dev/null
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientExpectTest.java
@@ -0,0 +1,458 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.extensions;
+
+import static org.testng.Assert.*;
+
+import java.net.URI;
+import java.util.Set;
+
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.date.DateService;
+import org.jclouds.date.internal.SimpleDateFormatDateService;
+import org.jclouds.openstack.nova.v1_1.domain.Volume;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot;
+import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientExpectTest;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions;
+import org.jclouds.rest.AuthorizationException;
+import org.jclouds.rest.ResourceNotFoundException;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+/**
+ * Tests VolumeClient guice wiring and parsing
+ *
+ * @author Adam Lowe
+ */
+@Test(groups = "unit", testName = "VolumeClientExpectTest")
+public class VolumeClientExpectTest extends BaseNovaClientExpectTest {
+   private DateService dateService = new SimpleDateFormatDateService();
+
+   public void testListVolumes() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/volume_list.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<Volume> volumes = client.listVolumes();
+      assertEquals(volumes, ImmutableSet.of(testVolume()));
+   }
+
+   public void testListVolumesFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<Volume> volumes = client.listVolumes();
+      assertTrue(volumes.isEmpty());
+   }
+
+   public void testListVolumesInDetail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/detail");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/volume_list_detail.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<Volume> volumes = client.listVolumesInDetail();
+      assertEquals(volumes, ImmutableSet.of(testVolume()));
+   }
+
+   public void testListVolumesInDetailFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/detail");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<Volume> volumes = client.listVolumesInDetail();
+      assertTrue(volumes.isEmpty());
+   }
+   
+   public void testCreateVolume() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint)
+                  .method("POST")
+                  .payload(payloadFromStringWithContentType("{\"volume\":{\"display_name\":\"jclouds-test-volume\",\"display_description\":\"description of test volume\",\"size\":\"1\"}}", MediaType.APPLICATION_JSON))
+                  .build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/volume_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Volume volume = client.createVolume(1, CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume"));
+      assertEquals(volume, testVolume());
+   }
+
+   @Test(expectedExceptions = ResourceNotFoundException.class)
+   public void testCreateVolumeFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint)
+               .endpoint(endpoint)
+               .method("POST")
+               .payload(payloadFromStringWithContentType("{\"volume\":{\"display_name\":\"jclouds-test-volume\",\"display_description\":\"description of test volume\",\"size\":\"1\"}}", MediaType.APPLICATION_JSON))
+               .build(),
+            standardResponseBuilder(404).payload(payloadFromResource("/volume_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      client.createVolume(1, CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume"));
+   }
+
+   public void testGetVolume() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/volume_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Volume volume = client.getVolume("1");
+      assertEquals(volume, testVolume());
+      // double-check equals()
+      assertEquals(volume.getStatus(), Volume.Status.IN_USE);
+      assertEquals(volume.getDescription(), "This is a test volume");
+      assertEquals(volume.getZone(), "nova");
+      assertEquals(volume.getName(), "test");
+      assertEquals(volume.getStatus(), Volume.Status.IN_USE);
+      assertEquals(Iterables.getOnlyElement(volume.getAttachments()), testAttachment());
+   }
+
+   public void testGetVolumeFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertNull(client.getVolume("1"));
+   }
+
+   public void testDeleteVolume() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertTrue(client.deleteVolume("1"));
+   }
+
+   public void testDeleteVolumeFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-volumes/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertFalse(client.deleteVolume("1"));
+   }
+
+   public void testListAttachments() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/attachment_list.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<VolumeAttachment> attachments = client.listAttachmentsOnServer("instance-1");
+      assertEquals(attachments, ImmutableSet.of(testAttachment()));
+      // double-check individual fields
+      VolumeAttachment attachment = Iterables.getOnlyElement(attachments);
+      assertEquals(attachment.getDevice(), "/dev/vdc");
+      assertEquals(attachment.getServerId(), "b4785058-cb80-491b-baa3-e4ee6546450e");
+      assertEquals(attachment.getId(), "1");
+      assertEquals(attachment.getVolumeId(), "1");
+   }
+
+   @Test(expectedExceptions = AuthorizationException.class)
+   public void testListAttachmentsFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-2/os-volume_attachments");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(401).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      client.listAttachmentsOnServer("instance-2");
+   }
+   
+   public void testGetAttachment() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      VolumeAttachment attachment = client.getAttachmentForVolumeOnServer("1", "instance-1");
+      assertEquals(attachment, testAttachment());
+   }
+
+   public void testGetAttachmentFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+     assertNull(client.getAttachmentForVolumeOnServer("1", "instance-1"));
+   }
+
+   public void testAttachVolume() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("POST")
+                  .payload(payloadFromStringWithContentType("{\"volumeAttachment\":{\"volumeId\":\"1\",\"device\":\"/dev/vdc\"}}", MediaType.APPLICATION_JSON)).endpoint(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      VolumeAttachment result = client.attachVolumeToServerAsDevice("1", "instance-1", "/dev/vdc");
+      assertEquals(result, testAttachment());
+   }
+
+   @Test(expectedExceptions = ResourceNotFoundException.class)
+   public void testAttachVolumeFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("POST")
+                  .payload(payloadFromStringWithContentType("{\"volumeAttachment\":{\"volumeId\":\"1\",\"device\":\"/dev/vdc\"}}", MediaType.APPLICATION_JSON)).endpoint(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      client.attachVolumeToServerAsDevice("1", "instance-1","/dev/vdc");
+   }
+
+   public void testDetachVolume() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/attachment_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertTrue(client.detachVolumeFromServer("1", "instance-1"));
+   }
+
+   public void testDetachVolumeFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/servers/instance-1/os-volume_attachments/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertFalse(client.detachVolumeFromServer("1", "instance-1"));
+   }
+
+   public void testListSnapshots() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/snapshot_list.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<VolumeSnapshot> snapshots = client.listSnapshots();
+      assertEquals(snapshots, ImmutableSet.of(testSnapshot()));
+   }
+
+   public void testListSnapshotsFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<VolumeSnapshot> snapshots = client.listSnapshots();
+      assertTrue(snapshots.isEmpty());
+   }
+
+   public void testGetSnapshot() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      VolumeSnapshot snapshot = client.getSnapshot("1");
+      assertEquals(snapshot, testSnapshot());
+   }
+
+   public void testGetSnapshotFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertNull(client.getSnapshot("1"));
+   }
+
+   public void testListSnapshotsInDetail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/detail");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/snapshot_list_detail.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<VolumeSnapshot> snapshots = client.listSnapshotsInDetail();
+      assertEquals(snapshots, ImmutableSet.of(testSnapshot()));
+
+      // double-check individual fields
+      VolumeSnapshot snappy = Iterables.getOnlyElement(snapshots);
+      assertEquals(snappy.getId(), "7");
+      assertEquals(snappy.getVolumeId(), "9");
+      assertEquals(snappy.getStatus(), Volume.Status.AVAILABLE);
+      assertEquals(snappy.getDescription(), "jclouds live test snapshot");
+      assertEquals(snappy.getName(), "jclouds-live-test");
+      assertEquals(snappy.getSize(), 1);
+   }
+
+   public void testListSnapshotsInDetailFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/detail");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).build(),
+            standardResponseBuilder(404).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      Set<VolumeSnapshot> snapshots = client.listSnapshotsInDetail();
+      assertTrue(snapshots.isEmpty());
+   }
+   
+   public void testCreateSnapshot() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint)
+                  .method("POST")
+                  .payload(payloadFromStringWithContentType("{\"snapshot\":{\"display_name\":\"jclouds-live-test\",\"volume_id\":\"13\",\"display_description\":\"jclouds live test snapshot\",\"force\":\"true\"}}", MediaType.APPLICATION_JSON))
+                  .build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      VolumeSnapshot snapshot = client.createSnapshot("13", CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description("jclouds live test snapshot").force());
+      assertEquals(snapshot, testSnapshot());
+   }
+
+   @Test(expectedExceptions = AuthorizationException.class)
+   public void testCreateSnapshotFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint)
+                  .method("POST")
+                  .payload(payloadFromStringWithContentType("{\"snapshot\":{\"display_name\":\"jclouds-live-test\",\"volume_id\":\"13\",\"display_description\":\"jclouds live test snapshot\",\"force\":\"true\"}}", MediaType.APPLICATION_JSON))
+                  .build(),
+            standardResponseBuilder(401).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      client.createSnapshot("13", CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description("jclouds live test snapshot").force());
+   }
+
+   public void testDeleteSnapshot() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(200).payload(payloadFromResource("/snapshot_details.json")).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      assertTrue(client.deleteSnapshot("1"));
+   }
+
+   @Test(expectedExceptions = AuthorizationException.class)
+   public void testDeleteSnapshotFail() {
+      URI endpoint = URI.create("https://compute.north.host/v1.1/3456/os-snapshots/1");
+      VolumeClient client = requestsSendResponses(
+            keystoneAuthWithUsernameAndPassword,
+            responseWithKeystoneAccess, extensionsOfNovaRequest, extensionsOfNovaResponse,
+            standardRequestBuilder(endpoint).method("DELETE").build(),
+            standardResponseBuilder(401).build()
+      ).getVolumeExtensionForZone("az-1.region-a.geo-1").get();
+
+      client.deleteSnapshot("1");
+   }
+   
+   protected Volume testVolume() {
+      return Volume.builder().status(Volume.Status.IN_USE).description("This is a test volume").zone("nova").name("test")
+            .attachments(ImmutableSet.of(testAttachment())).size(1).id("1").created(dateService.iso8601SecondsDateParse("2012-04-23 12:16:45")).build();
+   }
+
+   protected VolumeAttachment testAttachment() {
+      return VolumeAttachment.builder().device("/dev/vdc").serverId("b4785058-cb80-491b-baa3-e4ee6546450e").id("1").volumeId("1").build();
+   }
+
+   protected VolumeSnapshot testSnapshot() {
+      return VolumeSnapshot.builder().id("7").volumeId("9").description("jclouds live test snapshot").status(Volume.Status.AVAILABLE)
+            .name("jclouds-live-test").size(1).created(dateService.iso8601SecondsDateParse("2012-04-24 13:34:42")).build();
+   }
+}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java
new file mode 100644
index 0000000..05d1e26
--- /dev/null
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/extensions/VolumeClientLiveTest.java
@@ -0,0 +1,283 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.openstack.nova.v1_1.extensions;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Set;
+
+import org.jclouds.openstack.nova.v1_1.domain.Volume;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeAttachment;
+import org.jclouds.openstack.nova.v1_1.domain.VolumeSnapshot;
+import org.jclouds.openstack.nova.v1_1.internal.BaseNovaClientLiveTest;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeOptions;
+import org.jclouds.openstack.nova.v1_1.options.CreateVolumeSnapshotOptions;
+import org.jclouds.predicates.RetryablePredicate;
+import org.testng.annotations.AfterGroups;
+import org.testng.annotations.BeforeGroups;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+/**
+ * Tests behavior of VolumeClient
+ * 
+ * @author Adam Lowe
+ */
+@Test(groups = "live", testName = "VolumeClientLiveTest", singleThreaded = true)
+public class VolumeClientLiveTest extends BaseNovaClientLiveTest {
+
+   private Optional<VolumeClient> volumeOption;
+   private String zone;
+
+   private Volume testVolume;
+   private VolumeSnapshot testSnapshot;
+
+   @BeforeGroups(groups = { "integration", "live" })
+   @Override
+   public void setupContext() {
+      super.setupContext();
+      zone = Iterables.getLast(novaContext.getApi().getConfiguredZones(), "nova");
+      volumeOption = novaContext.getApi().getVolumeExtensionForZone(zone);
+   }
+
+   @AfterGroups(groups = "live", alwaysRun = true)
+   @Override
+   protected void tearDown() {
+      if (volumeOption.isPresent()) {
+         if (testSnapshot != null) {
+            final String snapshotId = testSnapshot.getId();
+            assertTrue(volumeOption.get().deleteSnapshot(snapshotId));
+            assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+               @Override
+               public boolean apply(VolumeClient volumeClient) {
+                  return volumeOption.get().getSnapshot(snapshotId) == null;
+               }
+            }, 30 * 1000L).apply(volumeOption.get()));
+         }
+         if (testVolume != null) {
+            final String volumeId = testVolume.getId();
+            assertTrue(volumeOption.get().deleteVolume(volumeId));
+            assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+               @Override
+               public boolean apply(VolumeClient volumeClient) {
+                  return volumeOption.get().getVolume(volumeId) == null;
+               }
+            }, 180 * 1000L).apply(volumeOption.get()));
+         }
+      }
+      super.tearDown();
+   }
+
+   public void testCreateVolume() {
+      if (volumeOption.isPresent()) {
+         testVolume = volumeOption.get().createVolume(
+                  1,
+                  CreateVolumeOptions.Builder.name("jclouds-test-volume").description("description of test volume")
+                           .availabilityZone(zone));
+         assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+            @Override
+            public boolean apply(VolumeClient volumeClient) {
+               return volumeOption.get().getVolume(testVolume.getId()).getStatus() == Volume.Status.AVAILABLE;
+            }
+         }, 180 * 1000L).apply(volumeOption.get()));
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateVolume")
+   public void testListVolumes() {
+      if (volumeOption.isPresent()) {
+         Set<Volume> volumes = volumeOption.get().listVolumes();
+         assertNotNull(volumes);
+         boolean foundIt = false;
+         for (Volume vol : volumes) {
+            Volume details = volumeOption.get().getVolume(vol.getId());
+            assertNotNull(details);
+            if (Objects.equal(details.getId(), testVolume.getId())) {
+               foundIt = true;
+            }
+         }
+         assertTrue(foundIt, "Failed to find the volume we created in listVolumes() response");
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateVolume")
+   public void testListVolumesInDetail() {
+      if (volumeOption.isPresent()) {
+         Set<Volume> volumes = volumeOption.get().listVolumesInDetail();
+         assertNotNull(volumes);
+         assertTrue(volumes.contains(testVolume));
+         boolean foundIt = false;
+         for (Volume vol : volumes) {
+            Volume details = volumeOption.get().getVolume(vol.getId());
+            assertNotNull(details);
+            assertNotNull(details.getId());
+            assertNotNull(details.getCreated());
+            assertTrue(details.getSize() > -1);
+
+            assertEquals(details.getId(), vol.getId());
+            assertEquals(details.getSize(), vol.getSize());
+            assertEquals(details.getName(), vol.getName());
+            assertEquals(details.getDescription(), vol.getDescription());
+            assertEquals(details.getCreated(), vol.getCreated());
+            if (Objects.equal(details.getId(), testVolume.getId())) {
+               foundIt = true;
+            }
+         }
+         assertTrue(foundIt, "Failed to find the volume we previously created in listVolumesInDetail() response");
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateVolume")
+   public void testCreateSnapshot() {
+      if (volumeOption.isPresent()) {
+         testSnapshot = volumeOption.get().createSnapshot(
+                  testVolume.getId(),
+                  CreateVolumeSnapshotOptions.Builder.name("jclouds-live-test").description(
+                           "jclouds live test snapshot").force());
+         assertNotNull(testSnapshot);
+         assertNotNull(testSnapshot.getId());
+         final String snapshotId = testSnapshot.getId();
+         assertNotNull(testSnapshot.getStatus());
+         assertTrue(testSnapshot.getSize() > -1);
+         assertNotNull(testSnapshot.getCreated());
+
+         assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+            @Override
+            public boolean apply(VolumeClient volumeClient) {
+               return volumeOption.get().getSnapshot(snapshotId).getStatus() == Volume.Status.AVAILABLE;
+            }
+         }, 30 * 1000L).apply(volumeOption.get()));
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateSnapshot")
+   public void testListSnapshots() {
+      if (volumeOption.isPresent()) {
+         Set<VolumeSnapshot> snapshots = volumeOption.get().listSnapshots();
+         assertNotNull(snapshots);
+         boolean foundIt = false;
+         for (VolumeSnapshot snap : snapshots) {
+            VolumeSnapshot details = volumeOption.get().getSnapshot(snap.getId());
+            if (Objects.equal(snap.getVolumeId(), testVolume.getId())) {
+               foundIt = true;
+            }
+            assertNotNull(details);
+            assertEquals(details.getId(), snap.getId());
+            assertEquals(details.getVolumeId(), snap.getVolumeId());
+         }
+         assertTrue(foundIt, "Failed to find the snapshot we previously created in listSnapshots() response");
+      }
+   }
+
+   @Test(dependsOnMethods = "testCreateSnapshot")
+   public void testListSnapshotsInDetail() {
+      if (volumeOption.isPresent()) {
+         Set<VolumeSnapshot> snapshots = volumeOption.get().listSnapshotsInDetail();
+         assertNotNull(snapshots);
+         boolean foundIt = false;
+         for (VolumeSnapshot snap : snapshots) {
+            VolumeSnapshot details = volumeOption.get().getSnapshot(snap.getId());
+            if (Objects.equal(snap.getVolumeId(), testVolume.getId())) {
+               foundIt = true;
+               assertSame(details, testSnapshot);
+            }
+            assertSame(details, snap);
+         }
+
+         assertTrue(foundIt, "Failed to find the snapshot we created in listSnapshotsInDetail() response");
+      }
+   }
+
+   private void assertSame(VolumeSnapshot a, VolumeSnapshot b) {
+      assertNotNull(a);
+      assertNotNull(b);
+      assertEquals(a.getId(), b.getId());
+      assertEquals(a.getDescription(), b.getDescription());
+      assertEquals(a.getName(), b.getName());
+      assertEquals(a.getVolumeId(), b.getVolumeId());
+   }
+
+   @Test(dependsOnMethods = "testCreateVolume")
+   public void testAttachments() {
+      if (volumeOption.isPresent()) {
+         String server_id = null;
+         try {
+            final String serverId = server_id = createServerInZone(zone).getId();
+
+            Set<VolumeAttachment> attachments = volumeOption.get().listAttachmentsOnServer(serverId);
+            assertNotNull(attachments);
+            final int before = attachments.size();
+
+            VolumeAttachment testAttachment = volumeOption.get().attachVolumeToServerAsDevice(testVolume.getId(),
+                     serverId, "/dev/vdf");
+            assertNotNull(testAttachment.getId());
+            assertEquals(testAttachment.getVolumeId(), testVolume.getId());
+
+            assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+               @Override
+               public boolean apply(VolumeClient volumeClient) {
+                  return volumeOption.get().listAttachmentsOnServer(serverId).size() == before + 1;
+               }
+            }, 60 * 1000L).apply(volumeOption.get()));
+
+            attachments = volumeOption.get().listAttachmentsOnServer(serverId);
+            assertNotNull(attachments);
+            assertEquals(attachments.size(), before + 1);
+
+            assertEquals(volumeOption.get().getVolume(testVolume.getId()).getStatus(), Volume.Status.IN_USE);
+
+            boolean foundIt = false;
+            for (VolumeAttachment att : attachments) {
+               VolumeAttachment details = volumeOption.get()
+                        .getAttachmentForVolumeOnServer(att.getVolumeId(), serverId);
+               assertNotNull(details);
+               assertNotNull(details.getId());
+               assertNotNull(details.getServerId());
+               assertNotNull(details.getVolumeId());
+               if (Objects.equal(details.getVolumeId(), testVolume.getId())) {
+                  foundIt = true;
+                  assertEquals(details.getDevice(), "/dev/vdf");
+                  assertEquals(details.getServerId(), serverId);
+               }
+            }
+
+            assertTrue(foundIt, "Failed to find the attachment we created in listAttachments() response");
+
+            volumeOption.get().detachVolumeFromServer(testVolume.getId(), serverId);
+            assertTrue(new RetryablePredicate<VolumeClient>(new Predicate<VolumeClient>() {
+               @Override
+               public boolean apply(VolumeClient volumeClient) {
+                  return volumeOption.get().listAttachmentsOnServer(serverId).size() == before;
+               }
+            }, 60 * 1000L).apply(volumeOption.get()));
+
+         } finally {
+            if (server_id != null)
+               novaContext.getApi().getServerClientForZone(zone).deleteServer(server_id);
+         }
+
+      }
+   }
+}
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java
index ca8089b..2a37f52 100644
--- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaClientLiveTest.java
@@ -25,11 +25,19 @@
 import org.jclouds.openstack.nova.v1_1.NovaAsyncClient;
 import org.jclouds.openstack.nova.v1_1.NovaClient;
 import org.jclouds.openstack.nova.v1_1.config.NovaProperties;
+import org.jclouds.openstack.nova.v1_1.domain.Server;
+import org.jclouds.openstack.nova.v1_1.domain.Server.Status;
+import org.jclouds.openstack.nova.v1_1.features.FlavorClient;
+import org.jclouds.openstack.nova.v1_1.features.ImageClient;
+import org.jclouds.openstack.nova.v1_1.features.ServerClient;
 import org.jclouds.rest.RestContext;
 import org.testng.annotations.AfterGroups;
 import org.testng.annotations.BeforeGroups;
 import org.testng.annotations.Test;
 
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+
 /**
  * Tests behavior of {@code NovaClient}
  * 
@@ -64,5 +72,35 @@
       if (novaContext != null)
          novaContext.close();
    }
+   
+   protected Server createServerInZone(String zoneId) {
+      ServerClient serverClient = novaContext.getApi().getServerClientForZone(zoneId);
+      Server server = serverClient.createServer("test", imageIdForZone(zoneId), flavorRefForZone(zoneId));
+      blockUntilServerActive(server.getId(), serverClient);
+      return server;
+   }
+
+   private void blockUntilServerActive(String serverId, ServerClient client) {
+      Server currentDetails = null;
+      for (currentDetails = client.getServer(serverId); currentDetails.getStatus() != Status.ACTIVE; currentDetails = client
+            .getServer(serverId)) {
+         System.out.printf("blocking on status active%n%s%n", currentDetails);
+         try {
+            Thread.sleep(5 * 1000);
+         } catch (InterruptedException e) {
+            throw Throwables.propagate(e);
+         }
+      }
+   }
+   
+   protected String imageIdForZone(String zoneId) {
+      ImageClient imageClient = novaContext.getApi().getImageClientForZone(zoneId);
+      return Iterables.getLast(imageClient.listImages()).getId();
+   }
+
+   protected String flavorRefForZone(String zoneId) {
+      FlavorClient flavorClient = novaContext.getApi().getFlavorClientForZone(zoneId);
+      return Iterables.getLast(flavorClient.listFlavors()).getId();
+   }
 
 }
diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java
index 336f507..8815279 100644
--- a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java
+++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v1_1/internal/BaseNovaExpectTest.java
@@ -20,6 +20,8 @@
 
 import java.net.URI;
 
+import javax.ws.rs.core.MediaType;
+
 import org.jclouds.http.HttpRequest;
 import org.jclouds.http.HttpResponse;
 import org.jclouds.openstack.keystone.v2_0.internal.KeystoneFixture;
@@ -68,4 +70,14 @@
       unmatchedExtensionsOfNovaResponse = HttpResponse.builder().statusCode(200)
             .payload(payloadFromResource("/extension_list.json")).build();
    }
+   
+   protected HttpRequest.Builder standardRequestBuilder(URI endpoint) {
+      return HttpRequest.builder().method("GET")
+            .headers(ImmutableMultimap.of("Accept", MediaType.APPLICATION_JSON, "X-Auth-Token", authToken))
+            .endpoint(endpoint);
+   }
+
+   protected HttpResponse.Builder standardResponseBuilder(int status) {
+      return HttpResponse.builder().statusCode(status);
+   }
 }
diff --git a/apis/openstack-nova/src/test/resources/attachment_details.json b/apis/openstack-nova/src/test/resources/attachment_details.json
new file mode 100644
index 0000000..c436406
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/attachment_details.json
@@ -0,0 +1 @@
+{"volumeAttachment": {"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/attachment_list.json b/apis/openstack-nova/src/test/resources/attachment_list.json
new file mode 100644
index 0000000..b40de0b
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/attachment_list.json
@@ -0,0 +1 @@
+{"volumeAttachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}]}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/snapshot_details.json b/apis/openstack-nova/src/test/resources/snapshot_details.json
new file mode 100644
index 0000000..375127b
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/snapshot_details.json
@@ -0,0 +1 @@
+{"snapshot": {"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/snapshot_list.json b/apis/openstack-nova/src/test/resources/snapshot_list.json
new file mode 100644
index 0000000..03980b1
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/snapshot_list.json
@@ -0,0 +1 @@
+{"snapshots": [{"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}]}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/snapshot_list_detail.json b/apis/openstack-nova/src/test/resources/snapshot_list_detail.json
new file mode 100644
index 0000000..03980b1
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/snapshot_list_detail.json
@@ -0,0 +1 @@
+{"snapshots": [{"status": "available", "displayDescription": "jclouds live test snapshot", "displayName": "jclouds-live-test", "volumeId": 9, "id": 7, "createdAt": "2012-04-24 13:34:42", "size": 1}]}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/volume_details.json b/apis/openstack-nova/src/test/resources/volume_details.json
new file mode 100644
index 0000000..8dd11dc
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/volume_details.json
@@ -0,0 +1 @@
+{"volume": {"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/volume_list.json b/apis/openstack-nova/src/test/resources/volume_list.json
new file mode 100644
index 0000000..d2179e7
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/volume_list.json
@@ -0,0 +1 @@
+{"volumes": [{"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}]}
\ No newline at end of file
diff --git a/apis/openstack-nova/src/test/resources/volume_list_detail.json b/apis/openstack-nova/src/test/resources/volume_list_detail.json
new file mode 100644
index 0000000..d2179e7
--- /dev/null
+++ b/apis/openstack-nova/src/test/resources/volume_list_detail.json
@@ -0,0 +1 @@
+{"volumes": [{"status": "in-use", "displayDescription": "This is a test volume", "availabilityZone": "nova", "displayName": "test", "attachments": [{"device": "/dev/vdc", "serverId": "b4785058-cb80-491b-baa3-e4ee6546450e", "id": 1, "volumeId": 1}], "volumeType": null, "snapshotId": null, "size": 1, "id": 1, "createdAt": "2012-04-23 12:16:45", "metadata": {}}]}
\ No newline at end of file
diff --git a/blobstore/src/main/clojure/org/jclouds/blobstore2.clj b/blobstore/src/main/clojure/org/jclouds/blobstore2.clj
index 1bb2a87..4d69e74 100644
--- a/blobstore/src/main/clojure/org/jclouds/blobstore2.clj
+++ b/blobstore/src/main/clojure/org/jclouds/blobstore2.clj
@@ -118,7 +118,7 @@
                       (modules (apply modules (concat ext-modules (opts :extensions))))
                       (overrides (reduce #(do (.put %1 (name (first %2)) (second %2)) %1)
                         (Properties.) (dissoc opts :extensions)))
-                      (build BlobStoreContext))]
+                      (buildView BlobStoreContext))]
       (if (some #(= :async %) options)
         (.getAsyncBlobStore context)
         (.getBlobStore context)))))
diff --git a/compute/src/main/clojure/org/jclouds/compute2.clj b/compute/src/main/clojure/org/jclouds/compute2.clj
index 35169af..db3c4a6 100644
--- a/compute/src/main/clojure/org/jclouds/compute2.clj
+++ b/compute/src/main/clojure/org/jclouds/compute2.clj
@@ -90,7 +90,7 @@
           (modules (apply modules (concat ext-modules (opts :extensions))))
           (overrides (reduce #(do (.put %1 (name (first %2)) (second %2)) %1)
             (Properties.) (dissoc opts :extensions)))
-          (build ComputeServiceContext)
+          (buildView ComputeServiceContext)
           (getComputeService))))
   ([#^ComputeServiceContext compute-context]
     (.getComputeService compute-context)))
diff --git a/core/src/main/java/org/jclouds/ContextBuilder.java b/core/src/main/java/org/jclouds/ContextBuilder.java
index eed4b9c..9d80749 100644
--- a/core/src/main/java/org/jclouds/ContextBuilder.java
+++ b/core/src/main/java/org/jclouds/ContextBuilder.java
@@ -231,6 +231,8 @@
       try {
          return find(newArrayList(mutable.getProperty(prov + "." + key), mutable.getProperty("jclouds." + key)),
                   notNull());
+      } catch (NoSuchElementException e) {
+         throw new NoSuchElementException(String.format("property %s.%s not present in properties: %s", prov, key, mutable.keySet()));
       } finally {
          mutable.remove(prov + "." + key);
          mutable.remove("jclouds." + key);
diff --git a/labs/jenkins/pom.xml b/labs/jenkins/pom.xml
new file mode 100644
index 0000000..8c06216
--- /dev/null
+++ b/labs/jenkins/pom.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+  Licensed to jclouds, Inc. (jclouds) under one or more
+  contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  jclouds licenses this file
+  to you 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.jclouds</groupId>
+    <artifactId>jclouds-project</artifactId>
+    <version>1.5.0-SNAPSHOT</version>
+    <relativePath>../../project/pom.xml</relativePath>
+  </parent>
+  <groupId>org.jclouds.labs</groupId>
+  <artifactId>jenkins</artifactId>
+  <name>jcloud jenkins api</name>
+  <description>jclouds components to access an implementation of Jenkins</description>
+  <packaging>bundle</packaging>
+
+  <properties>
+    <test.jenkins.endpoint>http://localhost:8080</test.jenkins.endpoint>
+    <test.jenkins.api-version>1.0</test.jenkins.api-version>
+    <test.jenkins.build-version>1.460</test.jenkins.build-version>
+    <test.jenkins.identity>ANONYMOUS</test.jenkins.identity>
+    <test.jenkins.credential>ANONYMOUS</test.jenkins.credential>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.jclouds</groupId>
+      <artifactId>jclouds-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jclouds</groupId>
+      <artifactId>jclouds-core</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jclouds.driver</groupId>
+      <artifactId>jclouds-slf4j</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.0.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  
+  <profiles>
+    <profile>
+      <id>live</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>integration</id>
+                <phase>integration-test</phase>
+                <goals>
+                  <goal>test</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <test.jenkins.endpoint>${test.jenkins.endpoint}</test.jenkins.endpoint>
+                    <test.jenkins.api-version>${test.jenkins.api-version}</test.jenkins.api-version>
+                    <test.jenkins.build-version>${test.jenkins.build-version}</test.jenkins.build-version>
+                    <test.jenkins.identity>${test.jenkins.identity}</test.jenkins.identity>
+                    <test.jenkins.credential>${test.jenkins.credential}</test.jenkins.credential>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <build>
+    <plugins>
+      <plugin>  
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+            <Export-Package>org.jclouds.jenkins.v1*;version="${project.version}"</Export-Package>
+            <Import-Package>            
+                        org.jclouds.rest.internal;version="${project.version}",
+                        org.jclouds*;version="${project.version}",
+                                *
+                        </Import-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsApiMetadata.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsApiMetadata.java
new file mode 100644
index 0000000..d84743e
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsApiMetadata.java
@@ -0,0 +1,97 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1;
+
+import java.net.URI;
+import java.util.Properties;
+
+import org.jclouds.apis.ApiMetadata;
+import org.jclouds.jenkins.v1.config.JenkinsRestClientModule;
+import org.jclouds.rest.RestContext;
+import org.jclouds.rest.internal.BaseRestApiMetadata;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+import com.google.inject.Module;
+
+/**
+ * Implementation of {@link ApiMetadata} for Jenkins 1.0 API
+ * 
+ * @author Adrian Cole
+ */
+public class JenkinsApiMetadata extends BaseRestApiMetadata {
+   
+   public static final String ANONYMOUS_IDENTITY = "ANONYMOUS";
+   
+   /** The serialVersionUID */
+   private static final long serialVersionUID = 6725672099385580694L;
+
+   public static final TypeToken<RestContext<JenkinsClient, JenkinsAsyncClient>> CONTEXT_TOKEN = new TypeToken<RestContext<JenkinsClient, JenkinsAsyncClient>>() {
+      private static final long serialVersionUID = -5070937833892503232L;
+   };
+
+   @Override
+   public Builder toBuilder() {
+      return new Builder().fromApiMetadata(this);
+   }
+
+   public JenkinsApiMetadata() {
+      this(new Builder());
+   }
+
+   protected JenkinsApiMetadata(Builder builder) {
+      super(builder);
+   }
+
+   public static Properties defaultProperties() {
+      Properties properties = BaseRestApiMetadata.defaultProperties();
+      return properties;
+   }
+
+   public static class Builder extends BaseRestApiMetadata.Builder {
+
+      protected Builder() {
+         super(JenkinsClient.class, JenkinsAsyncClient.class);
+          id("jenkins")
+         .name("Jenkins API")
+         .identityName("Username (or " + ANONYMOUS_IDENTITY + " if anonymous)")
+         .defaultIdentity(ANONYMOUS_IDENTITY)
+         .credentialName("Password")
+         .defaultCredential(ANONYMOUS_IDENTITY)
+         .documentation(URI.create("https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API"))
+         .version("1.0")
+         .defaultEndpoint("http://localhost:8080")
+         .defaultProperties(JenkinsApiMetadata.defaultProperties())
+         .defaultModules(ImmutableSet.<Class<? extends Module>>of(JenkinsRestClientModule.class));
+      }
+      
+      @Override
+      public JenkinsApiMetadata build() {
+         return new JenkinsApiMetadata(this);
+      }
+
+      @Override
+      public Builder fromApiMetadata(ApiMetadata in) {
+         super.fromApiMetadata(in);
+         return this;
+      }
+
+   }
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncClient.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncClient.java
new file mode 100644
index 0000000..aac4675
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncClient.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1;
+
+import org.jclouds.jenkins.v1.features.ComputerAsyncClient;
+import org.jclouds.rest.annotations.Delegate;
+
+/**
+ * Provides asynchronous access to Jenkins via their REST API.
+ * <p/>
+ * 
+ * @see JenkinsClient
+ * @see <a href="https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API">api doc</a>
+ * @author Adrian Cole
+ */
+public interface JenkinsAsyncClient {
+
+
+   /**
+    * Provides asynchronous access to Computer features.
+    */
+   @Delegate
+   ComputerAsyncClient getComputerClient();
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsClient.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsClient.java
new file mode 100644
index 0000000..b0653af
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsClient.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1;
+
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.concurrent.Timeout;
+import org.jclouds.jenkins.v1.features.ComputerClient;
+import org.jclouds.rest.annotations.Delegate;
+
+/**
+ * Provides synchronous access to Jenkins.
+ * <p/>
+ * 
+ * @see JenkinsAsyncClient
+ * @see <a href="https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API">api doc</a>
+ * @author Adrian Cole
+ */
+@Timeout(duration = 60, timeUnit = TimeUnit.SECONDS)
+public interface JenkinsClient {
+   
+   /**
+    * Provides synchronous access to Computer features.
+    */
+   @Delegate
+   ComputerClient getComputerClient();
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsProperties.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsProperties.java
new file mode 100644
index 0000000..473db99
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsProperties.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.config;
+
+/**
+ * Configuration properties and constants used in Jenkins connections.
+ *
+ * @author Adrian Cole
+ */
+public class JenkinsProperties {
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsRestClientModule.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsRestClientModule.java
new file mode 100644
index 0000000..b0e11aa
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/config/JenkinsRestClientModule.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.config;
+
+import java.util.Map;
+
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
+import org.jclouds.jenkins.v1.JenkinsAsyncClient;
+import org.jclouds.jenkins.v1.JenkinsClient;
+import org.jclouds.jenkins.v1.features.ComputerAsyncClient;
+import org.jclouds.jenkins.v1.features.ComputerClient;
+import org.jclouds.jenkins.v1.handlers.JenkinsErrorHandler;
+import org.jclouds.rest.ConfiguresRestClient;
+import org.jclouds.rest.config.RestClientModule;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Configures the Jenkins connection.
+ * 
+ * @author Adrian Cole
+ */
+@ConfiguresRestClient
+public class JenkinsRestClientModule extends RestClientModule<JenkinsClient, JenkinsAsyncClient> {
+
+   public static final Map<Class<?>, Class<?>> DELEGATE_MAP = ImmutableMap.<Class<?>, Class<?>> builder()
+         .put(ComputerClient.class, ComputerAsyncClient.class)
+         .build();
+
+   public JenkinsRestClientModule() {
+      super(DELEGATE_MAP);
+   }
+   
+   @Override
+   protected void bindErrorHandlers() {
+      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(JenkinsErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(JenkinsErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(JenkinsErrorHandler.class);
+   }
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/Computer.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/Computer.java
new file mode 100644
index 0000000..065adde
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/Computer.java
@@ -0,0 +1,134 @@
+package org.jclouds.jenkins.v1.domain;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+
+/**
+ * @author Adrian Cole
+ * @see <a
+ *      href="http://ci.jruby.org/computer/api/">api
+ *      doc</a>
+ */
+public class Computer implements Comparable<Computer> {
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public Builder toBuilder() {
+      return builder().fromComputerMetadata(this);
+   }
+
+   public static class Builder {
+      protected String displayName;
+      protected boolean idle;
+      protected boolean offline;
+
+      /**
+       * @see Computer#getDisplayName()
+       */
+      public Builder displayName(String displayName) {
+         this.displayName = checkNotNull(displayName, "displayName");
+         return this;
+      }
+
+      /**
+       * @see Computer#isIdle()
+       */
+      public Builder idle(boolean idle) {
+         this.idle = idle;
+         return this;
+      }
+
+      /**
+       * @see Computer#isOffline()
+       */
+      public Builder offline(boolean offline) {
+         this.offline = offline;
+         return this;
+      }
+
+      public Computer build() {
+         return new Computer(displayName, idle, offline);
+      }
+
+      public Builder fromComputerMetadata(Computer from) {
+         return displayName(from.getDisplayName()).idle(from.isIdle()).offline(from.isOffline());
+      }
+   }
+
+   protected final String displayName;
+   protected final boolean idle;
+   protected final boolean offline;
+
+   public Computer(String displayName, boolean idle, boolean offline) {
+      this.displayName = checkNotNull(displayName, "displayName");
+      this.idle = idle;
+      this.offline = offline;
+   }
+
+   /**
+    * 
+    * @return the displayName of the computer
+    */
+   public String getDisplayName() {
+      return displayName;
+   }
+
+   /**
+    * 
+    * @return the number of objects in the computer
+    */
+   public boolean isIdle() {
+      return idle;
+   }
+
+   /**
+    * @return the total offline stored in this computer
+    */
+   public boolean isOffline() {
+      return offline;
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (object instanceof Computer) {
+         final Computer other = Computer.class.cast(object);
+         return equal(getDisplayName(), other.getDisplayName()) && equal(isIdle(), other.isIdle())
+                  && equal(isOffline(), other.isOffline());
+      } else {
+         return false;
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(getDisplayName(), isIdle(), isOffline());
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+   protected ToStringHelper string() {
+      return toStringHelper("").add("displayName", getDisplayName()).add("idle", isIdle()).add("offline", isOffline());
+   }
+
+   @Override
+   public int compareTo(Computer that) {
+      if (that == null)
+         return 1;
+      if (this == that)
+         return 0;
+      return this.getDisplayName().compareTo(that.getDisplayName());
+   }
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/ComputerView.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/ComputerView.java
new file mode 100644
index 0000000..3c505f7
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/domain/ComputerView.java
@@ -0,0 +1,168 @@
+package org.jclouds.jenkins.v1.domain;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Set;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableSet;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Adrian Cole
+ * @see <a
+ *      href="http://ci.jruby.org/computer/api/">api
+ *      doc</a>
+ */
+public class ComputerView implements Comparable<ComputerView> {
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public Builder toBuilder() {
+      return builder().fromComputerMetadata(this);
+   }
+
+   public static class Builder {
+      protected String displayName;
+      protected int busyExecutors;
+      protected int totalExecutors;
+      protected Set<Computer> computers = ImmutableSet.of();
+
+      /**
+       * @see ComputerView#getDisplayName()
+       */
+      public Builder displayName(String displayName) {
+         this.displayName = checkNotNull(displayName, "displayName");
+         return this;
+      }
+
+      /**
+       * @see ComputerView#getBusyExecutors()
+       */
+      public Builder busyExecutors(int busyExecutors) {
+         this.busyExecutors = busyExecutors;
+         return this;
+      }
+
+      /**
+       * @see ComputerView#getTotalExecutors()
+       */
+      public Builder totalExecutors(int totalExecutors) {
+         this.totalExecutors = totalExecutors;
+         return this;
+      }
+      
+      /**
+       * @see ComputerView#getLinks()
+       */
+      public Builder computers(Computer... computers) {
+         return computers(ImmutableSet.copyOf(checkNotNull(computers, "computers")));
+      }
+
+      /**
+       * @see ComputerView#getLinks()
+       */
+      public Builder computers(Set<Computer> computers) {
+         this.computers = ImmutableSet.copyOf(checkNotNull(computers, "computers"));
+         return this;
+      }
+      
+      public ComputerView build() {
+         return new ComputerView(displayName, busyExecutors, totalExecutors, computers);
+      }
+
+      public Builder fromComputerMetadata(ComputerView from) {
+         return displayName(from.getDisplayName()).busyExecutors(from.getBusyExecutors()).totalExecutors(from.getTotalExecutors()).computers(from.getComputers());
+      }
+   }
+
+   protected final String displayName;
+   protected final int busyExecutors;
+   protected final int totalExecutors;
+   @SerializedName("computer")
+   protected final Set<Computer> computers;
+
+   public ComputerView(String displayName, int busyExecutors, int totalExecutors, Set<Computer> computers) {
+      this.displayName = checkNotNull(displayName, "displayName");
+      this.busyExecutors = busyExecutors;
+      this.totalExecutors = totalExecutors;
+      this.computers = ImmutableSet.copyOf(checkNotNull(computers, "computers"));
+   }
+
+   /**
+    * 
+    * @return the displayName of the computer
+    */
+   public String getDisplayName() {
+      return displayName;
+   }
+
+   /**
+    * 
+    * @return the number of objects in the computer
+    */
+   public int getBusyExecutors() {
+      return busyExecutors;
+   }
+
+   /**
+    * @return the total totalExecutors stored in this computer
+    */
+   public int getTotalExecutors() {
+      return totalExecutors;
+   }
+   
+   /**
+    * @return the computers in this set
+    */
+   //TODO: create type adapter for gson that understands ForwardingSet so that we can implement the Set interface
+   public Set<Computer> getComputers() {
+      return computers;
+   }
+   
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (object instanceof ComputerView) {
+         final ComputerView other = ComputerView.class.cast(object);
+         return equal(getDisplayName(), other.getDisplayName()) && equal(getBusyExecutors(), other.getBusyExecutors())
+                  && equal(getTotalExecutors(), other.getTotalExecutors()) && equal(getComputers(), other.getComputers());
+      } else {
+         return false;
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(getDisplayName(), getBusyExecutors(), getTotalExecutors(), getComputers());
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+   protected ToStringHelper string() {
+      return toStringHelper("").add("displayName", getDisplayName()).add("busyExecutors", getBusyExecutors()).add(
+               "totalExecutors", getTotalExecutors()).add("computers", getComputers());
+   }
+
+   @Override
+   public int compareTo(ComputerView that) {
+      if (that == null)
+         return 1;
+      if (this == that)
+         return 0;
+      return this.getDisplayName().compareTo(that.getDisplayName());
+   }
+
+
+
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncClient.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncClient.java
new file mode 100644
index 0000000..0ef8154
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncClient.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.features;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.jenkins.v1.domain.Computer;
+import org.jclouds.jenkins.v1.domain.ComputerView;
+import org.jclouds.jenkins.v1.filters.BasicAuthenticationUnlessAnonymous;
+import org.jclouds.rest.annotations.ExceptionParser;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Computer Services
+ * 
+ * @see ComputerClient
+ * @author Adrian Cole
+ * @see <a href=
+ *      "http://ci.jruby.org/computer/api/"
+ *      >api doc</a>
+ */
+@RequestFilters(BasicAuthenticationUnlessAnonymous.class)
+public interface ComputerAsyncClient {
+
+   /**
+    * @see ComputerClient#getComputerView
+    */
+   @GET
+   @Path("/computer/api/json")
+   @Consumes(MediaType.APPLICATION_JSON)
+   ListenableFuture<ComputerView> getComputerView();
+   
+   /**
+    * @see ComputerClient#getComputer
+    */
+   @GET
+   @Path("/computer/{displayName}/api/json")
+   @Consumes(MediaType.APPLICATION_JSON)
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   ListenableFuture<Computer> getComputer(@PathParam("displayName") String displayName);
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerClient.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerClient.java
new file mode 100644
index 0000000..1ab93ad
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerClient.java
@@ -0,0 +1,48 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.features;
+
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.concurrent.Timeout;
+import org.jclouds.jenkins.v1.domain.Computer;
+import org.jclouds.jenkins.v1.domain.ComputerView;
+
+/**
+ * Computer Services
+ * 
+ * @see ComputerAsyncClient
+ * @author Adrian Cole
+ * @see <a href= "http://ci.jruby.org/computer/api/" >api doc</a>
+ */
+@Timeout(duration = 180, timeUnit = TimeUnit.SECONDS)
+public interface ComputerClient {
+
+   /**
+    * @return overview of all configured computers
+    */
+   ComputerView getComputerView();
+   
+   /**
+    * 
+    * @param displayName display name of the computer
+    * @return computer or null if not found
+    */
+   Computer getComputer(String displayName);
+}
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymous.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymous.java
new file mode 100644
index 0000000..de8183d
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymous.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.filters;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpException;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpRequestFilter;
+import org.jclouds.http.filters.BasicAuthentication;
+import org.jclouds.jenkins.v1.JenkinsApiMetadata;
+import org.jclouds.rest.annotations.Identity;
+
+import com.google.common.base.Optional;
+
+/**
+ * @author Adrian Cole
+ * 
+ */
+@Singleton
+public class BasicAuthenticationUnlessAnonymous implements HttpRequestFilter {
+
+   private final Optional<BasicAuthentication> auth;
+
+   @Inject
+   public BasicAuthenticationUnlessAnonymous(@Identity String user, BasicAuthentication auth) {
+      this.auth = JenkinsApiMetadata.ANONYMOUS_IDENTITY.equals(checkNotNull(user, "user")) ? Optional
+               .<BasicAuthentication> absent() : Optional.of(checkNotNull(auth, "auth"));
+   }
+
+   @Override
+   public HttpRequest filter(HttpRequest request) throws HttpException {
+      if (auth.isPresent())
+         return auth.get().filter(request);
+      return request;
+   }
+}
\ No newline at end of file
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/handlers/JenkinsErrorHandler.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/handlers/JenkinsErrorHandler.java
new file mode 100644
index 0000000..3fa8b80
--- /dev/null
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/handlers/JenkinsErrorHandler.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.handlers;
+
+import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
+
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpResponseException;
+import org.jclouds.rest.AuthorizationException;
+import org.jclouds.rest.ResourceNotFoundException;
+
+/**
+ * This will parse and set an appropriate exception on the command object.
+ * 
+ * @author Adrian Cole
+ * 
+ */
+// TODO: is there error spec someplace? let's type errors, etc.
+@Singleton
+public class JenkinsErrorHandler implements HttpErrorHandler {
+
+   public void handleError(HttpCommand command, HttpResponse response) {
+      // it is important to always read fully and close streams
+      byte[] data = closeClientButKeepContentStream(response);
+      String message = data != null ? new String(data) : null;
+
+      Exception exception = message != null ? new HttpResponseException(command, response, message)
+               : new HttpResponseException(command, response);
+      message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(),
+               response.getStatusLine());
+      switch (response.getStatusCode()) {
+         case 400:
+            break;
+         case 401:
+         case 403:
+            exception = new AuthorizationException(message, exception);
+            break;
+         case 404:
+            if (!command.getCurrentRequest().getMethod().equals("DELETE")) {
+               exception = new ResourceNotFoundException(message, exception);
+            }
+            break;
+      }
+      command.setException(exception);
+   }
+}
diff --git a/labs/jenkins/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata b/labs/jenkins/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
new file mode 100644
index 0000000..c80a7f3
--- /dev/null
+++ b/labs/jenkins/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
@@ -0,0 +1 @@
+org.jclouds.jenkins.v1.JenkinsApiMetadata
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsApiMetadataTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsApiMetadataTest.java
new file mode 100644
index 0000000..707c42e
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsApiMetadataTest.java
@@ -0,0 +1,37 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1;
+
+import org.jclouds.View;
+import org.jclouds.apis.internal.BaseApiMetadataTest;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "JenkinsApiMetadataTest")
+public class JenkinsApiMetadataTest extends BaseApiMetadataTest {
+   public JenkinsApiMetadataTest() {
+      super(new JenkinsApiMetadata(), ImmutableSet.<TypeToken<? extends View>> of());
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsErrorHandlerTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsErrorHandlerTest.java
new file mode 100644
index 0000000..bb26d01
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/JenkinsErrorHandlerTest.java
@@ -0,0 +1,98 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.verify;
+
+import java.net.URI;
+
+import org.easymock.IArgumentMatcher;
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.io.Payloads;
+import org.jclouds.jenkins.v1.handlers.JenkinsErrorHandler;
+import org.jclouds.rest.ResourceNotFoundException;
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "JenkinsErrorHandlerTest")
+public class JenkinsErrorHandlerTest {
+
+   @Test
+   public void test404WithHTMLDoesntBustParserAndMakesResourceNotFoundException() {
+      assertCodeMakes("GET", URI
+               .create("http://ci.jruby.org/computer/master/api/json"),
+               404, "Not Found", "<html></html>", ResourceNotFoundException.class);
+   }
+
+   private void assertCodeMakes(String method, URI uri, int statusCode, String message, String content,
+            Class<? extends Exception> expected) {
+      assertCodeMakes(method, uri, statusCode, message, "text/plain", content, expected);
+   }
+
+   private void assertCodeMakes(String method, URI uri, int statusCode, String message, String contentType,
+            String content, Class<? extends Exception> expected) {
+
+      JenkinsErrorHandler function = new JenkinsErrorHandler();
+
+      HttpCommand command = createMock(HttpCommand.class);
+      HttpRequest request = new HttpRequest(method, uri);
+      HttpResponse response = new HttpResponse(statusCode, message, Payloads.newInputStreamPayload(Strings2
+               .toInputStream(content)));
+      response.getPayload().getContentMetadata().setContentType(contentType);
+
+      expect(command.getCurrentRequest()).andReturn(request).atLeastOnce();
+      command.setException(classEq(expected));
+
+      replay(command);
+
+      function.handleError(command, response);
+
+      verify(command);
+   }
+
+   public static Exception classEq(final Class<? extends Exception> in) {
+      reportMatcher(new IArgumentMatcher() {
+
+         @Override
+         public void appendTo(StringBuffer buffer) {
+            buffer.append("classEq(");
+            buffer.append(in);
+            buffer.append(")");
+         }
+
+         @Override
+         public boolean matches(Object arg) {
+            return arg.getClass() == in;
+         }
+
+      });
+      return null;
+   }
+
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientExpectTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientExpectTest.java
new file mode 100644
index 0000000..47ebdcf
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientExpectTest.java
@@ -0,0 +1,77 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.features;
+
+import static org.testng.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.jenkins.v1.JenkinsClient;
+import org.jclouds.jenkins.v1.internal.BaseJenkinsClientExpectTest;
+import org.jclouds.jenkins.v1.parse.ParseComputerTest;
+import org.jclouds.jenkins.v1.parse.ParseComputerViewTest;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMultimap;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "ComputerClientExpectTest")
+public class ComputerClientExpectTest extends BaseJenkinsClientExpectTest {
+
+   public void testGetComputerViewWhenResponseIs2xx() {
+      HttpRequest getComputerView = HttpRequest
+            .builder()
+            .method("GET")
+            .endpoint(URI.create("http://localhost:8080/computer/api/json"))
+            .headers(
+                  ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
+                        .put("Authorization", "Basic aWRlbnRpdHk6Y3JlZGVudGlhbA==").build()).build();
+
+      HttpResponse getComputerViewResponse = HttpResponse.builder().statusCode(200)
+            .payload(payloadFromResource("/computerview.json")).build();
+
+      JenkinsClient clientWhenServersExist = requestSendsResponse(getComputerView, getComputerViewResponse);
+
+      assertEquals(clientWhenServersExist.getComputerClient().getComputerView().toString(),
+            new ParseComputerViewTest().expected().toString());
+   }
+   
+   public void testGetComputerWhenResponseIs2xx() {
+      HttpRequest getComputer = HttpRequest
+            .builder()
+            .method("GET")
+            .endpoint(URI.create("http://localhost:8080/computer/Ruboto/api/json"))
+            .headers(
+                  ImmutableMultimap.<String, String> builder().put("Accept", "application/json")
+                        .put("Authorization", "Basic aWRlbnRpdHk6Y3JlZGVudGlhbA==").build()).build();
+
+      HttpResponse getComputerResponse = HttpResponse.builder().statusCode(200)
+            .payload(payloadFromResource("/computer.json")).build();
+
+      JenkinsClient clientWhenServersExist = requestSendsResponse(getComputer, getComputerResponse);
+
+      assertEquals(clientWhenServersExist.getComputerClient().getComputer("Ruboto").toString(),
+            new ParseComputerTest().expected().toString());
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientLiveTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientLiveTest.java
new file mode 100644
index 0000000..45d0b3d
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/features/ComputerClientLiveTest.java
@@ -0,0 +1,52 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import org.jclouds.jenkins.v1.domain.Computer;
+import org.jclouds.jenkins.v1.domain.ComputerView;
+import org.jclouds.jenkins.v1.internal.BaseJenkinsClientLiveTest;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "live", testName = "ComputerClientLiveTest")
+public class ComputerClientLiveTest extends BaseJenkinsClientLiveTest {
+
+   public void testGetComputerView(){
+      ComputerView view = getClient().getComputerView();
+      assertNotNull(view);
+      assertNotNull(view.getDisplayName());
+      for (Computer computerFromView : view.getComputers()) {
+         assertNotNull(computerFromView.getDisplayName());
+         if (!"master".equals(computerFromView.getDisplayName())) {
+            Computer computerFromGetRequest = getClient().getComputer(computerFromView.getDisplayName());
+            assertEquals(computerFromGetRequest, computerFromView);
+         }
+      }
+   }
+
+   private ComputerClient getClient() {
+      return context.getApi().getComputerClient();
+   }
+}
\ No newline at end of file
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymousExpectTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymousExpectTest.java
new file mode 100644
index 0000000..f281b79
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/filters/BasicAuthenticationUnlessAnonymousExpectTest.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.filters;
+
+import static org.testng.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.jenkins.v1.JenkinsApiMetadata;
+import org.jclouds.jenkins.v1.JenkinsClient;
+import org.jclouds.jenkins.v1.internal.BaseJenkinsClientExpectTest;
+import org.jclouds.jenkins.v1.parse.ParseComputerViewTest;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableMultimap;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "BasicAuthenticationUnlessAnonymousExpectTest")
+public class BasicAuthenticationUnlessAnonymousExpectTest extends BaseJenkinsClientExpectTest {
+   
+   public BasicAuthenticationUnlessAnonymousExpectTest(){
+      identity = JenkinsApiMetadata.ANONYMOUS_IDENTITY;
+   }
+   
+   public void testWhenIdentityIsAnonymousNoAuthorizationHeader() {
+      HttpRequest getComputerView = HttpRequest
+            .builder()
+            .method("GET")
+            .endpoint(URI.create("http://localhost:8080/computer/api/json"))
+            .headers(
+                  ImmutableMultimap.<String, String> builder().put("Accept", "application/json").build()).build();
+
+      HttpResponse getComputerViewResponse = HttpResponse.builder().statusCode(200)
+            .payload(payloadFromResource("/computerview.json")).build();
+
+      JenkinsClient clientWhenServersExist = requestSendsResponse(getComputerView, getComputerViewResponse);
+
+      assertEquals(clientWhenServersExist.getComputerClient().getComputerView().toString(),
+            new ParseComputerViewTest().expected().toString());
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsAsyncClientExpectTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsAsyncClientExpectTest.java
new file mode 100644
index 0000000..fd8f448
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsAsyncClientExpectTest.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.internal;
+
+import java.util.Properties;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.jenkins.v1.JenkinsAsyncClient;
+
+import com.google.common.base.Function;
+import com.google.inject.Module;
+
+/**
+ * Base class for writing KeyStone Rest Client Expect tests
+ * 
+ * @author Adrian Cole
+ */
+public class BaseJenkinsAsyncClientExpectTest extends BaseJenkinsExpectTest<JenkinsAsyncClient> {
+   public JenkinsAsyncClient createClient(Function<HttpRequest, HttpResponse> fn, Module module, Properties props) {
+      return createInjector(fn, module, props).getInstance(JenkinsAsyncClient.class);
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientExpectTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientExpectTest.java
new file mode 100644
index 0000000..9b7fb63
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientExpectTest.java
@@ -0,0 +1,30 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.internal;
+
+import org.jclouds.jenkins.v1.JenkinsClient;
+
+/**
+ * Base class for writing Jenkins Expect tests
+ * 
+ * @author Adrian Cole
+ */
+public class BaseJenkinsClientExpectTest extends BaseJenkinsExpectTest<JenkinsClient> {
+
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientLiveTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientLiveTest.java
new file mode 100644
index 0000000..e934c87
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsClientLiveTest.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.internal;
+
+import org.jclouds.apis.BaseContextLiveTest;
+import org.jclouds.jenkins.v1.JenkinsApiMetadata;
+import org.jclouds.jenkins.v1.JenkinsAsyncClient;
+import org.jclouds.jenkins.v1.JenkinsClient;
+import org.jclouds.rest.RestContext;
+import org.testng.annotations.AfterGroups;
+import org.testng.annotations.BeforeGroups;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+/**
+ * Tests behavior of {@code JenkinsClient}
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "live")
+public class BaseJenkinsClientLiveTest extends BaseContextLiveTest<RestContext<JenkinsClient, JenkinsAsyncClient>> {
+
+   public BaseJenkinsClientLiveTest() {
+      provider = "jenkins";
+   }
+
+   protected RestContext<JenkinsClient, JenkinsAsyncClient> jenkinsContext;
+
+   @BeforeGroups(groups = { "integration", "live" })
+   @Override
+   public void setupContext() {
+      super.setupContext();
+      jenkinsContext = context;
+   }
+
+   @AfterGroups(groups = "live")
+   protected void tearDown() {
+      if (jenkinsContext != null)
+         jenkinsContext.close();
+   }
+
+   @Override
+   protected TypeToken<RestContext<JenkinsClient, JenkinsAsyncClient>> contextType() {
+      return JenkinsApiMetadata.CONTEXT_TOKEN;
+   }
+
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsExpectTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsExpectTest.java
new file mode 100644
index 0000000..b12fcac
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/internal/BaseJenkinsExpectTest.java
@@ -0,0 +1,32 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.internal;
+
+import org.jclouds.rest.internal.BaseRestClientExpectTest;
+
+/**
+ * Base class for writing Jenkins Expect tests
+ * 
+ * @author Adrian Cole
+ */
+public class BaseJenkinsExpectTest<T> extends BaseRestClientExpectTest<T> {
+    public BaseJenkinsExpectTest() {
+      provider = "jenkins";
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerTest.java
new file mode 100644
index 0000000..05c4e6a
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerTest.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.parse;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.jenkins.v1.domain.Computer;
+import org.jclouds.json.BaseItemParserTest;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "ParseComputerTest")
+public class ParseComputerTest extends BaseItemParserTest<Computer> {
+
+   @Override
+   public String resource() {
+      return "/computer.json";
+   }
+
+   @Override
+   @Consumes(MediaType.APPLICATION_JSON)
+   public Computer expected() {
+      return Computer.builder()
+                  .displayName("Ruboto")
+                  .idle(true)
+                  .offline(false)
+                  .build();
+   }
+}
diff --git a/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerViewTest.java b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerViewTest.java
new file mode 100644
index 0000000..c452966
--- /dev/null
+++ b/labs/jenkins/src/test/java/org/jclouds/jenkins/v1/parse/ParseComputerViewTest.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.jenkins.v1.parse;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.jenkins.v1.domain.Computer;
+import org.jclouds.jenkins.v1.domain.ComputerView;
+import org.jclouds.json.BaseItemParserTest;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "ParseComputerViewTest")
+public class ParseComputerViewTest extends BaseItemParserTest<ComputerView> {
+
+   @Override
+   public String resource() {
+      return "/computerview.json";
+   }
+
+   @Override
+   @Consumes(MediaType.APPLICATION_JSON)
+   public ComputerView expected() {
+      return ComputerView.builder()
+                        .displayName("nodes")
+                        .totalExecutors(4)
+                        .busyExecutors(0)
+                        .computers(ImmutableSet.<Computer>builder()
+                                 .add(Computer.builder()
+                                          .displayName("master")
+                                          .idle(true)
+                                          .offline(false).build())
+                                 .add(Computer.builder()
+                                              .displayName("Ruboto")
+                                              .idle(true)
+                                              .offline(false).build())
+                                 .add(Computer.builder()
+                                              .displayName("winserver2008-x86")
+                                              .idle(true)
+                                              .offline(false).build()).build()).build();
+   }
+}
diff --git a/labs/jenkins/src/test/resources/computer.json b/labs/jenkins/src/test/resources/computer.json
new file mode 100644
index 0000000..610126b
--- /dev/null
+++ b/labs/jenkins/src/test/resources/computer.json
@@ -0,0 +1,37 @@
+{

+    "actions": [],

+    "displayName": "Ruboto",

+    "executors": [{}],

+    "icon": "computer.png",

+    "idle": true,

+    "jnlpAgent": true,

+    "launchSupported": false,

+    "loadStatistics": {},

+    "manualLaunchAllowed": true,

+    "monitorData": {

+        "hudson.node_monitors.SwapSpaceMonitor": {

+            "availablePhysicalMemory": 1697591296,

+            "availableSwapSpace": 5626036224,

+            "totalPhysicalMemory": 4157317120,

+            "totalSwapSpace": 6568271872

+        },

+        "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",

+        "hudson.node_monitors.ResponseTimeMonitor": {

+            "average": 955

+        },

+        "hudson.node_monitors.TemporarySpaceMonitor": {

+            "size": 53646782464

+        },

+        "hudson.node_monitors.DiskSpaceMonitor": {

+            "size": 53646782464

+        },

+        "hudson.node_monitors.ClockMonitor": {

+            "diff": -309

+        }

+    },

+    "numExecutors": 1,

+    "offline": false,

+    "offlineCause": null,

+    "oneOffExecutors": [],

+    "temporarilyOffline": false

+}
\ No newline at end of file
diff --git a/labs/jenkins/src/test/resources/computerview.json b/labs/jenkins/src/test/resources/computerview.json
new file mode 100644
index 0000000..e9b0704
--- /dev/null
+++ b/labs/jenkins/src/test/resources/computerview.json
@@ -0,0 +1,114 @@
+{

+    "busyExecutors": 0,

+    "computer": [{

+        "actions": [],

+        "displayName": "master",

+        "executors": [{}, {}],

+        "icon": "computer.png",

+        "idle": true,

+        "jnlpAgent": false,

+        "launchSupported": true,

+        "loadStatistics": {},

+        "manualLaunchAllowed": true,

+        "monitorData": {

+            "hudson.node_monitors.SwapSpaceMonitor": {

+                "availablePhysicalMemory": 1385115648,

+                "availableSwapSpace": 32208396288,

+                "totalPhysicalMemory": 8053207040,

+                "totalSwapSpace": 32218378240

+            },

+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",

+            "hudson.node_monitors.ResponseTimeMonitor": {

+                "average": 1

+            },

+            "hudson.node_monitors.TemporarySpaceMonitor": {

+                "size": 6235500544

+            },

+            "hudson.node_monitors.DiskSpaceMonitor": {

+                "size": 79292284928

+            },

+            "hudson.node_monitors.ClockMonitor": {

+                "diff": 0

+            }

+        },

+        "numExecutors": 2,

+        "offline": false,

+        "offlineCause": null,

+        "oneOffExecutors": [],

+        "temporarilyOffline": false

+    }, {

+        "actions": [],

+        "displayName": "Ruboto",

+        "executors": [{}],

+        "icon": "computer.png",

+        "idle": true,

+        "jnlpAgent": true,

+        "launchSupported": false,

+        "loadStatistics": {},

+        "manualLaunchAllowed": true,

+        "monitorData": {

+            "hudson.node_monitors.SwapSpaceMonitor": {

+                "availablePhysicalMemory": 1684832256,

+                "availableSwapSpace": 5625421824,

+                "totalPhysicalMemory": 4157317120,

+                "totalSwapSpace": 6568271872

+            },

+            "hudson.node_monitors.ArchitectureMonitor": "Linux (amd64)",

+            "hudson.node_monitors.ResponseTimeMonitor": {

+                "average": 856

+            },

+            "hudson.node_monitors.TemporarySpaceMonitor": {

+                "size": 53648973824

+            },

+            "hudson.node_monitors.DiskSpaceMonitor": {

+                "size": 53648969728

+            },

+            "hudson.node_monitors.ClockMonitor": {

+                "diff": -462

+            }

+        },

+        "numExecutors": 1,

+        "offline": false,

+        "offlineCause": null,

+        "oneOffExecutors": [],

+        "temporarilyOffline": false

+    }, {

+        "actions": [],

+        "displayName": "winserver2008-x86",

+        "executors": [{}],

+        "icon": "computer.png",

+        "idle": true,

+        "jnlpAgent": true,

+        "launchSupported": false,

+        "loadStatistics": {},

+        "manualLaunchAllowed": true,

+        "monitorData": {

+            "hudson.node_monitors.SwapSpaceMonitor": {

+                "availablePhysicalMemory": 1117851648,

+                "availableSwapSpace": 1429299200,

+                "totalPhysicalMemory": 1781420032,

+                "totalSwapSpace": 1994350592

+            },

+            "hudson.node_monitors.ArchitectureMonitor": "Windows Server 2008 (x86)",

+            "hudson.node_monitors.ResponseTimeMonitor": {

+                "average": 1

+            },

+            "hudson.node_monitors.TemporarySpaceMonitor": {

+                "size": 19072663552

+            },

+            "hudson.node_monitors.DiskSpaceMonitor": {

+                "size": 19072663552

+            },

+            "hudson.node_monitors.ClockMonitor": {

+                "diff": 71

+            }

+        },

+        "numExecutors": 1,

+        "offline": false,

+        "offlineCause": null,

+        "oneOffExecutors": [],

+        "temporarilyOffline": false

+    }],

+    "displayName": "nodes",

+    "totalExecutors": 4

+}
\ No newline at end of file
diff --git a/labs/jenkins/src/test/resources/logback.xml b/labs/jenkins/src/test/resources/logback.xml
new file mode 100644
index 0000000..9679b2e
--- /dev/null
+++ b/labs/jenkins/src/test/resources/logback.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>

+<configuration scan="false">

+    <appender name="FILE" class="ch.qos.logback.core.FileAppender">

+        <file>target/test-data/jclouds.log</file>

+

+        <encoder>

+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>

+        </encoder>

+    </appender>

+

+    <appender name="WIREFILE" class="ch.qos.logback.core.FileAppender">

+        <file>target/test-data/jclouds-wire.log</file>

+

+        <encoder>

+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>

+        </encoder>

+    </appender>

+    

+    <root>

+        <level value="warn" />

+    </root>

+

+    <logger name="org.jclouds">

+        <level value="DEBUG" />

+        <appender-ref ref="FILE" />

+    </logger>

+

+    <logger name="jclouds.wire">

+        <level value="DEBUG" />

+        <appender-ref ref="WIREFILE" />

+    </logger>

+

+    <logger name="jclouds.headers">

+        <level value="DEBUG" />

+        <appender-ref ref="WIREFILE" />

+    </logger>

+

+</configuration>

diff --git a/labs/pom.xml b/labs/pom.xml
index b774ad0..a6d3e22 100644
--- a/labs/pom.xml
+++ b/labs/pom.xml
@@ -42,5 +42,6 @@
        <module>dmtf</module>
        <module>carrenza-vcloud-director</module>
        <module>openstack-swift</module>
+       <module>jenkins</module>
     </modules>
 </project>
diff --git a/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/features/HPCloudComputeVolumeClientLiveTest.java b/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/features/HPCloudComputeVolumeClientLiveTest.java
new file mode 100644
index 0000000..1dc049f
--- /dev/null
+++ b/providers/hpcloud-compute/src/test/java/org/jclouds/hpcloud/compute/features/HPCloudComputeVolumeClientLiveTest.java
@@ -0,0 +1,34 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you 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.jclouds.hpcloud.compute.features;
+
+import org.jclouds.openstack.nova.v1_1.extensions.VolumeClientLiveTest;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "live", testName = "HPCloudComputeVolumeClientLiveTest")
+public class HPCloudComputeVolumeClientLiveTest extends VolumeClientLiveTest {
+   public HPCloudComputeVolumeClientLiveTest() {
+      provider = "hpcloud-compute";
+   }
+
+}