You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2014/02/05 10:40:21 UTC

[3/4] Added the DigitalOcean provider

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Droplet.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Droplet.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Droplet.java
new file mode 100644
index 0000000..307cbc4
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Droplet.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * A droplet.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class Droplet {
+
+   public enum Status {
+      NEW, ACTIVE, ARCHIVE, OFF;
+
+      public static Status fromValue(String value) {
+         Optional<Status> status = Enums.getIfPresent(Status.class, value.toUpperCase());
+         checkArgument(status.isPresent(), "Expected one of %s but was %s", Joiner.on(',').join(Status.values()), value);
+         return status.get();
+      }
+   }
+
+   private final int id;
+   private final String name;
+   @Named("image_id")
+   private final int imageId;
+   @Named("size_id")
+   private final int sizeId;
+   @Named("region_id")
+   private final int regionId;
+   @Named("backups_active")
+   private final boolean backupsActive;
+   private final List<Object> backups;
+   private final List<Object> snapshots;
+   @Named("ip_address")
+   private final String ip;
+   @Named("private_ip_address")
+   private final String privateIp;
+   private final boolean locked;
+   private final Status status;
+   @Named("created_at")
+   private final Date creationDate;
+
+   @ConstructorProperties({ "id", "name", "image_id", "size_id", "region_id", "backups_active", "backups", "snapshots",
+         "ip_address", "private_ip_address", "locked", "status", "created_at" })
+   public Droplet(int id, String name, int imageId, int sizeId, int regionId, boolean backupsActive,
+         @Nullable List<Object> backups, @Nullable List<Object> snapshots, String ip, @Nullable String privateIp,
+         boolean locked, Status status, @Nullable Date creationDate) throws ParseException {
+      this.id = id;
+      this.name = checkNotNull(name, "name cannot be null");
+      this.imageId = imageId;
+      this.sizeId = sizeId;
+      this.regionId = regionId;
+      this.backupsActive = backupsActive;
+      this.backups = backups != null ? ImmutableList.copyOf(backups) : ImmutableList.of();
+      this.snapshots = snapshots != null ? ImmutableList.copyOf(snapshots) : ImmutableList.of();
+      this.ip = ip;
+      this.privateIp = privateIp;
+      this.locked = locked;
+      this.status = checkNotNull(status, "status cannot be null");
+      this.creationDate = creationDate;
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public int getImageId() {
+      return imageId;
+   }
+
+   public int getSizeId() {
+      return sizeId;
+   }
+
+   public int getRegionId() {
+      return regionId;
+   }
+
+   public boolean isBackupsActive() {
+      return backupsActive;
+   }
+
+   public List<Object> getBackups() {
+      return backups;
+   }
+
+   public List<Object> getSnapshots() {
+      return snapshots;
+   }
+
+   public String getIp() {
+      return ip;
+   }
+
+   public String getPrivateIp() {
+      return privateIp;
+   }
+
+   public boolean isLocked() {
+      return locked;
+   }
+
+   public Status getStatus() {
+      return status;
+   }
+
+   public Date getCreationDate() {
+      return creationDate;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + (backups == null ? 0 : backups.hashCode());
+      result = prime * result + (backupsActive ? 1231 : 1237);
+      result = prime * result + (creationDate == null ? 0 : creationDate.hashCode());
+      result = prime * result + id;
+      result = prime * result + imageId;
+      result = prime * result + (ip == null ? 0 : ip.hashCode());
+      result = prime * result + (locked ? 1231 : 1237);
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + (privateIp == null ? 0 : privateIp.hashCode());
+      result = prime * result + regionId;
+      result = prime * result + sizeId;
+      result = prime * result + (snapshots == null ? 0 : snapshots.hashCode());
+      result = prime * result + (status == null ? 0 : status.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Droplet other = (Droplet) obj;
+      if (backups == null) {
+         if (other.backups != null) {
+            return false;
+         }
+      } else if (!backups.equals(other.backups)) {
+         return false;
+      }
+      if (backupsActive != other.backupsActive) {
+         return false;
+      }
+      if (creationDate == null) {
+         if (other.creationDate != null) {
+            return false;
+         }
+      } else if (!creationDate.equals(other.creationDate)) {
+         return false;
+      }
+      if (id != other.id) {
+         return false;
+      }
+      if (imageId != other.imageId) {
+         return false;
+      }
+      if (ip == null) {
+         if (other.ip != null) {
+            return false;
+         }
+      } else if (!ip.equals(other.ip)) {
+         return false;
+      }
+      if (locked != other.locked) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (privateIp == null) {
+         if (other.privateIp != null) {
+            return false;
+         }
+      } else if (!privateIp.equals(other.privateIp)) {
+         return false;
+      }
+      if (regionId != other.regionId) {
+         return false;
+      }
+      if (sizeId != other.sizeId) {
+         return false;
+      }
+      if (snapshots == null) {
+         if (other.snapshots != null) {
+            return false;
+         }
+      } else if (!snapshots.equals(other.snapshots)) {
+         return false;
+      }
+      if (status != other.status) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Droplet [id=" + id + ", name=" + name + ", imageId=" + imageId + ", sizeId=" + sizeId + ", regionId="
+            + regionId + ", backupsActive=" + backupsActive + ", backups=" + backups + ", snapshots=" + snapshots
+            + ", ip=" + ip + ", privateIp=" + privateIp + ", locked=" + locked + ", status=" + status
+            + ", creationDate=" + creationDate + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/DropletCreation.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/DropletCreation.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/DropletCreation.java
new file mode 100644
index 0000000..6706ac6
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/DropletCreation.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+
+import com.google.inject.name.Named;
+
+/**
+ * An DropletCreation response.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class DropletCreation {
+
+   private final int id;
+   private final String name;
+   @Named("image_id")
+   private final int imageId;
+   @Named("size_id")
+   private final int sizeId;
+   @Named("event_id")
+   private final int eventId;
+
+   @ConstructorProperties({ "id", "name", "image_id", "size_id", "event_id" })
+   public DropletCreation(int id, String name, int imageId, int sizeId, int eventId) {
+      this.id = id;
+      this.name = checkNotNull(name, "name cannot be null");
+      this.imageId = imageId;
+      this.sizeId = sizeId;
+      this.eventId = eventId;
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public int getImageId() {
+      return imageId;
+   }
+
+   public int getSizeId() {
+      return sizeId;
+   }
+
+   public int getEventId() {
+      return eventId;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + eventId;
+      result = prime * result + id;
+      result = prime * result + imageId;
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + sizeId;
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      DropletCreation other = (DropletCreation) obj;
+      if (eventId != other.eventId) {
+         return false;
+      }
+      if (id != other.id) {
+         return false;
+      }
+      if (imageId != other.imageId) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (sizeId != other.sizeId) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "DropletCreation [id=" + id + ", name=" + name + ", imageId=" + imageId + ", sizeId=" + sizeId
+            + ", eventId=" + eventId + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Event.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Event.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Event.java
new file mode 100644
index 0000000..6b39da1
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Event.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.primitives.Ints.tryParse;
+
+import java.beans.ConstructorProperties;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.inject.name.Named;
+
+/**
+ * An Event.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class Event {
+
+   public enum Status {
+      DONE, PENDING;
+
+      public static Status fromValue(String value) {
+         // DigitalOcean return a 'null' status when the operation is still in
+         // progress
+         if (value == null) {
+            return PENDING;
+         }
+         Optional<Status> status = Enums.getIfPresent(Status.class, value.toUpperCase());
+         checkArgument(status.isPresent(), "Expected one of %s but was", Joiner.on(',').join(Status.values()), value);
+         return status.get();
+      }
+   }
+
+   private final int id;
+   @Named("action_status")
+   private final Status status;
+   @Named("event_type_id")
+   private final int typeId;
+   private final Integer percentage;
+   @Named("droplet_id")
+   private final int dropletId;
+
+   @ConstructorProperties({ "id", "action_status", "event_type_id", "percentage", "droplet_id" })
+   public Event(int id, @Nullable Status status, int typeId, @Nullable String percentage, int dropletId) {
+      this.id = id;
+      this.status = status == null ? Status.PENDING : status;
+      this.typeId = typeId;
+      this.percentage = percentage == null ? null : tryParse(percentage);
+      this.dropletId = dropletId;
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public Status getStatus() {
+      return status;
+   }
+
+   public int getTypeId() {
+      return typeId;
+   }
+
+   public Integer getPercentage() {
+      return percentage;
+   }
+
+   public int getDropletId() {
+      return dropletId;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + dropletId;
+      result = prime * result + id;
+      result = prime * result + (percentage == null ? 0 : percentage.hashCode());
+      result = prime * result + (status == null ? 0 : status.hashCode());
+      result = prime * result + typeId;
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Event other = (Event) obj;
+      if (dropletId != other.dropletId) {
+         return false;
+      }
+      if (id != other.id) {
+         return false;
+      }
+      if (percentage == null) {
+         if (other.percentage != null) {
+            return false;
+         }
+      } else if (!percentage.equals(other.percentage)) {
+         return false;
+      }
+      if (status != other.status) {
+         return false;
+      }
+      if (typeId != other.typeId) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Event [id=" + id + ", status=" + status + ", typeId=" + typeId + ", percentage=" + percentage
+            + ", dropletId=" + dropletId + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Image.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Image.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Image.java
new file mode 100644
index 0000000..e1bc545
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Image.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+
+import org.jclouds.javax.annotation.Nullable;
+
+/**
+ * An Image.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ * 
+ */
+public class Image {
+   private final int id;
+   private final String name;
+   private final OperatingSystem os;
+   private final boolean publicImage;
+   private final String slug;
+
+   @ConstructorProperties({ "id", "name", "distribution", "public", "slug" })
+   public Image(int id, String name, String distribution, boolean publicImage, @Nullable String slug) {
+      this.id = id;
+      this.name = checkNotNull(name, "name");
+      this.os = OperatingSystem.builder().from(name, checkNotNull(distribution, "distribution")).build();
+      this.publicImage = publicImage;
+      this.slug = slug;
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public OperatingSystem getOs() {
+      return os;
+   }
+
+   public boolean isPublicImage() {
+      return publicImage;
+   }
+
+   public String getSlug() {
+      return slug;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + id;
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + (os == null ? 0 : os.hashCode());
+      result = prime * result + (publicImage ? 1231 : 1237);
+      result = prime * result + (slug == null ? 0 : slug.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Image other = (Image) obj;
+      if (id != other.id) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (os == null) {
+         if (other.os != null) {
+            return false;
+         }
+      } else if (!os.equals(other.os)) {
+         return false;
+      }
+      if (publicImage != other.publicImage) {
+         return false;
+      }
+      if (slug == null) {
+         if (other.slug != null) {
+            return false;
+         }
+      } else if (!slug.equals(other.slug)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Image [id=" + id + ", name=" + name + ", os=" + os + ", publicImage=" + publicImage + ", slug=" + slug
+            + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/OperatingSystem.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/OperatingSystem.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/OperatingSystem.java
new file mode 100644
index 0000000..ac67c51
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/OperatingSystem.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.nullToEmpty;
+import static java.util.regex.Pattern.compile;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The operating system of an image.
+ * <p>
+ * This class parses the <code>name</code> string (e.g. "Ubuntu 12.10 x64") of the images and properly sets each field
+ * to the right value.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class OperatingSystem {
+
+   // Parse something like "Ubuntu 12.10 x64"
+   private static final Pattern VERSION_PATTERN = compile("\\s(\\d+(?:\\.?\\d+)?)");
+   private static final Pattern ARCH_PATTERN = compile("x\\d{2}");
+   private static final String IS_64_BIT = "x64";
+
+   private final Distribution distribution;
+   private final String version;
+   private final String arch;
+
+   private OperatingSystem(String distribution, String version, String arch) {
+      this.distribution = checkNotNull(Distribution.fromValue(distribution), "distribution cannot be null");
+      this.version = checkNotNull(version, "version cannot be null");
+      this.arch = checkNotNull(arch, "arch cannot be null");
+   }
+
+   public Distribution getDistribution() {
+      return distribution;
+   }
+
+   public String getVersion() {
+      return version;
+   }
+
+   public String getArch() {
+      return arch;
+   }
+
+   public boolean is64bit() {
+      return IS_64_BIT.equals(arch);
+   }
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public static class Builder {
+      private String name;
+      private String distribution;
+
+      public Builder from(String name, String distribution) {
+         this.name = checkNotNull(name, "name cannot be null");
+         this.distribution = checkNotNull(distribution, "distribution cannot be null");
+         return this;
+      }
+
+      public OperatingSystem build() {
+         return new OperatingSystem(distribution, match(VERSION_PATTERN, name, 1), match(ARCH_PATTERN, name, 0));
+      }
+   }
+
+   private static String match(final Pattern pattern, final String input, int group) {
+      Matcher m = pattern.matcher(input);
+      return m.find() ? nullToEmpty(m.group(group)) : "";
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + (arch == null ? 0 : arch.hashCode());
+      result = prime * result + (distribution == null ? 0 : distribution.hashCode());
+      result = prime * result + (version == null ? 0 : version.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(final Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      OperatingSystem other = (OperatingSystem) obj;
+      if (arch == null) {
+         if (other.arch != null) {
+            return false;
+         }
+      } else if (!arch.equals(other.arch)) {
+         return false;
+      }
+      if (distribution != other.distribution) {
+         return false;
+      }
+      if (version == null) {
+         if (other.version != null) {
+            return false;
+         }
+      } else if (!version.equals(other.version)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "OperatingSystem [distribution=" + distribution + ", version=" + version + ", arch=" + arch + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Region.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Region.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Region.java
new file mode 100644
index 0000000..cfefada
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Region.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+
+/**
+ * A Region.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class Region {
+
+   private final int id;
+   private final String name;
+   private final String slug;
+
+   @ConstructorProperties({ "id", "name", "slug" })
+   public Region(int id, String name, String slug) {
+      this.id = id;
+      this.name = checkNotNull(name, "name cannot be null");
+      this.slug = checkNotNull(slug, "slug cannot be null");
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public String getSlug() {
+      return slug;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + id;
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + (slug == null ? 0 : slug.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Region other = (Region) obj;
+      if (id != other.id) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (slug == null) {
+         if (other.slug != null) {
+            return false;
+         }
+      } else if (!slug.equals(other.slug)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Region [id=" + id + ", name=" + name + ", slug=" + slug + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java
new file mode 100644
index 0000000..b1c7c63
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+
+import javax.inject.Named;
+
+import org.jclouds.javax.annotation.Nullable;
+
+/**
+ * A Size.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class Size {
+
+   private final int id;
+   private final String name;
+   private final String slug;
+   private final int memory;
+   private final int cpu;
+   private final int disk;
+   @Named("cost_per_hour")
+   private final String costPerHour;
+   @Named("cost_per_month")
+   private final String costPerMonth;
+
+   @ConstructorProperties({ "id", "name", "slug", "memory", "cpu", "disk", "cost_per_hour", "cost_per_month" })
+   public Size(int id, String name, @Nullable String slug, int memory, int cpu, int disk, String costPerHour,
+         String costPerMonth) {
+      this.id = id;
+      this.name = checkNotNull(name, "name cannot be null");
+      this.slug = slug;
+      this.memory = memory;
+      this.cpu = cpu;
+      this.disk = disk;
+      this.costPerHour = checkNotNull(costPerHour, "costPerHour cannot be null");
+      this.costPerMonth = checkNotNull(costPerMonth, "costPerMonth cannot be null");
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public String getSlug() {
+      return slug;
+   }
+
+   public int getMemory() {
+      return memory;
+   }
+
+   public int getCpu() {
+      return cpu;
+   }
+
+   public int getDisk() {
+      return disk;
+   }
+
+   public String getCostPerHour() {
+      return costPerHour;
+   }
+
+   public String getCostPerMonth() {
+      return costPerMonth;
+   }
+
+   @Override
+   public int hashCode() {
+      int prime = 31;
+      int result = 1;
+      result = prime * result + (costPerHour == null ? 0 : costPerHour.hashCode());
+      result = prime * result + (costPerMonth == null ? 0 : costPerMonth.hashCode());
+      result = prime * result + cpu;
+      result = prime * result + disk;
+      result = prime * result + id;
+      result = prime * result + memory;
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      result = prime * result + (slug == null ? 0 : slug.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      Size other = (Size) obj;
+      if (costPerHour == null) {
+         if (other.costPerHour != null) {
+            return false;
+         }
+      } else if (!costPerHour.equals(other.costPerHour)) {
+         return false;
+      }
+      if (costPerMonth == null) {
+         if (other.costPerMonth != null) {
+            return false;
+         }
+      } else if (!costPerMonth.equals(other.costPerMonth)) {
+         return false;
+      }
+      if (cpu != other.cpu) {
+         return false;
+      }
+      if (disk != other.disk) {
+         return false;
+      }
+      if (id != other.id) {
+         return false;
+      }
+      if (memory != other.memory) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      if (slug == null) {
+         if (other.slug != null) {
+            return false;
+         }
+      } else if (!slug.equals(other.slug)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "Size [id=" + id + ", name=" + name + ", slug=" + slug + ", memory=" + memory + ", cpu=" + cpu + ", disk="
+            + disk + ", costPerHour=" + costPerHour + ", costPerMonth=" + costPerMonth + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/SshKey.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/SshKey.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/SshKey.java
new file mode 100644
index 0000000..3e335c0
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/SshKey.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+import java.security.PublicKey;
+
+import org.jclouds.javax.annotation.Nullable;
+
+import com.google.inject.name.Named;
+
+/**
+ * A SSH Key.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class SshKey {
+
+   private final int id;
+   private final String name;
+   @Named("ssh_pub_key")
+   private final PublicKey publicKey;
+
+   @ConstructorProperties({ "id", "name", "ssh_pub_key" })
+   public SshKey(int id, String name, @Nullable PublicKey publicKey) {
+      this.id = id;
+      this.name = checkNotNull(name, "name cannot be null");
+      this.publicKey = publicKey;
+   }
+
+   public int getId() {
+      return id;
+   }
+
+   public String getName() {
+      return name;
+   }
+
+   public PublicKey getPublicKey() {
+      return publicKey;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + id;
+      result = prime * result + (name == null ? 0 : name.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (obj == null) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      SshKey other = (SshKey) obj;
+      if (id != other.id) {
+         return false;
+      }
+      if (name == null) {
+         if (other.name != null) {
+            return false;
+         }
+      } else if (!name.equals(other.name)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "SshKey [id=" + id + ", name=" + name + ", publicKey=" + publicKey + "]";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/domain/options/CreateDropletOptions.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/options/CreateDropletOptions.java b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/options/CreateDropletOptions.java
new file mode 100644
index 0000000..aba2114
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/options/CreateDropletOptions.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.domain.options;
+
+import java.util.Set;
+
+import org.jclouds.http.options.BaseHttpRequestOptions;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Options to customize droplet creation.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+public class CreateDropletOptions extends BaseHttpRequestOptions {
+
+   private final Set<Integer> sshKeyIds;
+   private final Boolean privateNetworking;
+   private final Boolean backupsEnabled;
+
+   public CreateDropletOptions(Set<Integer> sshKeyIds, Boolean privateNetworking, Boolean backupsEnabled) {
+      this.sshKeyIds = sshKeyIds;
+      this.privateNetworking = privateNetworking;
+      this.backupsEnabled = backupsEnabled;
+
+      if (!sshKeyIds.isEmpty()) {
+         queryParameters.put("ssh_key_ids", Joiner.on(',').join(sshKeyIds));
+      }
+      if (privateNetworking != null) {
+         queryParameters.put("private_networking", privateNetworking.toString());
+      }
+      if (backupsEnabled != null) {
+         queryParameters.put("backups_enabled", backupsEnabled.toString());
+      }
+   }
+
+   public Iterable<Integer> getSshKeyIds() {
+      return sshKeyIds;
+   }
+
+   public Boolean getPrivateNetworking() {
+      return privateNetworking;
+   }
+
+   public Boolean getBackupsEnabled() {
+      return backupsEnabled;
+   }
+
+   @Override
+   public int hashCode() {
+      final int prime = 31;
+      int result = super.hashCode();
+      result = prime * result + (backupsEnabled == null ? 0 : backupsEnabled.hashCode());
+      result = prime * result + (privateNetworking == null ? 0 : privateNetworking.hashCode());
+      result = prime * result + (sshKeyIds == null ? 0 : sshKeyIds.hashCode());
+      return result;
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (!super.equals(obj)) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      CreateDropletOptions other = (CreateDropletOptions) obj;
+      if (backupsEnabled == null) {
+         if (other.backupsEnabled != null) {
+            return false;
+         }
+      } else if (!backupsEnabled.equals(other.backupsEnabled)) {
+         return false;
+      }
+      if (privateNetworking == null) {
+         if (other.privateNetworking != null) {
+            return false;
+         }
+      } else if (!privateNetworking.equals(other.privateNetworking)) {
+         return false;
+      }
+      if (sshKeyIds == null) {
+         if (other.sshKeyIds != null) {
+            return false;
+         }
+      } else if (!sshKeyIds.equals(other.sshKeyIds)) {
+         return false;
+      }
+      return true;
+   }
+
+   @Override
+   public String toString() {
+      return "CreateDropletOptions [sshKeyIds=" + sshKeyIds + ", privateNetworking=" + privateNetworking
+            + ", backupsEnabled=" + backupsEnabled + "]";
+   }
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public static class Builder {
+      private ImmutableSet.Builder<Integer> sshKeyIds = ImmutableSet.builder();
+      private Boolean privateNetworking;
+      private Boolean backupsEnabled;
+
+      /**
+       * Adds a set of ssh key ids to be added to the droplet.
+       */
+      public Builder addSshKeyIds(Iterable<Integer> sshKeyIds) {
+         this.sshKeyIds.addAll(sshKeyIds);
+         return this;
+      }
+
+      /**
+       * Adds an ssh key id to be added to the droplet.
+       */
+      public Builder addSshKeyId(int sshKeyId) {
+         this.sshKeyIds.add(sshKeyId);
+         return this;
+      }
+
+      /**
+       * Enables a private network interface if the region supports private
+       * networking.
+       */
+      public Builder privateNetworking(boolean privateNetworking) {
+         this.privateNetworking = privateNetworking;
+         return this;
+      }
+
+      /**
+       * Enabled backups for the droplet.
+       */
+      public Builder backupsEnabled(boolean backupsEnabled) {
+         this.backupsEnabled = backupsEnabled;
+         return this;
+      }
+
+      public CreateDropletOptions build() {
+         return new CreateDropletOptions(sshKeyIds.build(), privateNetworking, backupsEnabled);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java
new file mode 100644
index 0000000..4c57db2
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.digitalocean.domain.Droplet;
+import org.jclouds.digitalocean.domain.DropletCreation;
+import org.jclouds.digitalocean.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the Droplet management features.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/droplets")
+public interface DropletApi extends Closeable {
+
+   /**
+    * Lists all existing droplets.
+    * 
+    * @return The list of all existing droplets.
+    */
+   @Named("droplet:list")
+   @GET
+   @SelectJson("droplets")
+   List<Droplet> list();
+
+   /**
+    * Gets the details of the given droplet.
+    * 
+    * @param id The id of the droplet to get.
+    * @return The details of the droplet or <code>null</code> if no droplet exists with the given id.
+    */
+   @Named("droplet:get")
+   @GET
+   @Path("/{id}")
+   @SelectJson("droplet")
+   @Fallback(NullOnNotFoundOr404.class)
+   Droplet get(@PathParam("id") int id);
+
+   /**
+    * Creates a new droplet.
+    * 
+    * @param name The name for the new droplet.
+    * @param imageId The id of the image to use to create the droplet.
+    * @param sizeId The size to use to create the droplet.
+    * @param regionId The region where the droplet must be created.
+    * @return The created droplet.
+    */
+   @Named("droplet:create")
+   @GET
+   @Path("/new")
+   @SelectJson("droplet")
+   DropletCreation create(@QueryParam("name") String name, @QueryParam("image_id") int imageId,
+         @QueryParam("size_id") int sizeId, @QueryParam("region_id") int regionId);
+
+   /**
+    * Creates a new droplet.
+    * 
+    * @param name The name for the new droplet.
+    * @param imageId The id of the image to use to create the droplet.
+    * @param sizeId The size to use to create the droplet.
+    * @param regionId The region where the droplet must be created.
+    * @param options Custom options to create the droplet.
+    * @return The created droplet.
+    */
+   @Named("droplet:create")
+   @GET
+   @Path("/new")
+   @SelectJson("droplet")
+   DropletCreation create(@QueryParam("name") String name, @QueryParam("image_id") int imageId,
+         @QueryParam("size_id") int sizeId, @QueryParam("region_id") int regionId, CreateDropletOptions options);
+
+   /**
+    * Reboots the given droplet.
+    * 
+    * @param id The id of the droplet to reboot.
+    * @return The id of the event to track the reboot process.
+    */
+   @Named("droplet:reboot")
+   @GET
+   @Path("/{id}/reboot")
+   @SelectJson("event_id")
+   int reboot(@PathParam("id") int id);
+
+   /**
+    * Power cycles the given droplet.
+    * 
+    * @param id The id of the droplet to power cycle.
+    * @return The id of the event to track the power cycle process.
+    */
+   @Named("droplet:powercycle")
+   @GET
+   @Path("/{id}/power_cycle")
+   @SelectJson("event_id")
+   int powerCycle(@PathParam("id") int id);
+
+   /**
+    * Shuts down the given droplet.
+    * 
+    * @param id The id of the droplet to shutdown.
+    * @return The id of the event to track the shutdown process.
+    */
+   @Named("droplet:shutdown")
+   @GET
+   @Path("/{id}/shutdown")
+   @SelectJson("event_id")
+   int shutdown(@PathParam("id") int id);
+
+   /**
+    * Powers off the given droplet.
+    * 
+    * @param id The id of the droplet to power off.
+    * @return The id of the event to track the power off process.
+    */
+   @Named("droplet:poweroff")
+   @GET
+   @Path("/{id}/power_off")
+   @SelectJson("event_id")
+   int powerOff(@PathParam("id") int id);
+
+   /**
+    * Powers on the given droplet.
+    * 
+    * @param id The id of the droplet to power on.
+    * @return The id of the event to track the power on process.
+    */
+   @Named("droplet:poweron")
+   @GET
+   @Path("/{id}/power_on")
+   @SelectJson("event_id")
+   int powerOn(@PathParam("id") int id);
+
+   /**
+    * Resets the password for the given droplet.
+    * 
+    * @param id The id of the droplet to reset the password to.
+    * @return The id of the event to track the password reset process.
+    */
+   @Named("droplet:resetpassword")
+   @GET
+   @Path("/{id}/password_reset")
+   @SelectJson("event_id")
+   int resetPassword(@PathParam("id") int id);
+
+   /**
+    * Changes the size for the given droplet.
+    * 
+    * @param id The id of the droplet to change the size to.
+    * @param sizeId The id of the new size for the droplet.
+    * @return The id of the event to track the resize process.
+    */
+   @Named("droplet:resize")
+   @GET
+   @Path("/{id}/resize")
+   @SelectJson("event_id")
+   int resize(@PathParam("id") int id, @QueryParam("size_id") int sizeId);
+
+   /**
+    * Takes a snapshot of the droplet once it has been powered off.
+    * 
+    * @param id The id of the droplet to take the snapshot of.
+    * @return The id of the event to track the snapshot process.
+    */
+   @Named("droplet:snapshot")
+   @GET
+   @Path("/{id}/snapshot")
+   @SelectJson("event_id")
+   int snapshot(@PathParam("id") int id);
+
+   /**
+    * Takes a snapshot of the droplet once it has been powered off.
+    * 
+    * @param id The id of the droplet to take the snapshot of.
+    * @param name The name for the snapshot.
+    * @return The id of the event to track the snapshot process.
+    */
+   @Named("droplet:snapshot")
+   @GET
+   @Path("/{id}/snapshot")
+   @SelectJson("event_id")
+   int snapshot(@PathParam("id") int id, @QueryParam("name") String name);
+
+   /**
+    * Restores a droplet with a previous image or snapshot.
+    * <p>
+    * This will be a mirror copy of the image or snapshot to your droplet. Be sure you have backed up any necessary
+    * information prior to restore.
+    * 
+    * @param id The id of the droplet to restore.
+    * @param imageId The id of the image or snapshot to use to restore the droplet.
+    * @return The id of the event to track the restore process.
+    */
+   @Named("droplet:restore")
+   @GET
+   @Path("/{id}/restore")
+   @SelectJson("event_id")
+   int restore(@PathParam("id") int id, @QueryParam("image_id") int imageId);
+
+   /**
+    * Rebuilds a droplet with a default image.
+    * <p>
+    * This is useful if you want to start again but retain the same IP address for your droplet.
+    * 
+    * @param id The id of the droplet to rebuild.
+    * @param imageId The id of the image or snapshot to use to restore the droplet.
+    * @return The id of the event to track the restore process.
+    */
+   @Named("droplet:rebuild")
+   @GET
+   @Path("/{id}/rebuild")
+   @SelectJson("event_id")
+   int rebuild(@PathParam("id") int id, @QueryParam("image_id") int imageId);
+
+   /**
+    * Renames a droplet to the specified name.
+    * 
+    * @param id The id of the droplet to rename.
+    * @param name The new name for the droplet.
+    * @return The id of the event to track the rename process.
+    */
+   @Named("droplet:rename")
+   @GET
+   @Path("/{id}/rename")
+   @SelectJson("event_id")
+   int rename(@PathParam("id") int id, @QueryParam("name") String name);
+
+   /**
+    * Destroys the given droplet.
+    * 
+    * @param id The id of the droplet to destroy.
+    * @return The id of the event to track the destroy process.
+    */
+   @Named("droplet:destroy")
+   @GET
+   @Path("/{id}/destroy")
+   @SelectJson("event_id")
+   int destroy(@PathParam("id") int id);
+
+   /**
+    * Destroys the given droplet.
+    * 
+    * @param id The id of the droplet to destroy.
+    * @param scrubData If true this will strictly write 0s to your prior partition to ensure that all data is completely
+    *        erased.
+    * @return The id of the event to track the destroy process.
+    */
+   @Named("droplet:destroy")
+   @GET
+   @Path("/{id}/destroy")
+   @SelectJson("event_id")
+   int destroy(@PathParam("id") int id, @QueryParam("scrub_data") boolean scrubData);
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java
new file mode 100644
index 0000000..5da2685
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+
+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.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.digitalocean.domain.Event;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the Event API.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/events")
+public interface EventApi extends Closeable {
+
+   /**
+    * Gets the details of a given event.
+    * 
+    * @param The id of the event to get.
+    * @return The details of the event or <code>null</code> if no event exists with the given id.
+    */
+   @Named("event:get")
+   @GET
+   @Path("/{id}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @SelectJson("event")
+   Event get(@PathParam("id") int id);
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java
new file mode 100644
index 0000000..53d99c5
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.digitalocean.domain.Image;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the Image management features.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/images")
+public interface ImageApi extends Closeable {
+
+   /**
+    * Lists all available images.
+    * 
+    * @return The list of all available images.
+    */
+   @Named("image:list")
+   @GET
+   @SelectJson("images")
+   List<Image> list();
+
+   /**
+    * Gets the details of the given image.
+    * 
+    * @param id The id of the image to get.
+    * @return The details of the image or <code>null</code> if no image exists
+    *         with the given id.
+    */
+   @Named("image:get")
+   @GET
+   @Path("/{id}")
+   @SelectJson("image")
+   @Fallback(NullOnNotFoundOr404.class)
+   Image get(@PathParam("id") int id);
+
+   /**
+    * Deletes an existing image.
+    * 
+    * @param id The id of the key pair.
+    */
+   @Named("image:delete")
+   @GET
+   @Path("/{id}/destroy")
+   void delete(@PathParam("id") int id);
+
+   /**
+    * Transfers the image to the given region.
+    * 
+    * @param id The id of the image to transfer.
+    * @param regionId The id of the region to which the image will be
+    *           transferred.
+    * @return The id of the event to track the transfer process.
+    */
+   @Named("image:transfer")
+   @GET
+   @Path("/{id}/transfer")
+   @SelectJson("event_id")
+   int transfer(@PathParam("id") int id, @QueryParam("region_id") int regionId);
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java
new file mode 100644
index 0000000..b8b2dfb
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the SSH key pair management features.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/ssh_keys")
+public interface KeyPairApi extends Closeable {
+
+   /**
+    * Lists all existing SSH key pairs.
+    * 
+    * @return The list of all existing SSH key pairs.
+    */
+   @Named("key:list")
+   @GET
+   @SelectJson("ssh_keys")
+   List<SshKey> list();
+
+   /**
+    * Gets the details of an existing SSH key pair.
+    * 
+    * @param id The id of the SSH key pair.
+    * @return The details of the SSH key pair or <code>null</code> if no key
+    *         exists with the given id.
+    */
+   @Named("key:get")
+   @GET
+   @Path("/{id}")
+   @SelectJson("ssh_key")
+   @Fallback(NullOnNotFoundOr404.class)
+   SshKey get(@PathParam("id") int id);
+
+   /**
+    * Creates a new SSH key pair.
+    * 
+    * @param name The name of the key pair.
+    * @param publicKey The public key.
+    * @return The details of the created key pair.
+    */
+   @Named("key:create")
+   @GET
+   @Path("/new")
+   @SelectJson("ssh_key")
+   SshKey create(@QueryParam("name") String name, @QueryParam("ssh_pub_key") String publicKey);
+
+   /**
+    * Changes the SSH key for the given key pair.
+    * 
+    * @param id The id of the key pair.
+    * @param newPublicKey The new public key.
+    * @return The details of the modified key pair.
+    */
+   @Named("key:edit")
+   @GET
+   @Path("/{id}/edit")
+   @SelectJson("ssh_key")
+   SshKey edit(@PathParam("id") int id, @QueryParam("ssh_pub_key") String newPublicKey);
+
+   /**
+    * Deletes an existing SSH key pair.
+    * 
+    * @param id The id of the key pair.
+    */
+   @Named("key:delete")
+   @GET
+   @Path("/{id}/destroy")
+   void delete(@PathParam("id") int id);
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/RegionApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/RegionApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/RegionApi.java
new file mode 100644
index 0000000..ae9a1cb
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/RegionApi.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.digitalocean.domain.Region;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the Region management features.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/regions")
+public interface RegionApi extends Closeable {
+
+   /**
+    * Lists all available regions.
+    * 
+    * @return The list of all available regions.
+    */
+   @Named("region:list")
+   @GET
+   @SelectJson("regions")
+   List<Region> list();
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/features/SizesApi.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/features/SizesApi.java b/digitalocean/src/main/java/org/jclouds/digitalocean/features/SizesApi.java
new file mode 100644
index 0000000..4dd1fd6
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/SizesApi.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.features;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.digitalocean.domain.Size;
+import org.jclouds.digitalocean.http.filters.AuthenticationFilter;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.SelectJson;
+
+import com.google.inject.name.Named;
+
+/**
+ * Provides access to the Size management features.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@RequestFilters(AuthenticationFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/sizes")
+public interface SizesApi extends Closeable {
+
+   /**
+    * Lists all available sizes.
+    * 
+    * @return The list of all available sizes.
+    */
+   @Named("size:list")
+   @GET
+   @SelectJson("sizes")
+   List<Size> list();
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/handlers/DigitalOceanErrorHandler.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/handlers/DigitalOceanErrorHandler.java b/digitalocean/src/main/java/org/jclouds/digitalocean/handlers/DigitalOceanErrorHandler.java
new file mode 100644
index 0000000..d16eaf3
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/handlers/DigitalOceanErrorHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.handlers;
+
+import javax.inject.Singleton;
+
+import org.jclouds.digitalocean.http.ResponseStatusFromPayloadHttpCommandExecutorService;
+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;
+
+/**
+ * Parse the errors in the response and propagate an appropriate exception.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ * 
+ * @see ResponseStatusFromPayloadHttpCommandExecutorService
+ */
+@Singleton
+public class DigitalOceanErrorHandler implements HttpErrorHandler {
+
+   @Override
+   public void handleError(HttpCommand command, HttpResponse response) {
+      Exception exception = null;
+
+      try {
+         // The response message is already properly populated by the
+         // ResponseStatusFromPayloadHttpCommandExecutorService
+         switch (response.getStatusCode()) {
+            case 401:
+               exception = new AuthorizationException(response.getMessage(), exception);
+               break;
+            case 404:
+               exception = new ResourceNotFoundException(response.getMessage(), exception);
+               break;
+            default:
+               exception = new HttpResponseException(response.getMessage(), command, response);
+               break;
+         }
+      } finally {
+         command.setException(exception);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/http/ResponseStatusFromPayloadHttpCommandExecutorService.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/http/ResponseStatusFromPayloadHttpCommandExecutorService.java b/digitalocean/src/main/java/org/jclouds/digitalocean/http/ResponseStatusFromPayloadHttpCommandExecutorService.java
new file mode 100644
index 0000000..40aed57
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/http/ResponseStatusFromPayloadHttpCommandExecutorService.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.http;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.io.Closeables.close;
+import static org.jclouds.io.Payloads.newInputStreamPayload;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URI;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+
+import org.jclouds.Constants;
+import org.jclouds.digitalocean.domain.BaseResponse;
+import org.jclouds.digitalocean.domain.BaseResponse.Status;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpUtils;
+import org.jclouds.http.IOExceptionRetryHandler;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.http.handlers.DelegatingErrorHandler;
+import org.jclouds.http.handlers.DelegatingRetryHandler;
+import org.jclouds.http.internal.HttpWire;
+import org.jclouds.http.internal.JavaUrlHttpCommandExecutorService;
+import org.jclouds.io.ContentMetadataCodec;
+import org.jclouds.io.Payload;
+
+import com.google.common.base.Function;
+import com.google.common.base.Supplier;
+import com.google.common.io.ByteStreams;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+/**
+ * Custom implementation of the HTTP driver to read the response body in order to get the real response status.
+ * <p>
+ * The DigitalOcean API always return 200 codes even if a request failed due to some internal error, but populates an
+ * <code>ERROR</code> string in the response payload.
+ * <p>
+ * This class will read the body of the response and populate a 500 status code if an error is found.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@Singleton
+public class ResponseStatusFromPayloadHttpCommandExecutorService extends JavaUrlHttpCommandExecutorService {
+
+   public static final String ACCESS_DENIED = "Access Denied";
+   public static final String NOT_FOUND = "Not Found";
+
+   private final ParseJson<BaseResponse> errorParser;
+
+   @Inject
+   ResponseStatusFromPayloadHttpCommandExecutorService(HttpUtils utils, ContentMetadataCodec contentMetadataCodec,
+         @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor,
+         DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler,
+         DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier,
+         @Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI,
+         ParseJson<BaseResponse> errorParser) throws SecurityException, NoSuchFieldException {
+      super(utils, contentMetadataCodec, ioExecutor, retryHandler, ioRetryHandler, errorHandler, wire, verifier,
+            untrustedSSLContextProvider, proxyForURI);
+      this.errorParser = checkNotNull(errorParser, "errorParser cannot be null");
+   }
+
+   @Override
+   protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException {
+      HttpResponse original = super.invoke(connection);
+      HttpResponse.Builder<?> response = original.toBuilder();
+
+      if (hasPayload(original)) {
+         // As we need to read the response body to determine if there are errors, but we may need to process the body
+         // again later in the response parsers if everything is OK, we buffer the body into an InputStream we can reset
+         InputStream in = null;
+         InputStream originalInputStream = original.getPayload().openStream();
+
+         if (originalInputStream instanceof ByteArrayInputStream) {
+            in = originalInputStream;
+         } else {
+            try {
+               in = new ByteArrayInputStream(ByteStreams.toByteArray(originalInputStream));
+            } finally {
+               close(originalInputStream, true);
+            }
+         }
+
+         // Process the payload and look for errors
+         BaseResponse responseContent = errorParser.apply(in);
+         if (responseContent != null && responseContent.getStatus() == Status.ERROR) {
+            // Yes, this is ugly, but the DigitalOcean API sometimes sets the status code to 200 for these errors and
+            // the only way to know what happened is parsing the error message
+            String message = responseContent.getMessage();
+            if (ACCESS_DENIED.equals(message)) {
+               response.statusCode(401);
+            } else if (NOT_FOUND.equals(message)) {
+               response.statusCode(404);
+            } else {
+               response.statusCode(500);
+            }
+            response.message(responseContent.getDetails());
+         }
+
+         // Reset the input stream and set the payload, so it can be read again
+         // by the response and error parsers
+         in.reset();
+         Payload payload = newInputStreamPayload(in);
+         contentMetadataCodec.fromHeaders(payload.getContentMetadata(), original.getHeaders());
+         response.payload(payload);
+      }
+
+      return response.build();
+   }
+
+   private static boolean hasPayload(final HttpResponse response) {
+      return response.getPayload() != null && response.getPayload().getRawContent() != null;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/http/filters/AuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/http/filters/AuthenticationFilter.java b/digitalocean/src/main/java/org/jclouds/digitalocean/http/filters/AuthenticationFilter.java
new file mode 100644
index 0000000..2998482
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/http/filters/AuthenticationFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.http.filters;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.domain.Credentials;
+import org.jclouds.http.HttpException;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpRequestFilter;
+import org.jclouds.location.Provider;
+
+import com.google.common.base.Supplier;
+
+/**
+ * Adds the authentication query parameters to the requests.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ */
+@Singleton
+public class AuthenticationFilter implements HttpRequestFilter {
+
+   public static final String IDENTITY_PARAM = "client_id";
+   public static final String CREDENTIAL_PARAM = "api_key";
+
+   private final Supplier<Credentials> credentials;
+
+   @Inject
+   AuthenticationFilter(@Provider final Supplier<Credentials> credentials) {
+      this.credentials = checkNotNull(credentials, "credential supplier cannot be null");
+   }
+
+   @Override
+   public HttpRequest filter(HttpRequest request) throws HttpException {
+      Credentials creds = credentials.get();
+      return request.toBuilder().addQueryParam(IDENTITY_PARAM, creds.identity)
+            .addQueryParam(CREDENTIAL_PARAM, creds.credential).build();
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/java/org/jclouds/digitalocean/ssh/DSAKeys.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/ssh/DSAKeys.java b/digitalocean/src/main/java/org/jclouds/digitalocean/ssh/DSAKeys.java
new file mode 100644
index 0000000..75fce0f
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/ssh/DSAKeys.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.digitalocean.ssh;
+
+import static com.google.common.base.Joiner.on;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Splitter.fixedLength;
+import static com.google.common.base.Throwables.propagate;
+import static com.google.common.collect.Iterables.get;
+import static com.google.common.collect.Iterables.size;
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.io.BaseEncoding.base64;
+import static org.jclouds.util.Strings2.toStringAndClose;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+
+import org.jclouds.crypto.Pems;
+import org.jclouds.ssh.SshKeys;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Splitter;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.InputSupplier;
+
+/**
+ * Utility methods to work with DSA SSH keys.
+ * <p>
+ * Methods in this class should be moved to the {@link SshKeys} class.
+ * 
+ * @author Sergi Castro
+ * @author Ignasi Barrera
+ * 
+ * @see SshKeys
+ */
+public class DSAKeys {
+
+   public static String encodeAsOpenSSH(DSAPublicKey key) {
+      DSAParams params = key.getParams();
+      byte[] keyBlob = keyBlob(params.getP(), params.getQ(), params.getG(), key.getY());
+      return "ssh-dss " + base64().encode(keyBlob);
+   }
+
+   /**
+    * Executes {@link Pems#publicKeySpecFromOpenSSH(InputSupplier)} on the
+    * string which was OpenSSH Base64 Encoded {@code id_rsa.pub}
+    * 
+    * @param idRsaPub formatted {@code ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAB...}
+    * @see Pems#publicKeySpecFromOpenSSH(InputSupplier)
+    */
+   public static DSAPublicKeySpec publicKeySpecFromOpenSSH(String idDsaPub) {
+      try {
+         return publicKeySpecFromOpenSSH(ByteStreams.newInputStreamSupplier(idDsaPub.getBytes(Charsets.UTF_8)));
+      } catch (IOException e) {
+         throw propagate(e);
+      }
+   }
+
+   /**
+    * Returns {@link DSAPublicKeySpec} which was OpenSSH Base64 Encoded
+    * {@code id_rsa.pub}
+    * 
+    * @param supplier the input stream factory, formatted
+    *        {@code ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAB...}
+    * 
+    * @return the {@link DSAPublicKeySpec} which was OpenSSH Base64 Encoded
+    *         {@code id_rsa.pub}
+    * @throws IOException if an I/O error occurs
+    */
+   public static DSAPublicKeySpec publicKeySpecFromOpenSSH(InputSupplier<? extends InputStream> supplier)
+         throws IOException {
+      InputStream stream = supplier.getInput();
+      Iterable<String> parts = Splitter.on(' ').split(toStringAndClose(stream).trim());
+      checkArgument(size(parts) >= 2 && "ssh-dss".equals(get(parts, 0)), "bad format, should be: ssh-dss AAAAB3...");
+      stream = new ByteArrayInputStream(base64().decode(get(parts, 1)));
+      String marker = new String(readLengthFirst(stream));
+      checkArgument("ssh-dss".equals(marker), "looking for marker ssh-dss but got %s", marker);
+      BigInteger p = new BigInteger(readLengthFirst(stream));
+      BigInteger q = new BigInteger(readLengthFirst(stream));
+      BigInteger g = new BigInteger(readLengthFirst(stream));
+      BigInteger y = new BigInteger(readLengthFirst(stream));
+      return new DSAPublicKeySpec(y, p, q, g);
+   }
+
+   /**
+    * @param publicKeyOpenSSH RSA public key in OpenSSH format
+    * @return fingerprint ex.
+    *         {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
+    */
+   public static String fingerprintPublicKey(String publicKeyOpenSSH) {
+      DSAPublicKeySpec publicKeySpec = publicKeySpecFromOpenSSH(publicKeyOpenSSH);
+      return fingerprint(publicKeySpec.getP(), publicKeySpec.getQ(), publicKeySpec.getG(), publicKeySpec.getY());
+   }
+
+   /**
+    * Create a fingerprint per the following <a
+    * href="http://tools.ietf.org/html/draft-friedl-secsh-fingerprint-00"
+    * >spec</a>
+    * 
+    * @return hex fingerprint ex.
+    *         {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
+    */
+   public static String fingerprint(BigInteger p, BigInteger q, BigInteger g, BigInteger y) {
+      byte[] keyBlob = keyBlob(p, q, g, y);
+      return hexColonDelimited(Hashing.md5().hashBytes(keyBlob));
+   }
+
+   /**
+    * @see SshKeys
+    */
+   private static String hexColonDelimited(HashCode hc) {
+      return on(':').join(fixedLength(2).split(base16().lowerCase().encode(hc.asBytes())));
+   }
+
+   /**
+    * @see SshKeys
+    */
+   private static byte[] keyBlob(BigInteger p, BigInteger q, BigInteger g, BigInteger y) {
+      try {
+         ByteArrayOutputStream out = new ByteArrayOutputStream();
+         writeLengthFirst("ssh-dss".getBytes(), out);
+         writeLengthFirst(p.toByteArray(), out);
+         writeLengthFirst(q.toByteArray(), out);
+         writeLengthFirst(g.toByteArray(), out);
+         writeLengthFirst(y.toByteArray(), out);
+         return out.toByteArray();
+      } catch (IOException e) {
+         throw propagate(e);
+      }
+   }
+
+   /**
+    * @see SshKeys
+    */
+   // http://www.ietf.org/rfc/rfc4253.txt
+   private static byte[] readLengthFirst(InputStream in) throws IOException {
+      int byte1 = in.read();
+      int byte2 = in.read();
+      int byte3 = in.read();
+      int byte4 = in.read();
+      int length = (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0);
+      byte[] val = new byte[length];
+      in.read(val, 0, length);
+      return val;
+   }
+
+   /**
+    * @see SshKeys
+    */
+   // http://www.ietf.org/rfc/rfc4253.txt
+   private static void writeLengthFirst(byte[] array, ByteArrayOutputStream out) throws IOException {
+      out.write(array.length >>> 24 & 0xFF);
+      out.write(array.length >>> 16 & 0xFF);
+      out.write(array.length >>> 8 & 0xFF);
+      out.write(array.length >>> 0 & 0xFF);
+      if (array.length == 1 && array[0] == (byte) 0x00) {
+         out.write(new byte[0]);
+      } else {
+         out.write(array);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/224b2828/digitalocean/src/main/resources/META-INF/services/org.jclouds.providers.ProviderMetadata
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/resources/META-INF/services/org.jclouds.providers.ProviderMetadata b/digitalocean/src/main/resources/META-INF/services/org.jclouds.providers.ProviderMetadata
new file mode 100644
index 0000000..0c26db4
--- /dev/null
+++ b/digitalocean/src/main/resources/META-INF/services/org.jclouds.providers.ProviderMetadata
@@ -0,0 +1 @@
+org.jclouds.digitalocean.DigitalOceanProviderMetadata
\ No newline at end of file