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 2016/01/21 01:03:31 UTC

[07/19] jclouds git commit: JCLOUDS-946: Properly scope images to the locations where they are available

JCLOUDS-946: Properly scope images to the locations where they are available


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

Branch: refs/heads/master
Commit: 26210fe0981ac3bd646fb9c4bf2d7591752266e9
Parents: 057be8d
Author: Ignasi Barrera <na...@apache.org>
Authored: Sun Jun 28 23:51:08 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Jun 30 23:08:07 2015 +0200

----------------------------------------------------------------------
 providers/digitalocean2/pom.xml                 |  2 +-
 .../digitalocean2/DigitalOcean2ApiMetadata.java |  2 +-
 .../DigitalOcean2ComputeServiceAdapter.java     | 45 +++++++--
 ...igitalOcean2ComputeServiceContextModule.java | 15 +--
 .../extensions/DigitalOcean2ImageExtension.java |  8 +-
 .../functions/DropletToNodeMetadata.java        | 25 ++---
 .../compute/functions/ImageInRegionToImage.java | 92 ++++++++++++++++++
 .../compute/functions/ImageToImage.java         | 65 -------------
 .../compute/internal/ImageInRegion.java         | 54 +++++++++++
 .../services/org.jclouds.apis.ApiMetadata       | 18 ----
 .../DigitalOcean2TemplateBuilderLiveTest.java   |  2 +-
 .../functions/DropletToNodeMetadataTest.java    |  4 +-
 .../functions/ImageInRegionToImageTest.java     | 98 ++++++++++++++++++++
 .../compute/functions/ImageToImageTest.java     | 57 ------------
 14 files changed, 308 insertions(+), 179 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/pom.xml
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/pom.xml b/providers/digitalocean2/pom.xml
index db5139f..5b211d4 100644
--- a/providers/digitalocean2/pom.xml
+++ b/providers/digitalocean2/pom.xml
@@ -36,7 +36,7 @@
         <test.digitalocean2.api-version>2</test.digitalocean2.api-version>
         <test.digitalocean2.identity>FIXME</test.digitalocean2.identity>
         <test.digitalocean2.credential>FIXME</test.digitalocean2.credential>
-        <test.digitalocean2.template>imageId=ubuntu-14-04-x64</test.digitalocean2.template>
+        <test.digitalocean2.template>osFamily=UBUNTU,os64Bit=true</test.digitalocean2.template>
     </properties>
 
     <dependencies>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
index 0b20b96..7e9861d 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
@@ -65,7 +65,7 @@ public class DigitalOcean2ApiMetadata extends BaseHttpApiMetadata<DigitalOcean2A
       properties.put(AUDIENCE, "https://cloud.digitalocean.com/v1/oauth/token");
       properties.put(CREDENTIAL_TYPE, BEARER_TOKEN_CREDENTIALS.toString());
       properties.put(PROPERTY_SESSION_INTERVAL, 3600);
-      properties.put(TEMPLATE, "imageId=ubuntu-14-04-x64");
+      properties.put(TEMPLATE, "osFamily=UBUNTU,os64Bit=true");
       properties.put(POLL_INITIAL_PERIOD, 5000);
       properties.put(POLL_MAX_PERIOD, 20000);
       return properties;

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java
index aa4f656..ec8dc11 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java
@@ -17,13 +17,19 @@
 package org.jclouds.digitalocean2.compute;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Predicates.notNull;
+import static com.google.common.collect.Iterables.concat;
 import static com.google.common.collect.Iterables.contains;
 import static com.google.common.collect.Iterables.filter;
 import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Sets.newHashSet;
 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 java.util.Set;
+
 import javax.annotation.Resource;
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -32,6 +38,7 @@ import org.jclouds.compute.ComputeServiceAdapter;
 import org.jclouds.compute.domain.Template;
 import org.jclouds.compute.reference.ComputeServiceConstants;
 import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.internal.ImageInRegion;
 import org.jclouds.digitalocean2.compute.options.DigitalOcean2TemplateOptions;
 import org.jclouds.digitalocean2.domain.Action;
 import org.jclouds.digitalocean2.domain.Droplet;
@@ -43,13 +50,14 @@ import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
 import org.jclouds.domain.LoginCredentials;
 import org.jclouds.logging.Logger;
 
