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/05/21 09:56:42 UTC

git commit: JCLOUDS-543/JCLOUDS-572: Use slugs as IDs and assume a node can have a null image if it used an imageId that has been removed

Repository: jclouds-labs
Updated Branches:
  refs/heads/master c6ec94490 -> c5afe3cc4


JCLOUDS-543/JCLOUDS-572: Use slugs as IDs and assume a node can have a null image if it used an imageId that has been removed


Project: http://git-wip-us.apache.org/repos/asf/jclouds-labs/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds-labs/commit/c5afe3cc
Tree: http://git-wip-us.apache.org/repos/asf/jclouds-labs/tree/c5afe3cc
Diff: http://git-wip-us.apache.org/repos/asf/jclouds-labs/diff/c5afe3cc

Branch: refs/heads/master
Commit: c5afe3cc4af9819f71a12aae74444090955f9d9a
Parents: c6ec944
Author: Ignasi Barrera <na...@apache.org>
Authored: Mon May 19 12:56:56 2014 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Wed May 21 09:54:09 2014 +0200

----------------------------------------------------------------------
 digitalocean/pom.xml                            |   2 +-
 .../extensions/DigitalOceanImageExtension.java  |  12 +-
 .../functions/DropletToNodeMetadata.java        |  75 ++++++++++---
 .../compute/functions/ImageToImage.java         |   4 +-
 .../compute/functions/RegionToLocation.java     |   3 +-
 .../compute/functions/SizeToHardware.java       |   3 +-
 .../DigitalOceanComputeServiceAdapter.java      |  18 ++-
 .../compute/util/LocationNamingUtils.java       |  70 ++++++++++++
 .../org/jclouds/digitalocean/domain/Size.java   |   7 +-
 .../digitalocean/features/DropletApi.java       |  46 +++++++-
 .../jclouds/digitalocean/features/EventApi.java |   2 +
 .../jclouds/digitalocean/features/ImageApi.java |  55 ++++++++-
 .../digitalocean/features/KeyPairApi.java       |   5 +-
 .../functions/DropletToNodeMetadataTest.java    |  29 ++++-
 .../compute/functions/ImageToImageTest.java     |   5 +-
 .../compute/functions/SizeToHardwareTest.java   |   5 +-
 .../compute/util/LocationNamingUtilsTest.java   | 111 +++++++++++++++++++
 .../features/DropletApiLiveTest.java            |  20 +++-
 .../features/DropletApiMockTest.java            |  52 +++++++++
 .../digitalocean/features/ImageApiLiveTest.java |  36 ++++++
 .../digitalocean/features/ImageApiMockTest.java |  54 ++++++++-
 digitalocean/src/test/resources/image1.json     |   2 +-
 digitalocean/src/test/resources/image2.json     |   2 +-
 digitalocean/src/test/resources/images.json     |   4 +-
 digitalocean/src/test/resources/sizes.json      |   8 +-
 25 files changed, 572 insertions(+), 58 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/pom.xml
----------------------------------------------------------------------
diff --git a/digitalocean/pom.xml b/digitalocean/pom.xml
index 92f77b3..ae8d972 100644
--- a/digitalocean/pom.xml
+++ b/digitalocean/pom.xml
@@ -39,7 +39,7 @@
     <test.digitalocean.identity>FIXME</test.digitalocean.identity>
     <test.digitalocean.credential>FIXME</test.digitalocean.credential>
     <!-- CentOS 6.5 x64 -->
-    <test.digitalocean.template>imageId=3448641</test.digitalocean.template>
+    <test.digitalocean.template>imageId=centos-6-5-x64</test.digitalocean.template>
     <jclouds.osgi.export>org.jclouds.digitalocean*;version="${jclouds.version}"</jclouds.osgi.export>
     <jclouds.osgi.import>
       org.jclouds.compute.internal;version="${jclouds.version}",

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/extensions/DigitalOceanImageExtension.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/extensions/DigitalOceanImageExtension.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/extensions/DigitalOceanImageExtension.java
index 2248b1e..fd1e761 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/extensions/DigitalOceanImageExtension.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/extensions/DigitalOceanImageExtension.java
@@ -43,6 +43,7 @@ import org.jclouds.logging.Logger;
 import com.google.common.base.Function;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