+import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.primitives.Ints;
 
 /**
  * Implementation of the Compute Service for the DigitalOcean API.
  */
-public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter<Droplet, Size, Image, Region> {
+public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter<Droplet, Size, ImageInRegion, Region> {
 
    @Resource
    @Named(ComputeServiceConstants.COMPUTE_LOGGER)
@@ -101,8 +109,30 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
    }
 
    @Override
-   public Iterable<Image> listImages() {
-      return api.imageApi().list().concat();
+   public Iterable<ImageInRegion> listImages() {
+      // Images can claim to be available in a region that is currently marked as "unavailable". We shouldn't return
+      // the images scoped to those regions.
+      final Set<String> availableRegionsIds = newHashSet(transform(listLocations(), new Function<Region, String>() {
+         @Override
+         public String apply(Region input) {
+            return input.slug();
+         }
+      }));
+
+      // Public images re globally available, but non-public ones can only be available in certain regions.
+      // For these kind of images, return one instance of an ImageInRegion for each region where the image is
+      // available. This way we can properly scope global and concrete images so they can be properly looked up.
+      return concat(filter(api.imageApi().list().concat().transform(new Function<Image, Iterable<ImageInRegion>>() {
+         @Override
+         public Iterable<ImageInRegion> apply(final Image image) {
+            return transform(image.regions(), new Function<String, ImageInRegion>() {
+               @Override
+               public ImageInRegion apply(String region) {
+                  return availableRegionsIds.contains(region) ? ImageInRegion.create(image, region) : null;
+               }
+            });
+         }
+      }), notNull()));
    }
 
    @Override
@@ -142,11 +172,14 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
    }
 
    @Override
-   public Image getImage(String id) {
+   public ImageInRegion getImage(String id) {
+      String region = ImageInRegion.extractRegion(id);
+      String imageId = ImageInRegion.extractImageId(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.imageApi().get(imageId) : api.imageApi().get(id);
+      Integer numericId = Ints.tryParse(imageId);
+      Image image = numericId == null ? api.imageApi().get(imageId) : api.imageApi().get(numericId);
+      return ImageInRegion.create(image, region);
    }
 
    @Override

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java
index 7809f9d..c2ed858 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java
@@ -28,6 +28,7 @@ import javax.inject.Singleton;
 import org.jclouds.compute.ComputeServiceAdapter;
 import org.jclouds.compute.config.ComputeServiceAdapterContextModule;
 import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.Image;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.NodeMetadata.Status;
 import org.jclouds.compute.extensions.ImageExtension;
@@ -41,15 +42,15 @@ import org.jclouds.digitalocean2.compute.DigitalOcean2ComputeServiceAdapter;
 import org.jclouds.digitalocean2.compute.extensions.DigitalOcean2ImageExtension;
 import org.jclouds.digitalocean2.compute.functions.DropletStatusToStatus;
 import org.jclouds.digitalocean2.compute.functions.DropletToNodeMetadata;
-import org.jclouds.digitalocean2.compute.functions.ImageToImage;
+import org.jclouds.digitalocean2.compute.functions.ImageInRegionToImage;
 import org.jclouds.digitalocean2.compute.functions.RegionToLocation;
 import org.jclouds.digitalocean2.compute.functions.SizeToHardware;
 import org.jclouds.digitalocean2.compute.functions.TemplateOptionsToStatementWithoutPublicKey;
+import org.jclouds.digitalocean2.compute.internal.ImageInRegion;
 import org.jclouds.digitalocean2.compute.options.DigitalOcean2TemplateOptions;
 import org.jclouds.digitalocean2.compute.strategy.CreateKeyPairsThenCreateNodes;
 import org.jclouds.digitalocean2.domain.Action;
 import org.jclouds.digitalocean2.domain.Droplet;
-import org.jclouds.digitalocean2.domain.Image;
 import org.jclouds.digitalocean2.domain.Region;
 import org.jclouds.digitalocean2.domain.Size;
 import org.jclouds.domain.Location;
@@ -67,19 +68,19 @@ import com.google.inject.name.Named;
  * Configures the compute service classes for the DigitalOcean API.
  */
 public class DigitalOcean2ComputeServiceContextModule extends
-      ComputeServiceAdapterContextModule<Droplet, Size, Image, Region> {
+      ComputeServiceAdapterContextModule<Droplet, Size, ImageInRegion, Region> {
 
    @Override
    protected void configure() {
       super.configure();
 
-      bind(new TypeLiteral<ComputeServiceAdapter<Droplet, Size, Image, Region>>() {
+      bind(new TypeLiteral<ComputeServiceAdapter<Droplet, Size, ImageInRegion, Region>>() {
       }).to(DigitalOcean2ComputeServiceAdapter.class);
 
       bind(new TypeLiteral<Function<Droplet, NodeMetadata>>() {
       }).to(DropletToNodeMetadata.class);
-      bind(new TypeLiteral<Function<Image, org.jclouds.compute.domain.Image>>() {
-      }).to(ImageToImage.class);
+      bind(new TypeLiteral<Function<ImageInRegion, Image>>() {
+      }).to(ImageInRegionToImage.class);
       bind(new TypeLiteral<Function<Region, Location>>() {
       }).to(RegionToLocation.class);
       bind(new TypeLiteral<Function<Size, Hardware>>() {
@@ -87,7 +88,7 @@ public class DigitalOcean2ComputeServiceContextModule extends
       bind(new TypeLiteral<Function<Droplet.Status, Status>>() {
       }).to(DropletStatusToStatus.class);
 
-      install(new LocationsFromComputeServiceAdapterModule<Droplet, Size, Image, Region>() {
+      install(new LocationsFromComputeServiceAdapterModule<Droplet, Size, ImageInRegion, Region>() {
       });
 
       bind(CreateNodesInGroupThenAddToSet.class).to(CreateKeyPairsThenCreateNodes.class);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java
index 41e3270..77ccd2a 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java
@@ -35,6 +35,7 @@ import org.jclouds.compute.domain.ImageTemplateBuilder;
 import org.jclouds.compute.extensions.ImageExtension;
 import org.jclouds.compute.reference.ComputeServiceConstants;
 import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.internal.ImageInRegion;
 import org.jclouds.digitalocean2.domain.Action;
 import org.jclouds.digitalocean2.domain.Droplet;
 import org.jclouds.digitalocean2.domain.Droplet.Status;
@@ -58,12 +59,12 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
    private final DigitalOcean2Api api;
    private final Predicate<Integer> imageAvailablePredicate;
    private final Predicate<Integer> nodeStoppedPredicate;
-   private final Function<org.jclouds.digitalocean2.domain.Image, Image> imageTransformer;
+   private final Function<ImageInRegion, Image> imageTransformer;
 
    @Inject DigitalOcean2ImageExtension(DigitalOcean2Api api,
          @Named(TIMEOUT_IMAGE_AVAILABLE) Predicate<Integer> imageAvailablePredicate,
          @Named(TIMEOUT_NODE_SUSPENDED) Predicate<Integer> nodeStoppedPredicate,
-         Function<org.jclouds.digitalocean2.domain.Image, Image> imageTransformer) {
+         Function<ImageInRegion, Image> imageTransformer) {
       this.api = api;
       this.imageAvailablePredicate = imageAvailablePredicate;
       this.nodeStoppedPredicate = nodeStoppedPredicate;
@@ -111,7 +112,8 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
                }
             }).get();
 
-      return immediateFuture(imageTransformer.apply(snapshot));
+      // By default snapshots are only available in the Droplet's region
+      return immediateFuture(imageTransformer.apply(ImageInRegion.create(snapshot, droplet.region().slug())));
    }
 
    @Override

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java
index eebc121..11594f8 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java
@@ -18,10 +18,11 @@ package org.jclouds.digitalocean2.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.digitalocean2.compute.internal.ImageInRegion.encodeId;
 
 import java.util.Map;
 import java.util.Set;
+
 import javax.annotation.Resource;
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -35,6 +36,7 @@ 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.digitalocean2.compute.internal.ImageInRegion;
 import org.jclouds.digitalocean2.domain.Droplet;
 import org.jclouds.digitalocean2.domain.Networks;
 import org.jclouds.digitalocean2.domain.Region;
@@ -42,6 +44,7 @@ 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;
@@ -91,7 +94,7 @@ public class DropletToNodeMetadata implements Function<Droplet, NodeMetadata> {
       builder.hardware(getHardware(input.sizeSlug()));
       builder.location(getLocation(input.region()));
 
-      Optional<? extends Image> image = findImage(input.image().id());
+      Optional<? extends Image> image = findImage(input.image(), input.region().slug());
       if (image.isPresent()) {
          builder.imageId(image.get().getId());
          builder.operatingSystem(image.get().getOperatingSystem());
@@ -138,22 +141,8 @@ 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 Optional<? extends Image> findImage(org.jclouds.digitalocean2.domain.Image image, String region) {
+      return Optional.fromNullable(images.get().get(encodeId(ImageInRegion.create(image, region))));
    }
 
    protected Hardware getHardware(final String slug) {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImage.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImage.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImage.java
new file mode 100644
index 0000000..08c6c71
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImage.java
@@ -0,0 +1,92 @@
+/*
+ * 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.digitalocean2.compute.functions;
+
+import static com.google.common.collect.Iterables.find;
+import static org.jclouds.compute.domain.OperatingSystem.builder;
+import static org.jclouds.digitalocean2.compute.internal.ImageInRegion.encodeId;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.collect.Memoized;
+import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.Image.Status;
+import org.jclouds.compute.domain.ImageBuilder;
+import org.jclouds.digitalocean2.compute.internal.ImageInRegion;
+import org.jclouds.digitalocean2.domain.OperatingSystem;
+import org.jclouds.domain.Location;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Transforms an {@link ImageInRegion} to the jclouds portable model.
+ */
+@Singleton
+public class ImageInRegionToImage implements Function<ImageInRegion, Image> {
+
+   private final Supplier<Set<? extends Location>> locations;
+
+   @Inject ImageInRegionToImage(@Memoized Supplier<Set<? extends Location>> locations) {
+      this.locations = locations;
+   }
+
+   @Override
+   public Image apply(final ImageInRegion input) {
+      String description = input.image().distribution() + " " + input.image().name();
+      ImageBuilder builder = new ImageBuilder();
+      // Private images don't have a slug
+      builder.id(encodeId(input));
+      builder.providerId(String.valueOf(input.image().id()));
+      builder.name(input.image().name());
+      builder.description(description);
+      builder.status(Status.AVAILABLE);
+      builder.location(getLocation(input.region()));
+
+      OperatingSystem os = OperatingSystem.create(input.image().name(), input.image().distribution());
+
+      builder.operatingSystem(builder()
+            .name(os.distribution().value())
+            .family(os.distribution().osFamily())
+            .description(description)
+            .arch(os.arch())
+            .version(os.version())
+            .is64Bit(os.is64bit())
+            .build());
+
+      ImmutableMap.Builder<String, String> metadata = ImmutableMap.builder();
+      metadata.put("publicImage", String.valueOf(input.image().isPublic()));
+      builder.userMetadata(metadata.build());
+
+      return builder.build();
+   }
+
+   protected Location getLocation(final String region) {
+      return find(locations.get(), new Predicate<Location>() {
+         @Override
+         public boolean apply(Location location) {
+            return region.equals(location.getId());
+         }
+      });
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageToImage.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageToImage.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageToImage.java
deleted file mode 100644
index 8f9ad92..0000000
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageToImage.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.digitalocean2.compute.functions;
-
-import static org.jclouds.compute.domain.OperatingSystem.builder;
-
-import javax.inject.Singleton;
-
-import org.jclouds.compute.domain.Image.Status;
-import org.jclouds.compute.domain.ImageBuilder;
-import org.jclouds.digitalocean2.domain.Image;
-import org.jclouds.digitalocean2.domain.OperatingSystem;
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableMap;
-
-/**
- * Transforms an {@link Image} to the jclouds portable model.
- */
-@Singleton
-public class ImageToImage implements Function<Image, org.jclouds.compute.domain.Image> {
-
-   @Override
-   public org.jclouds.compute.domain.Image apply(final Image input) {
-      String description = input.distribution() + " " + input.name();
-      ImageBuilder builder = new ImageBuilder();
-      // Private images don't have a slug
-      builder.id(input.slug() != null ? input.slug() : String.valueOf(input.id()));
-      builder.providerId(String.valueOf(input.id()));
-      builder.name(input.name());
-      builder.description(description);
-      builder.status(Status.AVAILABLE);
-
-      OperatingSystem os = OperatingSystem.create(input.name(), input.distribution());
-
-      builder.operatingSystem(builder()
-            .name(os.distribution().value())
-            .family(os.distribution().osFamily()) 
-            .description(description)
-            .arch(os.arch()) 
-            .version(os.version()) 
-            .is64Bit(os.is64bit()) 
-            .build());
-
-      ImmutableMap.Builder<String, String> metadata = ImmutableMap.builder();
-      metadata.put("publicImage", String.valueOf(input.isPublic()));
-      builder.userMetadata(metadata.build());
-
-      return builder.build();
-   }
-
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/internal/ImageInRegion.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/internal/ImageInRegion.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/internal/ImageInRegion.java
new file mode 100644
index 0000000..b5beb8d
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/internal/ImageInRegion.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.digitalocean2.compute.internal;
+
+import org.jclouds.digitalocean2.domain.Image;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Scopes an image to a particular region.
+ */
+@AutoValue
+public abstract class ImageInRegion {
+
+   public abstract Image image();
+   public abstract String region();
+
+   public static ImageInRegion create(Image image, String region) {
+      return new AutoValue_ImageInRegion(image, region);
+   }
+
+   public static String encodeId(ImageInRegion imageInRegion) {
+      // Private images don't have a slug
+      return String.format("%s/%s", imageInRegion.region(), slugOrId(imageInRegion.image()));
+   }
+
+   public static String extractRegion(String imageId) {
+      return imageId.substring(0, imageId.indexOf('/'));
+   }
+
+   public static String extractImageId(String imageId) {
+      return imageId.substring(imageId.indexOf('/') + 1);
+   }
+
+   private static String slugOrId(Image image) {
+      return image.slug() != null ? image.slug() : String.valueOf(image.id());
+   }
+
+   ImageInRegion() { }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata b/providers/digitalocean2/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
deleted file mode 100644
index 0be234c..0000000
--- a/providers/digitalocean2/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# 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.
-#
-
-org.jclouds.digitalocean2.DigitalOcean2ApiMetadata

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java
index e508789..8480cc1 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java
@@ -40,7 +40,7 @@ public class DigitalOcean2TemplateBuilderLiveTest extends BaseTemplateBuilderLiv
    @Override
    public void testDefaultTemplateBuilder() throws IOException {
       Template defaultTemplate = view.getComputeService().templateBuilder().build();
-      assert defaultTemplate.getImage().getOperatingSystem().getVersion().equals("14.04") : defaultTemplate
+      assert defaultTemplate.getImage().getOperatingSystem().getVersion().equals("15.04") : defaultTemplate
             .getImage().getOperatingSystem().getVersion();
       assertEquals(defaultTemplate.getImage().getOperatingSystem().is64Bit(), true);
       assertEquals(defaultTemplate.getImage().getOperatingSystem().getFamily(), OsFamily.UBUNTU);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java
index 27dbad9..ba6d3de 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java
@@ -83,7 +83,7 @@ public class DropletToNodeMetadataTest {
       region = Region.create("sfo1", "San Francisco 1", ImmutableList.of("2gb"), true, ImmutableList.<String> of());
       
       images = ImmutableSet.of(new ImageBuilder()
-            .id("ubuntu-1404-x86")
+            .id("sfo1/ubuntu-1404-x86")
             .providerId("1")
             .name("mock image")
             .status(AVAILABLE)
@@ -132,7 +132,7 @@ public class DropletToNodeMetadataTest {
                   ImmutableList.<Networks.Address> of()), null);
 
       NodeMetadata expected = new NodeMetadataBuilder().ids("1").hardware(getOnlyElement(hardwares))
-            .imageId("ubuntu-1404-x86").status(RUNNING).location(getOnlyElement(locations)).name("mock-droplet")
+            .imageId("sfo1/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())

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImageTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImageTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImageTest.java
new file mode 100644
index 0000000..f1072d6
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageInRegionToImageTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.digitalocean2.compute.functions;
+
+import static org.jclouds.compute.domain.Image.Status.AVAILABLE;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Date;
+import java.util.Set;
+
+import org.jclouds.compute.domain.ImageBuilder;
+import org.jclouds.compute.domain.OperatingSystem;
+import org.jclouds.compute.domain.OsFamily;
+import org.jclouds.digitalocean2.compute.internal.ImageInRegion;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+@Test(groups = "unit", testName = "ImageToImageTest")
+public class ImageInRegionToImageTest {
+
+   private Set<Location> locations;
+
+   private ImageInRegionToImage function;
+
+   @BeforeMethod
+   public void setup() {
+      locations = ImmutableSet.of(
+            new LocationBuilder()
+                  .id("sfo1")
+                  .description("sfo1/San Francisco 1")
+                  .scope(LocationScope.REGION)
+                  .parent(
+                        new LocationBuilder().id("0").description("mock parent location").scope(LocationScope.PROVIDER)
+                              .build()).build(),
+            new LocationBuilder()
+                  .id("lon1")
+                  .description("lon1/London 1")
+                  .scope(LocationScope.REGION)
+                  .parent(
+                        new LocationBuilder().id("0").description("mock parent location").scope(LocationScope.PROVIDER)
+                              .build()).build());
+
+      function = new ImageInRegionToImage(new Supplier<Set<? extends Location>>() {
+         @Override
+         public Set<? extends Location> get() {
+            return locations;
+         }
+      });
+   }
+
+   @Test
+   public void testConvertImage() {
+      Image image = Image.create(1, "14.04 x64", "distribution", "Ubuntu", "ubuntu-1404-x86", true,
+            ImmutableList.of("sfo1", "lon1"), new Date());
+      org.jclouds.compute.domain.Image expected = new ImageBuilder()
+            .id("lon1/ubuntu-1404-x86") // Location scoped images have the location encoded in the id
+            .providerId("1")
+            .name("14.04 x64")
+            .description("Ubuntu 14.04 x64")
+            .status(AVAILABLE)
+            .operatingSystem(
+                  OperatingSystem.builder().name("Ubuntu").description("Ubuntu 14.04 x64").family(OsFamily.UBUNTU)
+                        .version("14.04").arch("x64").is64Bit(true).build())
+            .location(Iterables.get(locations, 1))
+            .userMetadata(ImmutableMap.of("publicImage", "true")).build();
+
+      org.jclouds.compute.domain.Image result = function.apply(ImageInRegion.create(image, "lon1"));
+      assertEquals(result, expected);
+      assertEquals(result.getDescription(), expected.getDescription());
+      assertEquals(result.getOperatingSystem(), expected.getOperatingSystem());
+      assertEquals(result.getStatus(), expected.getStatus());
+      assertEquals(result.getLocation(), Iterables.get(locations, 1));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/26210fe0/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
deleted file mode 100644
index 6ab020c..0000000
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.digitalocean2.compute.functions;
-
-import static org.jclouds.compute.domain.Image.Status.AVAILABLE;
-import static org.testng.Assert.assertEquals;
-
-import java.util.Date;
-
-import org.jclouds.compute.domain.ImageBuilder;
-import org.jclouds.compute.domain.OperatingSystem;
-import org.jclouds.compute.domain.OsFamily;
-import org.jclouds.digitalocean2.domain.Image;
-import org.testng.annotations.Test;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
-@Test(groups = "unit", testName = "ImageToImageTest")
-public class ImageToImageTest {
-
-   @Test
-   public void testConvertImage() {
-      Image image = Image.create(1, "14.04 x64", "distribution", "Ubuntu", "ubuntu-1404-x86", true,
-            ImmutableList.of("sfo1"), new Date());
-      org.jclouds.compute.domain.Image expected = new ImageBuilder()
-            .id("ubuntu-1404-x86")
-            .providerId("1")
-            .name("14.04 x64")
-            .description("Ubuntu 14.04 x64")
-            .status(AVAILABLE)
-            .operatingSystem(
-                  OperatingSystem.builder().name("Ubuntu").description("Ubuntu 14.04 x64").family(OsFamily.UBUNTU)
-                        .version("14.04").arch("x64").is64Bit(true).build())
-            .userMetadata(ImmutableMap.of("publicImage", "true")).build();
-
-      org.jclouds.compute.domain.Image result = new ImageToImage().apply(image);
-      assertEquals(result, expected);
-      assertEquals(result.getDescription(), expected.getDescription());
-      assertEquals(result.getOperatingSystem(), expected.getOperatingSystem());
-      assertEquals(result.getStatus(), expected.getStatus());
-   }
-}