+import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.ListenableFuture;
 
 /**
@@ -116,7 +117,16 @@ public class DigitalOceanImageExtension implements ImageExtension {
    @Override
    public boolean deleteImage(String id) {
       try {
-         api.getImageApi().delete(Integer.parseInt(id));
+         // The id of the image can be an id or a slug. Use the corresponding method of the API depending on what is
+         // provided. If it can be parsed as a number, use the method to destroy by ID. Otherwise, destroy by slug.
+         Integer imageId = Ints.tryParse(id);
+         if (imageId != null) {
+            logger.debug(">> image does not have a slug. Using the id to delete the image...");
+            api.getImageApi().delete(imageId);
+         } else {
+            logger.debug(">> image has a slug. Using it to delete the image...");
+            api.getImageApi().delete(id);
+         }
          return true;
       } catch (Exception ex) {
          return false;

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadata.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadata.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadata.java
index db0f5c9..fdd6b71 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadata.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadata.java
@@ -18,11 +18,15 @@ package org.jclouds.digitalocean.compute.functions;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterables.find;
+import static com.google.common.collect.Iterables.tryFind;
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.extractRegionId;
 
 import java.util.Map;
 import java.util.Set;
 
+import javax.annotation.Resource;
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.jclouds.collect.Memoized;
@@ -32,15 +36,19 @@ import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.NodeMetadata.Status;
 import org.jclouds.compute.domain.NodeMetadataBuilder;
 import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.compute.reference.ComputeServiceConstants;
 import org.jclouds.digitalocean.domain.Droplet;
 import org.jclouds.domain.Credentials;
 import org.jclouds.domain.Location;
 import org.jclouds.domain.LoginCredentials;
+import org.jclouds.logging.Logger;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 
 /**
  * Transforms an {@link Droplet} to the jclouds portable model.
@@ -51,6 +59,10 @@ import com.google.common.collect.ImmutableSet;
 @Singleton
 public class DropletToNodeMetadata implements Function<Droplet, NodeMetadata> {
 
+   @Resource
+   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+   protected Logger logger = Logger.NULL;
+
    private final Supplier<Map<String, ? extends Image>> images;
    private final Supplier<Map<String, ? extends Hardware>> hardwares;
    private final Supplier<Set<? extends Location>> locations;
@@ -80,19 +92,18 @@ public class DropletToNodeMetadata implements Function<Droplet, NodeMetadata> {
       builder.hostname(input.getName());
       builder.group(groupNamingConvention.extractGroup(input.getName()));
 
-      builder.hardware(hardwares.get().get(String.valueOf(input.getSizeId())));
-
-      final String regionIdPattern = input.getRegionId() + "/";
-      builder.location(find(locations.get(), new Predicate<Location>() {
-         @Override
-         public boolean apply(Location location) {
-            return location.getDescription().startsWith(regionIdPattern);
-         }
-      }));
-
-      Image image = images.get().get(String.valueOf(input.getImageId()));
-      builder.imageId(image.getId());
-      builder.operatingSystem(image.getOperatingSystem());
+      builder.hardware(getHardware(input.getSizeId()));
+      builder.location(getLocation(input.getRegionId()));
+
+      Optional<? extends Image> image = findImage(input.getImageId());
+      if (image.isPresent()) {
+         builder.imageId(image.get().getId());
+         builder.operatingSystem(image.get().getOperatingSystem());
+      } else {
+         logger.info(">> image with id %s for droplet %s was not found. "
+               + "This might be because the image that was used to create the droplet has a new id.",
+               input.getImageId(), input.getId());
+      }
 
       builder.status(toPortableStatus.apply(input.getStatus()));
       builder.backendStatus(input.getStatus().name());
@@ -113,4 +124,42 @@ public class DropletToNodeMetadata implements Function<Droplet, NodeMetadata> {
 
       return builder.build();
    }
+
+   protected Optional<? extends Image> findImage(Integer id) {
+      // Try to find the image by ID in the cache. The cache is indexed by slug (for public images) and by id (for
+      // private ones).
+      final String imageId = String.valueOf(id);
+      Optional<? extends Image> image = Optional.fromNullable(images.get().get(imageId));
+      if (!image.isPresent()) {
+         // If it is a public image (indexed by slug) but the "int" form of the id was provided, try to find it in the
+         // whole list of cached images
+         image = tryFind(images.get().values(), new Predicate<Image>() {
+            @Override
+            public boolean apply(Image input) {
+               return input.getProviderId().equals(imageId);
+            }
+         });
+      }
+      return image;
+   }
+
+   protected Hardware getHardware(Integer id) {
+      // Hardwares are indexed by slug, but the droplet only provides its ID.
+      final String hardwareId = String.valueOf(id);
+      return Iterables.find(hardwares.get().values(), new Predicate<Hardware>() {
+         @Override
+         public boolean apply(Hardware input) {
+            return input.getProviderId().equals(hardwareId);
+         }
+      });
+   }
+
+   protected Location getLocation(final Integer id) {
+      return find(locations.get(), new Predicate<Location>() {
+         @Override
+         public boolean apply(Location location) {
+            return id.equals(extractRegionId(location));
+         }
+      });
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/ImageToImage.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/ImageToImage.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/ImageToImage.java
index d29b864..3e33ed9 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/ImageToImage.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/ImageToImage.java
@@ -40,7 +40,9 @@ public class ImageToImage implements Function<Image, org.jclouds.compute.domain.
    @Override
    public org.jclouds.compute.domain.Image apply(final Image input) {
       ImageBuilder builder = new ImageBuilder();
-      builder.ids(String.valueOf(input.getId()));
+      // Private images don't have a slug
+      builder.id(input.getSlug() != null ? input.getSlug() : String.valueOf(input.getId()));
+      builder.providerId(String.valueOf(input.getId()));
       builder.name(input.getName());
       builder.description(input.getName());
       builder.status(Status.AVAILABLE);

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/RegionToLocation.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/RegionToLocation.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/RegionToLocation.java
index f1c5ed1..0406be0 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/RegionToLocation.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/RegionToLocation.java
@@ -18,6 +18,7 @@ package org.jclouds.digitalocean.compute.functions;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.encodeRegionIdAndName;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -51,7 +52,7 @@ public class RegionToLocation implements Function<Region, Location> {
    public Location apply(Region input) {
       LocationBuilder builder = new LocationBuilder();
       builder.id(input.getSlug());
-      builder.description(input.getId() + "/" + input.getName());
+      builder.description(encodeRegionIdAndName(input));
       builder.scope(LocationScope.REGION);
       builder.parent(getOnlyElement(justProvider.get()));
       builder.iso3166Codes(ImmutableSet.<String> of());

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/SizeToHardware.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/SizeToHardware.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/SizeToHardware.java
index 6dccb64..dc7577b 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/SizeToHardware.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/functions/SizeToHardware.java
@@ -40,7 +40,8 @@ public class SizeToHardware implements Function<Size, Hardware> {
    @Override
    public Hardware apply(Size input) {
       HardwareBuilder builder = new HardwareBuilder();
-      builder.ids(String.valueOf(input.getId()));
+      builder.id(input.getSlug());
+      builder.providerId(String.valueOf(input.getId()));
       builder.name(input.getName());
       builder.ram(input.getMemory());
       // DigitalOcean does not provide the processor speed. We configure it to

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
index bef9e06..ff7150e 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
@@ -19,10 +19,10 @@ package org.jclouds.digitalocean.compute.strategy;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.collect.Iterables.contains;
 import static com.google.common.collect.Iterables.filter;
-import static com.google.common.collect.Iterables.find;
 import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_RUNNING;
 import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_SUSPENDED;
 import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_TERMINATED;
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.extractRegionId;
 
 import java.util.Map;
 
@@ -48,6 +48,7 @@ import org.jclouds.ssh.SshKeyPairGenerator;
 
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
 
 /**
  * Implementation of the Compute Service for the DigitalOcean API.
@@ -114,17 +115,11 @@ public class DigitalOceanComputeServiceAdapter implements ComputeServiceAdapter<
       }
 
       // Find the location where the Droplet has to be created
-      final String locationId = template.getLocation().getId();
-      Region region = find(api.getRegionApi().list(), new Predicate<Region>() {
-         @Override
-         public boolean apply(Region input) {
-            return input.getSlug().equals(locationId);
-         }
-      });
+      int regionId = extractRegionId(template.getLocation());
 
       DropletCreation dropletCreation = api.getDropletApi().create(name,
             Integer.parseInt(template.getImage().getProviderId()),
-            Integer.parseInt(template.getHardware().getProviderId()), region.getId(), options.build());
+            Integer.parseInt(template.getHardware().getProviderId()), regionId, options.build());
 
       // We have to actively wait until the droplet has been provisioned until
       // we can build the entire Droplet object we want to return
@@ -169,7 +164,10 @@ public class DigitalOceanComputeServiceAdapter implements ComputeServiceAdapter<
 
    @Override
    public Image getImage(String id) {
-      return api.getImageApi().get(Integer.parseInt(id));
+      // The id of the image can be an id or a slug. Use the corresponding method of the API depending on what is
+      // provided. If it can be parsed as a number, use the method to get by ID. Otherwise, get by slug.
+      Integer imageId = Ints.tryParse(id);
+      return imageId != null ? api.getImageApi().get(imageId) : api.getImageApi().get(id);
    }
 
    @Override

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/main/java/org/jclouds/digitalocean/compute/util/LocationNamingUtils.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/util/LocationNamingUtils.java b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/util/LocationNamingUtils.java
new file mode 100644
index 0000000..abbf4dc
--- /dev/null
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/util/LocationNamingUtils.java
@@ -0,0 +1,70 @@
+/*
+ * 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.compute.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.jclouds.digitalocean.domain.Region;
+import org.jclouds.domain.Location;
+
+/**
+ * Utility class to encode and decode the region id and name in a {@link Location}.
+ * 
+ * @author Ignasi Barrera
+ */
+public class LocationNamingUtils {
+
+   /**
+    * Extracts the region id for the given location.
+    * 
+    * @param location The location to extract the region id from.
+    * @return The id of the region.
+    */
+   public static int extractRegionId(Location location) {
+      checkNotNull(location, "location cannot be null");
+      String regionIdAndName = location.getDescription();
+      int index = regionIdAndName.indexOf('/');
+      checkArgument(index >= 0, "location description should be in the form 'regionId/regionName'");
+      return Integer.parseInt(regionIdAndName.substring(0, index));
+   }
+
+   /**
+    * Extracts the region name for the given location.
+    * 
+    * @param location The location to extract the region name from.
+    * @return The name of the region.
+    */
+   public static String extractRegionName(Location location) {
+      checkNotNull(location, "location cannot be null");
+      String regionIdAndName = location.getDescription();
+      int index = regionIdAndName.indexOf('/');
+      checkArgument(index >= 0, "location description should be in the form 'regionId/regionName'");
+      return regionIdAndName.substring(index + 1);
+   }
+
+   /**
+    * Encodes the id and name of the given region into a String so it can be populated in a {@link Location} object.
+    * 
+    * @param region The region to encode.
+    * @return The encoded id and name for the given region.
+    */
+   public static String encodeRegionIdAndName(Region region) {
+      checkNotNull(region, "region cannot be null");
+      return region.getId() + "/" + region.getName();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/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
index b1c7c63..6687564 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/domain/Size.java
@@ -22,8 +22,6 @@ import java.beans.ConstructorProperties;
 
 import javax.inject.Named;
 
-import org.jclouds.javax.annotation.Nullable;
-
 /**
  * A Size.
  * 
@@ -44,11 +42,10 @@ public class Size {
    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) {
+   public Size(int id, String name, 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.slug = checkNotNull(slug, "slug");
       this.memory = memory;
       this.cpu = cpu;
       this.disk = disk;

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/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
index 4c57db2..0cf838b 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/DropletApi.java
@@ -31,6 +31,7 @@ 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.javax.annotation.Nullable;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.RequestFilters;
 import org.jclouds.rest.annotations.SelectJson;
@@ -69,6 +70,7 @@ public interface DropletApi extends Closeable {
    @Path("/{id}")
    @SelectJson("droplet")
    @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
    Droplet get(@PathParam("id") int id);
 
    /**
@@ -79,6 +81,8 @@ public interface DropletApi extends Closeable {
     * @param sizeId The size to use to create the droplet.
     * @param regionId The region where the droplet must be created.
     * @return The created droplet.
+    * 
+    * @see #create(String, String, String, String)
     */
    @Named("droplet:create")
    @GET
@@ -96,6 +100,8 @@ public interface DropletApi extends Closeable {
     * @param regionId The region where the droplet must be created.
     * @param options Custom options to create the droplet.
     * @return The created droplet.
+    * 
+    * @see #create(String, String, String, String, CreateDropletOptions)
     */
    @Named("droplet:create")
    @GET
@@ -105,6 +111,44 @@ public interface DropletApi extends Closeable {
          @QueryParam("size_id") int sizeId, @QueryParam("region_id") int regionId, CreateDropletOptions options);
 
    /**
+    * Creates a new droplet.
+    * 
+    * @param name The name for the new droplet.
+    * @param imageSlug The slug of the image to use to create the droplet.
+    * @param sizeSlug The slug of the size to use to create the droplet.
+    * @param regionSlug The slug region where the droplet must be created.
+    * @return The created droplet.
+    * 
+    * @see #create(String, int, int, int)
+    */
+   @Named("droplet:create")
+   @GET
+   @Path("/new")
+   @SelectJson("droplet")
+   DropletCreation create(@QueryParam("name") String name, @QueryParam("image_slug") String imageSlug,
+         @QueryParam("size_slug") String sizeSlug, @QueryParam("region_slug") String regionSlug);
+
+   /**
+    * Creates a new droplet.
+    * 
+    * @param name The name for the new droplet.
+    * @param imageSlug The slug of the image to use to create the droplet.
+    * @param sizeSlug The slug of the size to use to create the droplet.
+    * @param regionSlug The slug region where the droplet must be created.
+    * @param options Custom options to create the droplet.
+    * @return The created droplet.
+    * 
+    * @see #create(String, int, int, int, CreateDropletOptions)
+    */
+   @Named("droplet:create")
+   @GET
+   @Path("/new")
+   @SelectJson("droplet")
+   DropletCreation create(@QueryParam("name") String name, @QueryParam("image_slug") String imageSlug,
+         @QueryParam("size_slug") String sizeSlug, @QueryParam("region_slug") String regionSlug,
+         CreateDropletOptions options);
+
+   /**
     * Reboots the given droplet.
     * 
     * @param id The id of the droplet to reboot.
@@ -275,7 +319,7 @@ public interface DropletApi extends Closeable {
     * 
     * @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.
+    *           erased.
     * @return The id of the event to track the destroy process.
     */
    @Named("droplet:destroy")

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/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
index 5da2685..aa7ebee 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/EventApi.java
@@ -27,6 +27,7 @@ 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.javax.annotation.Nullable;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.RequestFilters;
 import org.jclouds.rest.annotations.SelectJson;
@@ -55,5 +56,6 @@ public interface EventApi extends Closeable {
    @Path("/{id}")
    @Fallback(NullOnNotFoundOr404.class)
    @SelectJson("event")
+   @Nullable
    Event get(@PathParam("id") int id);
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/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
index 53d99c5..ecfba50 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/ImageApi.java
@@ -29,6 +29,7 @@ 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.javax.annotation.Nullable;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.RequestFilters;
 import org.jclouds.rest.annotations.SelectJson;
@@ -58,19 +59,39 @@ public interface ImageApi extends Closeable {
 
    /**
     * Gets the details of the given image.
+    * <p>
+    * Note that Image IDs can change. The recommended way to get an image is using the {@link #get(String)} method.
     * 
     * @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.
+    * @return The details of the image or <code>null</code> if no image exists with the given id.
+    * 
+    * @see #get(String)
     */
    @Named("image:get")
    @GET
    @Path("/{id}")
    @SelectJson("image")
    @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
    Image get(@PathParam("id") int id);
 
    /**
+    * Gets the details of the given image.
+    * 
+    * @param slug The slug of the image to get.
+    * @return The details of the image or <code>null</code> if no image exists with the given slug.
+    * 
+    * @see #get(int)
+    */
+   @Named("image:get")
+   @GET
+   @Path("/{slug}")
+   @SelectJson("image")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Image get(@PathParam("slug") String slug);
+
+   /**
     * Deletes an existing image.
     * 
     * @param id The id of the key pair.
@@ -81,16 +102,42 @@ public interface ImageApi extends Closeable {
    void delete(@PathParam("id") int id);
 
    /**
+    * Deletes an existing image.
+    * 
+    * @param slug The slug of the key pair.
+    */
+   @Named("image:delete")
+   @GET
+   @Path("/{slug}/destroy")
+   void delete(@PathParam("slug") String slug);
+
+   /**
     * 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.
+    * @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.
+    * 
+    * @see #transfer(String, int)
     */
    @Named("image:transfer")
    @GET
    @Path("/{id}/transfer")
    @SelectJson("event_id")
    int transfer(@PathParam("id") int id, @QueryParam("region_id") int regionId);
+
+   /**
+    * Transfers the image to the given region.
+    * 
+    * @param slug The slug 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.
+    * 
+    * @see #transfer(int, int)
+    */
+   @Named("image:transfer")
+   @GET
+   @Path("/{slug}/transfer")
+   @SelectJson("event_id")
+   int transfer(@PathParam("slug") String slug, @QueryParam("region_id") int regionId);
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/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
index b8b2dfb..8924a0b 100644
--- a/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java
+++ b/digitalocean/src/main/java/org/jclouds/digitalocean/features/KeyPairApi.java
@@ -29,6 +29,7 @@ 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.javax.annotation.Nullable;
 import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.RequestFilters;
 import org.jclouds.rest.annotations.SelectJson;
@@ -60,14 +61,14 @@ public interface KeyPairApi extends Closeable {
     * 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.
+    * @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)
+   @Nullable
    SshKey get(@PathParam("id") int id);
 
    /**

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadataTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadataTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadataTest.java
index 9a49df7..e0f1ae4 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadataTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/DropletToNodeMetadataTest.java
@@ -80,14 +80,15 @@ public class DropletToNodeMetadataTest {
    @BeforeMethod
    public void setup() {
       images = ImmutableSet.of(new ImageBuilder()
-            .ids("1")
+            .id("ubuntu-1404-x86")
+            .providerId("1")
             .name("mock image")
             .status(AVAILABLE)
             .operatingSystem(
                   OperatingSystem.builder().name("Ubuntu 14.04 x86_64").description("Ubuntu").family(OsFamily.UBUNTU)
                         .version("10.04").arch("x86_64").is64Bit(true).build()).build());
 
-      hardwares = ImmutableSet.of(new HardwareBuilder().ids("1").name("mock hardware")
+      hardwares = ImmutableSet.of(new HardwareBuilder().id("2gb").providerId("1").name("mock hardware")
             .processor(new Processor(1.0, 1.0)).ram(2048)
             .volume(new VolumeBuilder().size(20f).type(Type.LOCAL).build()).build());
 
@@ -109,14 +110,34 @@ public class DropletToNodeMetadataTest {
       Droplet droplet = new Droplet(1, "mock-droplet", 1, 1, 1, false, ImmutableList.of(), ImmutableList.of(),
             "84.45.69.3", "192.168.2.5", false, ACTIVE, new Date());
 
-      NodeMetadata expected = new NodeMetadataBuilder().ids("1").hardware(getOnlyElement(hardwares)).imageId("1")
+      NodeMetadata expected = new NodeMetadataBuilder().ids("1").hardware(getOnlyElement(hardwares))
+            .imageId("ubuntu-1404-x86").status(RUNNING).location(getOnlyElement(locations)).name("mock-droplet")
+            .hostname("mock-droplet").group("mock").credentials(credentials)
+            .publicAddresses(ImmutableSet.of("84.45.69.3")).privateAddresses(ImmutableSet.of("192.168.2.5"))
+            .providerId("1").backendStatus(ACTIVE.name()).operatingSystem(getOnlyElement(images).getOperatingSystem())
+            .build();
+
+      NodeMetadata actual = function.apply(droplet);
+      assertNodeEquals(actual, expected);
+   }
+
+   @Test
+   public void testConvertDropletOldImage() throws ParseException {
+      // Use an image id that is not in the list of images
+      Droplet droplet = new Droplet(1, "mock-droplet", 9999, 1, 1, false, ImmutableList.of(), ImmutableList.of(),
+            "84.45.69.3", "192.168.2.5", false, ACTIVE, new Date());
+
+      NodeMetadata expected = new NodeMetadataBuilder().ids("1").hardware(getOnlyElement(hardwares)).imageId(null)
             .status(RUNNING).location(getOnlyElement(locations)).name("mock-droplet").hostname("mock-droplet")
             .group("mock").credentials(credentials).publicAddresses(ImmutableSet.of("84.45.69.3"))
             .privateAddresses(ImmutableSet.of("192.168.2.5")).providerId("1").backendStatus(ACTIVE.name())
-            .operatingSystem(getOnlyElement(images).getOperatingSystem()).build();
+            .operatingSystem(null).build();
 
       NodeMetadata actual = function.apply(droplet);
+      assertNodeEquals(actual, expected);
+   }
 
+   private static void assertNodeEquals(NodeMetadata actual, NodeMetadata expected) {
       assertEquals(actual, expected);
       // NodeMetadata equals method does not use all fields in equals. It assumes that same ids in same locations
       // determine the equivalence

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/ImageToImageTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/ImageToImageTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/ImageToImageTest.java
index a591286..f05c78d 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/ImageToImageTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/ImageToImageTest.java
@@ -37,9 +37,10 @@ public class ImageToImageTest {
 
    @Test
    public void testConvertImage() {
-      Image image = new Image(1, "Ubuntu 14.04 x64", "Ubuntu 14.04 x64", true, "ubuntu-1404");
+      Image image = new Image(1, "Ubuntu 14.04 x64", "Ubuntu 14.04 x64", true, "ubuntu-1404-x86");
       org.jclouds.compute.domain.Image expected = new ImageBuilder()
-            .ids("1")
+            .id("ubuntu-1404-x86")
+            .providerId("1")
             .name("Ubuntu 14.04 x64")
             .status(AVAILABLE)
             .operatingSystem(

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/SizeToHardwareTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/SizeToHardwareTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/SizeToHardwareTest.java
index 507c626..9691c25 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/SizeToHardwareTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/functions/SizeToHardwareTest.java
@@ -38,8 +38,9 @@ public class SizeToHardwareTest {
 
    @Test
    public void testConvertSize() {
-      Size size = new Size(1, "Medium", "2GB", 2048, 1, 20, "0.05", "10");
-      Hardware expected = new HardwareBuilder().ids("1").name("Medium").processor(new Processor(1.0, 1.0)).ram(2048)
+      Size size = new Size(1, "Medium", "2gb", 2048, 1, 20, "0.05", "10");
+      Hardware expected = new HardwareBuilder().id("2gb").providerId("1").name("Medium")
+            .processor(new Processor(1.0, 1.0)).ram(2048)
             .volume(new VolumeBuilder().size(20f).type(Type.LOCAL).build())
             .userMetadata(ImmutableMap.of("costPerHour", "0.05", "costPerMonth", "10")).build();
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/compute/util/LocationNamingUtilsTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/compute/util/LocationNamingUtilsTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/util/LocationNamingUtilsTest.java
new file mode 100644
index 0000000..4f5ec06
--- /dev/null
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/compute/util/LocationNamingUtilsTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.compute.util;
+
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.encodeRegionIdAndName;
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.extractRegionId;
+import static org.jclouds.digitalocean.compute.util.LocationNamingUtils.extractRegionName;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
+
+import org.jclouds.digitalocean.domain.Region;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link LocationNamingUtils} class.
+ * 
+ * @author Ignasi Barrera
+ */
+@Test(groups = "unit", testName = "LocationNamingUtilsTest")
+public class LocationNamingUtilsTest {
+
+   @Test
+   public void testExtractRegionId() {
+      assertEquals(1, extractRegionId(location("1/foo")));
+      assertEquals(1, extractRegionId(location("1///foo")));
+      assertEquals(1, extractRegionId(location("1/2/3/foo")));
+   }
+
+   @Test
+   public void testExtractRegionIdInvalidEncodedForms() {
+      assertInvalidRegionIdFormat("/");
+      assertInvalidRegionIdFormat("/foo");
+      assertInvalidRegionIdFormat("/1/2/foo");
+   }
+
+   @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "location cannot be null")
+   public void testExtractRegionIdNullLocation() {
+      extractRegionId(null);
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "location description should be in the form 'regionId/regionName'")
+   public void testExtractRegionIdWithoutEncodedForm() {
+      extractRegionId(location("foobar"));
+   }
+
+   @Test
+   public void testExtractRegionName() {
+      assertEquals("foo", extractRegionName(location("1/foo")));
+      assertEquals("//foo", extractRegionName(location("1///foo")));
+      assertEquals("2/3/foo", extractRegionName(location("1/2/3/foo")));
+   }
+
+   @Test
+   public void testExtractRegionNameInvalidEncodedForms() {
+      assertEquals("", extractRegionName(location("/")));
+      assertEquals("foo", extractRegionName(location("/foo")));
+      assertEquals("1/2/foo", extractRegionName(location("/1/2/foo")));
+   }
+
+   @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "location cannot be null")
+   public void testExtractRegionNameNullLocation() {
+      extractRegionId(null);
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "location description should be in the form 'regionId/regionName'")
+   public void testExtractRegionNameWithoutEncodedForm() {
+      extractRegionId(location("foobar"));
+   }
+
+   @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "region cannot be null")
+   public void testEncodeRegionAndNameNullRegion() {
+      encodeRegionIdAndName(null);
+   }
+
+   @Test
+   public void testEncodeRegionAndName() {
+      assertEquals("1/foo", encodeRegionIdAndName(new Region(1, "foo", "bar")));
+      assertEquals("1/1", encodeRegionIdAndName(new Region(1, "1", "1")));
+      assertEquals("1///", encodeRegionIdAndName(new Region(1, "//", "1")));
+   }
+
+   private static void assertInvalidRegionIdFormat(String encoded) {
+      try {
+         extractRegionId(location(encoded));
+         fail("Encoded form [" + encoded + "] shouldn't produce a valid region id");
+      } catch (NumberFormatException ex) {
+         // Success
+      }
+   }
+
+   private static Location location(String description) {
+      return new LocationBuilder().id("location").description(description).scope(LocationScope.REGION).build();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiLiveTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiLiveTest.java
index 9816729..732207d 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiLiveTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiLiveTest.java
@@ -45,7 +45,9 @@ import com.google.common.base.Predicate;
 public class DropletApiLiveTest extends BaseDigitalOceanLiveTest {
 
    private DropletCreation dropletCreation;
+   private DropletCreation dropletCreationUsingSlugs;
    private Droplet droplet;
+   private Droplet dropletUsingSlugs;
    private Image snapshot;
 
    @Override
@@ -60,6 +62,10 @@ public class DropletApiLiveTest extends BaseDigitalOceanLiveTest {
          int event = api.getDropletApi().destroy(droplet.getId(), true);
          assertTrue(event > 0, "The event id should not be null");
       }
+      if (dropletUsingSlugs != null) {
+         int event = api.getDropletApi().destroy(dropletUsingSlugs.getId(), true);
+         assertTrue(event > 0, "The event id should not be null");
+      }
       if (snapshot != null) {
          api.getImageApi().delete(snapshot.getId());
       }
@@ -73,12 +79,24 @@ public class DropletApiLiveTest extends BaseDigitalOceanLiveTest {
       assertTrue(dropletCreation.getEventId() > 0, "Droplet creation event id should be > 0");
    }
 
-   @Test(dependsOnMethods = "testCreateDroplet")
+   public void testCreateDropletUsingSlugs() {
+      dropletCreationUsingSlugs = api.getDropletApi().create("droplettestwithslugs", defaultImage.getSlug(),
+            defaultSize.getSlug(), defaultRegion.getSlug());
+
+      assertTrue(dropletCreationUsingSlugs.getId() > 0, "Created droplet id should be > 0");
+      assertTrue(dropletCreationUsingSlugs.getEventId() > 0, "Droplet creation event id should be > 0");
+   }
+
+   @Test(dependsOnMethods = { "testCreateDroplet", "testCreateDropletUsingSlugs" })
    public void testGetDroplet() {
       waitForEvent(dropletCreation.getEventId());
+      waitForEvent(dropletCreationUsingSlugs.getEventId());
+
       droplet = api.getDropletApi().get(dropletCreation.getId());
+      dropletUsingSlugs = api.getDropletApi().get(dropletCreationUsingSlugs.getId());
 
       assertNotNull(droplet, "Created droplet should not be null");
+      assertNotNull(dropletUsingSlugs, "Created droplet using slugs should not be null");
    }
 
    @Test(dependsOnMethods = "testGetDroplet")

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiMockTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiMockTest.java
index 32c247d..3bf9088 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiMockTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/features/DropletApiMockTest.java
@@ -104,6 +104,58 @@ public class DropletApiMockTest extends BaseDigitalOceanMockTest {
       }
    }
 
+   public void testCreateDropletUsingSlugs() throws Exception {
+      MockWebServer server = mockWebServer();
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/droplet-creation.json")));
+
+      DigitalOceanApi api = api(server.getUrl("/"));
+      DropletApi dropletApi = api.getDropletApi();
+
+      try {
+         DropletCreation droplet = dropletApi.create("test", "img-1", "size-1", "region-1");
+
+         assertRequestHasParameters(server.takeRequest(), "/droplets/new", ImmutableMultimap.of("name", "test",
+               "image_slug", "img-1", "size_slug", "size-1", "region_slug", "region-1"));
+
+         assertNotNull(droplet);
+         assertEquals(droplet.getName(), "test");
+      } finally {
+         api.close();
+         server.shutdown();
+      }
+   }
+
+   public void testCreateDropletUsingSlugsWithOptions() throws Exception {
+      MockWebServer server = mockWebServer();
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/droplet-creation.json")));
+
+      DigitalOceanApi api = api(server.getUrl("/"));
+      DropletApi dropletApi = api.getDropletApi();
+
+      try {
+         CreateDropletOptions options = CreateDropletOptions.builder().addSshKeyId(5).addSshKeyId(4)
+               .privateNetworking(true).backupsEnabled(false).build();
+         DropletCreation droplet = dropletApi.create("test", "img-1", "size-1", "region-1", options);
+
+         ImmutableMultimap.Builder<String, String> params = ImmutableMultimap.builder();
+         params.put("name", "test");
+         params.put("image_slug", "img-1");
+         params.put("size_slug", "size-1");
+         params.put("region_slug", "region-1");
+         params.put("ssh_key_ids", "5,4");
+         params.put("private_networking", "true");
+         params.put("backups_enabled", "false");
+
+         assertRequestHasParameters(server.takeRequest(), "/droplets/new", params.build());
+
+         assertNotNull(droplet);
+         assertEquals(droplet.getName(), "test");
+      } finally {
+         api.close();
+         server.shutdown();
+      }
+   }
+
    public void testCreateDroplet() throws Exception {
       MockWebServer server = mockWebServer();
       server.enqueue(new MockResponse().setBody(payloadFromResource("/droplet-creation.json")));

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiLiveTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiLiveTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiLiveTest.java
index 2625424..75ba1e7 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiLiveTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiLiveTest.java
@@ -42,7 +42,9 @@ import com.google.common.base.Predicate;
 public class ImageApiLiveTest extends BaseDigitalOceanLiveTest {
 
    private Image snapshot;
+   private Image snapshotUsingSlug;
    private DropletCreation droplet;
+   private DropletCreation dropletUsingSlug;
 
    @Override
    protected void initialize() {
@@ -56,12 +58,20 @@ public class ImageApiLiveTest extends BaseDigitalOceanLiveTest {
          if (droplet != null) {
             api.getDropletApi().destroy(droplet.getId(), true);
          }
+         if (dropletUsingSlug != null) {
+            api.getDropletApi().destroy(dropletUsingSlug.getId(), true);
+         }
       } finally {
          if (snapshot != null) {
             api.getImageApi().delete(snapshot.getId());
             assertFalse(tryFind(api.getImageApi().list(), byName(snapshot.getName())).isPresent(),
                   "Snapshot should not exist after delete");
          }
+         if (snapshotUsingSlug != null) {
+            api.getImageApi().delete(snapshotUsingSlug.getId());
+            assertFalse(tryFind(api.getImageApi().list(), byName(snapshotUsingSlug.getName())).isPresent(),
+                  "Snapshot should not exist after delete");
+         }
       }
    }
 
@@ -69,6 +79,10 @@ public class ImageApiLiveTest extends BaseDigitalOceanLiveTest {
       assertNotNull(api.getImageApi().get(defaultImage.getId()), "The image should not be null");
    }
 
+   public void testGetImageBySlug() {
+      assertNotNull(api.getImageApi().get(defaultImage.getSlug()), "The image should not be null");
+   }
+
    public void testGetImageNotFound() {
       assertNull(api.getImageApi().get(-1));
    }
@@ -95,6 +109,28 @@ public class ImageApiLiveTest extends BaseDigitalOceanLiveTest {
       waitForEvent(transferEvent);
    }
 
+   public void testTransferImageUsingSlug() {
+      dropletUsingSlug = api.getDropletApi().create("imagetransferdropletusingslug", defaultImage.getSlug(),
+            defaultSize.getSlug(), defaultRegion.getSlug());
+
+      assertTrue(dropletUsingSlug.getId() > 0, "Created droplet id should be > 0");
+      assertTrue(dropletUsingSlug.getEventId() > 0, "Droplet creation event id should be > 0");
+
+      waitForEvent(dropletUsingSlug.getEventId());
+      int powerOffEvent = api.getDropletApi().powerOff(dropletUsingSlug.getId());
+      waitForEvent(powerOffEvent);
+
+      int snapshotEvent = api.getDropletApi().snapshot(dropletUsingSlug.getId(), "imagetransfersnapshotusingslug");
+      waitForEvent(snapshotEvent);
+
+      snapshotUsingSlug = find(api.getImageApi().list(), byName("imagetransfersnapshotusingslug"));
+
+      Region newRegion = regions.get(1);
+      int transferEvent = api.getImageApi().transfer(snapshotUsingSlug.getId(), newRegion.getId());
+      assertTrue(transferEvent > 0, "Transfer event id should be > 0");
+      waitForEvent(transferEvent);
+   }
+
    private static Predicate<Image> byName(final String name) {
       return new Predicate<Image>() {
          @Override

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiMockTest.java
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiMockTest.java b/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiMockTest.java
index 52bbaa1..1f61ede 100644
--- a/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiMockTest.java
+++ b/digitalocean/src/test/java/org/jclouds/digitalocean/features/ImageApiMockTest.java
@@ -64,7 +64,7 @@ public class ImageApiMockTest extends BaseDigitalOceanMockTest {
 
    public void testGetImage() throws Exception {
       MockWebServer server = mockWebServer();
-      String[] imageJsons = new String[] { "/image1.json", "/image2.json", "/image3.json" };
+      String[] imageJsons = new String[] { "/image1.json", "/image2.json", "/image3.json", "/image2.json" };
 
       for (String imageJson : imageJsons) {
          server.enqueue(new MockResponse().setBody(payloadFromResource(imageJson)));
@@ -84,6 +84,7 @@ public class ImageApiMockTest extends BaseDigitalOceanMockTest {
          assertEquals(image.getOs().getArch(), "x32");
          assertEquals(image.getName(), "Arch Linux 2013.05 x32");
          assertTrue(image.isPublicImage());
+         assertEquals(image.getSlug(), "arch-linux-x32");
 
          image = imageApi.get(2);
 
@@ -95,17 +96,32 @@ public class ImageApiMockTest extends BaseDigitalOceanMockTest {
          assertEquals(image.getOs().getArch(), "x64");
          assertEquals(image.getName(), "Fedora 17 x64 Desktop");
          assertTrue(image.isPublicImage());
+         assertEquals(image.getSlug(), "fedora-17-x64");
 
          image = imageApi.get(3);
 
          assertRequestHasCommonFields(server.takeRequest(), "/images/3");
          assertNotNull(image);
+         assertNull(image.getSlug());
          assertEquals(image.getId(), 3);
          assertEquals(image.getOs().getDistribution(), Distribution.UBUNTU);
          assertEquals(image.getOs().getVersion(), "13.04");
          assertEquals(image.getOs().getArch(), "");
          assertEquals(image.getName(), "Dokku on Ubuntu 13.04 0.2.0rc3");
          assertTrue(image.isPublicImage());
+         assertNull(image.getSlug());
+
+         image = imageApi.get("fedora-17-x64");
+
+         assertRequestHasCommonFields(server.takeRequest(), "/images/fedora-17-x64");
+         assertNotNull(image);
+         assertEquals(image.getId(), 2);
+         assertEquals(image.getOs().getDistribution(), Distribution.FEDORA);
+         assertEquals(image.getOs().getVersion(), "17");
+         assertEquals(image.getOs().getArch(), "x64");
+         assertEquals(image.getName(), "Fedora 17 x64 Desktop");
+         assertTrue(image.isPublicImage());
+         assertEquals(image.getSlug(), "fedora-17-x64");
       } finally {
          api.close();
          server.shutdown();
@@ -147,6 +163,23 @@ public class ImageApiMockTest extends BaseDigitalOceanMockTest {
       }
    }
 
+   public void testDeleteImageUsingSlug() throws Exception {
+      MockWebServer server = mockWebServer();
+      server.enqueue(new MockResponse());
+
+      DigitalOceanApi api = api(server.getUrl("/"));
+      ImageApi imageApi = api.getImageApi();
+
+      try {
+         imageApi.delete("img-15");
+
+         assertRequestHasCommonFields(server.takeRequest(), "/images/img-15/destroy");
+      } finally {
+         api.close();
+         server.shutdown();
+      }
+   }
+
    public void testDeleteUnexistingImage() throws Exception {
       MockWebServer server = mockWebServer();
       server.enqueue(new MockResponse().setResponseCode(404));
@@ -210,4 +243,23 @@ public class ImageApiMockTest extends BaseDigitalOceanMockTest {
          server.shutdown();
       }
    }
+
+   public void testTransferImageUsingSlug() throws Exception {
+      MockWebServer server = mockWebServer();
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/eventid.json")));
+
+      DigitalOceanApi api = api(server.getUrl("/"));
+      ImageApi imageApi = api.getImageApi();
+
+      try {
+         int eventId = imageApi.transfer("img-47", 23);
+
+         assertRequestHasParameters(server.takeRequest(), "/images/img-47/transfer",
+               ImmutableMultimap.of("region_id", "23"));
+         assertEquals(eventId, 7499);
+      } finally {
+         api.close();
+         server.shutdown();
+      }
+   }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/resources/image1.json
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/resources/image1.json b/digitalocean/src/test/resources/image1.json
index 879e375..97737d5 100644
--- a/digitalocean/src/test/resources/image1.json
+++ b/digitalocean/src/test/resources/image1.json
@@ -5,6 +5,6 @@
     "name" : "Arch Linux 2013.05 x32",
     "distribution" : "Arch Linux",
     "public" : true,
-    "slug" : null
+    "slug" : "arch-linux-x32"
   }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/resources/image2.json
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/resources/image2.json b/digitalocean/src/test/resources/image2.json
index ae680fe..2c5e16a 100644
--- a/digitalocean/src/test/resources/image2.json
+++ b/digitalocean/src/test/resources/image2.json
@@ -5,6 +5,6 @@
     "name" : "Fedora 17 x64 Desktop",
     "distribution" : "Fedora",
     "public" : true,
-    "slug" : null
+    "slug" : "fedora-17-x64"
   }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/resources/images.json
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/resources/images.json b/digitalocean/src/test/resources/images.json
index 44f081f..0eaef18 100644
--- a/digitalocean/src/test/resources/images.json
+++ b/digitalocean/src/test/resources/images.json
@@ -4,14 +4,14 @@
       {
          "id":1601,
          "name":"CentOS 5.8 x64",
-         "slug":null,
+         "slug":"centos-58-x64",
          "distribution":"CentOS",
          "public":true
       },
       {
          "id":1602,
          "name":"CentOS 5.8 x32",
-         "slug":null,
+         "slug":"centos-58-x32",
          "distribution":"CentOS",
          "public":true
       },

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c5afe3cc/digitalocean/src/test/resources/sizes.json
----------------------------------------------------------------------
diff --git a/digitalocean/src/test/resources/sizes.json b/digitalocean/src/test/resources/sizes.json
index ef1dd94..5bb2a1f 100644
--- a/digitalocean/src/test/resources/sizes.json
+++ b/digitalocean/src/test/resources/sizes.json
@@ -4,7 +4,7 @@
       {
          "id":66,
          "name":"512MB",
-         "slug":null,
+         "slug":"512mb",
          "memory":512,
          "cpu":1,
          "disk":20,
@@ -14,7 +14,7 @@
       {
          "id":63,
          "name":"1GB",
-         "slug":null,
+         "slug":"1gb",
          "memory":1024,
          "cpu":1,
          "disk":30,
@@ -24,7 +24,7 @@
       {
          "id":62,
          "name":"2GB",
-         "slug":null,
+         "slug":"2gb",
          "memory":2048,
          "cpu":2,
          "disk":40,
@@ -34,7 +34,7 @@
       {
          "id":64,
          "name":"4GB",
-         "slug":null,
+         "slug":"4gb",
          "memory":4096,
          "cpu":2,
          "disk":60,