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:25 UTC

[01/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Repository: jclouds
Updated Branches:
  refs/heads/master 52dc1a3cc -> edacad2b6


http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/regions-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/regions-last.json b/providers/digitalocean2/src/test/resources/regions-last.json
new file mode 100644
index 0000000..d332ed8
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/regions-last.json
@@ -0,0 +1,128 @@
+{
+   "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/regions?page=1&per_page=5",
+         "prev" : "https://api.digitalocean.com/v2/regions?page=1&per_page=5"
+      }
+   },
+   "meta" : {
+      "total" : 10
+   },
+   "regions" : [
+      {
+         "available" : true,
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "name" : "Singapore 1",
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "slug" : "sgp1"
+      },
+      {
+         "available" : true,
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "name" : "London 1",
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "slug" : "lon1"
+      },
+      {
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "name" : "New York 3",
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "available" : true,
+         "slug" : "nyc3"
+      },
+      {
+         "available" : true,
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "name" : "Amsterdam 3",
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "slug" : "ams3"
+      },
+      {
+         "available" : true,
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "name" : "Frankfurt 1",
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "slug" : "fra1"
+      }
+   ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/shutdown.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/shutdown.json b/providers/digitalocean2/src/test/resources/shutdown.json
new file mode 100644
index 0000000..9c03fcf
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/shutdown.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "shutdown"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/sizes-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/sizes-first.json b/providers/digitalocean2/src/test/resources/sizes-first.json
new file mode 100644
index 0000000..1ac9595
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/sizes-first.json
@@ -0,0 +1,123 @@
+{
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/sizes?page=2&per_page=5",
+         "last" : "https://api.digitalocean.com/v2/sizes?page=2&per_page=5"
+      }
+   },
+   "meta" : {
+      "total" : 9
+   },
+   "sizes" : [
+      {
+         "vcpus" : 1,
+         "disk" : 20,
+         "price_monthly" : 5,
+         "regions" : [
+            "nyc1",
+            "sgp1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "ams2",
+            "fra1"
+         ],
+         "price_hourly" : 0.00744,
+         "transfer" : 1,
+         "available" : true,
+         "slug" : "512mb",
+         "memory" : 512
+      },
+      {
+         "vcpus" : 1,
+         "disk" : 30,
+         "price_monthly" : 10,
+         "regions" : [
+            "nyc2",
+            "sgp1",
+            "ams1",
+            "sfo1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "nyc1",
+            "ams2",
+            "fra1"
+         ],
+         "price_hourly" : 0.01488,
+         "transfer" : 2,
+         "slug" : "1gb",
+         "available" : true,
+         "memory" : 1024
+      },
+      {
+         "price_hourly" : 0.02976,
+         "regions" : [
+            "nyc2",
+            "sfo1",
+            "ams1",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "nyc1",
+            "ams2",
+            "fra1"
+         ],
+         "transfer" : 3,
+         "available" : true,
+         "slug" : "2gb",
+         "memory" : 2048,
+         "vcpus" : 2,
+         "disk" : 40,
+         "price_monthly" : 20
+      },
+      {
+         "transfer" : 4,
+         "price_hourly" : 0.05952,
+         "regions" : [
+            "nyc2",
+            "ams1",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "nyc1",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "memory" : 4096,
+         "slug" : "4gb",
+         "available" : true,
+         "disk" : 60,
+         "vcpus" : 2,
+         "price_monthly" : 40
+      },
+      {
+         "available" : true,
+         "slug" : "8gb",
+         "memory" : 8192,
+         "regions" : [
+            "nyc2",
+            "sgp1",
+            "ams1",
+            "nyc1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "price_hourly" : 0.11905,
+         "transfer" : 5,
+         "price_monthly" : 80,
+         "vcpus" : 4,
+         "disk" : 80
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/sizes-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/sizes-last.json b/providers/digitalocean2/src/test/resources/sizes-last.json
new file mode 100644
index 0000000..c5b47f7
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/sizes-last.json
@@ -0,0 +1,98 @@
+{
+   "meta" : {
+      "total" : 9
+   },
+   "links" : {
+      "pages" : {
+         "prev" : "https://api.digitalocean.com/v2/sizes?page=1&per_page=5",
+         "first" : "https://api.digitalocean.com/v2/sizes?page=1&per_page=5"
+      }
+   },
+   "sizes" : [
+      {
+         "regions" : [
+            "sgp1",
+            "nyc1",
+            "nyc3",
+            "lon1",
+            "nyc2",
+            "ams1",
+            "ams3",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "transfer" : 6,
+         "price_monthly" : 160,
+         "available" : true,
+         "vcpus" : 8,
+         "disk" : 160,
+         "slug" : "16gb",
+         "memory" : 16384,
+         "price_hourly" : 0.2381
+      },
+      {
+         "price_hourly" : 0.47619,
+         "price_monthly" : 320,
+         "available" : true,
+         "transfer" : 7,
+         "regions" : [
+            "nyc2",
+            "sgp1",
+            "nyc1",
+            "lon1",
+            "ams3",
+            "nyc3",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "memory" : 32768,
+         "slug" : "32gb",
+         "disk" : 320,
+         "vcpus" : 12
+      },
+      {
+         "price_monthly" : 480,
+         "available" : true,
+         "transfer" : 8,
+         "regions" : [
+            "sgp1",
+            "nyc1",
+            "lon1",
+            "nyc2",
+            "ams3",
+            "nyc3",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "memory" : 49152,
+         "slug" : "48gb",
+         "disk" : 480,
+         "vcpus" : 16,
+         "price_hourly" : 0.71429
+      },
+      {
+         "price_hourly" : 0.95238,
+         "price_monthly" : 640,
+         "available" : true,
+         "transfer" : 9,
+         "regions" : [
+            "sgp1",
+            "nyc1",
+            "nyc2",
+            "lon1",
+            "ams3",
+            "nyc3",
+            "ams2",
+            "sfo1",
+            "fra1"
+         ],
+         "memory" : 65536,
+         "slug" : "64gb",
+         "disk" : 640,
+         "vcpus" : 20
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/snapshot.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/snapshot.json b/providers/digitalocean2/src/test/resources/snapshot.json
new file mode 100644
index 0000000..193d9da
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/snapshot.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "snapshot"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/snapshots-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/snapshots-first.json b/providers/digitalocean2/src/test/resources/snapshots-first.json
new file mode 100644
index 0000000..6befa29
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/snapshots-first.json
@@ -0,0 +1,27 @@
+{
+  "snapshots": [
+    {
+      "id": 7938206,
+      "name": "nginx-fresh",
+      "distribution": "Ubuntu",
+      "slug": null,
+      "public": false,
+      "regions": [
+        "nyc3",
+        "nyc3"
+      ],
+      "created_at": "2014-11-14T16:37:34Z",
+      "type": "snapshot",
+      "min_disk_size": 20
+    }
+  ],
+  "links": {
+      "pages" : {
+         "last" : "https://api.digitalocean.com/v2/droplets/3067509/snapshots?page=2",
+         "next" : "https://api.digitalocean.com/v2/droplets/3067509/snapshots?page=2"
+      }
+  },
+  "meta": {
+    "total": 2
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/snapshots-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/snapshots-last.json b/providers/digitalocean2/src/test/resources/snapshots-last.json
new file mode 100644
index 0000000..70e4162
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/snapshots-last.json
@@ -0,0 +1,27 @@
+{
+  "snapshots": [
+    {
+      "id": 7938206,
+      "name": "nginx-fresh",
+      "distribution": "Ubuntu",
+      "slug": null,
+      "public": false,
+      "regions": [
+        "nyc3",
+        "nyc3"
+      ],
+      "created_at": "2014-11-14T16:37:34Z",
+      "type": "snapshot",
+      "min_disk_size": 20
+    }
+  ],
+  "links": {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/droplets/3067509/snapshots?page=1",
+         "prev" : "https://api.digitalocean.com/v2/droplets/3067509/snapshots?page=1"
+      }
+  },
+  "meta": {
+    "total": 2
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/ssh-dsa.pub
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/ssh-dsa.pub b/providers/digitalocean2/src/test/resources/ssh-dsa.pub
new file mode 100644
index 0000000..3dc78e4
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/ssh-dsa.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAK5uLwicCrFEpaVKBzkWxC7RQn+smg5ZQb5keh9RQKo8AszFTol5npgUAr0JWmqKIHv7nof0HndO86x9iIqNjq3vrz9CIVcFfZM7poKBJZ27Hv3v0fmSKfAc6eGdx8eM9UkZe1gzcLXK8UP2HaeY1Y4LlaHXS5tPi/dXooFVgiA7AAAAFQCQl6LZo/VYB9VgPEZzOmsmQevnswAAAIBCNKGsVP5eZ+IJklXheUyzyuL75i04OOtEGW6MO5TymKMwTZlU9r4ukuwxty+T9Ot2LqlNRnLSPQUjb0vplasZ8Ix45JOpRbuSvPovryn7rvS7//klu9hIkFAAQ/AZfGTw+696EjFBg4F5tN6MGMA6KrTQVLXeuYcZeRXwE5t5lwAAAIEAl2xYh098bozJUANQ82DiZznjHc5FW76Xm1apEqsZtVRFuh3V9nc7QNcBekhmHp5Z0sHthXCm1XqnFbkRCdFlX02NpgtNs7OcKpaJP47N8C+C/Yrf8qK/Wt3fExrL2ZLX5XD2tiotugSkwZJMW5Bv0mtjrNt0Q7P45rZjNNTag2c=
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/ssh-ecdsa.pub
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/ssh-ecdsa.pub b/providers/digitalocean2/src/test/resources/ssh-ecdsa.pub
new file mode 100644
index 0000000..f04a852
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/ssh-ecdsa.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACeO2+Ci8Aw76SDF1FJyrwAp2fUhNeIgkP7G2x0SvF/yvZarOkZbCg1MAav3JobuqYZEA4GbLnoT2UK9OBAenucTgHXhEX91mQaFWe3pKqroealb7Vc059za+h6b6IfzONNhyXGpCVYlh6tUdq8y1nhGjMzIsUrma+hA1+ERJGFfGDLgA==


[10/19] jclouds git commit: JCLOUDS-1025: Add support for metadata and tags in the ComputeService

Posted by na...@apache.org.
JCLOUDS-1025: Add support for metadata and tags in the ComputeService


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

Branch: refs/heads/master
Commit: 4596471bb258c391af06b7bde6f017af02e965db
Parents: 200e0e1
Author: Ignasi Barrera <na...@apache.org>
Authored: Fri Oct 23 00:18:45 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Fri Oct 23 00:18:45 2015 +0200

----------------------------------------------------------------------
 .../DigitalOcean2ComputeServiceAdapter.java     | 21 ++++++++++++++++-
 .../compute/functions/RegionToLocation.java     |  2 ++
 .../domain/options/CreateDropletOptions.java    | 24 ++++++++++++++++++++
 .../DigitalOcean2ComputeServiceLiveTest.java    |  4 ++--
 .../compute/functions/RegionToLocationTest.java | 10 +++++---
 5 files changed, 55 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/4596471b/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 f1f5919..43b1585 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
@@ -27,7 +27,10 @@ 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 static org.jclouds.compute.util.ComputeServiceUtils.metadataAndTagsAsCommaDelimitedValue;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Resource;
@@ -48,6 +51,7 @@ import org.jclouds.digitalocean2.domain.Region;
 import org.jclouds.digitalocean2.domain.Size;
 import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
 import org.jclouds.domain.LoginCredentials;
+import org.jclouds.json.Json;
 import org.jclouds.logging.Logger;
 
 import com.google.common.base.Function;
@@ -67,15 +71,18 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
    private final Predicate<Integer> nodeRunningPredicate;
    private final Predicate<Integer> nodeStoppedPredicate;
    private final Predicate<Integer> nodeTerminatedPredicate;
+   private final Json json;
 
    @Inject DigitalOcean2ComputeServiceAdapter(DigitalOcean2Api api,
          @Named(TIMEOUT_NODE_RUNNING) Predicate<Integer> nodeRunningPredicate,
          @Named(TIMEOUT_NODE_SUSPENDED) Predicate<Integer> nodeStoppedPredicate,
-         @Named(TIMEOUT_NODE_TERMINATED) Predicate<Integer> nodeTerminatedPredicate) {
+         @Named(TIMEOUT_NODE_TERMINATED) Predicate<Integer> nodeTerminatedPredicate,
+         Json json) {
       this.api = api;
       this.nodeRunningPredicate = nodeRunningPredicate;
       this.nodeStoppedPredicate = nodeStoppedPredicate;
       this.nodeTerminatedPredicate = nodeTerminatedPredicate;
+      this.json = json;
    }
 
    @Override
@@ -91,6 +98,18 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
          options.addSshKeyIds(templateOptions.getSshKeyIds());
       }
 
+      Map<String, String> metadataAndTags = metadataAndTagsAsCommaDelimitedValue(templateOptions);
+      if (!metadataAndTags.isEmpty()) {
+         @SuppressWarnings("unchecked")
+         List<String> regionFeatures = (List<String>) template.getLocation().getMetadata().get("features");
+         if (regionFeatures.contains("metadata")) {
+            options.userData(json.toJson(metadataAndTags));
+         } else {
+            logger.debug(">> region %s does not support metadata, ignoring provided user data", template.getLocation()
+                  .getId());
+         }
+      }
+
       DropletCreate dropletCreated = api.dropletApi().create(name,
             template.getLocation().getId(),
             template.getHardware().getProviderId(),

http://git-wip-us.apache.org/repos/asf/jclouds/blob/4596471b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
index 4adf240..adde1b7 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
@@ -29,6 +29,7 @@ import org.jclouds.domain.LocationScope;
 import org.jclouds.location.suppliers.all.JustProvider;
 
 import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 /**
@@ -52,6 +53,7 @@ public class RegionToLocation implements Function<Region, Location> {
       builder.scope(LocationScope.REGION);
       builder.parent(getOnlyElement(justProvider.get()));
       builder.iso3166Codes(ImmutableSet.<String> of());
+      builder.metadata(ImmutableMap.<String, Object> of("available", input.available(), "features", input.features()));
       return builder.build();
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/4596471b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
index b20fc96..91fb090 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
@@ -104,6 +104,14 @@ public class CreateDropletOptions implements MapBinder {
       return backupsEnabled;
    }
 
+   public boolean isIpv6Enabled() {
+      return ipv6Enabled;
+   }
+
+   public String getUserData() {
+      return userData;
+   }
+
    public static Builder builder() {
       return new Builder();
    }
@@ -148,6 +156,22 @@ public class CreateDropletOptions implements MapBinder {
          return this;
       }
 
+      /**
+       * Sets the user data for the droplet.
+       */
+      public Builder userData(String userData) {
+         this.userData = userData;
+         return this;
+      }
+
+      /**
+       * Enables/disables IPv6 for the droplet.
+       */
+      public Builder ipv6Enabled(boolean ipv6Enabled) {
+         this.ipv6Enabled = ipv6Enabled;
+         return this;
+      }
+
       public CreateDropletOptions build() {
          return new CreateDropletOptions(sshKeyIds.build(), backupsEnabled, ipv6Enabled, privateNetworking, userData);
       }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/4596471b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
index b1dcc1b..b8fbbe7 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
@@ -47,12 +47,12 @@ public class DigitalOcean2ComputeServiceLiveTest extends BaseComputeServiceLiveT
 
    @Override
    protected void checkTagsInNodeEquals(NodeMetadata node, ImmutableSet<String> tags) {
-      // DigitalOcean does not support tags
+      // We encode the tags in the user data but the DigitalOcean API does not return it
    }
 
    @Override
    protected void checkUserMetadataContains(NodeMetadata node, ImmutableMap<String, String> userMetadata) {
-      // DigitalOcean does not support user metadata
+      // The DigitalOcean API does not return the user data
    }
 
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/4596471b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
index 879091b..7e6ecea 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
@@ -42,11 +42,15 @@ public class RegionToLocationTest {
       JustProvider locationsSupplier = new JustProvider(metadata.getId(), Suppliers.<URI> ofInstance(URI
             .create(metadata.getEndpoint())), ImmutableSet.<String> of());
 
-      Region region = Region.create("reg1", "Region1", ImmutableList.<String> of(), true, ImmutableList.<String> of());
+      Region region = Region.create("reg1", "Region1", ImmutableList.<String> of(), true,
+            ImmutableList.<String> of("virtio", "metadata"));
       Location expected = new LocationBuilder().id("reg1").description("reg1/Region 1")
             .parent(getOnlyElement(locationsSupplier.get())).scope(LocationScope.REGION).build();
 
-      RegionToLocation function = new RegionToLocation(locationsSupplier);
-      assertEquals(function.apply(region), expected);
+      Location location = new RegionToLocation(locationsSupplier).apply(region);
+
+      assertEquals(location, expected);
+      assertEquals(location.getMetadata().get("available"), true);
+      assertEquals(location.getMetadata().get("features"), ImmutableList.of("virtio", "metadata"));
    }
 }


[14/19] jclouds git commit: JCLOUDS-1033: Don't fail if no private key has been provided in DigitalOcean

Posted by na...@apache.org.
JCLOUDS-1033: Don't fail if no private key has been provided in DigitalOcean


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

Branch: refs/heads/master
Commit: 575d39e7c5c9974ecbe080a09a2787d08534ac3a
Parents: 3fbd399
Author: Ignasi Barrera <na...@apache.org>
Authored: Mon Nov 2 13:03:55 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Mon Nov 16 22:58:29 2015 +0100

----------------------------------------------------------------------
 .../compute/strategy/CreateKeyPairsThenCreateNodes.java  | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/575d39e7/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
index 3e4aae3..bc304b7 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
@@ -16,7 +16,6 @@
  */
 package org.jclouds.digitalocean2.compute.strategy;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.security.PublicKey;
@@ -101,11 +100,11 @@ public class CreateKeyPairsThenCreateNodes extends CreateNodesWithGroupEncodedIn
          generateKeyPairAndAddKeyToSet(options, generatedSshKeyIds, group);
       }
 
-      // If there is a script to run in the node, make sure a private key has been configured so jclouds will be able to
-      // access the node
-      if (options.getRunScript() != null) {
-         checkArgument(!Strings.isNullOrEmpty(options.getLoginPrivateKey()),
-               "no private key configured for: %s; please use options.overrideLoginPrivateKey(rsa_private_text)", group);
+      // If there is a script to run in the node, make sure a private key has
+      // been configured so jclouds will be able to access the node
+      if (options.getRunScript() != null && Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
+         logger.warn(">> A runScript has been configured but no SSH key has been provided."
+               + " Authentication will delegate to the ssh-agent");
       }
 
       // If there is a key configured, then make sure there is a key pair for it


[19/19] jclouds git commit: Amend poms after promoting DigitalOcean v2

Posted by na...@apache.org.
Amend poms after promoting DigitalOcean v2


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

Branch: refs/heads/master
Commit: edacad2b6e01ff94cdf4c5733f847cd9374110b9
Parents: 886aa15
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Jan 21 00:08:42 2016 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Thu Jan 21 01:02:58 2016 +0100

----------------------------------------------------------------------
 allcompute/pom.xml              |   5 +
 project/pom.xml                 |   1 +
 providers/digitalocean2/pom.xml | 263 ++++++++++++++++++-----------------
 providers/pom.xml               |   1 +
 4 files changed, 142 insertions(+), 128 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/edacad2b/allcompute/pom.xml
----------------------------------------------------------------------
diff --git a/allcompute/pom.xml b/allcompute/pom.xml
index 04edaa6..cf6b4e0 100644
--- a/allcompute/pom.xml
+++ b/allcompute/pom.xml
@@ -123,5 +123,10 @@
       <artifactId>google-compute-engine</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds.provider</groupId>
+      <artifactId>digitalocean2</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/edacad2b/project/pom.xml
----------------------------------------------------------------------
diff --git a/project/pom.xml b/project/pom.xml
index ed38a8f..7909e93 100644
--- a/project/pom.xml
+++ b/project/pom.xml
@@ -490,6 +490,7 @@
             <!-- SSH keys -->
             <exclude>**/test</exclude>
             <exclude>**/test.pub</exclude>
+            <exclude>**/src/test/resources/**/ssh-*.pub</exclude>
 
             <!-- temporary files or those generated by IDE or SCM -->
             <exclude>**/target/**</exclude>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/edacad2b/providers/digitalocean2/pom.xml
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/pom.xml b/providers/digitalocean2/pom.xml
index 5b211d4..c1dd83d 100644
--- a/providers/digitalocean2/pom.xml
+++ b/providers/digitalocean2/pom.xml
@@ -18,135 +18,142 @@
 
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>org.apache.jclouds.labs</groupId>
-        <artifactId>jclouds-labs</artifactId>
-        <version>2.0.0-SNAPSHOT</version>
-    </parent>
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.jclouds</groupId>
+    <artifactId>jclouds-project</artifactId>
+    <version>2.0.0-SNAPSHOT</version>
+    <relativePath>../../project/pom.xml</relativePath>
+  </parent>
 
-    <groupId>org.apache.jclouds.labs</groupId>
-    <artifactId>digitalocean2</artifactId>
-    <name>jclouds DigitalOcean v2 API Provider</name>
-    <description>jclouds provider for Digital Ocean v2 Compute API</description>
+  <groupId>org.apache.jclouds.provider</groupId>
+  <artifactId>digitalocean2</artifactId>
+  <name>jclouds DigitalOcean v2 API Provider</name>
+  <description>jclouds provider for Digital Ocean v2 Compute API</description>
 
-    <properties>
-        <test.digitalocean2.endpoint>https://api.digitalocean.com/v2/</test.digitalocean2.endpoint>
-        <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>osFamily=UBUNTU,os64Bit=true</test.digitalocean2.template>
-    </properties>
+  <properties>
+    <test.digitalocean2.endpoint>https://api.digitalocean.com/v2/</test.digitalocean2.endpoint>
+    <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>osFamily=UBUNTU,os64Bit=true</test.digitalocean2.template>
+    <jclouds.osgi.export>org.jclouds.digitalocean2*;version="${project.version}"</jclouds.osgi.export>
+    <jclouds.osgi.import>
+      org.jclouds.rest.internal;version="${project.version}",
+      org.jclouds*;version="${project.version}",
+      *
+    </jclouds.osgi.import>
+  </properties>
 
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.jclouds</groupId>
-            <artifactId>jclouds-core</artifactId>
-            <version>${jclouds.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds.api</groupId>
-            <artifactId>oauth</artifactId>
-            <version>${jclouds.version}</version>
-            <type>jar</type>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds.api</groupId>
-            <artifactId>oauth</artifactId>
-            <version>${jclouds.version}</version>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds</groupId>
-            <artifactId>jclouds-compute</artifactId>
-            <version>${jclouds.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds</groupId>
-            <artifactId>jclouds-compute</artifactId>
-            <version>${jclouds.version}</version>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds</groupId>
-            <artifactId>jclouds-core</artifactId>
-            <version>${jclouds.version}</version>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds.driver</groupId>
-            <artifactId>jclouds-slf4j</artifactId>
-            <version>${jclouds.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jclouds.driver</groupId>
-            <artifactId>jclouds-sshj</artifactId>
-            <version>${jclouds.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-classic</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.squareup.okhttp</groupId>
-            <artifactId>mockwebserver</artifactId>
-            <scope>test</scope>
-            <exclusions>
-                <!-- Already provided by jclouds-sshj -->
-                <exclusion>
-                    <groupId>org.bouncycastle</groupId>
-                    <artifactId>bcprov-jdk15on</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>com.google.auto.value</groupId>
-            <artifactId>auto-value</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.google.auto.service</groupId>
-            <artifactId>auto-service</artifactId>
-            <scope>provided</scope>
-        </dependency>
-    </dependencies>
-    <profiles>
-        <profile>
-            <id>live</id>
-            <build>
-                <plugins>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-surefire-plugin</artifactId>
-                        <executions>
-                            <execution>
-                                <id>integration</id>
-                                <phase>integration-test</phase>
-                                <goals>
-                                    <goal>test</goal>
-                                </goals>
-                                <configuration>
-                                    <systemPropertyVariables>
-                                        <test.digitalocean2.endpoint>${test.digitalocean2.endpoint}</test.digitalocean2.endpoint>
-                                        <test.digitalocean2.api-version>${test.digitalocean2.api-version}</test.digitalocean2.api-version>
-                                        <test.digitalocean2.build-version>${test.digitalocean2.build-version}</test.digitalocean2.build-version>
-                                        <test.digitalocean2.identity>${test.digitalocean2.identity}</test.digitalocean2.identity>
-                                        <test.digitalocean2.credential>${test.digitalocean2.credential}</test.digitalocean2.credential>
-                                        <test.digitalocean2.template>${test.digitalocean2.template}</test.digitalocean2.template>
-                                    </systemPropertyVariables>
-                                </configuration>
-                            </execution>
-                        </executions>
-                    </plugin>
-                </plugins>
-            </build>
-        </profile>
-    </profiles>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.jclouds</groupId>
+      <artifactId>jclouds-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds.api</groupId>
+      <artifactId>oauth</artifactId>
+      <version>${project.version}</version>
+      <type>jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds.api</groupId>
+      <artifactId>oauth</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds</groupId>
+      <artifactId>jclouds-compute</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds</groupId>
+      <artifactId>jclouds-compute</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds</groupId>
+      <artifactId>jclouds-core</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds.driver</groupId>
+      <artifactId>jclouds-slf4j</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.jclouds.driver</groupId>
+      <artifactId>jclouds-sshj</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <scope>test</scope>
+      <exclusions>
+        <!-- Already provided by jclouds-sshj -->
+        <exclusion>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk15on</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.google.auto.value</groupId>
+      <artifactId>auto-value</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.auto.service</groupId>
+      <artifactId>auto-service</artifactId>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+  <profiles>
+    <profile>
+      <id>live</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>integration</id>
+                <phase>integration-test</phase>
+                <goals>
+                  <goal>test</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <test.digitalocean2.endpoint>${test.digitalocean2.endpoint}</test.digitalocean2.endpoint>
+                    <test.digitalocean2.api-version>${test.digitalocean2.api-version}</test.digitalocean2.api-version>
+                    <test.digitalocean2.build-version>${test.digitalocean2.build-version}</test.digitalocean2.build-version>
+                    <test.digitalocean2.identity>${test.digitalocean2.identity}</test.digitalocean2.identity>
+                    <test.digitalocean2.credential>${test.digitalocean2.credential}</test.digitalocean2.credential>
+                    <test.digitalocean2.template>${test.digitalocean2.template}</test.digitalocean2.template>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
 </project>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/edacad2b/providers/pom.xml
----------------------------------------------------------------------
diff --git a/providers/pom.xml b/providers/pom.xml
index ec6fffe..353c2f4 100644
--- a/providers/pom.xml
+++ b/providers/pom.xml
@@ -36,6 +36,7 @@
     <module>aws-ec2</module>
     <module>aws-sqs</module>
     <module>aws-cloudwatch</module>
+    <module>digitalocean2</module>
     <module>elastichosts-lon-p</module>
     <module>elastichosts-sat-p</module>
     <module>elastichosts-lon-b</module>


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

Posted by na...@apache.org.
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());
-   }
-}


[08/19] jclouds git commit: JCLOUDS-1023: Fix DigitalOcean getImage() method

Posted by na...@apache.org.
JCLOUDS-1023: Fix DigitalOcean getImage() method


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

Branch: refs/heads/master
Commit: 6254526296117616cc6c3aacc26167296b6c0277
Parents: 26210fe
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Oct 22 11:13:30 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Thu Oct 22 11:13:30 2015 +0200

----------------------------------------------------------------------
 .../digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/62545262/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 ec8dc11..f1f5919 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
@@ -179,7 +179,7 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
       // provided. If it can be parsed as a number, use the method to get by ID. Otherwise, get by slug.
       Integer numericId = Ints.tryParse(imageId);
       Image image = numericId == null ? api.imageApi().get(imageId) : api.imageApi().get(numericId);
-      return ImageInRegion.create(image, region);
+      return image == null ? null : ImageInRegion.create(image, region);
    }
 
    @Override


[18/19] jclouds git commit: Promote DigitalOcean v2

Posted by na...@apache.org.
Promote DigitalOcean v2


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

Branch: refs/heads/master
Commit: 886aa156b44d34520ac2b4d2723f7de39086f850
Parents: 52dc1a3 c188570
Author: Ignasi Barrera <na...@apache.org>
Authored: Wed Jan 20 23:57:03 2016 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Wed Jan 20 23:57:03 2016 +0100

----------------------------------------------------------------------
 providers/digitalocean2/pom.xml                 | 152 +++++++
 .../jclouds/digitalocean2/DigitalOcean2Api.java |  73 ++++
 .../digitalocean2/DigitalOcean2ApiMetadata.java | 111 +++++
 .../DigitalOcean2ProviderMetadata.java          |  78 ++++
 .../DigitalOcean2ComputeServiceAdapter.java     | 243 +++++++++++
 ...igitalOcean2ComputeServiceContextModule.java | 218 ++++++++++
 .../extensions/DigitalOcean2ImageExtension.java | 149 +++++++
 .../functions/DropletStatusToStatus.java        |  46 +++
 .../functions/DropletToNodeMetadata.java        | 165 ++++++++
 .../compute/functions/ImageInRegionToImage.java |  92 +++++
 .../compute/functions/RegionToLocation.java     |  59 +++
 .../compute/functions/SizeToHardware.java       |  58 +++
 ...plateOptionsToStatementWithoutPublicKey.java |  59 +++
 .../compute/internal/ImageInRegion.java         |  54 +++
 .../options/DigitalOcean2TemplateOptions.java   | 174 ++++++++
 .../strategy/CreateKeyPairsThenCreateNodes.java | 216 ++++++++++
 .../config/DigitalOcean2HttpApiModule.java      |  57 +++
 .../config/DigitalOcean2Properties.java         |  33 ++
 .../config/DigitalOcean2RateLimitModule.java    |  30 ++
 .../config/DigitalOceanParserModule.java        | 144 +++++++
 .../jclouds/digitalocean2/domain/Action.java    |  71 ++++
 .../jclouds/digitalocean2/domain/Backup.java    |  43 ++
 .../digitalocean2/domain/Distribution.java      |  69 ++++
 .../jclouds/digitalocean2/domain/Droplet.java   |  92 +++++
 .../digitalocean2/domain/DropletCreate.java     |  66 +++
 .../org/jclouds/digitalocean2/domain/Image.java |  48 +++
 .../jclouds/digitalocean2/domain/Kernel.java    |  35 ++
 .../org/jclouds/digitalocean2/domain/Key.java   |  39 ++
 .../jclouds/digitalocean2/domain/Networks.java  |  77 ++++
 .../digitalocean2/domain/OperatingSystem.java   |  60 +++
 .../jclouds/digitalocean2/domain/Region.java    |  39 ++
 .../org/jclouds/digitalocean2/domain/Size.java  |  46 +++
 .../jclouds/digitalocean2/domain/Snapshot.java  |  47 +++
 .../domain/internal/PaginatedCollection.java    | 111 +++++
 .../domain/options/CreateDropletOptions.java    | 179 +++++++++
 .../domain/options/ImageListOptions.java        |  74 ++++
 .../domain/options/ListOptions.java             |  60 +++
 ...DigitalOcean2RateLimitExceededException.java |  81 ++++
 .../digitalocean2/features/ActionApi.java       | 113 ++++++
 .../digitalocean2/features/DropletApi.java      | 350 ++++++++++++++++
 .../digitalocean2/features/ImageApi.java        | 131 ++++++
 .../jclouds/digitalocean2/features/KeyApi.java  | 164 ++++++++
 .../digitalocean2/features/RegionApi.java       | 107 +++++
 .../jclouds/digitalocean2/features/SizeApi.java | 100 +++++
 .../functions/BaseToPagedIterable.java          |  59 +++
 .../functions/LinkToImageListOptions.java       |  67 ++++
 .../functions/LinkToListOptions.java            |  61 +++
 .../handlers/DigitalOcean2ErrorHandler.java     |  72 ++++
 .../handlers/RateLimitRetryHandler.java         | 111 +++++
 .../org/jclouds/digitalocean2/ssh/DSAKeys.java  | 172 ++++++++
 .../jclouds/digitalocean2/ssh/ECDSAKeys.java    | 343 ++++++++++++++++
 .../DigitalOcean2ProviderMetadataTest.java      |  29 ++
 .../DigitalOcean2ComputeServiceLiveTest.java    |  66 +++
 .../DigitalOcean2TemplateBuilderLiveTest.java   |  55 +++
 .../compute/config/ActionDonePredicateTest.java |  74 ++++
 .../config/DropletInStatusPredicateTest.java    |  58 +++
 .../config/DropletTerminatedPredicateTest.java  |  57 +++
 .../DigitalOcean2ImageExtensionLiveTest.java    |  40 ++
 .../functions/DropletStatusToStatusTest.java    |  36 ++
 .../functions/DropletToNodeMetadataTest.java    | 237 +++++++++++
 .../functions/ImageInRegionToImageTest.java     |  98 +++++
 .../compute/functions/RegionToLocationTest.java |  56 +++
 .../compute/functions/SizeToHardwareTest.java   |  49 +++
 ...eOptionsToStatementWithoutPublicKeyTest.java |  75 ++++
 .../DigitalOcean2TemplateOptionsTest.java       |  52 +++
 .../domain/OperatingSystemTest.java             | 104 +++++
 .../exceptions/RateLimitExceptionMockTest.java  |  63 +++
 .../features/ActionApiLiveTest.java             |  70 ++++
 .../features/ActionApiMockTest.java             | 110 +++++
 .../features/DropletApiLiveTest.java            | 186 +++++++++
 .../features/DropletApiMockTest.java            | 401 +++++++++++++++++++
 .../features/ImageApiLiveTest.java              |  97 +++++
 .../features/ImageApiMockTest.java              | 150 +++++++
 .../digitalocean2/features/KeyApiLiveTest.java  |  99 +++++
 .../digitalocean2/features/KeyApiMockTest.java  | 203 ++++++++++
 .../features/RegionApiLiveTest.java             |  62 +++
 .../features/RegionApiMockTest.java             |  77 ++++
 .../digitalocean2/features/SizeApiLiveTest.java |  62 +++
 .../digitalocean2/features/SizeApiMockTest.java |  77 ++++
 .../functions/LinkToImageListOptionsTest.java   |  65 +++
 .../functions/LinkToListOptionsTest.java        |  58 +++
 .../handlers/RateLimitRetryHandlerTest.java     | 153 +++++++
 .../internal/BaseDigitalOcean2ApiLiveTest.java  | 140 +++++++
 .../internal/BaseDigitalOcean2ApiMockTest.java  | 142 +++++++
 .../jclouds/digitalocean2/ssh/DSAKeysTest.java  |  54 +++
 .../digitalocean2/ssh/ECDSAKeysTest.java        |  55 +++
 .../src/test/resources/action.json              |  33 ++
 .../src/test/resources/actions-first.json       | 168 ++++++++
 .../src/test/resources/actions-last.json        | 106 +++++
 .../src/test/resources/backups-first.json       |  26 ++
 .../src/test/resources/backups-last.json        |  26 ++
 .../src/test/resources/droplet-create-req.json  |  12 +
 .../src/test/resources/droplet-create-res.json  |  35 ++
 .../src/test/resources/droplet.json             | 105 +++++
 .../src/test/resources/droplets-first.json      | 115 ++++++
 .../src/test/resources/droplets-last.json       | 115 ++++++
 .../digitalocean2/src/test/resources/image.json |  24 ++
 .../src/test/resources/images-first.json        | 108 +++++
 .../src/test/resources/images-last.json         | 123 ++++++
 .../src/test/resources/kernels-first.json       |  38 ++
 .../src/test/resources/kernels-last.json        |  38 ++
 .../digitalocean2/src/test/resources/key.json   |   8 +
 .../src/test/resources/keys-first.json          |  43 ++
 .../src/test/resources/keys-last.json           |  25 ++
 .../src/test/resources/logback-test.xml         |  42 ++
 .../src/test/resources/power-cycle.json         |  33 ++
 .../src/test/resources/power-off.json           |  33 ++
 .../src/test/resources/power-on.json            |  33 ++
 .../src/test/resources/reboot.json              |  33 ++
 .../src/test/resources/regions-first.json       | 111 +++++
 .../src/test/resources/regions-last.json        | 128 ++++++
 .../src/test/resources/shutdown.json            |  33 ++
 .../src/test/resources/sizes-first.json         | 123 ++++++
 .../src/test/resources/sizes-last.json          |  98 +++++
 .../src/test/resources/snapshot.json            |  33 ++
 .../src/test/resources/snapshots-first.json     |  27 ++
 .../src/test/resources/snapshots-last.json      |  27 ++
 .../src/test/resources/ssh-dsa.pub              |   1 +
 .../src/test/resources/ssh-ecdsa.pub            |   1 +
 119 files changed, 10612 insertions(+)
----------------------------------------------------------------------



[16/19] jclouds git commit: Remove unnecessary code to register the compute extensions

Posted by na...@apache.org.
Remove unnecessary code to register the compute extensions


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

Branch: refs/heads/master
Commit: 62f1c0bc9a1a7c4d4e5a4a4301505e4e6e1cc36e
Parents: 1ac6fa8
Author: Ignasi Barrera <na...@apache.org>
Authored: Fri Nov 27 09:40:45 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Jan 19 11:17:21 2016 +0100

----------------------------------------------------------------------
 .../config/DigitalOcean2ComputeServiceContextModule.java      | 7 -------
 1 file changed, 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/62f1c0bc/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 7159634..03caf85 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
@@ -57,9 +57,7 @@ import org.jclouds.domain.Location;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
-import com.google.inject.Injector;
 import com.google.inject.Provides;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
@@ -99,11 +97,6 @@ public class DigitalOcean2ComputeServiceContextModule extends
       }).to(DigitalOcean2ImageExtension.class);
    }
 
-   @Override
-   protected Optional<ImageExtension> provideImageExtension(Injector i) {
-      return Optional.of(i.getInstance(ImageExtension.class));
-   }
-
    @Provides
    @Named(TIMEOUT_NODE_RUNNING)
    protected Predicate<Integer> provideDropletRunningPredicate(final DigitalOcean2Api api, Timeouts timeouts,


[12/19] jclouds git commit: JCLOUDS-1027: When waiting to a droplet to be created we check the proper dropletId

Posted by na...@apache.org.
JCLOUDS-1027: When waiting to a droplet to be created we check the proper dropletId


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

Branch: refs/heads/master
Commit: 83ff38ebee5e410038aaae19b66ad5170757a6ac
Parents: 7e866ad
Author: Ruben Rubio Rey <ru...@manageacloud.com>
Authored: Tue Oct 27 11:45:30 2015 +1100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Oct 27 23:35:36 2015 +0100

----------------------------------------------------------------------
 .../DigitalOcean2ComputeServiceAdapter.java        | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/83ff38eb/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 43b1585..2d76176 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
@@ -21,7 +21,6 @@ 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;
@@ -118,7 +117,7 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
 
       // We have to actively wait until the droplet has been provisioned until
       // we can build the entire Droplet object we want to return
-      nodeRunningPredicate.apply(getOnlyElement(dropletCreated.links().actions()).id());
+      nodeRunningPredicate.apply(dropletCreated.droplet().id());
       Droplet droplet = api.dropletApi().get(dropletCreated.droplet().id());
 
       LoginCredentials defaultCredentials = LoginCredentials.builder().user("root")
@@ -219,25 +218,27 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
    public void rebootNode(String id) {
       // We have to wait here, as the api does not properly populate the state
       // but fails if there is a pending event
-      Action action = api.dropletApi().reboot(Integer.parseInt(id));
-      checkState(nodeRunningPredicate.apply(action.id()), "node did not restart in the configured timeout");
+      int dropletId = Integer.parseInt(id);
+      api.dropletApi().reboot(dropletId);
+      checkState(nodeRunningPredicate.apply(dropletId), "node did not restart in the configured timeout");
    }
 
    @Override
    public void resumeNode(String id) {
       // We have to wait here, as the api does not properly populate the state
       // but fails if there is a pending event
-      Action action = api.dropletApi().powerOn(Integer.parseInt(id));
-      checkState(nodeRunningPredicate.apply(action.id()), "node did not started in the configured timeout");
+      int dropletId = Integer.parseInt(id);
+      api.dropletApi().powerOn(dropletId);
+      checkState(nodeRunningPredicate.apply(dropletId), "node did not started in the configured timeout");
    }
 
    @Override
    public void suspendNode(String id) {
-      int dropletId = Integer.parseInt(id);
       // We have to wait here, as the api does not properly populate the state
       // but fails if there is a pending event
+      int dropletId = Integer.parseInt(id);
       Action action = api.dropletApi().powerOff(dropletId);
-      checkState(nodeStoppedPredicate.apply(action.id()), "node did not stop in the configured timeout");
+      checkState(nodeStoppedPredicate.apply(dropletId), "node did not stop in the configured timeout");
    }
 
 }


[04/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Posted by na...@apache.org.
http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/DropletApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/DropletApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/DropletApi.java
new file mode 100644
index 0000000..c2d9b71
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/DropletApi.java
@@ -0,0 +1,350 @@
+/*
+ * 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.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Backup;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Kernel;
+import org.jclouds.digitalocean2.domain.Snapshot;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.MapBinder;
+import org.jclouds.rest.annotations.Payload;
+import org.jclouds.rest.annotations.PayloadParam;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.annotations.Transform;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Droplets via their REST API.
+ *
+ * @see <a href="https://developers.digitalocean.com/v2/#droplets"/>
+ * @see DropletApi
+ */
+@Path("/droplets")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface DropletApi extends Closeable {
+
+   @Named("droplet:list")
+   @GET
+   @ResponseParser(ParseDroplets.class)
+   @Transform(ParseDroplets.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Droplet> list();
+
+   @Named("droplet:list")
+   @GET
+   @ResponseParser(ParseDroplets.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Droplet> list(ListOptions options);
+
+   static final class ParseDroplets extends ParseJson<ParseDroplets.Droplets> {
+      @Inject ParseDroplets(Json json) {
+         super(json, TypeLiteral.get(Droplets.class));
+      }
+
+      private static class Droplets extends PaginatedCollection<Droplet> {
+         @ConstructorProperties({ "droplets", "meta", "links" })
+         public Droplets(List<Droplet> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Droplet, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Droplet> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.dropletApi().list(options);
+         }
+      }
+   }
+
+   @Named("droplet:listkernels")
+   @GET
+   @Path("/{id}/kernels")
+   @ResponseParser(ParseKernels.class)
+   @Transform(ParseKernels.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Kernel> listKernels(@PathParam("id") int id);
+
+   @Named("droplet:listkernels")
+   @GET
+   @Path("/{id}/kernels")
+   @ResponseParser(ParseKernels.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Kernel> listKernels(@PathParam("id") int id, ListOptions options);
+
+   static final class ParseKernels extends ParseJson<ParseKernels.Kernels> {
+      @Inject ParseKernels(Json json) {
+         super(json, TypeLiteral.get(Kernels.class));
+      }
+
+      private static class Kernels extends PaginatedCollection<Kernel> {
+         @ConstructorProperties({ "kernels", "meta", "links" })
+         public Kernels(List<Kernel> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Kernel, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Kernel> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.dropletApi().listKernels((Integer) arg0.get(), options);
+         }
+      }
+   }
+
+   @Named("droplet:listsnapshots")
+   @GET
+   @Path("/{id}/snapshots")
+   @ResponseParser(ParseSnapshots.class)
+   @Transform(ParseSnapshots.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Snapshot> listSnapshots(@PathParam("id") int id);
+
+   @Named("droplet:listsnapshots")
+   @GET
+   @Path("/{id}/snapshots")
+   @ResponseParser(ParseSnapshots.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Snapshot> listSnapshots(@PathParam("id") int id, ListOptions options);
+
+   static final class ParseSnapshots extends ParseJson<ParseSnapshots.Snapshots> {
+      @Inject ParseSnapshots(Json json) {
+         super(json, TypeLiteral.get(Snapshots.class));
+      }
+
+      private static class Snapshots extends PaginatedCollection<Snapshot> {
+         @ConstructorProperties({ "snapshots", "meta", "links" })
+         public Snapshots(List<Snapshot> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Snapshot, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Snapshot> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.dropletApi().listSnapshots((Integer) arg0.get(), options);
+         }
+      }
+   }
+
+   @Named("droplet:listbackups")
+   @GET
+   @Path("/{id}/backups")
+   @ResponseParser(ParseBackups.class)
+   @Transform(ParseBackups.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Backup> listBackups(@PathParam("id") int id);
+
+   @Named("droplet:listbackups")
+   @GET
+   @Path("/{id}/backups")
+   @ResponseParser(ParseBackups.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Backup> listBackups(@PathParam("id") int id, ListOptions options);
+
+   static final class ParseBackups extends ParseJson<ParseBackups.Backups> {
+      @Inject ParseBackups(Json json) {
+         super(json, TypeLiteral.get(Backups.class));
+      }
+
+      private static class Backups extends PaginatedCollection<Backup> {
+         @ConstructorProperties({ "backups", "meta", "links" })
+         public Backups(List<Backup> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Backup, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Backup> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.dropletApi().listBackups((Integer) arg0.get(), options);
+         }
+      }
+   }
+
+   @Named("droplet:actions")
+   @GET
+   @Path("/{id}/actions")
+   @ResponseParser(ParseDropletActions.class)
+   @Transform(ParseDropletActions.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Action> listActions(@PathParam("id") int id);
+
+   @Named("droplet:actions")
+   @GET
+   @Path("/{id}/actions")
+   @ResponseParser(ParseDropletActions.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Action> listActions(@PathParam("id") int id, ListOptions options);
+
+   static final class ParseDropletActions extends ParseJson<ParseDropletActions.DropletActions> {
+      @Inject ParseDropletActions(Json json) {
+         super(json, TypeLiteral.get(DropletActions.class));
+      }
+
+      private static class DropletActions extends PaginatedCollection<Action> {
+         @ConstructorProperties({ "actions", "meta", "links" })
+         public DropletActions(List<Action> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Action, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Action> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.dropletApi().listActions((Integer) arg0.get(), options);
+         }
+      }
+   }
+
+   @Named("droplet:create")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @MapBinder(BindToJsonPayload.class)
+   DropletCreate create(@PayloadParam("name") String name, @PayloadParam("region") String region,
+         @PayloadParam("size") String size, @PayloadParam("image") String image);
+
+   @Named("droplet:create")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @MapBinder(CreateDropletOptions.class)
+   DropletCreate create(@PayloadParam("name") String name, @PayloadParam("region") String region,
+         @PayloadParam("size") String size, @PayloadParam("image") String image, CreateDropletOptions options);
+
+   @Named("droplet:get")
+   @GET
+   @SelectJson("droplet")
+   @Path("/{id}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Droplet get(@PathParam("id") int id);
+
+   @Named("droplet:delete")
+   @DELETE
+   @Path("/{id}")
+   @Fallback(VoidOnNotFoundOr404.class)
+   void delete(@PathParam("id") int id);
+
+   @Named("droplet:reboot")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("{\"type\":\"reboot\"}")
+   Action reboot(@PathParam("id") int id);
+
+   @Named("droplet:powercycle")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("{\"type\":\"power_cycle\"}")
+   Action powerCycle(@PathParam("id") int id);
+
+   @Named("droplet:shutdown")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("{\"type\":\"shutdown\"}")
+   Action shutdown(@PathParam("id") int id);
+
+   @Named("droplet:poweroff")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("{\"type\":\"power_off\"}")
+   Action powerOff(@PathParam("id") int id);
+
+   @Named("droplet:poweron")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("{\"type\":\"power_on\"}")
+   Action powerOn(@PathParam("id") int id);
+
+   @Named("droplet:snapshot")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("action")
+   @Path("/{id}/actions")
+   @Payload("%7B\"type\":\"snapshot\",\"name\":\"{name}\"%7D")
+   Action snapshot(@PathParam("id") int id, @PayloadParam("name") String name);
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ImageApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ImageApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ImageApi.java
new file mode 100644
index 0000000..9803ac3
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ImageApi.java
@@ -0,0 +1,131 @@
+/*
+ * 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.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.ImageListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.annotations.Transform;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Images via the REST API.
+ *
+ * @see <a href="https://developers.digitalocean.com/v2/#images"/>
+ * @see ImageApi
+ */
+@Path("/images")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface ImageApi extends Closeable {
+
+   @Named("image:list")
+   @GET
+   @ResponseParser(ParseImages.class)
+   @Transform(ParseImages.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Image> list();
+   
+   @Named("image:list")
+   @GET
+   @ResponseParser(ParseImages.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Image> list(ImageListOptions options);
+   
+   static final class ParseImages extends ParseJson<ParseImages.Images> {
+      @Inject ParseImages(Json json) {
+         super(json, TypeLiteral.get(Images.class));
+      }
+
+      private static class Images extends PaginatedCollection<Image> {
+         @ConstructorProperties({ "images", "meta", "links" })
+         public Images(List<Image> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Image, ImageListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ImageListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Image> fetchPageUsingOptions(ImageListOptions options, Optional<Object> arg0) {
+            return api.imageApi().list(options);
+         }
+      }
+   }
+
+   @Named("image:get")
+   @GET
+   @SelectJson("image")
+   @Path("/{id}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Image get(@PathParam("id") int id);
+
+   @Named("image:get")
+   @GET
+   @SelectJson("image")
+   @Path("/{slug}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Image get(@PathParam("slug") String slug);
+
+   @Named("image:delete")
+   @DELETE
+   @Path("/{id}")
+   @Fallback(VoidOnNotFoundOr404.class)
+   void delete(@PathParam("id") int id);
+
+   //TODO: Add delete and create
+
+}
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/KeyApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/KeyApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/KeyApi.java
new file mode 100644
index 0000000..4889c5d
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/KeyApi.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.MapBinder;
+import org.jclouds.rest.annotations.PayloadParam;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.annotations.Transform;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Keys via the REST API.
+ *
+ * @see <a href="https://developers.digitalocean.com/v2/#keys"/>
+ * @see KeyApi
+ */
+@Path("/account/keys")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface KeyApi extends Closeable {
+
+   @Named("key:list")
+   @GET
+   @ResponseParser(ParseKeys.class)
+   @Transform(ParseKeys.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Key> list();
+
+   @Named("key:list")
+   @GET
+   @ResponseParser(ParseKeys.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Key> list(ListOptions options);
+   
+   static final class ParseKeys extends ParseJson<ParseKeys.Keys> {
+      @Inject ParseKeys(Json json) {
+         super(json, TypeLiteral.get(Keys.class));
+      }
+
+      private static class Keys extends PaginatedCollection<Key> {
+         @ConstructorProperties({ "ssh_keys", "meta", "links" })
+         public Keys(List<Key> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Key, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Key> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.keyApi().list(options);
+         }
+      }
+   }
+
+   @Named("key:create")
+   @POST
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("ssh_key")
+   @MapBinder(BindToJsonPayload.class)
+   Key create(@PayloadParam("name") String name, @PayloadParam("public_key") String key);
+
+   @Named("key:get")
+   @GET
+   @SelectJson("ssh_key")
+   @Path("/{id}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Key get(@PathParam("id") int id);
+
+   @Named("key:get")
+   @GET
+   @SelectJson("ssh_key")
+   @Path("/{fingerprint}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Key get(@PathParam("fingerprint") String fingerprint);
+
+   @Named("key:update")
+   @PUT
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("ssh_key")
+   @Path("/{id}")
+   @MapBinder(BindToJsonPayload.class)
+   Key update(@PathParam("id") int id, @PayloadParam("name") String name);
+
+   @Named("key:update")
+   @PUT
+   @Produces(MediaType.APPLICATION_JSON)
+   @SelectJson("ssh_key")
+   @Path("/{fingerprint}")
+   @MapBinder(BindToJsonPayload.class)
+   Key update(@PathParam("fingerprint") String fingerprint, @PayloadParam("name") String name);
+
+   @Named("key:delete")
+   @DELETE
+   @Path("/{id}")
+   @Fallback(VoidOnNotFoundOr404.class)
+   void delete(@PathParam("id") int id);
+
+   @Named("key:delete")
+   @DELETE
+   @Path("/{fingerprint}")
+   @Fallback(VoidOnNotFoundOr404.class)
+   void delete(@PathParam("fingerprint") String fingerprint);
+
+}
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/RegionApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/RegionApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/RegionApi.java
new file mode 100644
index 0000000..9fb7128
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/RegionApi.java
@@ -0,0 +1,107 @@
+/*
+ * 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.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.Transform;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Regions via the REST API.
+ */
+@Path("/regions")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface RegionApi extends Closeable {
+
+   /**
+    * Get the list of all regions.
+    * 
+    * @return The (paginated) list of all regions.
+    */
+   @Named("region:list")
+   @GET
+   @ResponseParser(ParseRegions.class)
+   @Transform(ParseRegions.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Region> list();
+
+   /**
+    * Get a single page of the region list.
+    * 
+    * @param options The options to configure the page to get and the size of the page.
+    * @return The page with the requested regions.
+    */
+   @Named("region:list")
+   @GET
+   @ResponseParser(ParseRegions.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Region> list(ListOptions options);
+
+   static final class ParseRegions extends ParseJson<ParseRegions.Regions> {
+      @Inject ParseRegions(Json json) {
+         super(json, TypeLiteral.get(Regions.class));
+      }
+
+      private static class Regions extends PaginatedCollection<Region> {
+         @ConstructorProperties({ "regions", "meta", "links" })
+         public Regions(List<Region> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      static class ToPagedIterable extends BaseToPagedIterable<Region, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Region> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.regionApi().list(options);
+         }
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/SizeApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/SizeApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/SizeApi.java
new file mode 100644
index 0000000..9165809
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/SizeApi.java
@@ -0,0 +1,100 @@
+/*
+ * 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.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.Transform;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Sizes via the REST API.
+ *
+ * @see <a href="https://developers.digitalocean.com/v2/#sizes"/>
+ * @see org.jclouds.digitalocean2.features.SizeApi
+ */
+@Path("/sizes")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface SizeApi extends Closeable {
+
+   @Named("size:list")
+   @GET
+   @ResponseParser(ParseSizes.class)
+   @Transform(ParseSizes.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Size> list();
+
+   @Named("size:list")
+   @GET
+   @ResponseParser(ParseSizes.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Size> list(ListOptions options);
+
+   static final class ParseSizes extends ParseJson<ParseSizes.Sizes> {
+      @Inject ParseSizes(Json json) {
+         super(json, TypeLiteral.get(Sizes.class));
+      }
+
+      private static class Sizes extends PaginatedCollection<Size> {
+         @ConstructorProperties({ "sizes", "meta", "links" })
+         public Sizes(List<Size> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Size, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Size> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.sizeApi().list(options);
+         }
+      }
+   }
+}
+

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/BaseToPagedIterable.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/BaseToPagedIterable.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/BaseToPagedIterable.java
new file mode 100644
index 0000000..ebedef5
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/BaseToPagedIterable.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.functions;
+
+import java.net.URI;
+
+import javax.inject.Inject;
+
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.internal.Arg0ToPagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+
+/**
+ * Base class to implement the functions that build the
+ * <code>PagedIterable</code>. Subclasses just need to override the
+ * {@link #fetchPageUsingOptions(ListOptions, Optional)} to invoke the right API
+ * method with the given options parameter to get the next page.
+ */
+public abstract class BaseToPagedIterable<T, O extends ListOptions> extends
+      Arg0ToPagedIterable<T, BaseToPagedIterable<T, O>> {
+   private final Function<URI, O> linkToOptions;
+   protected final DigitalOcean2Api api;
+
+   @Inject protected BaseToPagedIterable(DigitalOcean2Api api, Function<URI, O> linkToOptions) {
+      this.api = api;
+      this.linkToOptions = linkToOptions;
+   }
+
+   protected abstract IterableWithMarker<T> fetchPageUsingOptions(O options, Optional<Object> arg0);
+
+   @Override protected Function<Object, IterableWithMarker<T>> markerToNextForArg0(final Optional<Object> arg0) {
+      return new Function<Object, IterableWithMarker<T>>() {
+         @Override
+         public IterableWithMarker<T> apply(Object input) {
+            O nextOptions = linkToOptions.apply(URI.class.cast(input));
+            return fetchPageUsingOptions(nextOptions, arg0);
+         }
+      };
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToImageListOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToImageListOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToImageListOptions.java
new file mode 100644
index 0000000..85701e5
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToImageListOptions.java
@@ -0,0 +1,67 @@
+/*
+ * 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.functions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.PRIVATE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.TYPE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PER_PAGE_PARAM;
+import static org.jclouds.digitalocean2.functions.LinkToListOptions.getFirstOrNull;
+import static org.jclouds.http.utils.Queries.queryParser;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.domain.options.ImageListOptions;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Multimap;
+
+/**
+ * Transforms a link returned by the API into a {@link ListOptions} that can be
+ * used to perform a request to get another page of a paginated list.
+ */
+public class LinkToImageListOptions implements Function<URI, ImageListOptions> {
+
+   @Override public ImageListOptions apply(URI input) {
+      checkNotNull(input, "input cannot be null");
+
+      Multimap<String, String> queryParams = queryParser().apply(input.getQuery());
+      String nextPage = getFirstOrNull(PAGE_PARAM, queryParams);
+      String nextPerPage = getFirstOrNull(PER_PAGE_PARAM, queryParams);
+      String nextType = getFirstOrNull(TYPE_PARAM, queryParams);
+      String nextPrivate = getFirstOrNull(PRIVATE_PARAM, queryParams);
+
+      ImageListOptions options = new ImageListOptions();
+      if (nextPage != null) {
+         options.page(Integer.parseInt(nextPage));
+      }
+      if (nextPerPage != null) {
+         options.perPage(Integer.parseInt(nextPerPage));
+      }
+      if (nextType != null) {
+         options.type(nextType);
+      }
+      if (nextPrivate != null) {
+         options.privateImages(Boolean.parseBoolean(nextPrivate));
+      }
+
+      return options;
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToListOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToListOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToListOptions.java
new file mode 100644
index 0000000..1dc22db
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/functions/LinkToListOptions.java
@@ -0,0 +1,61 @@
+/*
+ * 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.functions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.collect.Iterables.getFirst;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PER_PAGE_PARAM;
+import static org.jclouds.http.utils.Queries.queryParser;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Multimap;
+
+/**
+ * Transforms a link returned by the API into a {@link ListOptions} that can be
+ * used to perform a request to get another page of a paginated list.
+ */
+public class LinkToListOptions implements Function<URI, ListOptions> {
+
+   @Override public ListOptions apply(URI input) {
+      checkNotNull(input, "input cannot be null");
+
+      Multimap<String, String> queryParams = queryParser().apply(input.getQuery());
+      String nextPage = getFirstOrNull(PAGE_PARAM, queryParams);
+      String nextPerPage = getFirstOrNull(PER_PAGE_PARAM, queryParams);
+
+      ListOptions options = new ListOptions();
+      if (nextPage != null) {
+         options.page(Integer.parseInt(nextPage));
+      }
+      if (nextPerPage != null) {
+         options.perPage(Integer.parseInt(nextPerPage));
+      }
+
+      return options;
+   }
+
+   public static String getFirstOrNull(String key, Multimap<String, String> params) {
+      return params.containsKey(key) ? emptyToNull(getFirst(params.get(key), null)) : null;
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
new file mode 100644
index 0000000..5eda6eb
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
@@ -0,0 +1,67 @@
+/*
+ * 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.handlers;
+
+import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
+
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpResponseException;
+import org.jclouds.rest.AuthorizationException;
+import org.jclouds.rest.InsufficientResourcesException;
+import org.jclouds.rest.ResourceNotFoundException;
+
+/**
+ * This will parse and set an appropriate exception on the command object.
+ */
+@Singleton
+public class DigitalOcean2ErrorHandler implements HttpErrorHandler {
+   public void handleError(HttpCommand command, HttpResponse response) {
+      // it is important to always read fully and close streams
+      byte[] data = closeClientButKeepContentStream(response);
+      String message = data != null ? new String(data) : null;
+
+      Exception exception = message != null ? new HttpResponseException(command, response, message)
+              : new HttpResponseException(command, response);
+      message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(),
+              response.getStatusLine());
+      switch (response.getStatusCode()) {
+         case 400:
+            break;
+         case 401:
+         case 403:
+            if (message.contains("droplet limit")) {
+               exception = new InsufficientResourcesException(message, exception);
+            } else {
+               exception = new AuthorizationException(message, exception);
+            }
+            break;
+         case 404:
+            if (!command.getCurrentRequest().getMethod().equals("DELETE")) {
+               exception = new ResourceNotFoundException(message, exception);
+            }
+            break;
+         case 409:
+            exception = new IllegalStateException(message, exception);
+            break;
+      }
+      command.setException(exception);
+   }
+}

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

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/ssh/ECDSAKeys.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/ssh/ECDSAKeys.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/ssh/ECDSAKeys.java
new file mode 100644
index 0000000..f17098a
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/ssh/ECDSAKeys.java
@@ -0,0 +1,343 @@
+/*
+ * 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.ssh;
+
+import static com.google.common.base.Joiner.on;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Splitter.fixedLength;
+import static com.google.common.base.Throwables.propagate;
+import static com.google.common.collect.Iterables.get;
+import static com.google.common.collect.Iterables.size;
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.io.BaseEncoding.base64;
+import static org.jclouds.util.Strings2.toStringAndClose;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Map;
+import java.util.TreeMap;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Splitter;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteSource;
+import com.google.common.io.ByteStreams;
+
+/**
+ * Utility methods to work with ECDSA Elliptic Curve DSA keys.
+ * <p>
+ * Methods in this class should be moved to the {@link org.jclouds.ssh.SshKeys} class.
+ * 
+ * @see org.jclouds.ssh.SshKeys
+ */
+public class ECDSAKeys {
+   public static final String ECDSA_SHA2_PREFIX = "ecdsa-sha2-";
+
+   private static final String NISTP256 = "nistp256";
+   private static final String NISTP384 = "nistp384";
+   private static final String NISTP521 = "nistp521";
+
+   private static final Map<String, ECParameterSpec> CURVES = new TreeMap<String, ECParameterSpec>();
+   static {
+      CURVES.put(NISTP256, EllipticCurves.nistp256);
+      CURVES.put(NISTP384, EllipticCurves.nistp384);
+      CURVES.put(NISTP521, EllipticCurves.nistp521);
+   }
+
+   private static final Map<Integer, String> CURVE_SIZES = new TreeMap<Integer, String>();
+   static {
+      CURVE_SIZES.put(256, NISTP256);
+      CURVE_SIZES.put(384, NISTP384);
+      CURVE_SIZES.put(521, NISTP521);
+   }
+
+   public static String encodeAsOpenSSH(ECPublicKey key) {
+
+      String curveName = null;
+      try {
+         curveName = getCurveName(key.getParams());
+      } catch (IOException e) {
+         propagate(e);
+      }
+
+      String keyFormat = ECDSA_SHA2_PREFIX + curveName;
+
+      byte[] keyBlob = keyBlob(key);
+      return keyFormat + " " + base64().encode(keyBlob);
+   }
+
+   /**
+    * Executes {@link org.jclouds.crypto.Pems#publicKeySpecFromOpenSSH(com.google.common.io.InputSupplier)} on the
+    * string which was OpenSSH Base64 Encoded {@code id_rsa.pub}
+    * 
+    * @param idRsaPub formatted {@code ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAB...}
+    * @see org.jclouds.crypto.Pems#publicKeySpecFromOpenSSH(com.google.common.io.InputSupplier)
+    */
+   public static ECPublicKeySpec publicKeySpecFromOpenSSH(String ecDsaPub) {
+      try {
+         return publicKeySpecFromOpenSSH(ByteSource.wrap(ecDsaPub.getBytes(Charsets.UTF_8)));
+      } catch (IOException e) {
+         throw propagate(e);
+      }
+   }
+
+   /**
+    * Returns {@link java.security.spec.DSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub}
+    *
+    * @param supplier the input stream factory, formatted {@code ssh-dss AAAAB3NzaC1yc2EAAAADAQABAAAB...}
+    *
+    * @return the {@link java.security.spec.DSAPublicKeySpec} which was OpenSSH Base64 Encoded {@code id_rsa.pub}
+    * @throws java.io.IOException if an I/O error occurs
+    */
+   public static ECPublicKeySpec publicKeySpecFromOpenSSH(ByteSource supplier) throws IOException {
+      InputStream stream = supplier.openStream();
+      Iterable<String> parts = Splitter.on(' ').split(toStringAndClose(stream).trim());
+      String signatureFormat = get(parts, 0);
+      checkArgument(size(parts) >= 2 && signatureFormat.startsWith(ECDSA_SHA2_PREFIX), "bad format, should be: ecdsa-sha2-xxx AAAAB3...");
+
+      String curveName = signatureFormat.substring(ECDSA_SHA2_PREFIX.length());
+      if (!CURVES.containsKey(curveName)) {
+         throw new IOException("Unsupported curve: " + curveName);
+      }
+      ECParameterSpec spec = CURVES.get(curveName);
+      stream = new ByteArrayInputStream(base64().decode(get(parts, 1)));
+      @SuppressWarnings("unused")
+      String keyType = new String(readLengthFirst(stream));
+      String curveMarker = new String(readLengthFirst(stream));
+      checkArgument(curveName.equals(curveMarker), "looking for marker %s but got %s", curveName, curveMarker);
+
+      ECPoint ecPoint = decodeECPoint(readLengthFirst(stream), spec.getCurve());
+
+      return new ECPublicKeySpec(ecPoint, spec);
+   }
+
+   /**
+    * @param publicKeyOpenSSH RSA public key in OpenSSH format
+    * @return fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
+    */
+   public static String fingerprintPublicKey(String publicKeyOpenSSH) throws IOException {
+      ECPublicKeySpec publicKeySpec = publicKeySpecFromOpenSSH(publicKeyOpenSSH);
+      String fingerprint = null;
+      try {
+         ECPublicKey pk = (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
+         fingerprint = fingerprint(pk);
+      } catch (InvalidKeySpecException e) {
+         e.printStackTrace();
+      } catch (NoSuchAlgorithmException e) {
+         e.printStackTrace();
+      }
+      return fingerprint;
+   }
+
+   /**
+    * Create a fingerprint per the following <a href="http://tools.ietf.org/html/draft-friedl-secsh-fingerprint-00"
+    * >spec</a>
+    * 
+    * @return hex fingerprint ex. {@code 2b:a9:62:95:5b:8b:1d:61:e0:92:f7:03:10:e9:db:d9}
+    */
+   public static String fingerprint(ECPublicKey publicKey) {
+      byte[] keyBlob = keyBlob(publicKey);
+      return hexColonDelimited(Hashing.md5().hashBytes(keyBlob));
+   }
+
+   /**
+    * @see org.jclouds.ssh.SshKeys
+    */
+   private static String hexColonDelimited(HashCode hc) {
+      return on(':').join(fixedLength(2).split(base16().lowerCase().encode(hc.asBytes())));
+   }
+
+   private static byte[] keyBlob(ECPublicKey key) {
+      try {
+         String curveName = getCurveName(key.getParams());
+         ByteArrayOutputStream out = new ByteArrayOutputStream();
+         writeLengthFirst((ECDSA_SHA2_PREFIX + curveName).getBytes(), out);
+         writeLengthFirst(curveName.getBytes(), out);
+         writeLengthFirst(encodeECPoint(key.getW(), key.getParams().getCurve()), out);
+         return out.toByteArray();
+      } catch (IOException e) {
+         throw propagate(e);
+      }
+   }
+
+   /**
+    * @see org.jclouds.ssh.SshKeys
+    */
+   // http://www.ietf.org/rfc/rfc4253.txt
+   private static byte[] readLengthFirst(InputStream in) throws IOException {
+      int byte1 = in.read();
+      int byte2 = in.read();
+      int byte3 = in.read();
+      int byte4 = in.read();
+      int length = (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0);
+      byte[] val = new byte[length];
+      ByteStreams.readFully(in, val);
+      return val;
+   }
+
+   /**
+    * @see org.jclouds.ssh.SshKeys
+    */
+   // http://www.ietf.org/rfc/rfc4253.txt
+   private static void writeLengthFirst(byte[] array, ByteArrayOutputStream out) throws IOException {
+      out.write(array.length >>> 24 & 0xFF);
+      out.write(array.length >>> 16 & 0xFF);
+      out.write(array.length >>> 8 & 0xFF);
+      out.write(array.length >>> 0 & 0xFF);
+      if (array.length == 1 && array[0] == (byte) 0x00) {
+         out.write(new byte[0]);
+      } else {
+         out.write(array);
+      }
+   }
+
+   private static String getCurveName(ECParameterSpec params) throws IOException {
+      int fieldSize = getCurveSize(params);
+      String curveName = CURVE_SIZES.get(fieldSize);
+      if (curveName == null) {
+         throw new IOException("Unsupported curve field size: " + fieldSize);
+      }
+      return curveName;
+   }
+
+   private static int getCurveSize(ECParameterSpec params) {
+      return params.getCurve().getField().getFieldSize();
+   }
+
+   /**
+    * Encode EllipticCurvePoint to an OctetString
+    */
+   public static byte[] encodeECPoint(ECPoint group, EllipticCurve curve)
+   {
+      // M has len 2 ceil(log_2(q)/8) + 1 ?
+      int elementSize = (curve.getField().getFieldSize() + 7) / 8;
+      byte[] M = new byte[2 * elementSize + 1];
+
+      // Uncompressed format
+      M[0] = 0x04;
+
+      {
+         byte[] affineX = removeLeadingZeroes(group.getAffineX().toByteArray());
+         System.arraycopy(affineX, 0, M, 1 + elementSize - affineX.length, affineX.length);
+      }
+
+      {
+         byte[] affineY = removeLeadingZeroes(group.getAffineY().toByteArray());
+         System.arraycopy(affineY, 0, M, 1 + elementSize + elementSize - affineY.length,
+               affineY.length);
+      }
+
+      return M;
+   }
+
+   private static byte[] removeLeadingZeroes(byte[] input) {
+      if (input[0] != 0x00) {
+         return input;
+      }
+
+      int pos = 1;
+      while (pos < input.length - 1 && input[pos] == 0x00) {
+         pos++;
+      }
+
+      byte[] output = new byte[input.length - pos];
+      System.arraycopy(input, pos, output, 0, output.length);
+      return output;
+   }
+
+   /**
+    * Decode an OctetString to EllipticCurvePoint according to SECG 2.3.4
+    */
+   public static ECPoint decodeECPoint(byte[] M, EllipticCurve curve) {
+      if (M.length == 0) {
+         return null;
+      }
+
+      // M has len 2 ceil(log_2(q)/8) + 1 ?
+      int elementSize = (curve.getField().getFieldSize() + 7) / 8;
+      if (M.length != 2 * elementSize + 1) {
+         return null;
+      }
+
+      // step 3.2
+      if (M[0] != 0x04) {
+         return null;
+      }
+
+      // Step 3.3
+      byte[] xp = new byte[elementSize];
+      System.arraycopy(M, 1, xp, 0, elementSize);
+
+      // Step 3.4
+      byte[] yp = new byte[elementSize];
+      System.arraycopy(M, 1 + elementSize, yp, 0, elementSize);
+
+      ECPoint P = new ECPoint(new BigInteger(1, xp), new BigInteger(1, yp));
+
+      // TODO check point 3.5
+
+      // Step 3.6
+      return P;
+   }
+
+   public static class EllipticCurves {
+      public static ECParameterSpec nistp256 = new ECParameterSpec(
+            new EllipticCurve(
+                  new ECFieldFp(new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16)),
+                  new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16),
+                  new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16)),
+            new ECPoint(new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16),
+                  new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16)),
+            new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16),
+            1);
+
+      public static ECParameterSpec nistp384 = new ECParameterSpec(
+            new EllipticCurve(
+                  new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16)),
+                  new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16),
+                  new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16)),
+            new ECPoint(new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16),
+                  new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16)),
+            new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16),
+            1);
+
+      public static ECParameterSpec nistp521 = new ECParameterSpec(
+            new EllipticCurve(
+                  new ECFieldFp(new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)),
+                  new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC", 16),
+                  new BigInteger("0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00", 16)
+            ),
+            new ECPoint(new BigInteger("00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66", 16),
+                  new BigInteger("011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650", 16)
+            ),
+            new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409", 16),
+            1);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..0be234c
--- /dev/null
+++ b/providers/digitalocean2/src/main/resources/META-INF/services/org.jclouds.apis.ApiMetadata
@@ -0,0 +1,18 @@
+#
+# 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/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadataTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadataTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadataTest.java
new file mode 100644
index 0000000..7756813
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadataTest.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import org.jclouds.providers.internal.BaseProviderMetadataTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "DigitalOcean2ProviderMetadataTest")
+public class DigitalOcean2ProviderMetadataTest extends BaseProviderMetadataTest {
+
+   public DigitalOcean2ProviderMetadataTest() {
+      super(new DigitalOcean2ProviderMetadata(), new DigitalOcean2ApiMetadata());
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
new file mode 100644
index 0000000..b1dcc1b
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.compute;
+
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.internal.BaseComputeServiceLiveTest;
+import org.jclouds.sshj.config.SshjSshClientModule;
+import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Module;
+
+/**
+ * Live tests for the {@link org.jclouds.compute.ComputeService} integration.
+ */
+@Test(groups = "live", singleThreaded = true, testName = "DigitalOcean2ComputeServiceLiveTest")
+public class DigitalOcean2ComputeServiceLiveTest extends BaseComputeServiceLiveTest {
+
+   public DigitalOcean2ComputeServiceLiveTest() {
+      provider = "digitalocean2";
+   }
+
+   @Override
+   protected Module getSshModule() {
+      return new SshjSshClientModule();
+   }
+
+   @Override
+   public void testOptionToNotBlock() throws Exception {
+      // DigitalOcean ComputeService implementation has to block until the node
+      // is provisioned, to be able to return it.
+   }
+
+   @Override
+   protected void checkTagsInNodeEquals(NodeMetadata node, ImmutableSet<String> tags) {
+      // DigitalOcean does not support tags
+   }
+
+   @Override
+   protected void checkUserMetadataContains(NodeMetadata node, ImmutableMap<String, String> userMetadata) {
+      // DigitalOcean does not support user metadata
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..e508789
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2TemplateBuilderLiveTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import static org.jclouds.compute.util.ComputeServiceUtils.getCores;
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.jclouds.compute.domain.OsFamily;
+import org.jclouds.compute.domain.Template;
+import org.jclouds.compute.internal.BaseTemplateBuilderLiveTest;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+@Test(groups = "live", testName = "DigitalOcean2TemplateBuilderLiveTest")
+public class DigitalOcean2TemplateBuilderLiveTest extends BaseTemplateBuilderLiveTest {
+
+   public DigitalOcean2TemplateBuilderLiveTest() {
+      provider = "digitalocean2";
+   }
+
+   @Test
+   @Override
+   public void testDefaultTemplateBuilder() throws IOException {
+      Template defaultTemplate = view.getComputeService().templateBuilder().build();
+      assert defaultTemplate.getImage().getOperatingSystem().getVersion().equals("14.04") : defaultTemplate
+            .getImage().getOperatingSystem().getVersion();
+      assertEquals(defaultTemplate.getImage().getOperatingSystem().is64Bit(), true);
+      assertEquals(defaultTemplate.getImage().getOperatingSystem().getFamily(), OsFamily.UBUNTU);
+      assertEquals(getCores(defaultTemplate.getHardware()), 1.0d);
+   }
+
+   @Override
+   protected Set<String> getIso3166Codes() {
+      return ImmutableSet.<String> of();
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/ActionDonePredicateTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/ActionDonePredicateTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/ActionDonePredicateTest.java
new file mode 100644
index 0000000..0c852a1
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/ActionDonePredicateTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.config;
+
+import static org.easymock.EasyMock.anyInt;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Date;
+
+import org.easymock.EasyMock;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.config.DigitalOcean2ComputeServiceContextModule.ActionDonePredicate;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.features.ActionApi;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "ActionDonePredicateTest")
+public class ActionDonePredicateTest {
+
+   public void testActionStatusOk() {
+      ActionApi actionApi = EasyMock.createMock(ActionApi.class);
+      DigitalOcean2Api api = EasyMock.createMock(DigitalOcean2Api.class);
+
+      expect(actionApi.get(1)).andReturn(action(Action.Status.COMPLETED));
+      expect(actionApi.get(2)).andReturn(action(Action.Status.IN_PROGRESS));
+      expect(api.actionApi()).andReturn(actionApi).times(2);
+      replay(actionApi, api);
+
+      ActionDonePredicate predicate = new ActionDonePredicate(api);
+      assertTrue(predicate.apply(1));
+      assertFalse(predicate.apply(2));
+   }
+
+   public void testActionStatusError() {
+      ActionApi actionApi = EasyMock.createMock(ActionApi.class);
+      DigitalOcean2Api api = EasyMock.createMock(DigitalOcean2Api.class);
+
+      expect(actionApi.get(anyInt())).andReturn(action(Action.Status.ERRORED));
+      expect(api.actionApi()).andReturn(actionApi);
+      replay(actionApi, api);
+
+      ActionDonePredicate predicate = new ActionDonePredicate(api);
+
+      try {
+         predicate.apply(1);
+         fail("Method should have thrown an IllegalStateException");
+      } catch (IllegalStateException ex) {
+         assertEquals(ex.getMessage(), "Resource is in invalid status: ERRORED");
+      }
+   }
+
+   private static Action action(Action.Status status) {
+      return Action.create(1, status, "foo", new Date(), new Date(), 1, "", null, "");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletTerminatedPredicateTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletTerminatedPredicateTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletTerminatedPredicateTest.java
new file mode 100644
index 0000000..6858e6d
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletTerminatedPredicateTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.config;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Date;
+
+import org.easymock.EasyMock;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.config.DigitalOcean2ComputeServiceContextModule.DropletTerminatedPredicate;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.features.DropletApi;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+@Test(groups = "unit", testName = "DropletTerminatedPredicateTest")
+public class DropletTerminatedPredicateTest {
+
+   public void testDropletTerminated() {
+      DropletApi dropletApi = EasyMock.createMock(DropletApi.class);
+      DigitalOcean2Api api = EasyMock.createMock(DigitalOcean2Api.class);
+
+      expect(dropletApi.get(1)).andReturn(mockDroplet());
+      expect(dropletApi.get(2)).andReturn(null);
+      expect(api.dropletApi()).andReturn(dropletApi).times(2);
+      replay(dropletApi, api);
+
+      DropletTerminatedPredicate predicate = new DropletTerminatedPredicate(api);
+      assertFalse(predicate.apply(1));
+      assertTrue(predicate.apply(2));
+   }
+
+   private static Droplet mockDroplet() {
+      return Droplet.create(1, "foo", 1024, 1, 20, false, new Date(), Droplet.Status.ACTIVE,
+            ImmutableList.<Integer> of(), ImmutableList.<Integer> of(), ImmutableList.<String> of(), null, null, null,
+            "", null, null);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtensionLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtensionLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtensionLiveTest.java
new file mode 100644
index 0000000..623d136
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtensionLiveTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.extensions;
+
+import org.jclouds.compute.extensions.internal.BaseImageExtensionLiveTest;
+import org.jclouds.sshj.config.SshjSshClientModule;
+import org.testng.annotations.Test;
+
+import com.google.inject.Module;
+
+/**
+ * Live tests for the {@link org.jclouds.compute.extensions.ImageExtension} integration.
+ */
+@Test(groups = "live", singleThreaded = true, testName = "DigitalOcean2ImageExtensionLiveTest")
+public class DigitalOcean2ImageExtensionLiveTest extends BaseImageExtensionLiveTest {
+
+   public DigitalOcean2ImageExtensionLiveTest() {
+      provider = "digitalocean2";
+   }
+
+   @Override
+   protected Module getSshModule() {
+      return new SshjSshClientModule();
+   }
+
+}


[06/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Posted by na...@apache.org.
JCLOUDS-613: Implement the DigitalOcean v2 API

Thanks to [~nacx] for pagination, many tests, fixes, and improvements to help push this over the finish line!


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

Branch: refs/heads/master
Commit: 057be8df999bce950b672f9181547482e4a9b849
Parents: 
Author: Chris Custine <cc...@apache.org>
Authored: Sat Jun 27 19:52:05 2015 -0600
Committer: Ignasi Barrera <na...@apache.org>
Committed: Sun Jun 28 22:12:03 2015 +0200

----------------------------------------------------------------------
 providers/digitalocean2/pom.xml                 | 152 +++++++
 .../jclouds/digitalocean2/DigitalOcean2Api.java |  73 ++++
 .../digitalocean2/DigitalOcean2ApiMetadata.java | 103 +++++
 .../DigitalOcean2ProviderMetadata.java          |  78 ++++
 .../DigitalOcean2ComputeServiceAdapter.java     | 191 +++++++++
 ...igitalOcean2ComputeServiceContextModule.java | 205 ++++++++++
 .../extensions/DigitalOcean2ImageExtension.java | 132 ++++++
 .../functions/DropletStatusToStatus.java        |  46 +++
 .../functions/DropletToNodeMetadata.java        | 176 ++++++++
 .../compute/functions/ImageToImage.java         |  65 +++
 .../compute/functions/RegionToLocation.java     |  57 +++
 .../compute/functions/SizeToHardware.java       |  58 +++
 ...plateOptionsToStatementWithoutPublicKey.java |  59 +++
 .../options/DigitalOcean2TemplateOptions.java   | 174 ++++++++
 .../strategy/CreateKeyPairsThenCreateNodes.java | 217 ++++++++++
 .../config/DigitalOcean2HttpApiModule.java      |  57 +++
 .../config/DigitalOceanParserModule.java        | 144 +++++++
 .../jclouds/digitalocean2/domain/Action.java    |  71 ++++
 .../jclouds/digitalocean2/domain/Backup.java    |  43 ++
 .../digitalocean2/domain/Distribution.java      |  69 ++++
 .../jclouds/digitalocean2/domain/Droplet.java   |  92 +++++
 .../digitalocean2/domain/DropletCreate.java     |  66 +++
 .../org/jclouds/digitalocean2/domain/Image.java |  48 +++
 .../jclouds/digitalocean2/domain/Kernel.java    |  35 ++
 .../org/jclouds/digitalocean2/domain/Key.java   |  39 ++
 .../jclouds/digitalocean2/domain/Networks.java  |  77 ++++
 .../digitalocean2/domain/OperatingSystem.java   |  60 +++
 .../jclouds/digitalocean2/domain/Region.java    |  39 ++
 .../org/jclouds/digitalocean2/domain/Size.java  |  46 +++
 .../jclouds/digitalocean2/domain/Snapshot.java  |  47 +++
 .../domain/internal/PaginatedCollection.java    | 111 +++++
 .../domain/options/CreateDropletOptions.java    | 155 +++++++
 .../domain/options/ImageListOptions.java        |  74 ++++
 .../domain/options/ListOptions.java             |  60 +++
 .../digitalocean2/features/ActionApi.java       | 113 ++++++
 .../digitalocean2/features/DropletApi.java      | 350 ++++++++++++++++
 .../digitalocean2/features/ImageApi.java        | 131 ++++++
 .../jclouds/digitalocean2/features/KeyApi.java  | 164 ++++++++
 .../digitalocean2/features/RegionApi.java       | 107 +++++
 .../jclouds/digitalocean2/features/SizeApi.java | 100 +++++
 .../functions/BaseToPagedIterable.java          |  59 +++
 .../functions/LinkToImageListOptions.java       |  67 ++++
 .../functions/LinkToListOptions.java            |  61 +++
 .../handlers/DigitalOcean2ErrorHandler.java     |  67 ++++
 .../org/jclouds/digitalocean2/ssh/DSAKeys.java  | 172 ++++++++
 .../jclouds/digitalocean2/ssh/ECDSAKeys.java    | 343 ++++++++++++++++
 .../services/org.jclouds.apis.ApiMetadata       |  18 +
 .../DigitalOcean2ProviderMetadataTest.java      |  29 ++
 .../DigitalOcean2ComputeServiceLiveTest.java    |  58 +++
 .../DigitalOcean2TemplateBuilderLiveTest.java   |  55 +++
 .../compute/config/ActionDonePredicateTest.java |  74 ++++
 .../config/DropletTerminatedPredicateTest.java  |  57 +++
 .../DigitalOcean2ImageExtensionLiveTest.java    |  40 ++
 .../functions/DropletStatusToStatusTest.java    |  36 ++
 .../functions/DropletToNodeMetadataTest.java    | 237 +++++++++++
 .../compute/functions/ImageToImageTest.java     |  57 +++
 .../compute/functions/RegionToLocationTest.java |  52 +++
 .../compute/functions/SizeToHardwareTest.java   |  49 +++
 ...eOptionsToStatementWithoutPublicKeyTest.java |  75 ++++
 .../DigitalOcean2TemplateOptionsTest.java       |  52 +++
 .../domain/OperatingSystemTest.java             | 104 +++++
 .../features/ActionApiLiveTest.java             |  70 ++++
 .../features/ActionApiMockTest.java             | 110 +++++
 .../features/DropletApiLiveTest.java            | 195 +++++++++
 .../features/DropletApiMockTest.java            | 401 +++++++++++++++++++
 .../features/ImageApiLiveTest.java              |  97 +++++
 .../features/ImageApiMockTest.java              | 150 +++++++
 .../digitalocean2/features/KeyApiLiveTest.java  | 101 +++++
 .../digitalocean2/features/KeyApiMockTest.java  | 203 ++++++++++
 .../features/RegionApiLiveTest.java             |  62 +++
 .../features/RegionApiMockTest.java             |  77 ++++
 .../digitalocean2/features/SizeApiLiveTest.java |  62 +++
 .../digitalocean2/features/SizeApiMockTest.java |  77 ++++
 .../functions/LinkToImageListOptionsTest.java   |  65 +++
 .../functions/LinkToListOptionsTest.java        |  58 +++
 .../internal/BaseDigitalOcean2ApiLiveTest.java  | 120 ++++++
 .../internal/BaseDigitalOcean2ApiMockTest.java  | 137 +++++++
 .../jclouds/digitalocean2/ssh/DSAKeysTest.java  |  54 +++
 .../digitalocean2/ssh/ECDSAKeysTest.java        |  55 +++
 .../src/test/resources/action.json              |  33 ++
 .../src/test/resources/actions-first.json       | 168 ++++++++
 .../src/test/resources/actions-last.json        | 106 +++++
 .../src/test/resources/backups-first.json       |  26 ++
 .../src/test/resources/backups-last.json        |  26 ++
 .../src/test/resources/droplet-create-req.json  |  12 +
 .../src/test/resources/droplet-create-res.json  |  35 ++
 .../src/test/resources/droplet.json             | 105 +++++
 .../src/test/resources/droplets-first.json      | 115 ++++++
 .../src/test/resources/droplets-last.json       | 115 ++++++
 .../digitalocean2/src/test/resources/image.json |  24 ++
 .../src/test/resources/images-first.json        | 108 +++++
 .../src/test/resources/images-last.json         | 123 ++++++
 .../src/test/resources/kernels-first.json       |  38 ++
 .../src/test/resources/kernels-last.json        |  38 ++
 .../digitalocean2/src/test/resources/key.json   |   8 +
 .../src/test/resources/keys-first.json          |  43 ++
 .../src/test/resources/keys-last.json           |  25 ++
 .../src/test/resources/logback-test.xml         |  42 ++
 .../src/test/resources/power-cycle.json         |  33 ++
 .../src/test/resources/power-off.json           |  33 ++
 .../src/test/resources/power-on.json            |  33 ++
 .../src/test/resources/reboot.json              |  33 ++
 .../src/test/resources/regions-first.json       | 111 +++++
 .../src/test/resources/regions-last.json        | 128 ++++++
 .../src/test/resources/shutdown.json            |  33 ++
 .../src/test/resources/sizes-first.json         | 123 ++++++
 .../src/test/resources/sizes-last.json          |  98 +++++
 .../src/test/resources/snapshot.json            |  33 ++
 .../src/test/resources/snapshots-first.json     |  27 ++
 .../src/test/resources/snapshots-last.json      |  27 ++
 .../src/test/resources/ssh-dsa.pub              |   1 +
 .../src/test/resources/ssh-ecdsa.pub            |   1 +
 112 files changed, 9844 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/pom.xml
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/pom.xml b/providers/digitalocean2/pom.xml
new file mode 100644
index 0000000..db5139f
--- /dev/null
+++ b/providers/digitalocean2/pom.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.jclouds.labs</groupId>
+        <artifactId>jclouds-labs</artifactId>
+        <version>2.0.0-SNAPSHOT</version>
+    </parent>
+
+    <groupId>org.apache.jclouds.labs</groupId>
+    <artifactId>digitalocean2</artifactId>
+    <name>jclouds DigitalOcean v2 API Provider</name>
+    <description>jclouds provider for Digital Ocean v2 Compute API</description>
+
+    <properties>
+        <test.digitalocean2.endpoint>https://api.digitalocean.com/v2/</test.digitalocean2.endpoint>
+        <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>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.jclouds</groupId>
+            <artifactId>jclouds-core</artifactId>
+            <version>${jclouds.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds.api</groupId>
+            <artifactId>oauth</artifactId>
+            <version>${jclouds.version}</version>
+            <type>jar</type>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds.api</groupId>
+            <artifactId>oauth</artifactId>
+            <version>${jclouds.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds</groupId>
+            <artifactId>jclouds-compute</artifactId>
+            <version>${jclouds.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds</groupId>
+            <artifactId>jclouds-compute</artifactId>
+            <version>${jclouds.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds</groupId>
+            <artifactId>jclouds-core</artifactId>
+            <version>${jclouds.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds.driver</groupId>
+            <artifactId>jclouds-slf4j</artifactId>
+            <version>${jclouds.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jclouds.driver</groupId>
+            <artifactId>jclouds-sshj</artifactId>
+            <version>${jclouds.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <!-- Already provided by jclouds-sshj -->
+                <exclusion>
+                    <groupId>org.bouncycastle</groupId>
+                    <artifactId>bcprov-jdk15on</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.google.auto.value</groupId>
+            <artifactId>auto-value</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.auto.service</groupId>
+            <artifactId>auto-service</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+    <profiles>
+        <profile>
+            <id>live</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>integration</id>
+                                <phase>integration-test</phase>
+                                <goals>
+                                    <goal>test</goal>
+                                </goals>
+                                <configuration>
+                                    <systemPropertyVariables>
+                                        <test.digitalocean2.endpoint>${test.digitalocean2.endpoint}</test.digitalocean2.endpoint>
+                                        <test.digitalocean2.api-version>${test.digitalocean2.api-version}</test.digitalocean2.api-version>
+                                        <test.digitalocean2.build-version>${test.digitalocean2.build-version}</test.digitalocean2.build-version>
+                                        <test.digitalocean2.identity>${test.digitalocean2.identity}</test.digitalocean2.identity>
+                                        <test.digitalocean2.credential>${test.digitalocean2.credential}</test.digitalocean2.credential>
+                                        <test.digitalocean2.template>${test.digitalocean2.template}</test.digitalocean2.template>
+                                    </systemPropertyVariables>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2Api.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2Api.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2Api.java
new file mode 100644
index 0000000..773fa59
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2Api.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import java.io.Closeable;
+
+import org.jclouds.digitalocean2.features.ActionApi;
+import org.jclouds.digitalocean2.features.DropletApi;
+import org.jclouds.digitalocean2.features.ImageApi;
+import org.jclouds.digitalocean2.features.KeyApi;
+import org.jclouds.digitalocean2.features.RegionApi;
+import org.jclouds.digitalocean2.features.SizeApi;
+import org.jclouds.rest.annotations.Delegate;
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Provides access to DigitalOcean.
+ */
+@Beta
+public interface DigitalOcean2Api extends Closeable {
+
+   /**
+    * Provides access to Droplet features
+    */
+   @Delegate
+   DropletApi dropletApi();
+
+   /**
+    * Provides access to SSH Key features
+    */
+   @Delegate
+   KeyApi keyApi();
+
+   /**
+    * Provides access to Images
+    */
+   @Delegate
+   ImageApi imageApi();
+
+   /**
+    * Provides access to Actions
+    */
+   @Delegate
+   ActionApi actionApi();
+
+   /**
+    * Provides access to Sizes
+    */
+   @Delegate
+   SizeApi sizeApi();
+
+   /**
+    * Provides access to Regions
+    */
+   @Delegate
+   RegionApi regionApi();
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..0b20b96
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
+import static org.jclouds.compute.config.ComputeServiceProperties.POLL_INITIAL_PERIOD;
+import static org.jclouds.compute.config.ComputeServiceProperties.POLL_MAX_PERIOD;
+import static org.jclouds.compute.config.ComputeServiceProperties.TEMPLATE;
+import static org.jclouds.oauth.v2.config.CredentialType.BEARER_TOKEN_CREDENTIALS;
+import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
+import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
+import static org.jclouds.oauth.v2.config.OAuthProperties.JWS_ALG;
+import static org.jclouds.reflect.Reflection2.typeToken;
+
+import java.net.URI;
+import java.util.Properties;
+
+import org.jclouds.apis.ApiMetadata;
+import org.jclouds.compute.ComputeServiceContext;
+import org.jclouds.digitalocean2.compute.config.DigitalOcean2ComputeServiceContextModule;
+import org.jclouds.digitalocean2.config.DigitalOcean2HttpApiModule;
+import org.jclouds.digitalocean2.config.DigitalOceanParserModule;
+import org.jclouds.oauth.v2.config.OAuthModule;
+import org.jclouds.rest.internal.BaseHttpApiMetadata;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Module;
+
+/**
+ * Implementation of {@link ApiMetadata} for DigitalOcean v2 API
+ */
+public class DigitalOcean2ApiMetadata extends BaseHttpApiMetadata<DigitalOcean2Api> {
+
+   @Override
+   public Builder toBuilder() {
+      return new Builder().fromApiMetadata(this);
+   }
+
+   public DigitalOcean2ApiMetadata() {
+      this(new Builder());
+   }
+
+   protected DigitalOcean2ApiMetadata(Builder builder) {
+      super(builder);
+   }
+
+   public static Properties defaultProperties() {
+      Properties properties = BaseHttpApiMetadata.defaultProperties();
+      properties.put("oauth.endpoint", "https://cloud.digitalocean.com/v1/oauth/token");
+      properties.put(JWS_ALG, "RS256");
+      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(POLL_INITIAL_PERIOD, 5000);
+      properties.put(POLL_MAX_PERIOD, 20000);
+      return properties;
+   }
+
+   public static class Builder extends BaseHttpApiMetadata.Builder<DigitalOcean2Api, Builder> {
+
+      protected Builder() {
+         id("digitalocean2")
+                 .name("Digital Ocean v2 API")
+                 .identityName("Not used for OAuth")
+                 .credentialName("Must be oauth2 Bearer Token")
+                 .documentation(URI.create("https://developers.digitalocean.com/v2/"))
+                 .defaultEndpoint("https://api.digitalocean.com/v2")
+                 .defaultProperties(DigitalOcean2ApiMetadata.defaultProperties())
+                 .view(typeToken(ComputeServiceContext.class))
+                 .defaultModules(ImmutableSet.<Class<? extends Module>>builder()
+                       .add(DigitalOcean2HttpApiModule.class)
+                       .add(OAuthModule.class)
+                       .add(DigitalOceanParserModule.class)
+                       .add(DigitalOcean2ComputeServiceContextModule.class)
+                       .build());
+      }
+
+      @Override
+      public DigitalOcean2ApiMetadata build() {
+         return new DigitalOcean2ApiMetadata(this);
+      }
+
+      @Override
+      protected Builder self() {
+         return this;
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadata.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadata.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadata.java
new file mode 100644
index 0000000..0f64f78
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ProviderMetadata.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import java.net.URI;
+import java.util.Properties;
+
+import org.jclouds.providers.ProviderMetadata;
+import org.jclouds.providers.internal.BaseProviderMetadata;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * Implementation of {@link org.jclouds.providers.ProviderMetadata} for DigitalOcean.
+ */
+@AutoService(ProviderMetadata.class)
+public class DigitalOcean2ProviderMetadata extends BaseProviderMetadata {
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   @Override
+   public Builder toBuilder() {
+      return builder().fromProviderMetadata(this);
+   }
+
+   public DigitalOcean2ProviderMetadata() {
+      super(builder());
+   }
+
+   public DigitalOcean2ProviderMetadata(Builder builder) {
+      super(builder);
+   }
+
+   public static Properties defaultProperties() {
+      Properties properties = DigitalOcean2ApiMetadata.defaultProperties();
+      return properties;
+   }
+
+   public static class Builder extends BaseProviderMetadata.Builder {
+
+      protected Builder() {
+         id("digitalocean2")
+            .name("DigitalOcean Compute Services")
+            .apiMetadata(new DigitalOcean2ApiMetadata())
+            .homepage(URI.create("https://www.digitalocean.com/"))
+            .console(URI.create("https://cloud.digitalocean.com/"))
+            .endpoint("https://api.digitalocean.com/v2")
+            .defaultProperties(DigitalOcean2ProviderMetadata.defaultProperties());
+      }
+
+      @Override
+      public DigitalOcean2ProviderMetadata build() {
+         return new DigitalOcean2ProviderMetadata(this);
+      }
+
+      @Override
+      public Builder fromProviderMetadata(ProviderMetadata in) {
+         super.fromProviderMetadata(in);
+         return this;
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..aa4f656
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceAdapter.java
@@ -0,0 +1,191 @@
+/*
+ * 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;
+
+import static com.google.common.base.Preconditions.checkState;
+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 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 javax.annotation.Resource;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+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.options.DigitalOcean2TemplateOptions;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.domain.LoginCredentials;
+import org.jclouds.logging.Logger;
+
+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> {
+
+   @Resource
+   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+   protected Logger logger = Logger.NULL;
+
+   private final DigitalOcean2Api api;
+   private final Predicate<Integer> nodeRunningPredicate;
+   private final Predicate<Integer> nodeStoppedPredicate;
+   private final Predicate<Integer> nodeTerminatedPredicate;
+
+   @Inject DigitalOcean2ComputeServiceAdapter(DigitalOcean2Api api,
+         @Named(TIMEOUT_NODE_RUNNING) Predicate<Integer> nodeRunningPredicate,
+         @Named(TIMEOUT_NODE_SUSPENDED) Predicate<Integer> nodeStoppedPredicate,
+         @Named(TIMEOUT_NODE_TERMINATED) Predicate<Integer> nodeTerminatedPredicate) {
+      this.api = api;
+      this.nodeRunningPredicate = nodeRunningPredicate;
+      this.nodeStoppedPredicate = nodeStoppedPredicate;
+      this.nodeTerminatedPredicate = nodeTerminatedPredicate;
+   }
+
+   @Override
+   public NodeAndInitialCredentials<Droplet> createNodeWithGroupEncodedIntoName(String group, final String name,
+         Template template) {
+      DigitalOcean2TemplateOptions templateOptions = template.getOptions().as(DigitalOcean2TemplateOptions.class);
+      CreateDropletOptions.Builder options = CreateDropletOptions.builder();
+
+      // DigitalOcean specific options
+      options.privateNetworking(templateOptions.getPrivateNetworking());
+      options.backupsEnabled(templateOptions.getBackupsEnabled());
+      if (!templateOptions.getSshKeyIds().isEmpty()) {
+         options.addSshKeyIds(templateOptions.getSshKeyIds());
+      }
+
+      DropletCreate dropletCreated = api.dropletApi().create(name,
+            template.getLocation().getId(),
+            template.getHardware().getProviderId(),
+            template.getImage().getProviderId(),
+            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
+      nodeRunningPredicate.apply(getOnlyElement(dropletCreated.links().actions()).id());
+      Droplet droplet = api.dropletApi().get(dropletCreated.droplet().id());
+
+      LoginCredentials defaultCredentials = LoginCredentials.builder().user("root")
+            .privateKey(templateOptions.getLoginPrivateKey()).build();
+
+      return new NodeAndInitialCredentials<Droplet>(droplet, String.valueOf(droplet.id()), defaultCredentials);
+   }
+
+   @Override
+   public Iterable<Image> listImages() {
+      return api.imageApi().list().concat();
+   }
+
+   @Override
+   public Iterable<Size> listHardwareProfiles() {
+      return filter(api.sizeApi().list().concat(), new Predicate<Size>() {
+         @Override
+         public boolean apply(Size size) {
+            return size.available();
+         }
+      });
+   }
+
+   @Override
+   public Iterable<Region> listLocations() {
+      // DigitalOcean lists regions that are unavailable for droplet creation
+      return filter(api.regionApi().list().concat(), new Predicate<Region>() {
+         @Override
+         public boolean apply(Region region) {
+            return region.available();
+         }
+      });
+   }
+
+   @Override
+   public Iterable<Droplet> listNodes() {
+      return api.dropletApi().list().concat();
+   }
+
+   @Override
+   public Iterable<Droplet> listNodesByIds(final Iterable<String> ids) {
+      return filter(listNodes(), new Predicate<Droplet>() {
+         @Override
+         public boolean apply(Droplet droplet) {
+            return contains(ids, String.valueOf(droplet.id()));
+         }
+      });
+   }
+
+   @Override
+   public Image getImage(String 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);
+   }
+
+   @Override
+   public Droplet getNode(String id) {
+      return api.dropletApi().get(Integer.parseInt(id));
+   }
+
+   @Override
+   public void destroyNode(String id) {
+      // We have to wait here, as the api does not properly populate the state
+      // but fails if there is a pending event
+      int dropletId = Integer.parseInt(id);
+      api.dropletApi().delete(dropletId);
+      checkState(nodeTerminatedPredicate.apply(dropletId), "node was not destroyed in the configured timeout");
+   }
+
+   @Override
+   public void rebootNode(String id) {
+      // We have to wait here, as the api does not properly populate the state
+      // but fails if there is a pending event
+      Action action = api.dropletApi().reboot(Integer.parseInt(id));
+      checkState(nodeRunningPredicate.apply(action.id()), "node did not restart in the configured timeout");
+   }
+
+   @Override
+   public void resumeNode(String id) {
+      // We have to wait here, as the api does not properly populate the state
+      // but fails if there is a pending event
+      Action action = api.dropletApi().powerOn(Integer.parseInt(id));
+      checkState(nodeRunningPredicate.apply(action.id()), "node did not started in the configured timeout");
+   }
+
+   @Override
+   public void suspendNode(String id) {
+      int dropletId = Integer.parseInt(id);
+      // We have to wait here, as the api does not properly populate the state
+      // but fails if there is a pending event
+      Action action = api.dropletApi().powerOff(dropletId);
+      checkState(nodeStoppedPredicate.apply(action.id()), "node did not stop in the configured timeout");
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..7809f9d
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/config/DigitalOcean2ComputeServiceContextModule.java
@@ -0,0 +1,205 @@
+/*
+ * 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.config;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_IMAGE_AVAILABLE;
+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.util.Predicates2.retry;
+
+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.NodeMetadata;
+import org.jclouds.compute.domain.NodeMetadata.Status;
+import org.jclouds.compute.extensions.ImageExtension;
+import org.jclouds.compute.functions.TemplateOptionsToStatement;
+import org.jclouds.compute.options.TemplateOptions;
+import org.jclouds.compute.reference.ComputeServiceConstants.PollPeriod;
+import org.jclouds.compute.reference.ComputeServiceConstants.Timeouts;
+import org.jclouds.compute.strategy.CreateNodesInGroupThenAddToSet;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+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.RegionToLocation;
+import org.jclouds.digitalocean2.compute.functions.SizeToHardware;
+import org.jclouds.digitalocean2.compute.functions.TemplateOptionsToStatementWithoutPublicKey;
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+/**
+ * Configures the compute service classes for the DigitalOcean API.
+ */
+public class DigitalOcean2ComputeServiceContextModule extends
+      ComputeServiceAdapterContextModule<Droplet, Size, Image, Region> {
+
+   @Override
+   protected void configure() {
+      super.configure();
+
+      bind(new TypeLiteral<ComputeServiceAdapter<Droplet, Size, Image, 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<Region, Location>>() {
+      }).to(RegionToLocation.class);
+      bind(new TypeLiteral<Function<Size, Hardware>>() {
+      }).to(SizeToHardware.class);
+      bind(new TypeLiteral<Function<Droplet.Status, Status>>() {
+      }).to(DropletStatusToStatus.class);
+
+      install(new LocationsFromComputeServiceAdapterModule<Droplet, Size, Image, Region>() {
+      });
+
+      bind(CreateNodesInGroupThenAddToSet.class).to(CreateKeyPairsThenCreateNodes.class);
+      bind(TemplateOptions.class).to(DigitalOcean2TemplateOptions.class);
+      bind(TemplateOptionsToStatement.class).to(TemplateOptionsToStatementWithoutPublicKey.class);
+
+      bind(new TypeLiteral<ImageExtension>() {
+      }).to(DigitalOcean2ImageExtension.class);
+   }
+
+   @Override
+   protected Optional<ImageExtension> provideImageExtension(Injector i) {
+      return Optional.of(i.getInstance(ImageExtension.class));
+   }
+
+   @Provides
+   @Named(TIMEOUT_NODE_RUNNING)
+   protected Predicate<Integer> provideDropletRunningPredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new ActionDonePredicate(api), timeouts.nodeRunning, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @Provides
+   @Named(TIMEOUT_NODE_SUSPENDED)
+   protected Predicate<Integer> provideDropletSuspendedPredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new ActionDonePredicate(api), timeouts.nodeSuspended, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @Provides
+   @Named(TIMEOUT_NODE_TERMINATED)
+   protected Predicate<Integer> provideDropletTerminatedPredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new DropletTerminatedPredicate(api), timeouts.nodeTerminated, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @Provides
+   @Named(TIMEOUT_IMAGE_AVAILABLE)
+   protected Predicate<Integer> provideImageAvailablePredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new ActionDonePredicate(api), timeouts.imageAvailable, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @Provides
+   @Singleton
+   protected Predicate<Region> provideRegionAvailablePredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new RegionAvailablePredicate(), timeouts.imageAvailable, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @Provides
+   protected Predicate<Integer> provideActionCompletedPredicate(final DigitalOcean2Api api, Timeouts timeouts,
+         PollPeriod pollPeriod) {
+      return retry(new ActionDonePredicate(api), timeouts.imageAvailable, pollPeriod.pollInitialPeriod,
+            pollPeriod.pollMaxPeriod);
+   }
+
+   @VisibleForTesting
+   static class ActionDonePredicate implements Predicate<Integer> {
+
+      private final DigitalOcean2Api api;
+
+      public ActionDonePredicate(DigitalOcean2Api api) {
+         this.api = checkNotNull(api, "api must not be null");
+      }
+
+      @Override
+      public boolean apply(Integer input) {
+         checkNotNull(input, "action id cannot be null");
+         Action current = api.actionApi().get(input);
+         switch (current.status()) {
+            case COMPLETED:
+               return true;
+            case IN_PROGRESS:
+               return false;
+            case ERRORED:
+            default:
+               throw new IllegalStateException("Resource is in invalid status: " + current.status().name());
+         }
+      }
+
+   }
+
+   @VisibleForTesting
+   static class DropletTerminatedPredicate implements Predicate<Integer> {
+
+      private final DigitalOcean2Api api;
+
+      public DropletTerminatedPredicate(DigitalOcean2Api api) {
+         this.api = checkNotNull(api, "api must not be null");
+      }
+
+      @Override
+      public boolean apply(Integer input) {
+         checkNotNull(input, "droplet");
+         Droplet droplet = api.dropletApi().get(input);
+         return droplet == null;
+      }
+   }
+
+   @VisibleForTesting
+   static class RegionAvailablePredicate implements Predicate<Region> {
+      @Override
+      public boolean apply(Region input) {
+         return input.available();
+      }
+
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..41e3270
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/extensions/DigitalOcean2ImageExtension.java
@@ -0,0 +1,132 @@
+/*
+ * 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.extensions;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_IMAGE_AVAILABLE;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_SUSPENDED;
+
+import java.util.NoSuchElementException;
+
+import javax.annotation.Resource;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.CloneImageTemplate;
+import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.ImageTemplate;
+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.domain.Action;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.Droplet.Status;
+import org.jclouds.logging.Logger;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * The {@link org.jclouds.compute.extensions.ImageExtension} implementation for the DigitalOcean provider.
+ */
+@Singleton
+public class DigitalOcean2ImageExtension implements ImageExtension {
+
+   @Resource
+   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+   protected Logger logger = Logger.NULL;
+
+   private final DigitalOcean2Api api;
+   private final Predicate<Integer> imageAvailablePredicate;
+   private final Predicate<Integer> nodeStoppedPredicate;
+   private final Function<org.jclouds.digitalocean2.domain.Image, 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) {
+      this.api = api;
+      this.imageAvailablePredicate = imageAvailablePredicate;
+      this.nodeStoppedPredicate = nodeStoppedPredicate;
+      this.imageTransformer = imageTransformer;
+   }
+
+   @Override
+   public ImageTemplate buildImageTemplateFromNode(String name, String id) {
+      Droplet droplet = api.dropletApi().get(Integer.parseInt(id));
+
+      if (droplet == null) {
+         throw new NoSuchElementException("Cannot find droplet with id: " + id);
+      }
+
+      return new ImageTemplateBuilder.CloneImageTemplateBuilder().nodeId(id).name(name).build();
+   }
+
+   @Override
+   public ListenableFuture<Image> createImage(ImageTemplate template) {
+      checkState(template instanceof CloneImageTemplate, "DigitalOcean only supports creating images through cloning.");
+      final CloneImageTemplate cloneTemplate = (CloneImageTemplate) template;
+
+      // Droplet needs to be stopped
+      int dropletId = Integer.parseInt(cloneTemplate.getSourceNodeId());
+      Action powerOffEvent = api.dropletApi().powerOff(dropletId);
+      checkState(nodeStoppedPredicate.apply(powerOffEvent.id()), "node was not powered off in the configured timeout");
+      
+      Droplet droplet = api.dropletApi().get(dropletId);
+      checkState(droplet.status() == Status.OFF, "node was not powered off in the configured timeout");
+
+      Action snapshotEvent = api.dropletApi().snapshot(Integer.parseInt(cloneTemplate.getSourceNodeId()),
+            cloneTemplate.getName());
+
+      logger.info(">> registered new Image, waiting for it to become available");
+
+      // Until the process completes we don't have enough information to build an image to return
+      checkState(imageAvailablePredicate.apply(snapshotEvent.id()),
+            "snapshot failed to complete in the configured timeout");
+
+      org.jclouds.digitalocean2.domain.Image snapshot = api.imageApi().list().concat().firstMatch(
+            new Predicate<org.jclouds.digitalocean2.domain.Image>() {
+               @Override
+               public boolean apply(org.jclouds.digitalocean2.domain.Image input) {
+                  return input.name().equals(cloneTemplate.getName());
+               }
+            }).get();
+
+      return immediateFuture(imageTransformer.apply(snapshot));
+   }
+
+   @Override
+   public boolean deleteImage(String id) {
+      try {
+         // 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.imageApi().delete(imageId);
+         }
+         return true;
+      } catch (Exception ex) {
+         return false;
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatus.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatus.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatus.java
new file mode 100644
index 0000000..6edadb9
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatus.java
@@ -0,0 +1,46 @@
+/*
+ * 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 javax.inject.Singleton;
+
+import org.jclouds.compute.domain.NodeMetadata.Status;
+import org.jclouds.digitalocean2.domain.Droplet;
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Transforms an {@link org.jclouds.compute.domain.NodeMetadata.Status} to the jclouds portable model.
+ */
+@Singleton
+public class DropletStatusToStatus implements Function<Droplet.Status, Status> {
+
+   private static final Function<Droplet.Status, Status> toPortableStatus = Functions.forMap(
+         ImmutableMap.<Droplet.Status, Status> builder()
+               .put(Droplet.Status.NEW, Status.PENDING)
+               .put(Droplet.Status.ACTIVE, Status.RUNNING)
+               .put(Droplet.Status.ARCHIVE, Status.TERMINATED)
+               .put(Droplet.Status.OFF, Status.SUSPENDED)
+               .build(), 
+         Status.UNRECOGNIZED);
+
+   @Override
+   public Status apply(final Droplet.Status input) {
+      return toPortableStatus.apply(input);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..eebc121
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadata.java
@@ -0,0 +1,176 @@
+/*
+ * 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.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.find;
+import static com.google.common.collect.Iterables.tryFind;
+
+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;
+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.domain.NodeMetadataBuilder;
+import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.Networks;
+import org.jclouds.digitalocean2.domain.Region;
+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.FluentIterable;
+import com.google.common.collect.Iterables;
+
+/**
+ * Transforms an {@link Droplet} to the jclouds portable model.
+ */
+@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;
+   private final Function<Droplet.Status, Status> toPortableStatus;
+   private final GroupNamingConvention groupNamingConvention;
+   private final Map<String, Credentials> credentialStore;
+
+   @Inject
+   DropletToNodeMetadata(Supplier<Map<String, ? extends Image>> images,
+         Supplier<Map<String, ? extends Hardware>> hardwares, @Memoized Supplier<Set<? extends Location>> locations,
+         Function<Droplet.Status, Status> toPortableStatus, GroupNamingConvention.Factory groupNamingConvention,
+         Map<String, Credentials> credentialStore) {
+      this.images = checkNotNull(images, "images cannot be null");
+      this.hardwares = checkNotNull(hardwares, "hardwares cannot be null");
+      this.locations = checkNotNull(locations, "locations cannot be null");
+      this.toPortableStatus = checkNotNull(toPortableStatus, "toPortableStatus cannot be null");
+      this.groupNamingConvention = checkNotNull(groupNamingConvention, "groupNamingConvention cannot be null")
+            .createWithoutPrefix();
+      this.credentialStore = checkNotNull(credentialStore, "credentialStore cannot be null");
+   }
+
+   @Override
+   public NodeMetadata apply(Droplet input) {
+      NodeMetadataBuilder builder = new NodeMetadataBuilder();
+      builder.ids(String.valueOf(input.id()));
+      builder.name(input.name());
+      builder.hostname(input.name());
+      builder.group(groupNamingConvention.extractGroup(input.name()));
+
+      builder.hardware(getHardware(input.sizeSlug()));
+      builder.location(getLocation(input.region()));
+
+      Optional<? extends Image> image = findImage(input.image().id());
+      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.image().id(), input.id());
+      }
+
+      builder.status(toPortableStatus.apply(input.status()));
+      builder.backendStatus(input.status().name());
+
+      if (!input.getPublicAddresses().isEmpty()) {
+         builder.publicAddresses(FluentIterable
+                     .from(input.getPublicAddresses())
+                     .transform(new Function<Networks.Address, String>() {
+                        @Override
+                        public String apply(final Networks.Address input) {
+                           return input.ip();
+                        }
+                     })
+         );
+      }
+
+      if (!input.getPrivateAddresses().isEmpty()) {
+         builder.privateAddresses(FluentIterable
+               .from(input.getPrivateAddresses())
+               .transform(new Function<Networks.Address, String>() {
+                  @Override
+                  public String apply(final Networks.Address input) {
+                     return input.ip();
+                  }
+               })
+         );
+      }
+
+      // DigitalOcean does not provide a way to get the credentials.
+      // Try to return them from the credential store
+      Credentials credentials = credentialStore.get("node#" + input.id());
+      if (credentials instanceof LoginCredentials) {
+         builder.credentials(LoginCredentials.class.cast(credentials));
+      }
+
+      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(final String slug) {
+      return Iterables.find(hardwares.get().values(), new Predicate<Hardware>() {
+         @Override
+         public boolean apply(Hardware input) {
+            return input.getId().equals(slug);
+         }
+      });
+   }
+
+   protected Location getLocation(final Region region) {
+      return find(locations.get(), new Predicate<Location>() {
+         @Override
+         public boolean apply(Location location) {
+            return region != null && region.slug().equals(location.getId());
+         }
+      }, null);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..8f9ad92
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/ImageToImage.java
@@ -0,0 +1,65 @@
+/*
+ * 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/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
new file mode 100644
index 0000000..4adf240
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/RegionToLocation.java
@@ -0,0 +1,57 @@
+/*
+ * 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.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.jclouds.location.suppliers.all.JustProvider;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Transforms an {@link Region} to the jclouds portable model.
+ */
+@Singleton
+public class RegionToLocation implements Function<Region, Location> {
+
+   private final JustProvider justProvider;
+
+   @Inject
+   RegionToLocation(JustProvider justProvider) {
+      this.justProvider = checkNotNull(justProvider, "justProvider cannot be null");
+   }
+
+   @Override
+   public Location apply(Region input) {
+      LocationBuilder builder = new LocationBuilder();
+      builder.id(input.slug());
+      builder.description(input.name());
+      builder.scope(LocationScope.REGION);
+      builder.parent(getOnlyElement(justProvider.get()));
+      builder.iso3166Codes(ImmutableSet.<String> of());
+      return builder.build();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/SizeToHardware.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/SizeToHardware.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/SizeToHardware.java
new file mode 100644
index 0000000..5645d3b
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/SizeToHardware.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.compute.functions;
+
+import javax.inject.Singleton;
+
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.HardwareBuilder;
+import org.jclouds.compute.domain.Processor;
+import org.jclouds.compute.domain.Volume.Type;
+import org.jclouds.compute.domain.VolumeBuilder;
+import org.jclouds.digitalocean2.domain.Size;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Transforms an {@link Size} to the jclouds portable model.
+ */
+@Singleton
+public class SizeToHardware implements Function<Size, Hardware> {
+
+   @Override
+   public Hardware apply(Size input) {
+      HardwareBuilder builder = new HardwareBuilder();
+      builder.id(input.slug());
+      builder.providerId(input.slug());
+      builder.name(input.slug());
+      builder.ram(input.memory());
+      // No cpu speed from DigitalOcean API, so assume more cores == faster
+      builder.processor(new Processor(input.vcpus(), input.vcpus()));
+
+      builder.volume(new VolumeBuilder() 
+            .size(Float.valueOf(input.disk()))
+            .type(Type.LOCAL) 
+            .build());
+
+      ImmutableMap.Builder<String, String> metadata = ImmutableMap.builder();
+      metadata.put("costPerHour", String.valueOf(input.priceHourly()));
+      metadata.put("costPerMonth", String.valueOf(input.priceMonthly()));
+      builder.userMetadata(metadata.build());
+
+      return builder.build();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKey.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKey.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKey.java
new file mode 100644
index 0000000..52dcb0e
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKey.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.compute.functions;
+
+import javax.inject.Singleton;
+
+import org.jclouds.compute.functions.TemplateOptionsToStatement;
+import org.jclouds.compute.options.TemplateOptions;
+import org.jclouds.scriptbuilder.InitScript;
+import org.jclouds.scriptbuilder.domain.Statement;
+import org.jclouds.scriptbuilder.domain.StatementList;
+import org.jclouds.scriptbuilder.statements.ssh.InstallRSAPrivateKey;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Convert the template options into a statement, but ignoring the public key.
+ * <p>
+ * The {@link org.jclouds.DigitalOcean2ComputeServiceAdapter.compute.strategy.DigitalOceanComputeServiceAdapter} already takes care of
+ * installing it using the {@link org.jclouds.digitalocean.features.KeyPairApi}.
+ */
+@Singleton
+public class TemplateOptionsToStatementWithoutPublicKey extends TemplateOptionsToStatement {
+
+   @Override
+   public Statement apply(TemplateOptions options) {
+      ImmutableList.Builder<Statement> builder = ImmutableList.builder();
+      if (options.getRunScript() != null) {
+         builder.add(options.getRunScript());
+      }
+      if (options.getPrivateKey() != null) {
+         builder.add(new InstallRSAPrivateKey(options.getPrivateKey()));
+      }
+
+      ImmutableList<Statement> bootstrap = builder.build();
+      if (!bootstrap.isEmpty()) {
+         if (options.getTaskName() == null && !(options.getRunScript() instanceof InitScript)) {
+            options.nameTask("bootstrap");
+         }
+         return bootstrap.size() == 1 ? bootstrap.get(0) : new StatementList(bootstrap);
+      }
+
+      return null;
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptions.java
new file mode 100644
index 0000000..cafcdb1
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptions.java
@@ -0,0 +1,174 @@
+/*
+ * 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.options;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Set;
+
+import org.jclouds.compute.options.TemplateOptions;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Custom options for the DigitalOcean API.
+ */
+public class DigitalOcean2TemplateOptions extends TemplateOptions implements Cloneable {
+
+   private Set<Integer> sshKeyIds = ImmutableSet.of();
+   private boolean privateNetworking = false;
+   private boolean backupsEnabled = false;
+   private boolean autoCreateKeyPair = true;
+
+   /**
+    * Enables a private network interface if the region supports private networking.
+    */
+   public DigitalOcean2TemplateOptions privateNetworking(boolean privateNetworking) {
+      this.privateNetworking = privateNetworking;
+      return this;
+   }
+
+   /**
+    * Enabled backups for the droplet.
+    */
+   public DigitalOcean2TemplateOptions backupsEnabled(boolean backupsEnabled) {
+      this.backupsEnabled = backupsEnabled;
+      return this;
+   }
+
+   /**
+    * Sets the ssh key ids to be added to the droplet.
+    */
+   public DigitalOcean2TemplateOptions sshKeyIds(Iterable<Integer> sshKeyIds) {
+      this.sshKeyIds = ImmutableSet.copyOf(checkNotNull(sshKeyIds, "sshKeyIds cannot be null"));
+      return this;
+   }
+
+   /**
+    * Sets whether an SSH key pair should be created automatically.
+    */
+   public DigitalOcean2TemplateOptions autoCreateKeyPair(boolean autoCreateKeyPair) {
+      this.autoCreateKeyPair = autoCreateKeyPair;
+      return this;
+   }
+
+   public Set<Integer> getSshKeyIds() {
+      return sshKeyIds;
+   }
+
+   public boolean getPrivateNetworking() {
+      return privateNetworking;
+   }
+
+   public boolean getBackupsEnabled() {
+      return backupsEnabled;
+   }
+
+   public boolean getAutoCreateKeyPair() {
+      return autoCreateKeyPair;
+   }
+
+   @Override
+   public DigitalOcean2TemplateOptions clone() {
+      DigitalOcean2TemplateOptions options = new DigitalOcean2TemplateOptions();
+      copyTo(options);
+      return options;
+   }
+
+   @Override
+   public void copyTo(TemplateOptions to) {
+      super.copyTo(to);
+      if (to instanceof DigitalOcean2TemplateOptions) {
+         DigitalOcean2TemplateOptions eTo = DigitalOcean2TemplateOptions.class.cast(to);
+         eTo.privateNetworking(privateNetworking);
+         eTo.backupsEnabled(backupsEnabled);
+         eTo.autoCreateKeyPair(autoCreateKeyPair);
+         eTo.sshKeyIds(sshKeyIds);
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(super.hashCode(), backupsEnabled, privateNetworking, autoCreateKeyPair, sshKeyIds);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj) {
+         return true;
+      }
+      if (!super.equals(obj)) {
+         return false;
+      }
+      if (getClass() != obj.getClass()) {
+         return false;
+      }
+      DigitalOcean2TemplateOptions other = (DigitalOcean2TemplateOptions) obj;
+      return super.equals(other) && equal(this.backupsEnabled, other.backupsEnabled)
+            && equal(this.privateNetworking, other.privateNetworking)
+            && equal(this.autoCreateKeyPair, other.autoCreateKeyPair) && equal(this.sshKeyIds, other.sshKeyIds);
+   }
+
+   @Override
+   public ToStringHelper string() {
+      ToStringHelper toString = super.string().omitNullValues();
+      toString.add("privateNetworking", privateNetworking);
+      toString.add("backupsEnabled", backupsEnabled);
+      if (!sshKeyIds.isEmpty()) {
+         toString.add("sshKeyIds", sshKeyIds);
+      }
+      toString.add("autoCreateKeyPair", autoCreateKeyPair);
+      return toString;
+   }
+
+   public static class Builder {
+
+      /**
+       * @see DigitalOcean2TemplateOptions#privateNetworking
+       */
+      public static DigitalOcean2TemplateOptions privateNetworking(boolean privateNetworking) {
+         DigitalOcean2TemplateOptions options = new DigitalOcean2TemplateOptions();
+         return options.privateNetworking(privateNetworking);
+      }
+
+      /**
+       * @see DigitalOcean2TemplateOptions#backupsEnabled
+       */
+      public static DigitalOcean2TemplateOptions backupsEnabled(boolean backupsEnabled) {
+         DigitalOcean2TemplateOptions options = new DigitalOcean2TemplateOptions();
+         return options.backupsEnabled(backupsEnabled);
+      }
+
+      /**
+       * @see DigitalOcean2TemplateOptions#sshKeyIds
+       */
+      public static DigitalOcean2TemplateOptions sshKeyIds(Iterable<Integer> sshKeyIds) {
+         DigitalOcean2TemplateOptions options = new DigitalOcean2TemplateOptions();
+         return options.sshKeyIds(sshKeyIds);
+      }
+
+      /**
+       * @see DigitalOcean2TemplateOptions#autoCreateKeyPair
+       */
+      public static DigitalOcean2TemplateOptions autoCreateKeyPair(boolean autoCreateKeyPair) {
+         DigitalOcean2TemplateOptions options = new DigitalOcean2TemplateOptions();
+         return options.autoCreateKeyPair(autoCreateKeyPair);
+      }
+   }
+}


[13/19] jclouds git commit: Prefer polling the status of the node

Posted by na...@apache.org.
Prefer polling the status of the node


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

Branch: refs/heads/master
Commit: 3fbd399f6b3c35f1cb2e0b0af797c9db5f72fe39
Parents: 83ff38e
Author: Ignasi Barrera <na...@apache.org>
Authored: Wed Oct 28 18:14:42 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Thu Oct 29 15:55:23 2015 +0100

----------------------------------------------------------------------
 .../DigitalOcean2ComputeServiceAdapter.java     |  3 +--
 .../DigitalOcean2TemplateBuilderLiveTest.java   |  2 +-
 .../features/DropletApiLiveTest.java            | 27 +++++++-------------
 .../digitalocean2/features/KeyApiLiveTest.java  |  2 --
 .../internal/BaseDigitalOcean2ApiLiveTest.java  | 27 +++++++++++++++-----
 5 files changed, 31 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/3fbd399f/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 2d76176..f520f45 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
@@ -42,7 +42,6 @@ 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;
 import org.jclouds.digitalocean2.domain.DropletCreate;
 import org.jclouds.digitalocean2.domain.Image;
@@ -237,7 +236,7 @@ public class DigitalOcean2ComputeServiceAdapter implements ComputeServiceAdapter
       // We have to wait here, as the api does not properly populate the state
       // but fails if there is a pending event
       int dropletId = Integer.parseInt(id);
-      Action action = api.dropletApi().powerOff(dropletId);
+      api.dropletApi().powerOff(dropletId);
       checkState(nodeStoppedPredicate.apply(dropletId), "node did not stop in the configured timeout");
    }
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3fbd399f/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 8480cc1..ee7b962 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("15.04") : defaultTemplate
+      assert defaultTemplate.getImage().getOperatingSystem().getVersion().equals("15.10") : 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/3fbd399f/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
index f451d2e..c10fa54 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
@@ -16,10 +16,7 @@
  */
 package org.jclouds.digitalocean2.features;
 
-import static com.google.common.collect.Iterables.getOnlyElement;
 import static java.util.logging.Logger.getAnonymousLogger;
-import static org.jclouds.digitalocean2.domain.Droplet.Status.ACTIVE;
-import static org.jclouds.digitalocean2.domain.Droplet.Status.OFF;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
@@ -77,8 +74,8 @@ public class DropletApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
    public void testCreate() {
       DropletCreate dropletCreate = api().create(prefix + "-droplet-livetest", region.slug(), size.slug(), image.slug(),
             CreateDropletOptions.builder().backupsEnabled(true).addSshKeyId(key.id()).build());
-      assertActionCompleted(getOnlyElement(dropletCreate.links().actions()).id());
       dropletId = dropletCreate.droplet().id();
+      assertNodeRunning(dropletId);
       Droplet droplet = api().get(dropletId);
       assertNotNull(droplet, "Droplet should not be null");
    }
@@ -101,10 +98,8 @@ public class DropletApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
    
    @Test(dependsOnMethods = "testListKernels")
    public void testPowerOff() {
-      Action action = api().powerOff(dropletId);
-      assertActionCompleted(action.id());
-      Droplet droplet = api().get(dropletId);
-      assertEquals(droplet.status(), OFF, "Droplet should be off");
+      api().powerOff(dropletId);
+      assertNodeStopped(dropletId);
    }
 
    @Test(groups = "live", dependsOnMethods = "testPowerOff")
@@ -146,29 +141,25 @@ public class DropletApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
    @Test(groups = "live", dependsOnMethods = "testSnapshots")
    public void testPowerOn() {
       // Apparently droplets are automatically powered on after the snapshot process
-      Action action = api().powerOff(dropletId);
-      assertActionCompleted(action.id());
+      api().powerOff(dropletId);
+      assertNodeStopped(dropletId);
       
-      action = api().powerOn(dropletId);
-      assertActionCompleted(action.id());
-      Droplet droplet = api().get(dropletId);
-      assertEquals(droplet.status(), ACTIVE, "Droplet should be Active");
+      api().powerOn(dropletId);
+      assertNodeRunning(dropletId);
    }
    
    @Test(groups = "live", dependsOnMethods = "testPowerOn")
    public void testReboot() {
       Action action = api().reboot(dropletId);
       assertActionCompleted(action.id());
-      Droplet droplet = api().get(dropletId);
-      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+      assertNodeRunning(dropletId);
    }
    
    @Test(groups = "live", dependsOnMethods = "testReboot")
    public void testPowerCycle() {
       Action action = api().powerCycle(dropletId);
       assertActionCompleted(action.id());
-      Droplet droplet = api().get(dropletId);
-      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+      assertNodeRunning(dropletId);
    }
    
    @Test(groups = "live", dependsOnMethods = "testPowerCycle")

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3fbd399f/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
index 2911d79..e0ec1c9 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
@@ -69,8 +69,6 @@ public class KeyApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
    public void testUpdateKey() {
       api().update(dsa.id(), "jclouds-test-dsa-updated");
       assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated");
-      api().update(dsa.fingerprint(), "jclouds-test-dsa-updated2");
-      assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated2");
    }
 
    @AfterClass(alwaysRun = true)

http://git-wip-us.apache.org/repos/asf/jclouds/blob/3fbd399f/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
index b210c93..ee5bb55 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
@@ -17,16 +17,17 @@
 package org.jclouds.digitalocean2.internal;
 
 import static com.google.common.base.Preconditions.checkState;
+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.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
 import static org.testng.util.Strings.isNullOrEmpty;
 
 import java.util.Properties;
-import java.util.concurrent.atomic.AtomicReference;
 
 import org.jclouds.apis.BaseApiLiveTest;
 import org.jclouds.compute.config.ComputeServiceProperties;
-import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.digitalocean2.DigitalOcean2Api;
 import org.jclouds.digitalocean2.config.DigitalOcean2RateLimitModule;
 import org.jclouds.digitalocean2.domain.Action;
@@ -46,10 +47,10 @@ import com.google.inject.name.Names;
 
 public class BaseDigitalOcean2ApiLiveTest extends BaseApiLiveTest<DigitalOcean2Api> {
 
-   protected Predicate<Integer> actionCompleted;
-   protected Predicate<AtomicReference<NodeMetadata>> nodeRunning;
-   protected Predicate<Integer> nodeTerminated;
-   protected Predicate<Integer> dropletOff;
+   private Predicate<Integer> actionCompleted;
+   private Predicate<Integer> nodeTerminated;
+   private Predicate<Integer> nodeStopped;
+   private Predicate<Integer> nodeRunning;
 
    public BaseDigitalOcean2ApiLiveTest() {
       provider = "digitalocean2";
@@ -67,6 +68,10 @@ public class BaseDigitalOcean2ApiLiveTest extends BaseApiLiveTest<DigitalOcean2A
       actionCompleted = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){}));
       nodeTerminated = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){},
             Names.named(TIMEOUT_NODE_TERMINATED)));
+      nodeStopped = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){},
+            Names.named(TIMEOUT_NODE_SUSPENDED)));
+      nodeRunning = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){},
+            Names.named(TIMEOUT_NODE_RUNNING)));
       return injector.getInstance(DigitalOcean2Api.class);
    }
 
@@ -81,8 +86,16 @@ public class BaseDigitalOcean2ApiLiveTest extends BaseApiLiveTest<DigitalOcean2A
       assertEquals(action.status(), Action.Status.COMPLETED);
    }
 
+   protected void assertNodeStopped(int dropletId) {
+      assertTrue(nodeStopped.apply(dropletId), String.format("Droplet %s did not stop in the configured timeout", dropletId));
+   }
+
+   protected void assertNodeRunning(int dropletId) {
+      assertTrue(nodeRunning.apply(dropletId), String.format("Droplet %s did not start in the configured timeout", dropletId));
+   }
+
    protected void assertNodeTerminated(int dropletId) {
-      assertEquals(nodeTerminated.apply(dropletId), true, String.format("Timeout waiting for dropletId: %s", dropletId));
+      assertTrue(nodeTerminated.apply(dropletId), String.format("Droplet %s was not terminated in the configured timeout", dropletId));
    }
    
    protected Region firstAvailableRegion() {


[09/19] jclouds git commit: JCLOUDS-1024: ImageExtension can take snapshots of stopped droplets

Posted by na...@apache.org.
JCLOUDS-1024: ImageExtension can take snapshots of stopped droplets


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

Branch: refs/heads/master
Commit: 200e0e12ba1c8eac60d9022d26c2833aba1f57dc
Parents: 6254526
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Oct 22 11:14:09 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Thu Oct 22 15:53:06 2015 +0200

----------------------------------------------------------------------
 .../digitalocean2/DigitalOcean2ApiMetadata.java |  8 +++
 ...igitalOcean2ComputeServiceContextModule.java | 29 ++++++++--
 .../extensions/DigitalOcean2ImageExtension.java | 12 ++--
 .../config/DropletInStatusPredicateTest.java    | 58 ++++++++++++++++++++
 4 files changed, 96 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/200e0e12/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 7e9861d..25b42c7 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/DigitalOcean2ApiMetadata.java
@@ -20,6 +20,9 @@ import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
 import static org.jclouds.compute.config.ComputeServiceProperties.POLL_INITIAL_PERIOD;
 import static org.jclouds.compute.config.ComputeServiceProperties.POLL_MAX_PERIOD;
 import static org.jclouds.compute.config.ComputeServiceProperties.TEMPLATE;
+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.oauth.v2.config.CredentialType.BEARER_TOKEN_CREDENTIALS;
 import static org.jclouds.oauth.v2.config.OAuthProperties.AUDIENCE;
 import static org.jclouds.oauth.v2.config.OAuthProperties.CREDENTIAL_TYPE;
@@ -68,6 +71,11 @@ public class DigitalOcean2ApiMetadata extends BaseHttpApiMetadata<DigitalOcean2A
       properties.put(TEMPLATE, "osFamily=UBUNTU,os64Bit=true");
       properties.put(POLL_INITIAL_PERIOD, 5000);
       properties.put(POLL_MAX_PERIOD, 20000);
+      // Node operations in DigitalOcean can be quite slow. Use a 5 minutes
+      // timeout by default
+      properties.put(TIMEOUT_NODE_RUNNING, 300000);
+      properties.put(TIMEOUT_NODE_SUSPENDED, 300000);
+      properties.put(TIMEOUT_NODE_TERMINATED, 300000);
       return properties;
    }
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/200e0e12/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 c2ed858..7159634 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
@@ -108,16 +108,16 @@ public class DigitalOcean2ComputeServiceContextModule extends
    @Named(TIMEOUT_NODE_RUNNING)
    protected Predicate<Integer> provideDropletRunningPredicate(final DigitalOcean2Api api, Timeouts timeouts,
          PollPeriod pollPeriod) {
-      return retry(new ActionDonePredicate(api), timeouts.nodeRunning, pollPeriod.pollInitialPeriod,
-            pollPeriod.pollMaxPeriod);
+      return retry(new DropletInStatusPredicate(api, Droplet.Status.ACTIVE), timeouts.nodeRunning,
+            pollPeriod.pollInitialPeriod, pollPeriod.pollMaxPeriod);
    }
 
    @Provides
    @Named(TIMEOUT_NODE_SUSPENDED)
    protected Predicate<Integer> provideDropletSuspendedPredicate(final DigitalOcean2Api api, Timeouts timeouts,
          PollPeriod pollPeriod) {
-      return retry(new ActionDonePredicate(api), timeouts.nodeSuspended, pollPeriod.pollInitialPeriod,
-            pollPeriod.pollMaxPeriod);
+      return retry(new DropletInStatusPredicate(api, Droplet.Status.OFF), timeouts.nodeSuspended,
+            pollPeriod.pollInitialPeriod, pollPeriod.pollMaxPeriod);
    }
 
    @Provides
@@ -188,11 +188,30 @@ public class DigitalOcean2ComputeServiceContextModule extends
 
       @Override
       public boolean apply(Integer input) {
-         checkNotNull(input, "droplet");
+         checkNotNull(input, "droplet id");
          Droplet droplet = api.dropletApi().get(input);
          return droplet == null;
       }
    }
+   
+   @VisibleForTesting
+   static class DropletInStatusPredicate implements Predicate<Integer> {
+
+      private final DigitalOcean2Api api;
+      private final Droplet.Status status;
+
+      public DropletInStatusPredicate(DigitalOcean2Api api, Droplet.Status status) {
+         this.api = checkNotNull(api, "api must not be null");
+         this.status = checkNotNull(status, "status must not be null");
+      }
+
+      @Override
+      public boolean apply(Integer input) {
+         checkNotNull(input, "droplet id");
+         Droplet droplet = api.dropletApi().get(input);
+         return droplet != null && status == droplet.status();
+      }
+   }
 
    @VisibleForTesting
    static class RegionAvailablePredicate implements Predicate<Region> {

http://git-wip-us.apache.org/repos/asf/jclouds/blob/200e0e12/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 77ccd2a..524e4d1 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
@@ -86,14 +86,14 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
    public ListenableFuture<Image> createImage(ImageTemplate template) {
       checkState(template instanceof CloneImageTemplate, "DigitalOcean only supports creating images through cloning.");
       final CloneImageTemplate cloneTemplate = (CloneImageTemplate) template;
+      int dropletId = Integer.parseInt(cloneTemplate.getSourceNodeId());
 
       // Droplet needs to be stopped
-      int dropletId = Integer.parseInt(cloneTemplate.getSourceNodeId());
-      Action powerOffEvent = api.dropletApi().powerOff(dropletId);
-      checkState(nodeStoppedPredicate.apply(powerOffEvent.id()), "node was not powered off in the configured timeout");
-      
       Droplet droplet = api.dropletApi().get(dropletId);
-      checkState(droplet.status() == Status.OFF, "node was not powered off in the configured timeout");
+      if (droplet.status() != Status.OFF) {
+         api.dropletApi().powerOff(dropletId);
+         checkState(nodeStoppedPredicate.apply(dropletId), "node was not powered off in the configured timeout");
+      }
 
       Action snapshotEvent = api.dropletApi().snapshot(Integer.parseInt(cloneTemplate.getSourceNodeId()),
             cloneTemplate.getName());
@@ -103,7 +103,7 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
       // Until the process completes we don't have enough information to build an image to return
       checkState(imageAvailablePredicate.apply(snapshotEvent.id()),
             "snapshot failed to complete in the configured timeout");
-
+      
       org.jclouds.digitalocean2.domain.Image snapshot = api.imageApi().list().concat().firstMatch(
             new Predicate<org.jclouds.digitalocean2.domain.Image>() {
                @Override

http://git-wip-us.apache.org/repos/asf/jclouds/blob/200e0e12/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletInStatusPredicateTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletInStatusPredicateTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletInStatusPredicateTest.java
new file mode 100644
index 0000000..4445907
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/config/DropletInStatusPredicateTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.compute.config;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Date;
+
+import org.easymock.EasyMock;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.config.DigitalOcean2ComputeServiceContextModule.DropletInStatusPredicate;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.Droplet.Status;
+import org.jclouds.digitalocean2.features.DropletApi;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+@Test(groups = "unit", testName = "DropletInStatusPredicateTest")
+public class DropletInStatusPredicateTest {
+
+   public void testDropletSuspended() {
+      DropletApi dropletApi = EasyMock.createMock(DropletApi.class);
+      DigitalOcean2Api api = EasyMock.createMock(DigitalOcean2Api.class);
+
+      expect(dropletApi.get(1)).andReturn(mockDroplet(Status.ACTIVE));
+      expect(dropletApi.get(2)).andReturn(mockDroplet(Status.OFF));
+      expect(api.dropletApi()).andReturn(dropletApi).times(2);
+      replay(dropletApi, api);
+
+      DropletInStatusPredicate predicate = new DropletInStatusPredicate(api, Status.OFF);
+      assertFalse(predicate.apply(1));
+      assertTrue(predicate.apply(2));
+   }
+
+   private static Droplet mockDroplet(Status status) {
+      return Droplet.create(1, "foo", 1024, 1, 20, false, new Date(), status,
+            ImmutableList.<Integer> of(), ImmutableList.<Integer> of(), ImmutableList.<String> of(), null, null, null,
+            "", null, null);
+   }
+}


[15/19] jclouds git commit: JCLOUDS-1052: Fix DigitalOcean2 deleteImage

Posted by na...@apache.org.
JCLOUDS-1052: Fix DigitalOcean2 deleteImage


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

Branch: refs/heads/master
Commit: 1ac6fa8426d645d74409801464101d85bff03625
Parents: 575d39e
Author: Ignasi Barrera <na...@apache.org>
Authored: Tue Dec 22 23:05:29 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Dec 22 23:12:59 2015 +0100

----------------------------------------------------------------------
 .../extensions/DigitalOcean2ImageExtension.java | 24 ++++++++++++--------
 1 file changed, 14 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/1ac6fa84/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 524e4d1..56e67de 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
@@ -118,17 +118,21 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
 
    @Override
    public boolean deleteImage(String id) {
-      try {
-         // 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.imageApi().delete(imageId);
+      String imageId = ImageInRegion.extractImageId(id);
+      Integer numericId = Ints.tryParse(imageId); // User images don't have a slug, so we expect a numeric id here
+
+      if (numericId != null) {
+         try {
+            logger.debug(">> deleting image %s...", id);
+            api.imageApi().delete(numericId);
+            return true;
+         } catch (Exception ex) {
+            logger.error(ex, ">> error deleting image %s", id);
          }
-         return true;
-      } catch (Exception ex) {
-         return false;
+      } else {
+         logger.warn(">> image %s is not a user image and cannot be deleted", id);
       }
+
+      return false;
    }
 }


[17/19] jclouds git commit: Improved DigitalOcean image extension

Posted by na...@apache.org.
Improved DigitalOcean image extension

Do not block when waiting for the images to become available.


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

Branch: refs/heads/master
Commit: c188570444961347e05b555f5162570139954a4c
Parents: 62f1c0b
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Dec 24 11:29:46 2015 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Wed Jan 20 12:24:46 2016 +0100

----------------------------------------------------------------------
 .../extensions/DigitalOcean2ImageExtension.java | 47 ++++++++++++--------
 1 file changed, 29 insertions(+), 18 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/c1885704/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 56e67de..3baf146 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
@@ -17,17 +17,18 @@
 package org.jclouds.digitalocean2.compute.extensions;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.common.util.concurrent.Futures.immediateFuture;
 import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_IMAGE_AVAILABLE;
 import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_SUSPENDED;
 
 import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
 
 import javax.annotation.Resource;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
+import org.jclouds.Constants;
 import org.jclouds.compute.domain.CloneImageTemplate;
 import org.jclouds.compute.domain.Image;
 import org.jclouds.compute.domain.ImageTemplate;
@@ -45,6 +46,8 @@ import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.primitives.Ints;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.UncheckedTimeoutException;
 
 /**
  * The {@link org.jclouds.compute.extensions.ImageExtension} implementation for the DigitalOcean provider.
@@ -60,15 +63,18 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
    private final Predicate<Integer> imageAvailablePredicate;
    private final Predicate<Integer> nodeStoppedPredicate;
    private final Function<ImageInRegion, Image> imageTransformer;
+   private final ListeningExecutorService userExecutor;
 
    @Inject DigitalOcean2ImageExtension(DigitalOcean2Api api,
          @Named(TIMEOUT_IMAGE_AVAILABLE) Predicate<Integer> imageAvailablePredicate,
          @Named(TIMEOUT_NODE_SUSPENDED) Predicate<Integer> nodeStoppedPredicate,
-         Function<ImageInRegion, Image> imageTransformer) {
+         Function<ImageInRegion, Image> imageTransformer,
+         @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor) {
       this.api = api;
       this.imageAvailablePredicate = imageAvailablePredicate;
       this.nodeStoppedPredicate = nodeStoppedPredicate;
       this.imageTransformer = imageTransformer;
+      this.userExecutor = userExecutor;
    }
 
    @Override
@@ -89,31 +95,36 @@ public class DigitalOcean2ImageExtension implements ImageExtension {
       int dropletId = Integer.parseInt(cloneTemplate.getSourceNodeId());
 
       // Droplet needs to be stopped
-      Droplet droplet = api.dropletApi().get(dropletId);
+      final Droplet droplet = api.dropletApi().get(dropletId);
       if (droplet.status() != Status.OFF) {
          api.dropletApi().powerOff(dropletId);
          checkState(nodeStoppedPredicate.apply(dropletId), "node was not powered off in the configured timeout");
       }
 
-      Action snapshotEvent = api.dropletApi().snapshot(Integer.parseInt(cloneTemplate.getSourceNodeId()),
+      final Action snapshotEvent = api.dropletApi().snapshot(Integer.parseInt(cloneTemplate.getSourceNodeId()),
             cloneTemplate.getName());
 
       logger.info(">> registered new Image, waiting for it to become available");
 
-      // Until the process completes we don't have enough information to build an image to return
-      checkState(imageAvailablePredicate.apply(snapshotEvent.id()),
-            "snapshot failed to complete in the configured timeout");
-      
-      org.jclouds.digitalocean2.domain.Image snapshot = api.imageApi().list().concat().firstMatch(
-            new Predicate<org.jclouds.digitalocean2.domain.Image>() {
-               @Override
-               public boolean apply(org.jclouds.digitalocean2.domain.Image input) {
-                  return input.name().equals(cloneTemplate.getName());
-               }
-            }).get();
-
-      // By default snapshots are only available in the Droplet's region
-      return immediateFuture(imageTransformer.apply(ImageInRegion.create(snapshot, droplet.region().slug())));
+      return userExecutor.submit(new Callable<Image>() {
+         @Override
+         public Image call() throws Exception {
+            if (imageAvailablePredicate.apply(snapshotEvent.id())) {
+               org.jclouds.digitalocean2.domain.Image snapshot = api.imageApi().list().concat()
+                     .firstMatch(new Predicate<org.jclouds.digitalocean2.domain.Image>() {
+                        @Override
+                        public boolean apply(org.jclouds.digitalocean2.domain.Image input) {
+                           return input.name().equals(cloneTemplate.getName());
+                        }
+                     }).get();
+
+               return imageTransformer.apply(ImageInRegion.create(snapshot, droplet.region().slug()));
+            }
+
+            throw new UncheckedTimeoutException("Image was not created within the time limit: "
+                  + cloneTemplate.getName());
+         }
+      });
    }
 
    @Override


[03/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Posted by na...@apache.org.
http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatusTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatusTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatusTest.java
new file mode 100644
index 0000000..9da855c
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletStatusToStatusTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.NodeMetadata.Status.UNRECOGNIZED;
+import static org.testng.Assert.assertNotEquals;
+
+import org.jclouds.digitalocean2.domain.Droplet.Status;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "DropletStatusToStatusTest")
+public class DropletStatusToStatusTest {
+
+   @Test
+   public void testAllStatesHaveMapping() {
+      DropletStatusToStatus function = new DropletStatusToStatus();
+      for (Status status : Status.values()) {
+         assertNotEquals(function.apply(status), UNRECOGNIZED);
+      }
+
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..27dbad9
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/DropletToNodeMetadataTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.getOnlyElement;
+import static org.jclouds.compute.domain.Image.Status.AVAILABLE;
+import static org.jclouds.compute.domain.NodeMetadata.Status.RUNNING;
+import static org.jclouds.digitalocean2.domain.Droplet.Status.ACTIVE;
+import static org.testng.Assert.assertEquals;
+
+import java.text.ParseException;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.HardwareBuilder;
+import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.ImageBuilder;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.NodeMetadataBuilder;
+import org.jclouds.compute.domain.OperatingSystem;
+import org.jclouds.compute.domain.OsFamily;
+import org.jclouds.compute.domain.Processor;
+import org.jclouds.compute.domain.Volume.Type;
+import org.jclouds.compute.domain.VolumeBuilder;
+import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.Networks;
+import org.jclouds.digitalocean2.domain.Networks.Address;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.domain.Credentials;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.jclouds.domain.LoginCredentials;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+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.Maps;
+import com.google.inject.Guice;
+
+@Test(groups = "unit", testName = "DropletToNodeMetadataTest")
+public class DropletToNodeMetadataTest {
+
+   private org.jclouds.digitalocean2.domain.Image image;
+   
+   private Region region;
+   
+   private Set<Hardware> hardwares;
+
+   private Set<Image> images;
+
+   private Set<Location> locations;
+
+   private LoginCredentials credentials;
+
+   private DropletToNodeMetadata function;
+
+   @BeforeMethod
+   public void setup() {
+      image = org.jclouds.digitalocean2.domain.Image.create(1, "14.04 x64",
+            "distribution", "Ubuntu", "ubuntu-1404-x86", true, ImmutableList.of("sfo1"), new Date());
+      region = Region.create("sfo1", "San Francisco 1", ImmutableList.of("2gb"), true, ImmutableList.<String> of());
+      
+      images = ImmutableSet.of(new ImageBuilder()
+            .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().id("2gb").providerId("2gb").name("mock hardware")
+            .processor(new Processor(1.0, 1.0)).ram(2048)
+            .volume(new VolumeBuilder().size(20f).type(Type.LOCAL).build()).build());
+
+      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());
+
+      credentials = LoginCredentials.builder().user("foo").password("bar").build();
+
+      function = createNodeParser(hardwares, images, locations, ImmutableMap.of("node#1", (Credentials) credentials));
+   }
+
+   @Test
+   public void testConvertDroplet() throws ParseException {
+      Droplet droplet = Droplet.create(
+            1,
+            "mock-droplet",
+            1,
+            1,
+            1,
+            false,
+            new Date(),
+            Droplet.Status.ACTIVE,
+            ImmutableList.<Integer> of(),
+            ImmutableList.<Integer> of(),
+            ImmutableList.<String> of(),
+            region,
+            image,
+            null,
+            "2gb",
+            Networks.create(
+                  ImmutableList.of(Address.create("84.45.69.3", "255.255.255.0", "84.45.69.1", "public"),
+                        Address.create("192.168.2.5", "255.255.255.0", "192.168.2.1", "private")),
+                  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")
+            .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
+      org.jclouds.digitalocean2.domain.Image image = org.jclouds.digitalocean2.domain.Image.create(2, "14.04 x64",
+            "distribution", "Ubuntu", "ubuntu2-1404-x86", true, ImmutableList.of("sfo1"), new Date());
+      
+      Droplet droplet = Droplet.create(
+            1,
+            "mock-droplet",
+            1,
+            1,
+            1,
+            false,
+            new Date(),
+            Droplet.Status.ACTIVE,
+            ImmutableList.<Integer> of(),
+            ImmutableList.<Integer> of(),
+            ImmutableList.<String> of(),
+            region,
+            image,
+            null,
+            "2gb",
+            Networks.create(
+                  ImmutableList.of(Address.create("84.45.69.3", "255.255.255.0", "84.45.69.1", "public"),
+                        Address.create("192.168.2.5", "255.255.255.0", "192.168.2.1", "private")),
+                  ImmutableList.<Networks.Address> of()), null);
+
+      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(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
+      assertEquals(actual.getStatus(), expected.getStatus());
+      assertEquals(actual.getBackendStatus(), expected.getBackendStatus());
+      assertEquals(actual.getLoginPort(), expected.getLoginPort());
+      assertEquals(actual.getPublicAddresses(), expected.getPublicAddresses());
+      assertEquals(actual.getPrivateAddresses(), expected.getPrivateAddresses());
+      assertEquals(actual.getCredentials(), expected.getCredentials());
+      assertEquals(actual.getGroup(), expected.getGroup());
+      assertEquals(actual.getImageId(), expected.getImageId());
+      assertEquals(actual.getHardware(), expected.getHardware());
+      assertEquals(actual.getOperatingSystem(), expected.getOperatingSystem());
+      assertEquals(actual.getHostname(), expected.getHostname());
+   }
+
+   private DropletToNodeMetadata createNodeParser(final Set<Hardware> hardware, final Set<Image> images,
+         final Set<Location> locations, Map<String, Credentials> credentialStore) {
+      Supplier<Set<? extends Location>> locationSupplier = new Supplier<Set<? extends Location>>() {
+         @Override
+         public Set<? extends Location> get() {
+            return locations;
+         }
+      };
+
+      Supplier<Map<String, ? extends Hardware>> hardwareSupplier = new Supplier<Map<String, ? extends Hardware>>() {
+         @Override
+         public Map<String, ? extends Hardware> get() {
+            return Maps.uniqueIndex(hardware, new Function<Hardware, String>() {
+               @Override
+               public String apply(Hardware input) {
+                  return input.getId();
+               }
+            });
+         }
+      };
+
+      Supplier<Map<String, ? extends Image>> imageSupplier = new Supplier<Map<String, ? extends Image>>() {
+         @Override
+         public Map<String, ? extends Image> get() {
+            return Maps.uniqueIndex(images, new Function<Image, String>() {
+               @Override
+               public String apply(Image input) {
+                  return input.getId();
+               }
+            });
+         }
+      };
+
+      GroupNamingConvention.Factory namingConvention = Guice.createInjector().getInstance(GroupNamingConvention.Factory.class);
+
+      return new DropletToNodeMetadata(imageSupplier, hardwareSupplier, locationSupplier, new DropletStatusToStatus(),
+            namingConvention, credentialStore);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/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
new file mode 100644
index 0000000..6ab020c
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/ImageToImageTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
new file mode 100644
index 0000000..879091b
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/RegionToLocationTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.getOnlyElement;
+import static org.testng.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.DigitalOcean2ProviderMetadata;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.domain.Location;
+import org.jclouds.domain.LocationBuilder;
+import org.jclouds.domain.LocationScope;
+import org.jclouds.location.suppliers.all.JustProvider;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+@Test(groups = "unit", testName = "RegionToLocationTest")
+public class RegionToLocationTest {
+
+   @Test
+   public void testConvertRegion() {
+      DigitalOcean2ProviderMetadata metadata = new DigitalOcean2ProviderMetadata();
+      JustProvider locationsSupplier = new JustProvider(metadata.getId(), Suppliers.<URI> ofInstance(URI
+            .create(metadata.getEndpoint())), ImmutableSet.<String> of());
+
+      Region region = Region.create("reg1", "Region1", ImmutableList.<String> of(), true, ImmutableList.<String> of());
+      Location expected = new LocationBuilder().id("reg1").description("reg1/Region 1")
+            .parent(getOnlyElement(locationsSupplier.get())).scope(LocationScope.REGION).build();
+
+      RegionToLocation function = new RegionToLocation(locationsSupplier);
+      assertEquals(function.apply(region), expected);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
new file mode 100644
index 0000000..cba55bf
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/SizeToHardwareTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.testng.Assert.assertEquals;
+
+import org.jclouds.compute.domain.Hardware;
+import org.jclouds.compute.domain.HardwareBuilder;
+import org.jclouds.compute.domain.Processor;
+import org.jclouds.compute.domain.Volume.Type;
+import org.jclouds.compute.domain.VolumeBuilder;
+import org.jclouds.digitalocean2.domain.Size;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Unit tests for the {@link SizeToHardware} class.
+ */
+@Test(groups = "unit", testName = "SizeToHardwareTest")
+public class SizeToHardwareTest {
+
+   @Test
+   public void testConvertSize() {
+      Size size = Size.create("2gb", true, 1.0f, 10f, 0.05f, 2048, 1, 20, ImmutableList.<String> of());
+      Hardware expected = new HardwareBuilder().id("2gb").providerId("2gb").name("2gb")
+            .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();
+
+      SizeToHardware function = new SizeToHardware();
+      assertEquals(function.apply(size), expected);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
new file mode 100644
index 0000000..c3a6cd2
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/functions/TemplateOptionsToStatementWithoutPublicKeyTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.compute.options.TemplateOptions;
+import org.jclouds.scriptbuilder.domain.OsFamily;
+import org.jclouds.scriptbuilder.domain.Statement;
+import org.jclouds.scriptbuilder.domain.StatementList;
+import org.jclouds.scriptbuilder.statements.ssh.InstallRSAPrivateKey;
+import org.jclouds.ssh.SshKeys;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link TemplateOptionsToStatementWithoutPublicKey} class.
+ */
+@Test(groups = "unit", testName = "TemplateOptionsToStatementWithoutPublicKeyTest")
+public class TemplateOptionsToStatementWithoutPublicKeyTest {
+
+   @Test
+   public void testPublicKeyDoesNotGenerateAuthorizePublicKeyStatementIfOnlyPublicKeyOptionsConfigured() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public"));
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      assertNull(function.apply(options));
+   }
+
+   @Test
+   public void testPublicAndRunScriptKeyDoesNotGenerateAuthorizePublicKeyStatementIfRunScriptPresent() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public")).runScript("uptime");
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      Statement statement = function.apply(options);
+
+      assertEquals(statement.render(OsFamily.UNIX), "uptime\n");
+   }
+
+   @Test
+   public void testPublicAndPrivateKeyAndRunScriptDoesNotGenerateAuthorizePublicKeyStatementIfOtherOptionsPresent() {
+      Map<String, String> keys = SshKeys.generate();
+      TemplateOptions options = TemplateOptions.Builder.authorizePublicKey(keys.get("public"))
+            .installPrivateKey(keys.get("private")).runScript("uptime");
+
+      TemplateOptionsToStatementWithoutPublicKey function = new TemplateOptionsToStatementWithoutPublicKey();
+      Statement statement = function.apply(options);
+
+      assertTrue(statement instanceof StatementList);
+      StatementList statements = (StatementList) statement;
+
+      assertEquals(statements.size(), 2);
+      assertEquals(statements.get(0).render(OsFamily.UNIX), "uptime\n");
+      assertTrue(statements.get(1) instanceof InstallRSAPrivateKey);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
new file mode 100644
index 0000000..982224c
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/options/DigitalOcean2TemplateOptionsTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.options;
+
+import static org.testng.Assert.assertEquals;
+
+import org.jclouds.compute.options.TemplateOptions;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+@Test(groups = "unit", testName = "DigitalOcean2TemplateOptionsTest")
+public class DigitalOcean2TemplateOptionsTest {
+
+   @Test
+   public void testSShKeyIds() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().sshKeyIds(ImmutableSet.of(1, 2, 3));
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getSshKeyIds(), ImmutableSet.of(1, 2, 3));
+   }
+
+   @Test
+   public void testPrivateNetworking() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().privateNetworking(true);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getPrivateNetworking(), true);
+   }
+
+   @Test
+   public void testBackupsEnabled() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().backupsEnabled(true);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getBackupsEnabled(), true);
+   }
+   
+   @Test
+   public void testAutoCreateKeyPair() {
+      TemplateOptions options = new DigitalOcean2TemplateOptions().autoCreateKeyPair(false);
+      assertEquals(options.as(DigitalOcean2TemplateOptions.class).getAutoCreateKeyPair(), false);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
new file mode 100644
index 0000000..d6bd0fc
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/domain/OperatingSystemTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.domain;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "OperatingSystemTest")
+public class OperatingSystemTest {
+
+   public void testParseStandard64bit() {
+      OperatingSystem os = OperatingSystem.create("12.10 x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testLongVersionStandard64bit() {
+      OperatingSystem os = OperatingSystem.create("12.10.1 x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10.1");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseStandard64bitWithPrefix() {
+      OperatingSystem os = OperatingSystem.create("Arch Linux 12.10 x64 Desktop", "Arch Linux");
+
+      assertEquals(os.distribution(), Distribution.ARCHLINUX);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseStandard() {
+      OperatingSystem os = OperatingSystem.create("12.10 x32", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "x32");
+      assertFalse(os.is64bit());
+
+      os = OperatingSystem.create("6.5 x64", "CentOS");
+
+      assertEquals(os.distribution(), Distribution.CENTOS);
+      assertEquals(os.version(), "6.5");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+
+      os = OperatingSystem.create("6.5 x64", "Centos");
+
+      assertEquals(os.distribution(), Distribution.CENTOS);
+      assertEquals(os.version(), "6.5");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseNoArch() {
+      OperatingSystem os = OperatingSystem.create("12.10", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "12.10");
+      assertEquals(os.arch(), "");
+      assertFalse(os.is64bit());
+   }
+
+   public void testParseNoVersion() {
+      OperatingSystem os = OperatingSystem.create("x64", "Ubuntu");
+
+      assertEquals(os.distribution(), Distribution.UBUNTU);
+      assertEquals(os.version(), "");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+
+   public void testParseUnknownDistribution() {
+      OperatingSystem os = OperatingSystem.create("12.04 x64", "Foo");
+
+      assertEquals(os.distribution(), Distribution.UNRECOGNIZED);
+      assertEquals(os.version(), "12.04");
+      assertEquals(os.arch(), "x64");
+      assertTrue(os.is64bit());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.java
new file mode 100644
index 0000000..44a9a17
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiLiveTest.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.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "ActionApiLiveTest")
+public class ActionApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   public void testListActions() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.type());
+         }
+      }), "All actions must have the 'type' field populated");
+      assertTrue(found.get() > 0, "Expected some actions to be returned");
+   }
+   
+   public void testListActionsOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1).perPage(5)).allMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.type());
+         }
+      }), "All actions must have the 'type' field populated");
+      assertTrue(found.get() > 0, "Expected some actions to be returned");
+   }
+   
+   public void testGetAction() {
+      Optional<Action> first = api().list().concat().first();
+      assertTrue(first.isPresent(), "At least one action was expected to exist");
+      assertNotNull(api().get(first.get().id()));
+   }
+   
+   private ActionApi api() {
+      return api.actionApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
new file mode 100644
index 0000000..aa890d5
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ActionApiMockTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "ActionApiMockTest", singleThreaded = true)
+public class ActionApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListActions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+      server.enqueue(jsonResponse("/actions-last.json"));
+
+      Iterable<Action> actions = api.actionApi().list().concat();
+
+      assertEquals(size(actions), 8); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/actions");
+      assertSent(server, "GET", "/actions?page=2&per_page=5");
+   }
+
+   public void testListActionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.actionApi().list().concat();
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions");
+   }
+
+   public void testListActionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+
+      Iterable<Action> actions = api.actionApi().list(page(1).perPage(5));
+
+      assertEquals(size(actions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/actions?page=1&per_page=5");
+   }
+
+   public void testListActionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.actionApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions?page=1&per_page=5");
+   }
+   
+   public void testGetAction() throws InterruptedException {
+      server.enqueue(jsonResponse("/action.json"));
+
+      Action action = api.actionApi().get(1);
+
+      assertEquals(action, actionFromResource("/action.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions/1");
+   }
+
+   public void testGetActionReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Action action = api.actionApi().get(1);
+
+      assertNull(action);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/actions/1");
+   }
+   
+   private Action actionFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Action>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
new file mode 100644
index 0000000..f451d2e
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiLiveTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.util.logging.Logger.getAnonymousLogger;
+import static org.jclouds.digitalocean2.domain.Droplet.Status.ACTIVE;
+import static org.jclouds.digitalocean2.domain.Droplet.Status.OFF;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jclouds.compute.ComputeTestUtils;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Backup;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.domain.Kernel;
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.domain.Snapshot;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+
+@Test(groups = "live", testName = "DropletApiLiveTest")
+public class DropletApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   private Region region;
+   private Size size;
+   private Image image;
+   private Key key;
+   private int dropletId = -1;
+   
+   @BeforeClass
+   public void setupDroplet() {
+      region = firstAvailableRegion();
+      size = cheapestSizeInRegion(region);
+      image = ubuntuImageInRegion(region);
+      
+      Map<String, String> keyPair = ComputeTestUtils.setupKeyPair();
+      key = api.keyApi().create(prefix + "-droplet-livetest", keyPair.get("public"));
+   }
+   
+   @AfterClass(alwaysRun = true)
+   public void tearDown() {
+      if (key != null) {
+         api.keyApi().delete(key.id());
+      }
+   }
+   
+   public void testCreate() {
+      DropletCreate dropletCreate = api().create(prefix + "-droplet-livetest", region.slug(), size.slug(), image.slug(),
+            CreateDropletOptions.builder().backupsEnabled(true).addSshKeyId(key.id()).build());
+      assertActionCompleted(getOnlyElement(dropletCreate.links().actions()).id());
+      dropletId = dropletCreate.droplet().id();
+      Droplet droplet = api().get(dropletId);
+      assertNotNull(droplet, "Droplet should not be null");
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testCreate")
+   public void testListDroplets() {
+      assertTrue(api().list().concat().anyMatch(new Predicate<Droplet>() {
+         @Override
+         public boolean apply(Droplet input) {
+            return input.id() == dropletId;
+         }
+      }), "The created droplet must be in the list");
+   }
+
+   @Test(dependsOnMethods = "testCreate")
+   public void testListKernels() {
+      Iterable<Kernel> kernels = api().listKernels(dropletId).concat();
+      assertEquals(kernels.iterator().next().name(), "DO-recovery-static-fsck");
+   }
+   
+   @Test(dependsOnMethods = "testListKernels")
+   public void testPowerOff() {
+      Action action = api().powerOff(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), OFF, "Droplet should be off");
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testPowerOff")
+   public void testSnapshots() {
+      Action action = api().snapshot(dropletId, prefix + dropletId + "-snapshot");
+      assertActionCompleted(action.id());
+      
+      List<Snapshot> snapshots = api().listSnapshots(dropletId).concat().toList();
+      assertEquals(snapshots.size(), 1, "Must contain 1 snapshot");
+      
+      for (Snapshot snapshot : snapshots) {
+         try {
+            api.imageApi().delete(snapshot.id());
+         } catch (Exception ex) {
+            getAnonymousLogger().warning("Could not delete snapshot: " + snapshot.id());
+         }
+      }
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testBackups() {
+      Iterable<Backup> backups = api().listBackups(dropletId).concat();
+      // Backups are automatically taken by DO on a weekly basis, so we can't guarantee
+      // there will be any backup available. Just check that the call succeeds
+      assertNotNull(backups);
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testListActions() {
+      FluentIterable<Action> actions = api().listActions(dropletId).concat();
+      assertTrue(actions.anyMatch(new Predicate<Action>() {
+         @Override
+         public boolean apply(Action input) {
+            return "snapshot".equals(input.type());
+         }
+      }));
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testSnapshots")
+   public void testPowerOn() {
+      // Apparently droplets are automatically powered on after the snapshot process
+      Action action = api().powerOff(dropletId);
+      assertActionCompleted(action.id());
+      
+      action = api().powerOn(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be Active");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testPowerOn")
+   public void testReboot() {
+      Action action = api().reboot(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testReboot")
+   public void testPowerCycle() {
+      Action action = api().powerCycle(dropletId);
+      assertActionCompleted(action.id());
+      Droplet droplet = api().get(dropletId);
+      assertEquals(droplet.status(), ACTIVE, "Droplet should be off");
+   }
+   
+   @Test(groups = "live", dependsOnMethods = "testPowerCycle")
+   public void testShutdown() {
+      Action action = api().shutdown(dropletId);
+      assertActionCompleted(action.id());
+      // The shutdown action can fail if the shutdown command fails in the guest OS
+      // We can not guarantee that a graceful shutdown action will en up in the droplet
+      // being in OFF state
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testShutdown", alwaysRun = true)
+   public void testDelete() throws InterruptedException {
+      if (dropletId != -1) {
+         api().delete(dropletId);
+         assertNodeTerminated(dropletId);
+         assertNull(api().get(dropletId));
+      }
+   }
+   
+   private DropletApi api() {
+      return api.dropletApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
new file mode 100644
index 0000000..dcd6352
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/DropletApiMockTest.java
@@ -0,0 +1,401 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Backup;
+import org.jclouds.digitalocean2.domain.Droplet;
+import org.jclouds.digitalocean2.domain.DropletCreate;
+import org.jclouds.digitalocean2.domain.Kernel;
+import org.jclouds.digitalocean2.domain.Snapshot;
+import org.jclouds.digitalocean2.domain.options.CreateDropletOptions;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "DropletApiMockTest", singleThreaded = true)
+public class DropletApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListDroplets() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplets-first.json"));
+      server.enqueue(jsonResponse("/droplets-last.json"));
+
+      Iterable<Droplet> droplets = api.dropletApi().list().concat();
+
+      assertEquals(size(droplets), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets");
+      assertSent(server, "GET", "/droplets?page=2&per_page=1");
+   }
+
+   public void testListDropletsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Droplet> droplets = api.dropletApi().list().concat();
+
+      assertTrue(isEmpty(droplets));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets");
+   }
+
+   public void testListDropletsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplets-first.json"));
+
+      Iterable<Droplet> droplets = api.dropletApi().list(page(1).perPage(20));
+
+      assertEquals(size(droplets), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets?page=1&per_page=20");
+   }
+
+   public void testListDropletsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Droplet> droplets = api.dropletApi().list(page(1).perPage(20));
+
+      assertTrue(isEmpty(droplets));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets?page=1&per_page=20");
+   }
+   
+   public void testGetDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplet.json"));
+
+      Droplet droplet = api.dropletApi().get(1);
+
+      assertEquals(droplet, dropletFromResource("/droplet.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1");
+   }
+
+   public void testGetDropletReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Droplet droplet = api.dropletApi().get(1);
+
+      assertNull(droplet);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1");
+   }
+   
+   public void testCreateDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/droplet-create-res.json"));
+
+      DropletCreate droplet = api.dropletApi().create("digitalocean2-s-d5e", "sfo1", "512mb", "6374124", CreateDropletOptions.builder().addSshKeyId(421192).build());
+
+      assertEquals(droplet, objectFromResource("/droplet-create-res.json", DropletCreate.class));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets", stringFromResource("/droplet-create-req.json"));
+   }
+   
+   public void testListKernels() throws InterruptedException {
+      server.enqueue(jsonResponse("/kernels-first.json"));
+      server.enqueue(jsonResponse("/kernels-last.json"));
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561).concat();
+
+      assertEquals(size(kernels), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/kernels");
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=2");
+   }
+
+   public void testListKernelsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561).concat();
+
+      assertTrue(isEmpty(kernels));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/kernels");
+   }
+   
+   public void testListKernelsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/kernels-first.json"));
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561, page(1).perPage(20));
+
+      assertEquals(size(kernels), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=1&per_page=20");
+   }
+
+   public void testListKernelsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Kernel> kernels = api.dropletApi().listKernels(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(kernels));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/kernels?page=1&per_page=20");
+   }
+   
+   public void testListActions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+      server.enqueue(jsonResponse("/actions-last.json"));
+
+      Iterable<Action> actions = api.dropletApi().listActions(1).concat();
+
+      assertEquals(size(actions), 8); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/1/actions");
+      assertSent(server, "GET", "/droplets/1/actions?page=2&per_page=5");
+   }
+
+   public void testListActionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.dropletApi().listActions(1).concat();
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1/actions");
+   }
+
+   public void testListActionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/actions-first.json"));
+
+      Iterable<Action> actions = api.dropletApi().listActions(1, page(1).perPage(5));
+
+      assertEquals(size(actions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/1/actions?page=1&per_page=5");
+   }
+
+   public void testListActionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Action> actions = api.dropletApi().listActions(1, page(1).perPage(5));
+
+      assertTrue(isEmpty(actions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/1/actions?page=1&per_page=5");
+   }
+   
+   public void testListBackups() throws InterruptedException {
+      server.enqueue(jsonResponse("/backups-first.json"));
+      server.enqueue(jsonResponse("/backups-last.json"));
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561).concat();
+
+      assertEquals(size(backups), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/backups");
+      assertSent(server, "GET", "/droplets/5425561/backups?page=2");
+   }
+
+   public void testListBackupsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561).concat();
+
+      assertTrue(isEmpty(backups));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/backups");
+   }
+   
+   public void testListBackupsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/backups-first.json"));
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561, page(1).perPage(20));
+
+      assertEquals(size(backups), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/backups?page=1&per_page=20");
+   }
+
+   public void testListBackupsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Backup> backups = api.dropletApi().listBackups(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(backups));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/backups?page=1&per_page=20");
+   }
+   
+   public void testListSnapshots() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshots-first.json"));
+      server.enqueue(jsonResponse("/snapshots-last.json"));
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561).concat();
+
+      assertEquals(size(snapshots), 2); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/droplets/5425561/snapshots");
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=2");
+   }
+
+   public void testListSnapshotsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561).concat();
+
+      assertTrue(isEmpty(snapshots));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/snapshots");
+   }
+   
+   public void testListSnapshotsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshots-first.json"));
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561, page(1).perPage(20));
+
+      assertEquals(size(snapshots), 1);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=1&per_page=20");
+   }
+
+   public void testListSnapshotsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Snapshot> snapshots = api.dropletApi().listSnapshots(5425561, page(1).perPage(20));
+
+      assertTrue(isEmpty(snapshots));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/droplets/5425561/snapshots?page=1&per_page=20");
+   }
+   
+   public void testDeleteDroplet() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.dropletApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/droplets/1");
+   }
+
+   public void testDeleteDropletReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.dropletApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/droplets/1");
+   }
+   
+   public void testPowerCycleDroplet() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-cycle.json"));
+
+      Action action = api.dropletApi().powerCycle(1);
+      
+      assertEquals(action, actionFromResource("/power-cycle.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_cycle\"}");
+   }
+   
+   public void testPowerOn() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-on.json"));
+
+      Action action = api.dropletApi().powerOn(1);
+      
+      assertEquals(action, actionFromResource("/power-on.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_on\"}");
+   }
+   
+   public void testPowerOff() throws InterruptedException {
+      server.enqueue(jsonResponse("/power-off.json"));
+
+      Action action = api.dropletApi().powerOff(1);
+      
+      assertEquals(action, actionFromResource("/power-off.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"power_off\"}");
+   }
+   
+   public void testReboot() throws InterruptedException {
+      server.enqueue(jsonResponse("/reboot.json"));
+
+      Action action = api.dropletApi().reboot(1);
+      
+      assertEquals(action, actionFromResource("/reboot.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"reboot\"}");
+   }
+   
+   public void testShutdown() throws InterruptedException {
+      server.enqueue(jsonResponse("/shutdown.json"));
+
+      Action action = api.dropletApi().shutdown(1);
+      
+      assertEquals(action, actionFromResource("/shutdown.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"shutdown\"}");
+   }
+   
+   public void testSnapshot() throws InterruptedException {
+      server.enqueue(jsonResponse("/snapshot.json"));
+
+      Action action = api.dropletApi().snapshot(1, "foo");
+      
+      assertEquals(action, actionFromResource("/snapshot.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/droplets/1/actions", "{\"type\":\"snapshot\",\"name\":\"foo\"}");
+   }
+   
+   private Droplet dropletFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Droplet>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+   
+   private Action actionFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Action>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
new file mode 100644
index 0000000..8c4c96e
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiLiveTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.features;
+
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.type;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "ImageApiLiveTest")
+public class ImageApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   public void testListImages() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.name());
+         }
+      }), "All images must have the 'name' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testListImagesOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1).perPage(5)).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.name());
+         }
+      }), "All images must have the 'name' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testListImagesByType() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(type("distribution").perPage(5)).allMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.distribution());
+         }
+      }), "All images must have the 'distribution' field populated");
+      assertTrue(found.get() > 0, "Expected some images to be returned");
+   }
+   
+   public void testGetImage() {
+      Optional<Image> first = api().list().concat().first();
+      assertTrue(first.isPresent(), "At least one image was expected to exist");
+      assertNotNull(api().get(first.get().id()));
+   }
+   
+   public void testGetImageBySlug() {
+      Optional<Image> first = api().list().concat().firstMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            return !isNullOrEmpty(input.slug());
+         }
+      });
+      
+      assertTrue(first.isPresent(), "At least one image with the 'slug' field set was expected to exist");
+      assertNotNull(api().get(first.get().slug()));
+   }
+   
+   // TODO: Delete live test once the create/transfer operations are implemented
+   
+   private ImageApi api() {
+      return api.imageApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
new file mode 100644
index 0000000..d172174
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/ImageApiMockTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "ImageApiMockTest", singleThreaded = true)
+public class ImageApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListImages() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+      server.enqueue(jsonResponse("/images-last.json"));
+
+      Iterable<Image> images = api.imageApi().list().concat();
+
+      assertEquals(size(images), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/images");
+      assertSent(server, "GET", "/images?page=2&per_page=5&type=distribution");
+   }
+
+   public void testListImagesReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Image> images = api.imageApi().list().concat();
+
+      assertTrue(isEmpty(images));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images");
+   }
+
+   public void testListImagesWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+
+      Iterable<Image> images = api.imageApi().list(page(1).perPage(5).type("distribution"));
+
+      assertEquals(size(images), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/images?page=1&per_page=5&type=distribution");
+   }
+
+   public void testListImagesWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Image> images = api.imageApi().list(page(1).perPage(5).type("distribution"));
+
+      assertTrue(isEmpty(images));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images?page=1&per_page=5&type=distribution");
+   }
+   
+   public void testGetImage() throws InterruptedException {
+      server.enqueue(jsonResponse("/image.json"));
+
+      Image image = api.imageApi().get(1);
+
+      assertEquals(image, imageFromResource("/image.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/1");
+   }
+
+   public void testGetImageReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Image image = api.imageApi().get(1);
+
+      assertNull(image);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/1");
+   }
+   
+   public void testGetImageUsingSlug() throws InterruptedException {
+      server.enqueue(jsonResponse("/image.json"));
+
+      Image image = api.imageApi().get("foo");
+
+      assertEquals(image, imageFromResource("/image.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/foo");
+   }
+
+   public void testGetImageUsingSlugReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Image image = api.imageApi().get("foo");
+
+      assertNull(image);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/images/foo");
+   }
+   
+   public void testDeleteImage() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.imageApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/images/1");
+   }
+
+   public void testDeleteImageReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.imageApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/images/1");
+   }
+   
+   private Image imageFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Image>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
new file mode 100644
index 0000000..2911d79
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiLiveTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.io.Resources;
+
+@Test(groups = "live", testName = "KeyApiLiveTest")
+public class KeyApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+
+   private Key dsa;
+   private Key ecdsa;
+
+   public void testCreateKey() {
+      dsa = api().create("jclouds-test-dsa", loadKey("/ssh-dsa.pub"));
+      ecdsa = api().create("jclouds-test-ecdsa", loadKey("/ssh-ecdsa.pub"));
+      
+      assertEquals(dsa.name(), "jclouds-test-dsa");
+      assertEquals(ecdsa.name(), "jclouds-test-ecdsa");
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testListKeys() {
+      FluentIterable<Key> keys = api().list().concat();
+      assertTrue(keys.size() >= 2, "At least the two created keys must exist");
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testListKeysOnePAge() {
+      FluentIterable<Key> keys = api().list(page(1));
+      assertTrue(keys.size() >= 2, "At least the two created keys must exist");
+   }
+
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testGetKey() {
+      assertEquals(api().get(dsa.id()).fingerprint(), dsa.fingerprint());
+      assertEquals(api().get(ecdsa.fingerprint()).id(), ecdsa.id());
+   }
+   
+   @Test(dependsOnMethods = "testCreateKey")
+   public void testUpdateKey() {
+      api().update(dsa.id(), "jclouds-test-dsa-updated");
+      assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated");
+      api().update(dsa.fingerprint(), "jclouds-test-dsa-updated2");
+      assertEquals(api().get(dsa.id()).name(), "jclouds-test-dsa-updated2");
+   }
+
+   @AfterClass(alwaysRun = true)
+   public void testDeleteKey() {
+      if (dsa != null) {
+         api().delete(dsa.id());
+         FluentIterable<Key> keys = api().list().concat();
+         assertFalse(keys.contains(dsa), "dsa key must not be present in list");
+      }
+      if (ecdsa != null) {
+         api().delete(ecdsa.fingerprint());
+         FluentIterable<Key>  keys = api().list().concat();
+         assertFalse(keys.contains(ecdsa), "dsa key must not be present in list");
+      }
+   }
+   
+   private String loadKey(String resourceName) {
+      try {
+         return Resources.toString(getClass().getResource(resourceName), Charsets.UTF_8);
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+
+   private KeyApi api() {
+      return api.keyApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
new file mode 100644
index 0000000..2f9d5d3
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/KeyApiMockTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.google.common.reflect.TypeToken;
+
+@Test(groups = "unit", testName = "KeyApiMockTest", singleThreaded = true)
+public class KeyApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListKeys() throws InterruptedException {
+      server.enqueue(jsonResponse("/keys-first.json"));
+      server.enqueue(jsonResponse("/keys-last.json"));
+
+      Iterable<Key> keys = api.keyApi().list().concat();
+
+      assertEquals(size(keys), 7); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/account/keys");
+      assertSent(server, "GET", "/account/keys?page=2&per_page=5");
+   }
+
+   public void testListKeysReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Key> keys = api.keyApi().list().concat();
+
+      assertTrue(isEmpty(keys));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys");
+   }
+
+   public void testListKeysWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/keys-first.json"));
+
+      Iterable<Key> keys = api.keyApi().list(page(1).perPage(5));
+
+      assertEquals(size(keys), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/account/keys?page=1&per_page=5");
+   }
+
+   public void testListKeysWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Key> keys = api.keyApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(keys));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys?page=1&per_page=5");
+   }
+   
+   public void testCreateKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json").setStatus("HTTP/1.1 201 Created"));
+      
+      String dsa = stringFromResource("/ssh-dsa.pub");
+      
+      Key key = api.keyApi().create("foo", dsa);
+      
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "/account/keys", String.format("{\"name\":\"foo\", \"public_key\":\"%s\"}", dsa));
+   }
+   
+   public void testGetKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().get(1);
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1");
+   }
+
+   public void testGetKeyReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Key key = api.keyApi().get(1);
+
+      assertNull(key);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1");
+   }
+   
+   public void testGetKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().get("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+
+   public void testGetKeyUsingFingerprintReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Key key = api.keyApi().get("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertNull(key);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+   
+   public void testUpdateKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().update(1, "foo");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "PUT", "/account/keys/1", "{\"name\":\"foo\"}");
+   }
+   
+   public void testUpdateKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(jsonResponse("/key.json"));
+
+      Key key = api.keyApi().update("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90", "foo");
+
+      assertEquals(key, keyFromResource("/key.json"));
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "PUT", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90", "{\"name\":\"foo\"}");
+   }
+   
+   public void testDeleteKey() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.keyApi().delete(1);
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1");
+   }
+
+   public void testDeleteKeyReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.keyApi().delete(1);
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1");
+   }
+   
+   public void testDeleteKeyUsingFingerprint() throws InterruptedException {
+      server.enqueue(response204());
+
+      api.keyApi().delete("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+      
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+
+   public void testDeleteKeyUsingfingerprintReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      api.keyApi().delete("1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "DELETE", "/account/keys/1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90");
+   }
+   
+   private Key keyFromResource(String resource) {
+      return onlyObjectFromResource(resource, new TypeToken<Map<String, Key>>() {
+         private static final long serialVersionUID = 1L;
+      }); 
+   }
+}


[11/19] jclouds git commit: JCLOUDS-1022: Automatically handle DigitalOcean rate limit

Posted by na...@apache.org.
JCLOUDS-1022: Automatically handle DigitalOcean rate limit


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

Branch: refs/heads/master
Commit: 7e866ad6a134357ce34ba7ae5247a088aac5c79e
Parents: 4596471
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Oct 22 00:31:58 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Oct 27 23:30:43 2015 +0100

----------------------------------------------------------------------
 .../config/DigitalOcean2Properties.java         |  33 ++++
 .../config/DigitalOcean2RateLimitModule.java    |  30 ++++
 ...DigitalOcean2RateLimitExceededException.java |  81 ++++++++++
 .../handlers/DigitalOcean2ErrorHandler.java     |   9 +-
 .../handlers/RateLimitRetryHandler.java         | 111 ++++++++++++++
 .../DigitalOcean2ComputeServiceLiveTest.java    |   8 +
 .../exceptions/RateLimitExceptionMockTest.java  |  63 ++++++++
 .../handlers/RateLimitRetryHandlerTest.java     | 153 +++++++++++++++++++
 .../internal/BaseDigitalOcean2ApiLiveTest.java  |   7 +
 .../internal/BaseDigitalOcean2ApiMockTest.java  |   7 +-
 10 files changed, 499 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2Properties.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2Properties.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2Properties.java
new file mode 100644
index 0000000..d0d1098
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2Properties.java
@@ -0,0 +1,33 @@
+/*
+ * 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.config;
+
+public final class DigitalOcean2Properties {
+
+   /**
+    * Maximum amount of time (in milliseconds) a request will wait until retrying if
+    * the rate limit is exhausted.
+    * <p>
+    * Default value: 2 minutes.
+    */
+   public static final String MAX_RATE_LIMIT_WAIT = "jclouds.max-ratelimit-wait";
+
+   private DigitalOcean2Properties() {
+      throw new AssertionError("intentionally unimplemented");
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2RateLimitModule.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2RateLimitModule.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2RateLimitModule.java
new file mode 100644
index 0000000..1b0a95f
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2RateLimitModule.java
@@ -0,0 +1,30 @@
+/*
+ * 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.config;
+
+import org.jclouds.digitalocean2.handlers.RateLimitRetryHandler;
+import org.jclouds.http.HttpRetryHandler;
+import org.jclouds.http.annotation.ClientError;
+
+import com.google.inject.AbstractModule;
+
+public class DigitalOcean2RateLimitModule extends AbstractModule {
+   @Override
+   protected void configure() {
+      bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(RateLimitRetryHandler.class);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/exceptions/DigitalOcean2RateLimitExceededException.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/exceptions/DigitalOcean2RateLimitExceededException.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/exceptions/DigitalOcean2RateLimitExceededException.java
new file mode 100644
index 0000000..fc54a7c
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/exceptions/DigitalOcean2RateLimitExceededException.java
@@ -0,0 +1,81 @@
+/*
+ * 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.exceptions;
+
+import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.millisUntilNextAvailableRequest;
+
+import org.jclouds.http.HttpResponse;
+import org.jclouds.rest.RateLimitExceededException;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+
+/**
+ * Provides detailed information for rate limit exceptions.
+ */
+@Beta
+public class DigitalOcean2RateLimitExceededException extends RateLimitExceededException {
+   private static final long serialVersionUID = 1L;
+   private static final String RATE_LIMIT_HEADER_PREFIX = "RateLimit-";
+
+   private Integer totalRequestsPerHour;
+   private Integer remainingRequests;
+   private Long timeToNextAvailableRequest;
+
+   public DigitalOcean2RateLimitExceededException(HttpResponse response) {
+      super(response.getStatusLine() + "\n" + rateLimitHeaders(response));
+      parseRateLimitInfo(response);
+   }
+
+   public DigitalOcean2RateLimitExceededException(HttpResponse response, Throwable cause) {
+      super(response.getStatusLine() + "\n" + rateLimitHeaders(response), cause);
+      parseRateLimitInfo(response);
+   }
+
+   public Integer totalRequestsPerHour() {
+      return totalRequestsPerHour;
+   }
+
+   public Integer remainingRequests() {
+      return remainingRequests;
+   }
+
+   public Long timeToNextAvailableRequest() {
+      return timeToNextAvailableRequest;
+   }
+
+   private void parseRateLimitInfo(HttpResponse response) {
+      String limit = response.getFirstHeaderOrNull("RateLimit-Limit");
+      String remaining = response.getFirstHeaderOrNull("RateLimit-Remaining");
+      String reset = response.getFirstHeaderOrNull("RateLimit-Reset");
+
+      totalRequestsPerHour = limit == null ? null : Integer.valueOf(limit);
+      remainingRequests = remaining == null ? null : Integer.valueOf(remaining);
+      timeToNextAvailableRequest = reset == null ? null : millisUntilNextAvailableRequest(Long.valueOf(reset));
+   }
+
+   private static Multimap<String, String> rateLimitHeaders(HttpResponse response) {
+      return Multimaps.filterKeys(response.getHeaders(), new Predicate<String>() {
+         @Override
+         public boolean apply(String input) {
+            return input.startsWith(RATE_LIMIT_HEADER_PREFIX);
+         }
+      });
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
index 5eda6eb..29b7eba 100644
--- a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/DigitalOcean2ErrorHandler.java
@@ -20,6 +20,7 @@ import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
 
 import javax.inject.Singleton;
 
+import org.jclouds.digitalocean2.exceptions.DigitalOcean2RateLimitExceededException;
 import org.jclouds.http.HttpCommand;
 import org.jclouds.http.HttpErrorHandler;
 import org.jclouds.http.HttpResponse;
@@ -33,15 +34,16 @@ import org.jclouds.rest.ResourceNotFoundException;
  */
 @Singleton
 public class DigitalOcean2ErrorHandler implements HttpErrorHandler {
+
    public void handleError(HttpCommand command, HttpResponse response) {
       // it is important to always read fully and close streams
       byte[] data = closeClientButKeepContentStream(response);
       String message = data != null ? new String(data) : null;
 
       Exception exception = message != null ? new HttpResponseException(command, response, message)
-              : new HttpResponseException(command, response);
+            : new HttpResponseException(command, response);
       message = message != null ? message : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(),
-              response.getStatusLine());
+            response.getStatusLine());
       switch (response.getStatusCode()) {
          case 400:
             break;
@@ -61,6 +63,9 @@ public class DigitalOcean2ErrorHandler implements HttpErrorHandler {
          case 409:
             exception = new IllegalStateException(message, exception);
             break;
+         case 429:
+            exception = new DigitalOcean2RateLimitExceededException(response, exception);
+            break;
       }
       command.setException(exception);
    }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandler.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandler.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandler.java
new file mode 100644
index 0000000..d72a9fa
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandler.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.digitalocean2.handlers;
+
+import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
+import static org.jclouds.digitalocean2.config.DigitalOcean2Properties.MAX_RATE_LIMIT_WAIT;
+
+import javax.annotation.Resource;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpRetryHandler;
+import org.jclouds.logging.Logger;
+
+import com.google.common.annotations.Beta;
+import com.google.inject.Inject;
+
+/**
+ * Retry handler that takes into account the DigitalOcean rate limit and delays
+ * the requests until they are known to succeed.
+ */
+@Beta
+@Singleton
+public class RateLimitRetryHandler implements HttpRetryHandler {
+
+   static final String RATE_LIMIT_RESET_HEADER = "RateLimit-Reset";
+
+   @Resource
+   protected Logger logger = Logger.NULL;
+
+   @Inject(optional = true)
+   @Named(PROPERTY_MAX_RETRIES)
+   private int retryCountLimit = 5;
+
+   @Inject(optional = true)
+   @Named(MAX_RATE_LIMIT_WAIT)
+   private int maxRateLimitWait = 120000;
+
+   @Override
+   public boolean shouldRetryRequest(final HttpCommand command, final HttpResponse response) {
+      command.incrementFailureCount();
+
+      // Do not retry client errors that are not rate limit errors
+      if (response.getStatusCode() != 429) {
+         return false;
+      } else if (!command.isReplayable()) {
+         logger.error("Cannot retry after rate limit error, command is not replayable: %1$s", command);
+         return false;
+      } else if (command.getFailureCount() > retryCountLimit) {
+         logger.error("Cannot retry after rate limit error, command has exceeded retry limit %1$d: %2$s",
+               retryCountLimit, command);
+         return false;
+      } else {
+         return delayRequestUntilAllowed(command, response);
+      }
+   }
+
+   private boolean delayRequestUntilAllowed(final HttpCommand command, final HttpResponse response) {
+      // The header is the Unix epoch time when the next request can be done
+      String epochForNextAvailableRequest = response.getFirstHeaderOrNull(RATE_LIMIT_RESET_HEADER);
+      if (epochForNextAvailableRequest == null) {
+         logger.error("Cannot retry after rate limit error, no retry information provided in the response");
+         return false;
+      }
+
+      long waitPeriod = millisUntilNextAvailableRequest(Long.parseLong(epochForNextAvailableRequest));
+
+      if (waitPeriod > 0) {
+         if (waitPeriod > maxRateLimitWait) {
+            logger.error("Max wait for rate limited requests is %s seconds but need to wait %s seconds, aborting",
+                  maxRateLimitWait, waitPeriod);
+            return false;
+         }
+
+         try {
+            logger.debug("Waiting %s seconds before retrying, as defined by the rate limit", waitPeriod);
+            // Do not use Uninterrumpibles or similar, to let the jclouds
+            // tiemout configuration interrupt this thread
+            Thread.sleep(waitPeriod);
+         } catch (InterruptedException ex) {
+            // If the request is being executed and has a timeout configured,
+            // the thread may be interrupted when the timeout is reached.
+            logger.error("Request execution was interrupted, aborting");
+            Thread.currentThread().interrupt();
+            return false;
+         }
+      }
+
+      return true;
+   }
+
+   public static long millisUntilNextAvailableRequest(long epochForNextAvailableRequest) {
+      return (epochForNextAvailableRequest * 1000) - System.currentTimeMillis();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
index b8fbbe7..f45a73f 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/compute/DigitalOcean2ComputeServiceLiveTest.java
@@ -18,8 +18,10 @@ package org.jclouds.digitalocean2.compute;
 
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.internal.BaseComputeServiceLiveTest;
+import org.jclouds.digitalocean2.config.DigitalOcean2RateLimitModule;
 import org.jclouds.sshj.config.SshjSshClientModule;
 import org.testng.annotations.Test;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.inject.Module;
@@ -40,6 +42,12 @@ public class DigitalOcean2ComputeServiceLiveTest extends BaseComputeServiceLiveT
    }
 
    @Override
+   protected Iterable<Module> setupModules() {
+      return ImmutableSet.<Module> builder().addAll(super.setupModules()).add(new DigitalOcean2RateLimitModule())
+            .build();
+   }
+
+   @Override
    public void testOptionToNotBlock() throws Exception {
       // DigitalOcean ComputeService implementation has to block until the node
       // is provisioned, to be able to return it.

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/exceptions/RateLimitExceptionMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/exceptions/RateLimitExceptionMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/exceptions/RateLimitExceptionMockTest.java
new file mode 100644
index 0000000..e7831a5
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/exceptions/RateLimitExceptionMockTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.exceptions;
+
+import static org.jclouds.Constants.PROPERTY_MAX_RETRIES;
+import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.millisUntilNextAvailableRequest;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Properties;
+
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+import com.squareup.okhttp.mockwebserver.MockResponse;
+
+@Test(groups = "unit", testName = "RateLimitExceptionMockTest", singleThreaded = true)
+public class RateLimitExceptionMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   @Override
+   protected Properties overrides() {
+      Properties overrides = super.overrides();
+      overrides.put(PROPERTY_MAX_RETRIES, "0"); // Do not retry
+      return overrides;
+   }
+
+   public void testRateLimitExceptionIsThrown() throws InterruptedException {
+      long reset = (System.currentTimeMillis() / 1000) + 3600; // Epoch for one
+                                                               // hour from now
+      long millisToReset = millisUntilNextAvailableRequest(reset);
+
+      server.enqueue(new MockResponse().setResponseCode(429).addHeader("RateLimit-Limit", "5000")
+            .addHeader("RateLimit-Remaining", "1235").addHeader("RateLimit-Reset", String.valueOf(reset)));
+
+      try {
+         api.keyApi().list();
+         fail("Expected a DigitalOcean2RateLimitExceededException to be thrown");
+      } catch (DigitalOcean2RateLimitExceededException ex) {
+         assertEquals(ex.totalRequestsPerHour().intValue(), 5000);
+         assertEquals(ex.remainingRequests().intValue(), 1235);
+         // Can't verify with millisecond precision. Use an interval to have a
+         // consistent test.
+         assertTrue(ex.timeToNextAvailableRequest() < millisToReset
+               && ex.timeToNextAvailableRequest() > millisToReset - 1800000);
+      }
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandlerTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandlerTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandlerTest.java
new file mode 100644
index 0000000..6c7c87f
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/handlers/RateLimitRetryHandlerTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.handlers;
+
+import static org.jclouds.digitalocean2.handlers.RateLimitRetryHandler.RATE_LIMIT_RESET_HEADER;
+import static org.jclouds.http.HttpUtils.releasePayload;
+import static org.jclouds.io.Payloads.newInputStreamPayload;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.io.Payload;
+import org.testng.annotations.Test;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+
+@Test(groups = "unit", testName = "RateLimitRetryHandlerTest")
+public class RateLimitRetryHandlerTest {
+
+   // Configure a safe timeout of one minute to abort the tests in case they get
+   // stuck
+   private static final long TEST_SAFE_TIMEOUT = 60000;
+
+   private final RateLimitRetryHandler rateLimitRetryHandler = new RateLimitRetryHandler();
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDoNotRetryIfNoRateLimit() {
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(450).build();
+
+      assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDoNotRetryIfNotReplayable() {
+      // InputStream payloads are not replayable
+      Payload payload = newInputStreamPayload(new ByteArrayInputStream(new byte[0]));
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost")
+            .payload(payload).build());
+      HttpResponse response = HttpResponse.builder().statusCode(429).build();
+
+      try {
+         assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+      } finally {
+         releasePayload(command.getCurrentRequest());
+      }
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDoNotRetryIfNoRateLimitResetHeader() {
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(429).build();
+
+      assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDoNotRetryIfTooMuchWait() {
+      // 5 minutes Unix epoch timestamp
+      long rateLimitResetEpoch = (System.currentTimeMillis() + 300000) / 1000;
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(429)
+            .addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
+
+      assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testRequestIsDelayed() {
+      // 5 seconds Unix epoch timestamp
+      long rateLimitResetEpoch = (System.currentTimeMillis() + 5000) / 1000;
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(429)
+            .addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
+
+      long start = System.currentTimeMillis();
+
+      assertTrue(rateLimitRetryHandler.shouldRetryRequest(command, response));
+      // Should have blocked the amount of time configured in the header. Use a
+      // smaller value to compensate the time it takes to reach the code that
+      // computes the amount of time to wait.
+      assertTrue(System.currentTimeMillis() - start > 2500);
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDoNotRetryIfRequestIsAborted() throws Exception {
+      // 10 seconds Unix epoch timestamp
+      long rateLimitResetEpoch = (System.currentTimeMillis() + 10000) / 1000;
+      final HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost")
+            .build());
+      final HttpResponse response = HttpResponse.builder().statusCode(429)
+            .addHeader(RATE_LIMIT_RESET_HEADER, String.valueOf(rateLimitResetEpoch)).build();
+
+      final Thread requestThread = Thread.currentThread();
+      Thread killer = new Thread() {
+         @Override
+         public void run() {
+            Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS);
+            requestThread.interrupt();
+         }
+      };
+
+      // Start the killer thread that will abort the rate limit wait
+      killer.start();
+      assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testIncrementsFailureCount() {
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(429).build();
+
+      rateLimitRetryHandler.shouldRetryRequest(command, response);
+      assertEquals(command.getFailureCount(), 1);
+
+      rateLimitRetryHandler.shouldRetryRequest(command, response);
+      assertEquals(command.getFailureCount(), 2);
+
+      rateLimitRetryHandler.shouldRetryRequest(command, response);
+      assertEquals(command.getFailureCount(), 3);
+   }
+
+   @Test(timeOut = TEST_SAFE_TIMEOUT)
+   public void testDisallowExcessiveRetries() {
+      HttpCommand command = new HttpCommand(HttpRequest.builder().method("GET").endpoint("http://localhost").build());
+      HttpResponse response = HttpResponse.builder().statusCode(429).addHeader(RATE_LIMIT_RESET_HEADER, "0").build();
+
+      for (int i = 0; i < 5; i++) {
+         assertTrue(rateLimitRetryHandler.shouldRetryRequest(command, response));
+      }
+      assertFalse(rateLimitRetryHandler.shouldRetryRequest(command, response));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
index 18f97c6..b210c93 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
@@ -28,6 +28,7 @@ import org.jclouds.apis.BaseApiLiveTest;
 import org.jclouds.compute.config.ComputeServiceProperties;
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.config.DigitalOcean2RateLimitModule;
 import org.jclouds.digitalocean2.domain.Action;
 import org.jclouds.digitalocean2.domain.Image;
 import org.jclouds.digitalocean2.domain.Region;
@@ -35,6 +36,7 @@ import org.jclouds.digitalocean2.domain.Size;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Ordering;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -68,6 +70,11 @@ public class BaseDigitalOcean2ApiLiveTest extends BaseApiLiveTest<DigitalOcean2A
       return injector.getInstance(DigitalOcean2Api.class);
    }
 
+   @Override protected Iterable<Module> setupModules() {
+      return ImmutableSet.<Module> builder().addAll(super.setupModules()).add(new DigitalOcean2RateLimitModule())
+            .build();
+   }
+
    protected void assertActionCompleted(int actionId) {
       checkState(actionCompleted.apply(actionId), "Timeout waiting for action: %s", actionId);
       Action action = api.actionApi().get(actionId);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/7e866ad6/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
index 78550a5..ca0c4bd 100644
--- a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
@@ -23,6 +23,7 @@ import static org.testng.Assert.assertEquals;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 
 import org.jclouds.ContextBuilder;
@@ -59,7 +60,6 @@ public class BaseDigitalOcean2ApiMockTest {
    // So that we can ignore formatting.
    private final JsonParser parser = new JsonParser();
    
-   
    @BeforeMethod
    public void start() throws IOException {
       server = new MockWebServer();
@@ -68,6 +68,7 @@ public class BaseDigitalOcean2ApiMockTest {
             .credentials("", MOCK_BEARER_TOKEN)
             .endpoint(url(""))
             .modules(modules)
+            .overrides(overrides())
             .build();
       json = ctx.utils().injector().getInstance(Json.class);
       api = ctx.getApi();
@@ -79,6 +80,10 @@ public class BaseDigitalOcean2ApiMockTest {
       api.close();
    }
    
+   protected Properties overrides() {
+      return new Properties();
+   }
+
    protected String url(String path) {
       return server.getUrl(path).toString();
    }


[02/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Posted by na...@apache.org.
http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
new file mode 100644
index 0000000..73c7451
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiLiveTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.features;
+
+import static org.testng.Assert.assertTrue;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+import org.testng.util.Strings;
+
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "RegionApiLiveTest")
+public class RegionApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+   
+   public void testListRegions() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Region>() {
+         @Override
+         public boolean apply(Region input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All regions must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some regions to be returned");
+   }
+   
+   public void testListRegionsOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1)).allMatch(new Predicate<Region>() {
+         @Override
+         public boolean apply(Region input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All regions must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some regions to be returned");
+   }
+   
+   private RegionApi api() {
+      return api.regionApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
new file mode 100644
index 0000000..8c8c326
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/RegionApiMockTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "RegionApiMockTest", singleThreaded = true)
+public class RegionApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListRegions() throws InterruptedException {
+      server.enqueue(jsonResponse("/regions-first.json"));
+      server.enqueue(jsonResponse("/regions-last.json"));
+
+      Iterable<Region> regions = api.regionApi().list().concat();
+
+      assertEquals(size(regions), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/regions");
+      assertSent(server, "GET", "/regions?page=2&per_page=5");
+   }
+
+   public void testListRegionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Region> regions = api.regionApi().list().concat();
+
+      assertTrue(isEmpty(regions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/regions");
+   }
+
+   public void testListRegionsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/regions-first.json"));
+
+      Iterable<Region> regions = api.regionApi().list(page(1).perPage(5));
+
+      assertEquals(size(regions), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/regions?page=1&per_page=5");
+   }
+
+   public void testListRegionsWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Region> regions = api.regionApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(regions));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/regions?page=1&per_page=5");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
new file mode 100644
index 0000000..a9a8566
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiLiveTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.features;
+
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiLiveTest;
+import org.testng.annotations.Test;
+import org.testng.util.Strings;
+
+import com.google.common.base.Predicate;
+
+@Test(groups = "live", testName = "SizeApiLiveTest")
+public class SizeApiLiveTest extends BaseDigitalOcean2ApiLiveTest {
+   
+   public void testListSizes() {
+      final AtomicInteger found = new AtomicInteger(0);
+      // DigitalOcean return 25 records per page by default. Inspect at most 2 pages
+      assertTrue(api().list().concat().limit(50).allMatch(new Predicate<Size>() {
+         @Override
+         public boolean apply(Size input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All sizes must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some sizes to be returned");
+   }
+   
+   public void testListSizesOnePage() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(page(1)).allMatch(new Predicate<Size>() {
+         @Override
+         public boolean apply(Size input) {
+            found.incrementAndGet();
+            return !Strings.isNullOrEmpty(input.slug());
+         }
+      }), "All sizes must have the 'slug' field populated");
+      assertTrue(found.get() > 0, "Expected some sizes to be returned");
+   }
+   
+   private SizeApi api() {
+      return api.sizeApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
new file mode 100644
index 0000000..7403518
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/features/SizeApiMockTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.features;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.Builder.page;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.jclouds.digitalocean2.domain.Size;
+import org.jclouds.digitalocean2.internal.BaseDigitalOcean2ApiMockTest;
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "SizeApiMockTest", singleThreaded = true)
+public class SizeApiMockTest extends BaseDigitalOcean2ApiMockTest {
+
+   public void testListSizes() throws InterruptedException {
+      server.enqueue(jsonResponse("/sizes-first.json"));
+      server.enqueue(jsonResponse("/sizes-last.json"));
+
+      Iterable<Size> sizes = api.sizeApi().list().concat();
+
+      assertEquals(size(sizes), 9); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+
+      assertSent(server, "GET", "/sizes");
+      assertSent(server, "GET", "/sizes?page=2&per_page=5");
+   }
+
+   public void testListSizesReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Size> sizes = api.sizeApi().list().concat();
+
+      assertTrue(isEmpty(sizes));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/sizes");
+   }
+
+   public void testListSizesWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/sizes-first.json"));
+
+      Iterable<Size> sizes = api.sizeApi().list(page(1).perPage(5));
+
+      assertEquals(size(sizes), 5);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "/sizes?page=1&per_page=5");
+   }
+
+   public void testListSizesWithOptionsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+
+      Iterable<Size> sizes = api.sizeApi().list(page(1).perPage(5));
+
+      assertTrue(isEmpty(sizes));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "/sizes?page=1&per_page=5");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToImageListOptionsTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToImageListOptionsTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToImageListOptionsTest.java
new file mode 100644
index 0000000..22985fa
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToImageListOptionsTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.functions;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.PRIVATE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ImageListOptions.TYPE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PER_PAGE_PARAM;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.domain.options.ImageListOptions;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Multimap;
+
+@Test(groups = "unit", testName = "LinkToImageListOptionsTest")
+public class LinkToImageListOptionsTest {
+
+   public void testNoOptions() {
+      LinkToImageListOptions function = new LinkToImageListOptions();
+
+      ImageListOptions options = function.apply(URI.create("https://api.digitalocean.com/v2/images"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertFalse(params.containsKey(PAGE_PARAM));
+      assertFalse(params.containsKey(PER_PAGE_PARAM));
+      assertFalse(params.containsKey(TYPE_PARAM));
+      assertFalse(params.containsKey(PRIVATE_PARAM));
+   }
+
+   public void testWithOptions() {
+      LinkToImageListOptions function = new LinkToImageListOptions();
+
+      ImageListOptions options = function.apply(URI
+            .create("https://api.digitalocean.com/v2/images?page=1&per_page=5&type=distribution&private=true"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertEquals(getOnlyElement(params.get(PAGE_PARAM)), "1");
+      assertEquals(getOnlyElement(params.get(PER_PAGE_PARAM)), "5");
+      assertEquals(getOnlyElement(params.get(TYPE_PARAM)), "distribution");
+      assertEquals(getOnlyElement(params.get(PRIVATE_PARAM)), "true");
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToListOptionsTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToListOptionsTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToListOptionsTest.java
new file mode 100644
index 0000000..2bb3544
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/functions/LinkToListOptionsTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.functions;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PAGE_PARAM;
+import static org.jclouds.digitalocean2.domain.options.ListOptions.PER_PAGE_PARAM;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Multimap;
+
+@Test(groups = "unit", testName = "LinkToListOptionsTest")
+public class LinkToListOptionsTest {
+
+   public void testNoOptions() {
+      LinkToListOptions function = new LinkToListOptions();
+
+      ListOptions options = function.apply(URI.create("https://api.digitalocean.com/v2/actions"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertFalse(params.containsKey(PAGE_PARAM));
+      assertFalse(params.containsKey(PER_PAGE_PARAM));
+   }
+
+   public void testWithOptions() {
+      LinkToListOptions function = new LinkToListOptions();
+
+      ListOptions options = function.apply(URI.create("https://api.digitalocean.com/v2/actions?page=2&per_page=5"));
+      assertNotNull(options);
+
+      Multimap<String, String> params = options.buildQueryParameters();
+      assertEquals(getOnlyElement(params.get(PAGE_PARAM)), "2");
+      assertEquals(getOnlyElement(params.get(PER_PAGE_PARAM)), "5");
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
new file mode 100644
index 0000000..18f97c6
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiLiveTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.internal;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_TERMINATED;
+import static org.testng.Assert.assertEquals;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.jclouds.apis.BaseApiLiveTest;
+import org.jclouds.compute.config.ComputeServiceProperties;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.Image;
+import org.jclouds.digitalocean2.domain.Region;
+import org.jclouds.digitalocean2.domain.Size;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+public class BaseDigitalOcean2ApiLiveTest extends BaseApiLiveTest<DigitalOcean2Api> {
+
+   protected Predicate<Integer> actionCompleted;
+   protected Predicate<AtomicReference<NodeMetadata>> nodeRunning;
+   protected Predicate<Integer> nodeTerminated;
+   protected Predicate<Integer> dropletOff;
+
+   public BaseDigitalOcean2ApiLiveTest() {
+      provider = "digitalocean2";
+   }
+
+   @Override protected Properties setupProperties() {
+      Properties props = super.setupProperties();
+      props.put(ComputeServiceProperties.POLL_INITIAL_PERIOD, 1000);
+      props.put(ComputeServiceProperties.POLL_MAX_PERIOD, 10000);
+      return props;
+   }
+
+   @Override protected DigitalOcean2Api create(Properties props, Iterable<Module> modules) {
+      Injector injector = newBuilder().modules(modules).overrides(props).buildInjector();
+      actionCompleted = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){}));
+      nodeTerminated = injector.getInstance(Key.get(new TypeLiteral<Predicate<Integer>>(){},
+            Names.named(TIMEOUT_NODE_TERMINATED)));
+      return injector.getInstance(DigitalOcean2Api.class);
+   }
+
+   protected void assertActionCompleted(int actionId) {
+      checkState(actionCompleted.apply(actionId), "Timeout waiting for action: %s", actionId);
+      Action action = api.actionApi().get(actionId);
+      assertEquals(action.status(), Action.Status.COMPLETED);
+   }
+
+   protected void assertNodeTerminated(int dropletId) {
+      assertEquals(nodeTerminated.apply(dropletId), true, String.format("Timeout waiting for dropletId: %s", dropletId));
+   }
+   
+   protected Region firstAvailableRegion() {
+      return api.regionApi().list().concat().firstMatch(new Predicate<Region>() {
+         @Override
+         public boolean apply(Region input) {
+            return input.available();
+         }
+      }).get();
+   }
+   
+   protected Size cheapestSizeInRegion(final Region region) {
+      return sizesByPrice().min(api.sizeApi().list().concat().filter(new Predicate<Size>() {
+         @Override
+         public boolean apply(Size input) {
+            return input.available() && input.regions().contains(region.slug());
+         }
+      }));
+   }
+   
+   protected Image ubuntuImageInRegion(final Region region) {
+      return api.imageApi().list().concat().firstMatch(new Predicate<Image>() {
+         @Override
+         public boolean apply(Image input) {
+            return "Ubuntu".equalsIgnoreCase(input.distribution()) && !isNullOrEmpty(input.slug())
+                  && input.regions().contains(region.slug());
+         }
+      }).get();
+   }
+   
+   protected static Ordering<Size> sizesByPrice() {
+      return new Ordering<Size>() {
+         @Override
+         public int compare(Size left, Size right) {
+            return ComparisonChain.start()
+                  .compare(left.priceHourly(), right.priceHourly())
+                  .compare(left.priceMonthly(), right.priceMonthly())
+                  .result();
+         }
+      };
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
new file mode 100644
index 0000000..78550a5
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/internal/BaseDigitalOcean2ApiMockTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.internal;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import org.jclouds.ContextBuilder;
+import org.jclouds.concurrent.config.ExecutorServiceModule;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.DigitalOcean2ProviderMetadata;
+import org.jclouds.json.Json;
+import org.jclouds.rest.ApiContext;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Resources;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.JsonParser;
+import com.google.inject.Module;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+public class BaseDigitalOcean2ApiMockTest {
+   
+   private static final String MOCK_BEARER_TOKEN = "c5401990f0c24135e8d6b5d260603fc71696d4738da9aa04a720229a01a2521d";
+   private static final String DEFAULT_ENDPOINT = new DigitalOcean2ProviderMetadata().getEndpoint();
+   
+   private final Set<Module> modules = ImmutableSet.<Module> of(new ExecutorServiceModule(sameThreadExecutor()));
+   
+   protected MockWebServer server;
+   protected DigitalOcean2Api api;
+   private Json json;
+   
+   // So that we can ignore formatting.
+   private final JsonParser parser = new JsonParser();
+   
+   
+   @BeforeMethod
+   public void start() throws IOException {
+      server = new MockWebServer();
+      server.play();
+      ApiContext<DigitalOcean2Api> ctx = ContextBuilder.newBuilder("digitalocean2")
+            .credentials("", MOCK_BEARER_TOKEN)
+            .endpoint(url(""))
+            .modules(modules)
+            .build();
+      json = ctx.utils().injector().getInstance(Json.class);
+      api = ctx.getApi();
+   }
+
+   @AfterMethod(alwaysRun = true)
+   public void stop() throws IOException {
+      server.shutdown();
+      api.close();
+   }
+   
+   protected String url(String path) {
+      return server.getUrl(path).toString();
+   }
+
+   protected MockResponse jsonResponse(String resource) {
+      return new MockResponse().addHeader("Content-Type", "application/json").setBody(stringFromResource(resource));
+   }
+
+   protected MockResponse response404() {
+      return new MockResponse().setStatus("HTTP/1.1 404 Not Found");
+   }
+   
+   protected MockResponse response204() {
+      return new MockResponse().setStatus("HTTP/1.1 204 No Content");
+   }
+
+   protected String stringFromResource(String resourceName) {
+      try {
+         return Resources.toString(getClass().getResource(resourceName), Charsets.UTF_8)
+               .replace(DEFAULT_ENDPOINT, url(""));
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+   
+   protected <T> T onlyObjectFromResource(String resourceName, TypeToken<Map<String, T>> type) {
+      // Assume JSON objects passed here will be in the form: { "entity": { ... } }
+      String text = stringFromResource(resourceName);
+      Map<String, T> object = json.fromJson(text, type.getType());
+      checkArgument(!object.isEmpty(), "The given json does not contain any object: %s", text);
+      checkArgument(object.keySet().size() == 1, "The given json does not contain more than one object: %s", text);
+      return object.get(getOnlyElement(object.keySet()));
+   }
+   
+   protected <T> T objectFromResource(String resourceName, Class<T> type) {
+      String text = stringFromResource(resourceName);
+      return json.fromJson(text, type);
+   }
+
+   protected RecordedRequest assertSent(MockWebServer server, String method, String path) throws InterruptedException {
+      RecordedRequest request = server.takeRequest();
+      assertEquals(request.getMethod(), method);
+      assertEquals(request.getPath(), path);
+      assertEquals(request.getHeader("Accept"), "application/json");
+      assertEquals(request.getHeader("Authorization"), "Bearer " + MOCK_BEARER_TOKEN);
+      return request;
+   }
+
+   protected RecordedRequest assertSent(MockWebServer server, String method, String path, String json)
+         throws InterruptedException {
+      RecordedRequest request = assertSent(server, method, path);
+      assertEquals(request.getHeader("Content-Type"), "application/json");
+      assertEquals(parser.parse(new String(request.getBody(), Charsets.UTF_8)), parser.parse(json));
+      return request;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/DSAKeysTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/DSAKeysTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/DSAKeysTest.java
new file mode 100644
index 0000000..91f3c96
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/DSAKeysTest.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.ssh;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.DSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link DSAKeys} class.
+ */
+@Test(groups = "unit", testName = "DSAKeysTest")
+public class DSAKeysTest {
+
+   private static final String expectedFingerPrint = "2a:54:bb:8e:ba:44:96:c8:6c:9c:40:34:3c:4d:38:e4";
+
+   @Test
+   public void testCanReadRsaAndCompareFingerprintOnPublicRSAKey() throws IOException {
+      String dsa = Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-dsa.pub"));
+      String fingerPrint = DSAKeys.fingerprintPublicKey(dsa);
+      assertEquals(fingerPrint, expectedFingerPrint);
+   }
+
+   @Test
+   public void testEncodeAsOpenSSH() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
+      String dsa = Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-dsa.pub"));
+      DSAPublicKeySpec spec = DSAKeys.publicKeySpecFromOpenSSH(dsa);
+      DSAPublicKey key = (DSAPublicKey) KeyFactory.getInstance("DSA").generatePublic(spec);
+
+      assertEquals(DSAKeys.encodeAsOpenSSH(key), dsa);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/ECDSAKeysTest.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/ECDSAKeysTest.java b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/ECDSAKeysTest.java
new file mode 100644
index 0000000..2053ac7
--- /dev/null
+++ b/providers/digitalocean2/src/test/java/org/jclouds/digitalocean2/ssh/ECDSAKeysTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.ssh;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link ECDSAKeysTest} class.
+ */
+@Test(groups = "unit", testName = "ECDSAKeysTest")
+public class ECDSAKeysTest {
+
+   private static final String expectedFingerPrint = "0e:9f:aa:cc:3e:79:5d:1e:f9:19:58:08:dc:c4:5e:1c";
+
+   @Test
+   public void testCanReadRsaAndCompareFingerprintOnPublicECDSAKey() throws IOException {
+      String ecdsa = Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-ecdsa.pub"));
+      String fingerPrint = ECDSAKeys.fingerprintPublicKey(ecdsa);
+      assertEquals(fingerPrint, expectedFingerPrint);
+   }
+
+   @Test
+   public void testEncodeAsOpenSSH() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
+      String ecdsa = Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-ecdsa.pub"));
+      ECPublicKeySpec spec = ECDSAKeys.publicKeySpecFromOpenSSH(ecdsa);
+      ECPublicKey key = (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(spec);
+
+      assertTrue(ecdsa.startsWith(ECDSAKeys.encodeAsOpenSSH(key)));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/action.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/action.json b/providers/digitalocean2/src/test/resources/action.json
new file mode 100644
index 0000000..0202ca0
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/action.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "completed",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "destroy"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/actions-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/actions-first.json b/providers/digitalocean2/src/test/resources/actions-first.json
new file mode 100644
index 0000000..c19fde3
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/actions-first.json
@@ -0,0 +1,168 @@
+{
+   "actions" : [
+      {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "completed",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "destroy"
+      },
+      {
+         "started_at" : "2015-05-19T15:07:55Z",
+         "region" : {
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true,
+            "slug" : "nyc1",
+            "name" : "New York 1"
+         },
+         "status" : "completed",
+         "resource_type" : "droplet",
+         "region_slug" : "nyc1",
+         "id" : 50899364,
+         "resource_id" : 5346565,
+         "completed_at" : "2015-05-19T15:08:04Z",
+         "type" : "destroy"
+      },
+      {
+         "completed_at" : "2015-05-19T13:39:59Z",
+         "type" : "create",
+         "region" : {
+            "name" : "New York 1",
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true,
+            "slug" : "nyc1"
+         },
+         "started_at" : "2015-05-19T13:39:12Z",
+         "resource_type" : "droplet",
+         "status" : "completed",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50892713
+      },
+      {
+         "resource_id" : 5346565,
+         "region_slug" : "nyc1",
+         "id" : 50888077,
+         "status" : "completed",
+         "resource_type" : "droplet",
+         "region" : {
+            "name" : "New York 1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true,
+            "slug" : "nyc1",
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T12:37:23Z",
+         "type" : "create",
+         "completed_at" : "2015-05-19T12:38:13Z"
+      },
+      {
+         "completed_at" : "2015-05-19T11:33:00Z",
+         "type" : "destroy",
+         "status" : "completed",
+         "resource_type" : "droplet",
+         "started_at" : "2015-05-19T11:32:55Z",
+         "region" : {
+            "available" : true,
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "slug" : "nyc1",
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "name" : "New York 1"
+         },
+         "region_slug" : "nyc1",
+         "id" : 50884032,
+         "resource_id" : 5344505
+      }
+   ],
+   "links" : {
+      "pages" : {
+         "last" : "https://api.digitalocean.com/v2/actions?page=2&per_page=5",
+         "next" : "https://api.digitalocean.com/v2/actions?page=2&per_page=5"
+      }
+   },
+   "meta" : {
+      "total" : 8
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/actions-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/actions-last.json b/providers/digitalocean2/src/test/resources/actions-last.json
new file mode 100644
index 0000000..5d86e7a
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/actions-last.json
@@ -0,0 +1,106 @@
+{
+   "meta" : {
+      "total" : 8
+   },
+   "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/actions?page=1&per_page=5",
+         "prev" : "https://api.digitalocean.com/v2/actions?page=1&per_page=5"
+      }
+   },
+   "actions" : [
+      {
+         "region" : {
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "available" : true,
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "slug" : "nyc1",
+            "name" : "New York 1"
+         },
+         "started_at" : "2014-01-18T22:39:08Z",
+         "type" : "create",
+         "resource_type" : "droplet",
+         "id" : 14115951,
+         "completed_at" : "2014-01-18T22:41:14Z",
+         "region_slug" : "nyc1",
+         "resource_id" : 1010699,
+         "status" : "completed"
+      },
+      {
+         "started_at" : "2014-01-18T22:39:06Z",
+         "type" : "create",
+         "resource_type" : "droplet",
+         "region" : {
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "slug" : "nyc1",
+            "name" : "New York 1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true
+         },
+         "resource_id" : 1010697,
+         "region_slug" : "nyc1",
+         "status" : "completed",
+         "id" : 14115948,
+         "completed_at" : "2014-01-18T22:40:43Z"
+      },
+      {
+         "region_slug" : "nyc1",
+         "resource_id" : 1010698,
+         "status" : "completed",
+         "id" : 14115949,
+         "completed_at" : "2014-01-18T22:44:08Z",
+         "type" : "create",
+         "started_at" : "2014-01-18T22:39:06Z",
+         "resource_type" : "droplet",
+         "region" : {
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ],
+            "slug" : "nyc1",
+            "name" : "New York 1",
+            "available" : true,
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ]
+         }
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/backups-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/backups-first.json b/providers/digitalocean2/src/test/resources/backups-first.json
new file mode 100644
index 0000000..f1083f1
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/backups-first.json
@@ -0,0 +1,26 @@
+{
+  "backups": [
+    {
+      "id": 7622989,
+      "name": "example.com 2014-11-14",
+      "distribution": "Ubuntu",
+      "slug": null,
+      "public": false,
+      "regions": [
+        "nyc3"
+      ],
+      "created_at": "2014-11-14T16:07:38Z",
+      "type": "snapshot",
+      "min_disk_size": 20
+    }
+  ],
+  "links" : {
+      "pages" : {
+         "last" : "https://api.digitalocean.com/v2/droplets/3067509/backups?page=2",
+         "next" : "https://api.digitalocean.com/v2/droplets/3067509/backups?page=2"
+      }
+  },
+  "meta": {
+    "total": 2
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/backups-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/backups-last.json b/providers/digitalocean2/src/test/resources/backups-last.json
new file mode 100644
index 0000000..c927c19
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/backups-last.json
@@ -0,0 +1,26 @@
+{
+  "backups": [
+    {
+      "id": 76229890,
+      "name": "example.com 2014-11-14",
+      "distribution": "Ubuntu",
+      "slug": null,
+      "public": false,
+      "regions": [
+        "nyc3"
+      ],
+      "created_at": "2014-11-14T16:07:38Z",
+      "type": "snapshot",
+      "min_disk_size": 20
+    }
+  ],
+  "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/droplets/3067509/backups?page=1",
+         "prev" : "https://api.digitalocean.com/v2/droplets/3067509/backups?page=1"
+      }
+  },
+  "meta": {
+    "total": 2
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/droplet-create-req.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/droplet-create-req.json b/providers/digitalocean2/src/test/resources/droplet-create-req.json
new file mode 100644
index 0000000..3ed5273
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/droplet-create-req.json
@@ -0,0 +1,12 @@
+{
+    "name": "digitalocean2-s-d5e",
+    "region": "sfo1",
+    "size": "512mb",
+    "image": "6374124",
+    "ssh_keys": [
+        421192
+    ],
+    "backups": false,
+    "ipv6": false,
+    "private_networking": false
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/droplet-create-res.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/droplet-create-res.json b/providers/digitalocean2/src/test/resources/droplet-create-res.json
new file mode 100644
index 0000000..a7d37a5
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/droplet-create-res.json
@@ -0,0 +1,35 @@
+{
+    "droplet": {
+        "id": 2987224,
+        "name": "digitalocean2-s-d5e",
+        "memory": 512,
+        "vcpus": 1,
+        "disk": 20,
+        "locked": true,
+        "status": "new",
+        "kernel": {
+            "id": 70,
+            "name": "Ubuntu 10.04 x64 vmlinuz-2.6.32-41-server",
+            "version": "2.6.32-41-server"
+        },
+        "created_at": "2014-10-27T19:33:34Z",
+        "features": [
+            "virtio"
+        ],
+        "backup_ids": [],
+        "snapshot_ids": [],
+        "image": {},
+        "size_slug": "512mb",
+        "networks": {},
+        "region": {}
+    },
+    "links": {
+        "actions": [
+            {
+                "id": 35383956,
+                "rel": "create",
+                "href": "https://api.digitalocean.com/v2/actions/35383956"
+            }
+        ]
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/droplet.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/droplet.json b/providers/digitalocean2/src/test/resources/droplet.json
new file mode 100644
index 0000000..fb995c5
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/droplet.json
@@ -0,0 +1,105 @@
+{
+   "droplet" :
+      {
+         "created_at" : "2015-05-25T15:50:48Z",
+         "region" : {
+            "name" : "New York 1",
+            "sizes" : [
+               "32gb",
+               "16gb",
+               "2gb",
+               "1gb",
+               "4gb",
+               "8gb",
+               "512mb",
+               "64gb",
+               "48gb"
+            ],
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true
+         },
+         "id" : 5425561,
+         "disk" : 20,
+         "networks" : {
+            "v6" : [],
+            "v4" : [
+               {
+                  "type" : "public",
+                  "ip_address" : "162.243.167.46",
+                  "netmask" : "255.255.255.0",
+                  "gateway" : "162.243.167.1"
+               }
+            ]
+         },
+         "backup_ids" : [],
+         "image" : {
+            "slug" : "ubuntu-14-10-x32",
+            "public" : true,
+            "created_at" : "2015-01-08T18:41:22Z",
+            "distribution" : "Ubuntu",
+            "id" : 9801951,
+            "type" : "snapshot",
+            "regions" : [
+               "nyc1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "ams2",
+               "sgp1",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "fra1"
+            ],
+            "name" : "14.10 x32",
+            "min_disk_size" : 20
+         },
+         "vcpus" : 1,
+         "next_backup_window" : {
+            "end" : "2015-06-01T23:00:00Z",
+            "start" : "2015-06-01T00:00:00Z"
+         },
+         "locked" : false,
+         "snapshot_ids" : [],
+         "kernel" : {
+            "name" : "Ubuntu 14.10 x32 vmlinuz-3.16.0-28-generic",
+            "id" : 2926,
+            "version" : "3.16.0-28-generic"
+         },
+         "status" : "active",
+         "features" : [
+            "backups",
+            "virtio"
+         ],
+         "size" : {
+            "price_hourly" : 0.00744,
+            "slug" : "512mb",
+            "disk" : 20,
+            "available" : true,
+            "transfer" : 1,
+            "price_monthly" : 5,
+            "regions" : [
+               "nyc1",
+               "sgp1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "ams2",
+               "fra1"
+            ],
+            "memory" : 512,
+            "vcpus" : 1
+         },
+         "name" : "test1",
+         "size_slug" : "512mb",
+         "memory" : 512
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/droplets-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/droplets-first.json b/providers/digitalocean2/src/test/resources/droplets-first.json
new file mode 100644
index 0000000..5493f7f
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/droplets-first.json
@@ -0,0 +1,115 @@
+{
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/droplets/5425561?page=2&per_page=1",
+         "last" : "https://api.digitalocean.com/v2/droplets/5425561?page=2&per_page=1"
+      }
+   },
+   "meta" : {
+      "total" : 2
+   },
+   "droplets" : [
+      {
+         "created_at" : "2015-05-25T15:50:48Z",
+         "region" : {
+            "name" : "New York 1",
+            "sizes" : [
+               "32gb",
+               "16gb",
+               "2gb",
+               "1gb",
+               "4gb",
+               "8gb",
+               "512mb",
+               "64gb",
+               "48gb"
+            ],
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true
+         },
+         "id" : 5425561,
+         "disk" : 20,
+         "networks" : {
+            "v6" : [],
+            "v4" : [
+               {
+                  "type" : "public",
+                  "ip_address" : "162.243.167.46",
+                  "netmask" : "255.255.255.0",
+                  "gateway" : "162.243.167.1"
+               }
+            ]
+         },
+         "backup_ids" : [],
+         "image" : {
+            "slug" : "ubuntu-14-10-x32",
+            "public" : true,
+            "created_at" : "2015-01-08T18:41:22Z",
+            "distribution" : "Ubuntu",
+            "id" : 9801951,
+            "type" : "snapshot",
+            "regions" : [
+               "nyc1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "ams2",
+               "sgp1",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "fra1"
+            ],
+            "name" : "14.10 x32",
+            "min_disk_size" : 20
+         },
+         "vcpus" : 1,
+         "next_backup_window" : {
+            "end" : "2015-06-01T23:00:00Z",
+            "start" : "2015-06-01T00:00:00Z"
+         },
+         "locked" : false,
+         "snapshot_ids" : [],
+         "kernel" : {
+            "name" : "Ubuntu 14.10 x32 vmlinuz-3.16.0-28-generic",
+            "id" : 2926,
+            "version" : "3.16.0-28-generic"
+         },
+         "status" : "active",
+         "features" : [
+            "backups",
+            "virtio"
+         ],
+         "size" : {
+            "price_hourly" : 0.00744,
+            "slug" : "512mb",
+            "disk" : 20,
+            "available" : true,
+            "transfer" : 1,
+            "price_monthly" : 5,
+            "regions" : [
+               "nyc1",
+               "sgp1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "ams2",
+               "fra1"
+            ],
+            "memory" : 512,
+            "vcpus" : 1
+         },
+         "name" : "test1",
+         "size_slug" : "512mb",
+         "memory" : 512
+      }
+   ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/droplets-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/droplets-last.json b/providers/digitalocean2/src/test/resources/droplets-last.json
new file mode 100644
index 0000000..beeb654
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/droplets-last.json
@@ -0,0 +1,115 @@
+{
+   "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/droplets/5425561?page=2&per_page=1",
+         "prev" : "https://api.digitalocean.com/v2/droplets/5425561?page=2&per_page=1"
+      }
+   },
+   "meta" : {
+      "total" : 2
+   },
+   "droplets" : [
+      {
+         "created_at" : "2015-05-25T15:50:48Z",
+         "region" : {
+            "name" : "New York 1",
+            "sizes" : [
+               "32gb",
+               "16gb",
+               "2gb",
+               "1gb",
+               "4gb",
+               "8gb",
+               "512mb",
+               "64gb",
+               "48gb"
+            ],
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "available" : true
+         },
+         "id" : 5425561,
+         "disk" : 20,
+         "networks" : {
+            "v6" : [],
+            "v4" : [
+               {
+                  "type" : "public",
+                  "ip_address" : "162.243.167.46",
+                  "netmask" : "255.255.255.0",
+                  "gateway" : "162.243.167.1"
+               }
+            ]
+         },
+         "backup_ids" : [],
+         "image" : {
+            "slug" : "ubuntu-14-10-x32",
+            "public" : true,
+            "created_at" : "2015-01-08T18:41:22Z",
+            "distribution" : "Ubuntu",
+            "id" : 9801951,
+            "type" : "snapshot",
+            "regions" : [
+               "nyc1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "ams2",
+               "sgp1",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "fra1"
+            ],
+            "name" : "14.10 x32",
+            "min_disk_size" : 20
+         },
+         "vcpus" : 1,
+         "next_backup_window" : {
+            "end" : "2015-06-01T23:00:00Z",
+            "start" : "2015-06-01T00:00:00Z"
+         },
+         "locked" : false,
+         "snapshot_ids" : [],
+         "kernel" : {
+            "name" : "Ubuntu 14.10 x32 vmlinuz-3.16.0-28-generic",
+            "id" : 2926,
+            "version" : "3.16.0-28-generic"
+         },
+         "status" : "active",
+         "features" : [
+            "backups",
+            "virtio"
+         ],
+         "size" : {
+            "price_hourly" : 0.00744,
+            "slug" : "512mb",
+            "disk" : 20,
+            "available" : true,
+            "transfer" : 1,
+            "price_monthly" : 5,
+            "regions" : [
+               "nyc1",
+               "sgp1",
+               "ams1",
+               "sfo1",
+               "nyc2",
+               "lon1",
+               "nyc3",
+               "ams3",
+               "ams2",
+               "fra1"
+            ],
+            "memory" : 512,
+            "vcpus" : 1
+         },
+         "name" : "test1",
+         "size_slug" : "512mb",
+         "memory" : 512
+      }
+   ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/image.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/image.json b/providers/digitalocean2/src/test/resources/image.json
new file mode 100644
index 0000000..e66fda9
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/image.json
@@ -0,0 +1,24 @@
+{
+   "image" : {
+         "type" : "snapshot",
+         "id" : 11732785,
+         "name" : "Maintenance Mode",
+         "min_disk_size" : 20,
+         "distribution" : "Debian",
+         "created_at" : "2015-05-05T21:21:25Z",
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "slug" : null,
+         "public" : true
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/images-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/images-first.json b/providers/digitalocean2/src/test/resources/images-first.json
new file mode 100644
index 0000000..7d98cf9
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/images-first.json
@@ -0,0 +1,108 @@
+{
+   "meta" : {
+      "total" : 54
+   },
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/images?page=2&per_page=5&type=distribution",
+         "last" : "https://api.digitalocean.com/v2/images?page=11&per_page=5&type=distribution"
+      }
+   },
+   "images" : [
+      {
+         "type" : "snapshot",
+         "id" : 11732785,
+         "name" : "Maintenance Mode",
+         "min_disk_size" : 20,
+         "distribution" : "Debian",
+         "created_at" : "2015-05-05T21:21:25Z",
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "slug" : null,
+         "public" : true
+      },
+      {
+         "type" : "snapshot",
+         "id" : 11833262,
+         "distribution" : "CoreOS",
+         "created_at" : "2015-05-12T17:41:36Z",
+         "regions" : [
+            "nyc1",
+            "sfo1",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "public" : true,
+         "slug" : "coreos-stable",
+         "name" : "647.0.0 (stable)",
+         "min_disk_size" : 20
+      },
+      {
+         "min_disk_size" : 20,
+         "name" : "668.3.0 (beta)",
+         "created_at" : "2015-05-18T18:14:12Z",
+         "public" : true,
+         "regions" : [
+            "nyc1",
+            "sfo1",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "slug" : "coreos-beta",
+         "distribution" : "CoreOS",
+         "id" : 11919888,
+         "type" : "snapshot"
+      },
+      {
+         "type" : "snapshot",
+         "id" : 11919908,
+         "distribution" : "CoreOS",
+         "created_at" : "2015-05-18T18:20:08Z",
+         "public" : true,
+         "regions" : [
+            "nyc1",
+            "sfo1",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "slug" : "coreos-alpha",
+         "name" : "681.0.0 (alpha)",
+         "min_disk_size" : 20
+      },
+      {
+         "min_disk_size" : 30,
+         "name" : "vum-easter-move",
+         "slug" : null,
+         "regions" : [
+            "ams1"
+         ],
+         "public" : true,
+         "created_at" : "2015-04-10T07:31:20Z",
+         "distribution" : "Debian",
+         "id" : 11385199,
+         "type" : "snapshot"
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/images-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/images-last.json b/providers/digitalocean2/src/test/resources/images-last.json
new file mode 100644
index 0000000..89642fe
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/images-last.json
@@ -0,0 +1,123 @@
+{
+   "meta" : {
+      "total" : 54
+   },
+   "links" : {
+      "pages" : {
+         "prev" : "https://api.digitalocean.com/v2/images?page=1&per_page=5&type=distribution",
+         "first" : "https://api.digitalocean.com/v2/images?page=1&per_page=5&type=distribution"
+      }
+   },
+   "images" : [
+      {
+         "distribution" : "Fedora",
+         "min_disk_size" : 20,
+         "id" : 6370882,
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "name" : "20 x64",
+         "type" : "snapshot",
+         "slug" : "fedora-20-x64",
+         "public" : true,
+         "created_at" : "2014-09-26T15:29:01Z"
+      },
+      {
+         "name" : "20 x32",
+         "type" : "snapshot",
+         "distribution" : "Fedora",
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "id" : 6370885,
+         "min_disk_size" : 20,
+         "created_at" : "2014-09-26T15:29:18Z",
+         "public" : true,
+         "slug" : "fedora-20-x32"
+      },
+      {
+         "created_at" : "2014-09-26T16:40:18Z",
+         "slug" : "centos-5-8-x64",
+         "public" : true,
+         "type" : "snapshot",
+         "name" : "5.10 x64",
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "min_disk_size" : 20,
+         "id" : 6372321,
+         "distribution" : "CentOS"
+      },
+      {
+         "public" : true,
+         "slug" : "centos-5-8-x32",
+         "created_at" : "2014-09-26T16:45:29Z",
+         "id" : 6372425,
+         "min_disk_size" : 20,
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "distribution" : "CentOS",
+         "type" : "snapshot",
+         "name" : "5.10 x32"
+      },
+      {
+         "created_at" : "2014-09-26T16:56:00Z",
+         "public" : true,
+         "slug" : "debian-6-0-x64",
+         "type" : "snapshot",
+         "name" : "6.0 x64",
+         "regions" : [
+            "nyc1",
+            "ams1",
+            "sfo1",
+            "nyc2",
+            "ams2",
+            "sgp1",
+            "lon1",
+            "nyc3",
+            "ams3",
+            "fra1"
+         ],
+         "min_disk_size" : 20,
+         "id" : 6372581,
+         "distribution" : "Debian"
+      }
+   ]
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/kernels-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/kernels-first.json b/providers/digitalocean2/src/test/resources/kernels-first.json
new file mode 100644
index 0000000..3ec05e0
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/kernels-first.json
@@ -0,0 +1,38 @@
+{
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/droplets/5425561/kernels?page=2",
+         "last" : "https://api.digitalocean.com/v2/droplets/5425561/kernels?page=2"
+      }
+   },
+   "meta" : {
+      "total" : 10
+   },
+   "kernels" : [
+      {
+         "id" : 231,
+         "version" : "3.8.0-25-generic",
+         "name" : "DO-recovery-static-fsck"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-41-generic-pae",
+         "version" : "2.6.32-41-generic-pae",
+         "id" : 61
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-56-generic-pae",
+         "id" : 946,
+         "version" : "2.6.32-56-generic-pae"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-57-generic-pae",
+         "id" : 987,
+         "version" : "2.6.32-57-generic-pae"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-58-generic-pae",
+         "id" : 1269,
+         "version" : "2.6.32-58-generic-pae"
+      }
+   ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/kernels-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/kernels-last.json b/providers/digitalocean2/src/test/resources/kernels-last.json
new file mode 100644
index 0000000..ca6c4f6
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/kernels-last.json
@@ -0,0 +1,38 @@
+{
+   "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/droplets/5425561/kernels?page=2",
+         "prev" : "https://api.digitalocean.com/v2/droplets/5425561/kernels?page=2"
+      }
+   },
+   "meta" : {
+      "total" : 10
+   },
+   "kernels" : [
+      {
+         "id" : 2311,
+         "version" : "3.8.0-25-generic",
+         "name" : "DO-recovery-static-fsck"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-41-generic-pae",
+         "version" : "2.6.32-41-generic-pae",
+         "id" : 6111
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-56-generic-pae",
+         "id" : 94611,
+         "version" : "2.6.32-56-generic-pae"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-57-generic-pae",
+         "id" : 98711,
+         "version" : "2.6.32-57-generic-pae"
+      },
+      {
+         "name" : "Ubuntu 10.04 x32 vmlinuz-2.6.32-58-generic-pae",
+         "id" : 1269,
+         "version" : "2.6.32-58-generic-pae"
+      }
+   ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/key.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/key.json b/providers/digitalocean2/src/test/resources/key.json
new file mode 100644
index 0000000..db77937
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/key.json
@@ -0,0 +1,8 @@
+{
+   "ssh_key" : {
+     "id" : 767051,
+     "name" : "ubuntu-1204-64bit-338",
+     "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCIshoBsRRK73Kwq9zlKauwNWClpaR99TpAxjtP5DcL3EMCTCDepaNPMHhPYO1rogRyjZrc2nsJ1ImCbZ3eEGFjspyhIyPk1NbYkAFsoSV7eBtqy1V7ddZ8t1ZsDexigAA5GnXZSWL4O0oVyZpBTrkzhZ49lbWUq8ch3Hhulvml1BR4nTx92ZcYIFFr1S7NEtua9xhKvRvUcSmgL/0A8deOBgkc85y5ADcEIt+nrlrOtIOW/agX0VPXNFjRxYW7MCkRGoDObOYEaT/mj7PyKk0kimmemxAH5wd6aOI4C82TmHjQmXTwXZgpVlrsdbxomOGDDGNSy7HoQLI/xMRvEf+9",
+     "fingerprint" : "1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90"
+    }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/keys-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/keys-first.json b/providers/digitalocean2/src/test/resources/keys-first.json
new file mode 100644
index 0000000..93cbdd7
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/keys-first.json
@@ -0,0 +1,43 @@
+{
+   "meta" : {
+      "total" : 17
+   },
+   "ssh_keys" : [
+      {
+         "id" : 767051,
+         "name" : "ubuntu-1204-64bit-338",
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCIshoBsRRK73Kwq9zlKauwNWClpaR99TpAxjtP5DcL3EMCTCDepaNPMHhPYO1rogRyjZrc2nsJ1ImCbZ3eEGFjspyhIyPk1NbYkAFsoSV7eBtqy1V7ddZ8t1ZsDexigAA5GnXZSWL4O0oVyZpBTrkzhZ49lbWUq8ch3Hhulvml1BR4nTx92ZcYIFFr1S7NEtua9xhKvRvUcSmgL/0A8deOBgkc85y5ADcEIt+nrlrOtIOW/agX0VPXNFjRxYW7MCkRGoDObOYEaT/mj7PyKk0kimmemxAH5wd6aOI4C82TmHjQmXTwXZgpVlrsdbxomOGDDGNSy7HoQLI/xMRvEf+9",
+         "fingerprint" : "1a:cc:9b:88:c8:4f:b8:77:96:15:d2:0c:95:86:ff:90"
+      },
+      {
+         "id" : 767067,
+         "name" : "ubuntu-1204-64bit-ea4",
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCdkjezVub0t+lSNSQYbO+Kcd/SoOJBCYgETqroI+ysHpPjRRNTYp3XSqSB0O3snnXM1GRfjPFatYFBUO9ajsb8cq0pzDPEIh3+cn+ZEoQ5WPg8OpaU6zVt2fPZNwqE2FCWEr2naH2yj2qV7Wzd1T9xYRfFdQ19YU1HuUhs16zvF6cO4BJH0YX2HLtZkxtOHonSbBx78/hBoV1NgSKlzlHVj9rhqmJO0fcEBeia4y/6G/65n8Z3hrM3zwyoN/eBKV7ngeJ70VU8tcP/QK9anB6hx4lOOSnWro2j3GKDwjSADL2vbOGl4OS6kM2y+6I04A6UAUkMu7KWCSr1Zf0zC45N",
+         "fingerprint" : "8f:81:51:d2:91:09:25:8f:c2:9c:b2:c3:6f:67:55:77"
+      },
+      {
+         "name" : "ubuntu-1204-64bit-jdk8-850",
+         "id" : 767363,
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0zjvxYEyNYsVd7lfYXJxLON98OTgdMizD14XYCHwhVdJkS5ht9h+6dpiWbgatVgb/xyB63N2EeW+yZvQGlohWFytd1HvX7C8LFVtS/h4nrFJxtEYc2QXRl+WRRys6ekxYy6cwf/WV8nQPitUIPQru7vKR8bRpMPDBh+2Va/k7ApLd83AM9UYwXgCxDC7wlL/ttdJCV40buwPVvbOot97+qFPyg600/s6MGMX5448VifeBEEZG/qjzHj4SSGpQn+12O0dFLpYxOruzLLBpu2QxwvIqj1Nsm2ij6seMh0J8a5rQPl3ssPYrDCi8QGhL2NyzDSmT/9tYMLOlhBj7x3zN",
+         "fingerprint" : "1c:17:a2:c2:00:42:89:63:23:79:01:d7:b8:e6:a6:b4"
+      },
+      {
+         "name" : "ubuntu-1204-64bit-jdk8-ead",
+         "id" : 767375,
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCTMLrMCDSPF3BBeDjzSIYtVi8ACkxrvG4VT58sI+XjpeE9c3KGekqXqeTKZFwLxRllgbox8j610ABvcdAstBBSooSYmuDjT2nFotiTcw+5iHUe4A8BHG3HSlqXfuPlBCZ9bE4MdDBTPXqaDjiuvUqWdjpUihZeX2W2OtznDXDMFtn846Df0hD2V7ixHqVVQRXgflSfIoajEY5MlVG/Iuh3cjPpoJZ/IN/FcQnHjNe1FdJM8fC7kF+yY8sEE+u10KlfR1Y9WTdJSIiz5QDdlR4RTPXYyb9wyctD/sxCJ3xAgfZOG7Ey8eZF0+0MSbxkUGQll/xLuaP2aA5owIfMkEoj",
+         "fingerprint" : "a1:4d:20:c8:25:ce:2f:fa:43:8d:21:6f:65:77:c9:c4"
+      },
+      {
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCj9vCuDlTrQd+9WqlsQxWLg7VO2oVwq960CyW9C7rEro07wPGDdiOQornhyy7KiSL0mqYzFT5j0vTICSYeiGXIO/4SFPekz7vRN1yjO2GKaPokl/eYnbt1ABqB41jL0YX4ySwTm11O8A5yqWMouyxiGlSr13p61S2LTim5r5Z2C+4dKWHduloxnyCJJsh/56ZquS/ERgcrZusis9ybIP3aP+5DbXmyokGU83qGuNqSjZXaWc7MGFRTZzzzR9dQsbMvTgPwpyFXtSruIC+u7ca8oJ0b7+9WCAj2g/GXzbv/dsiJyJkEOtx6k1NN8n02PmL3o50vM1GGAjBFUN65JghD",
+         "name" : "ubuntu-1204-64bit-jdk8-ce2",
+         "id" : 767390,
+         "fingerprint" : "7f:bc:1c:af:e5:51:b6:1b:f0:bd:93:de:60:1a:e9:25"
+      }
+   ],
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/account/keys?page=2&per_page=5",
+         "last" : "https://api.digitalocean.com/v2/account/keys?page=2&per_page=5"
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/keys-last.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/keys-last.json b/providers/digitalocean2/src/test/resources/keys-last.json
new file mode 100644
index 0000000..becbed1
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/keys-last.json
@@ -0,0 +1,25 @@
+{
+   "meta" : {
+      "total" : 7
+   },
+   "ssh_keys" : [
+      {
+         "fingerprint" : "a1:df:a0:64:e4:36:10:6b:de:8d:e6:65:55:17:8f:31",
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCHcVV1Rb5+/sCDF0eZ9Hw8vrHF+HYxptZRaNmnWsjzjeupeIiFgq6W1gRrDKB6pjA1t8R0b1xlcfb0361B5fRuwkeQTr2ALior6cipTgUUHT4UKcZMah7n09/YMPj2I7vzxjdX07PyB1z4OkL7FT3zBrsDTaR+ZngeFAVG59WmCj6vD2EqoZ2PeiEKETZOvQnZ3MEGiU5sYX9odIh5mjO5Jg1Q7oLjFzBbmMQ0oo/gkte8nBybHlD20iq1kzvmoSV4ECbTIa48yr8cgsX0d4M29YV9v2WCBfqaSRTRGVhfsalu9sgeSTHZ7F68g1wEkPXOzE3ZCUlZsKpskiYmCcTP",
+         "name" : "ubuntu-1204-64bit-jdk8-77a",
+         "id" : 799690
+      },
+      {
+         "name" : "ubuntu-1204-64bit-jdk8-a14",
+         "public_key" : "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCeU4rK0wlk8YOOAWd9GURgJzOlgNkLEarq/y9RMXKq1zUW9Jf5J4UuztZm6HsSpAbiBfwL/QYBsFEhfDUZiW0B5p6CL0cR93vgUhGVgoi+v5Nl3KDEgMDryja48CdmzkCB3KoX4aRGRkahx6chrFByM5laEyGJETnDl0s4VyxZH07FGgQYqdWj8jg99stIvZ8Ktzlq5RMXPYePEA03vzzc5JrTEtdhuiy/wjxY8piq1j40uch6JEfcEzfPdkWY7LZt5KlOtrYoVFVmu+Mp3QSknhZPyMMZW5GOEtcr1IGtOEBCL3gJWz0E/H4e4Itt+3L2bNpNfHJmISJRXsNanuyn",
+         "fingerprint" : "f3:34:8d:dc:26:31:90:2b:55:f0:d4:77:d4:17:1e:f2",
+         "id" : 802469
+      }
+   ],
+   "links" : {
+      "pages" : {
+         "first" : "https://api.digitalocean.com/v2/account/keys?page=1&per_page=5",
+         "prev" : "https://api.digitalocean.com/v2/account/keys?page=1&per_page=5"
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/logback-test.xml
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/logback-test.xml b/providers/digitalocean2/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..4cac342
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/logback-test.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<configuration scan="false">
+    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/test-data/jclouds.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+    <appender name="WIREFILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/test-data/jclouds-wire.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+    <appender name="COMPUTEFILE" class="ch.qos.logback.core.FileAppender">
+        <file>target/jclouds-compute.log</file>
+        <encoder>
+            <Pattern>%d %-5p [%c] [%thread] %m%n</Pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.jclouds">
+        <level value="DEBUG" />
+        <appender-ref ref="FILE" />
+    </logger>
+    <logger name="jclouds.compute">
+        <level value="DEBUG" />
+        <appender-ref ref="COMPUTEFILE" />
+    </logger>
+    <logger name="jclouds.wire">
+        <level value="DEBUG" />
+        <appender-ref ref="WIREFILE" />
+    </logger>
+    <logger name="jclouds.headers">
+        <level value="DEBUG" />
+        <appender-ref ref="WIREFILE" />
+    </logger>
+    
+    <root>
+        <level value="INFO" />
+    </root>
+</configuration>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/power-cycle.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/power-cycle.json b/providers/digitalocean2/src/test/resources/power-cycle.json
new file mode 100644
index 0000000..7af5963
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/power-cycle.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "power_cycle"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/power-off.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/power-off.json b/providers/digitalocean2/src/test/resources/power-off.json
new file mode 100644
index 0000000..7affe0a
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/power-off.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "power_off"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/power-on.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/power-on.json b/providers/digitalocean2/src/test/resources/power-on.json
new file mode 100644
index 0000000..32e6e83
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/power-on.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "power_on"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/reboot.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/reboot.json b/providers/digitalocean2/src/test/resources/reboot.json
new file mode 100644
index 0000000..799500a
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/reboot.json
@@ -0,0 +1,33 @@
+{
+    "action": {
+         "region" : {
+            "name" : "New York 1",
+            "available" : true,
+            "slug" : "nyc1",
+            "features" : [
+               "virtio",
+               "backups",
+               "metadata"
+            ],
+            "sizes" : [
+               "512mb",
+               "8gb",
+               "16gb",
+               "32gb",
+               "48gb",
+               "64gb",
+               "1gb",
+               "2gb",
+               "4gb"
+            ]
+         },
+         "started_at" : "2015-05-19T15:17:55Z",
+         "status" : "in-progress",
+         "resource_type" : "droplet",
+         "resource_id" : 5347489,
+         "region_slug" : "nyc1",
+         "id" : 50900149,
+         "completed_at" : "2015-05-19T15:18:01Z",
+         "type" : "reboot"
+      }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/test/resources/regions-first.json
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/test/resources/regions-first.json b/providers/digitalocean2/src/test/resources/regions-first.json
new file mode 100644
index 0000000..5be4b3f
--- /dev/null
+++ b/providers/digitalocean2/src/test/resources/regions-first.json
@@ -0,0 +1,111 @@
+{
+   "links" : {
+      "pages" : {
+         "next" : "https://api.digitalocean.com/v2/regions?page=2&per_page=5",
+         "last" : "https://api.digitalocean.com/v2/regions?page=2&per_page=5"
+      }
+   },
+   "meta" : {
+      "total" : 10
+   },
+   "regions" : [
+      {
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "name" : "New York 1",
+         "slug" : "nyc1",
+         "features" : [
+            "virtio",
+            "backups",
+            "metadata"
+         ],
+         "available" : true
+      },
+      {
+         "slug" : "ams1",
+         "name" : "Amsterdam 1",
+         "available" : false,
+         "features" : [
+            "virtio",
+            "backups"
+         ],
+         "sizes" : []
+      },
+      {
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "name" : "San Francisco 1",
+         "slug" : "sfo1",
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "available" : true
+      },
+      {
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ],
+         "available" : true,
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups"
+         ],
+         "name" : "New York 2",
+         "slug" : "nyc2"
+      },
+      {
+         "features" : [
+            "virtio",
+            "private_networking",
+            "backups",
+            "ipv6",
+            "metadata"
+         ],
+         "available" : true,
+         "slug" : "ams2",
+         "name" : "Amsterdam 2",
+         "sizes" : [
+            "32gb",
+            "16gb",
+            "2gb",
+            "1gb",
+            "4gb",
+            "8gb",
+            "512mb",
+            "64gb",
+            "48gb"
+         ]
+      }
+   ]
+}
\ No newline at end of file


[05/19] jclouds git commit: JCLOUDS-613: Implement the DigitalOcean v2 API

Posted by na...@apache.org.
http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
new file mode 100644
index 0000000..3e4aae3
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/compute/strategy/CreateKeyPairsThenCreateNodes.java
@@ -0,0 +1,217 @@
+/*
+ * 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.strategy;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.List;
+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.Constants;
+import org.jclouds.compute.config.CustomizationResponse;
+import org.jclouds.compute.domain.NodeMetadata;
+import org.jclouds.compute.domain.Template;
+import org.jclouds.compute.functions.GroupNamingConvention;
+import org.jclouds.compute.reference.ComputeServiceConstants;
+import org.jclouds.compute.strategy.CreateNodeWithGroupEncodedIntoName;
+import org.jclouds.compute.strategy.CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap;
+import org.jclouds.compute.strategy.ListNodesStrategy;
+import org.jclouds.compute.strategy.impl.CreateNodesWithGroupEncodedIntoNameThenAddToSet;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.compute.options.DigitalOcean2TemplateOptions;
+import org.jclouds.digitalocean2.domain.Key;
+import org.jclouds.digitalocean2.ssh.DSAKeys;
+import org.jclouds.digitalocean2.ssh.ECDSAKeys;
+import org.jclouds.logging.Logger;
+import org.jclouds.ssh.SshKeyPairGenerator;
+import org.jclouds.ssh.SshKeys;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+@Singleton
+public class CreateKeyPairsThenCreateNodes extends CreateNodesWithGroupEncodedIntoNameThenAddToSet {
+
+   @Resource
+   @Named(ComputeServiceConstants.COMPUTE_LOGGER)
+   protected Logger logger = Logger.NULL;
+
+   private final DigitalOcean2Api api;
+   private final SshKeyPairGenerator keyGenerator;
+   private final Function<String, PublicKey> sshKeyToPublicKey;
+
+   @Inject
+   protected CreateKeyPairsThenCreateNodes(
+         CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy,
+         ListNodesStrategy listNodesStrategy,
+         GroupNamingConvention.Factory namingConvention,
+         @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor,
+         CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
+         DigitalOcean2Api api, SshKeyPairGenerator keyGenerator, Function<String, PublicKey> sshKeyToPublicKey) {
+      super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor,
+            customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory);
+      this.api = checkNotNull(api, "api cannot be null");
+      this.keyGenerator = checkNotNull(keyGenerator, "keyGenerator cannot be null");
+      checkNotNull(userExecutor, "userExecutor cannot be null");
+      this.sshKeyToPublicKey = checkNotNull(sshKeyToPublicKey, "sshKeyToPublicKey cannot be null");
+   }
+
+   @Override
+   public Map<?, ListenableFuture<Void>> execute(String group, int count, Template template,
+         Set<NodeMetadata> goodNodes, Map<NodeMetadata, Exception> badNodes,
+         Multimap<NodeMetadata, CustomizationResponse> customizationResponses) {
+
+      DigitalOcean2TemplateOptions options = template.getOptions().as(DigitalOcean2TemplateOptions.class);
+      Set<Integer> generatedSshKeyIds = Sets.newHashSet();
+
+      // If no key has been configured and the auto-create option is set, then generate a key pair
+      if (options.getSshKeyIds().isEmpty() && options.getAutoCreateKeyPair()
+            && Strings.isNullOrEmpty(options.getPublicKey())) {
+         generateKeyPairAndAddKeyToSet(options, generatedSshKeyIds, group);
+      }
+
+      // If there is a script to run in the node, make sure a private key has been configured so jclouds will be able to
+      // access the node
+      if (options.getRunScript() != null) {
+         checkArgument(!Strings.isNullOrEmpty(options.getLoginPrivateKey()),
+               "no private key configured for: %s; please use options.overrideLoginPrivateKey(rsa_private_text)", group);
+      }
+
+      // If there is a key configured, then make sure there is a key pair for it
+      if (!Strings.isNullOrEmpty(options.getPublicKey())) {
+         createKeyPairForPublicKeyInOptionsAndAddToSet(options, generatedSshKeyIds);
+      }
+
+      // Set all keys (the provided and the auto-generated) in the options object so the
+      // DigitalOceanComputeServiceAdapter adds them all
+      options.sshKeyIds(Sets.union(generatedSshKeyIds, options.getSshKeyIds()));
+
+      Map<?, ListenableFuture<Void>> responses = super.execute(group, count, template, goodNodes, badNodes,
+            customizationResponses);
+
+      // Key pairs in DigitalOcean are only required to create the Droplets. They aren't used anymore so it is better
+      // to delete the auto-generated key pairs at this point where we know exactly which ones have been
+      // auto-generated by jclouds.
+      registerAutoGeneratedKeyPairCleanupCallbacks(responses, generatedSshKeyIds);
+
+      return responses;
+   }
+
+   private void createKeyPairForPublicKeyInOptionsAndAddToSet(DigitalOcean2TemplateOptions options,
+         Set<Integer> generatedSshKeyIds) {
+      logger.debug(">> checking if the key pair already exists...");
+
+      PublicKey userKey = sshKeyToPublicKey.apply(options.getPublicKey());
+      String userFingerprint = computeFingerprint(userKey);
+      Key key = api.keyApi().get(userFingerprint);  
+
+      if (key == null) {
+         logger.debug(">> key pair not found. creating a new one...");
+
+         Key newKey = api.keyApi().create(userFingerprint, options.getPublicKey());
+
+         generatedSshKeyIds.add(newKey.id());
+         logger.debug(">> key pair created! %s", newKey);
+      } else {
+         logger.debug(">> key pair found! %s", key);
+         generatedSshKeyIds.add(key.id());
+      }
+   }
+
+   private void generateKeyPairAndAddKeyToSet(DigitalOcean2TemplateOptions options, Set<Integer> generatedSshKeyIds, String prefix) {
+      logger.debug(">> creating default keypair for node...");
+
+      Map<String, String> defaultKeys = keyGenerator.get();
+
+      Key defaultKey = api.keyApi().create(prefix + "-" + System.getProperty("user.name"), defaultKeys.get("public"));
+      generatedSshKeyIds.add(defaultKey.id());
+
+      logger.debug(">> keypair created! %s", defaultKey);
+
+      // If a private key has not been explicitly set, configure the auto-generated one
+      if (Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
+         options.overrideLoginPrivateKey(defaultKeys.get("private"));
+      }
+   }
+
+   private void registerAutoGeneratedKeyPairCleanupCallbacks(Map<?, ListenableFuture<Void>> responses,
+         final Set<Integer> generatedSshKeyIds) {
+      // The Futures.allAsList fails immediately if some of the futures fail. The Futures.successfulAsList, however,
+      // returns a list containing the results or 'null' for those futures that failed. We want to wait for all them
+      // (even if they fail), so better use the latter form.
+      ListenableFuture<List<Void>> aggregatedResponses = Futures.successfulAsList(responses.values());
+
+      // Key pairs must be cleaned up after all futures completed (even if some failed).
+      Futures.addCallback(aggregatedResponses, new FutureCallback<List<Void>>() {
+         @Override
+         public void onSuccess(List<Void> result) {
+            cleanupAutoGeneratedKeyPairs(generatedSshKeyIds);
+         }
+
+         @Override
+         public void onFailure(Throwable t) {
+            cleanupAutoGeneratedKeyPairs(generatedSshKeyIds);
+         }
+
+         private void cleanupAutoGeneratedKeyPairs(Set<Integer> generatedSshKeyIds) {
+            logger.debug(">> cleaning up auto-generated key pairs...");
+            for (Integer sshKeyId : generatedSshKeyIds) {
+               try {
+                  api.keyApi().delete(sshKeyId);
+               } catch (Exception ex) {
+                  logger.warn(">> could not delete key pair %s: %s", sshKeyId, ex.getMessage());
+               }
+            }
+         }
+
+      }, userExecutor);
+   }
+   
+   private static String computeFingerprint(PublicKey key) {
+      if (key instanceof RSAPublicKey) {
+         RSAPublicKey rsaKey = (RSAPublicKey) key;
+         return SshKeys.fingerprint(rsaKey.getPublicExponent(), rsaKey.getModulus());
+      } else if (key instanceof DSAPublicKey) {
+         DSAPublicKey dsaKey = (DSAPublicKey) key;
+         return DSAKeys.fingerprint(dsaKey.getParams().getP(), dsaKey.getParams().getQ(), dsaKey.getParams().getG(),
+               dsaKey.getY());
+      } else if (key instanceof ECPublicKey) {
+         ECPublicKey ecdsaKey = (ECPublicKey) key;
+         return ECDSAKeys.fingerprint(ecdsaKey);
+      } else {
+         throw new IllegalArgumentException("Only RSA and DSA keys are supported");
+      }
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2HttpApiModule.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2HttpApiModule.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2HttpApiModule.java
new file mode 100644
index 0000000..8bfe266
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOcean2HttpApiModule.java
@@ -0,0 +1,57 @@
+/*
+ * 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.config;
+
+import java.net.URI;
+
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.options.ImageListOptions;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.LinkToImageListOptions;
+import org.jclouds.digitalocean2.functions.LinkToListOptions;
+import org.jclouds.digitalocean2.handlers.DigitalOcean2ErrorHandler;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
+import org.jclouds.oauth.v2.config.OAuthScopes;
+import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+import com.google.common.base.Function;
+import com.google.inject.TypeLiteral;
+
+@ConfiguresHttpApi
+public class DigitalOcean2HttpApiModule extends HttpApiModule<DigitalOcean2Api> {
+
+   @Override
+   protected void bindErrorHandlers() {
+      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(DigitalOcean2ErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(DigitalOcean2ErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(DigitalOcean2ErrorHandler.class);
+   }
+
+   @Override
+   protected void configure() {
+      super.configure();
+      bind(OAuthScopes.class).toInstance(OAuthScopes.ReadOrWriteScopes.create("read", "read write"));
+      bind(new TypeLiteral<Function<URI, ListOptions>>() {
+      }).to(LinkToListOptions.class);
+      bind(new TypeLiteral<Function<URI, ImageListOptions>>() {
+      }).to(LinkToImageListOptions.class);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOceanParserModule.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOceanParserModule.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOceanParserModule.java
new file mode 100644
index 0000000..e4bb9bc
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/config/DigitalOceanParserModule.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean2.config;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Throwables.propagate;
+import static com.google.common.collect.Iterables.get;
+import static com.google.common.collect.Iterables.size;
+import static com.google.inject.Scopes.SINGLETON;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.digitalocean2.ssh.DSAKeys;
+import org.jclouds.digitalocean2.ssh.ECDSAKeys;
+import org.jclouds.json.config.GsonModule.DateAdapter;
+import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
+import org.jclouds.ssh.SshKeys;
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+
+/**
+ * Custom parser bindings.
+ */
+public class DigitalOceanParserModule extends AbstractModule {
+
+   @Override
+   protected void configure() {
+      bind(DateAdapter.class).to(Iso8601DateAdapter.class).in(SINGLETON);
+   }
+
+   @Singleton
+   public static class SshPublicKeyAdapter extends TypeAdapter<PublicKey> {
+
+      private final Function<PublicKey, String> publicKeyToSshKey;
+      private final Function<String, PublicKey> sshKeyToPublicKey;
+
+      @Inject
+      public SshPublicKeyAdapter(Function<PublicKey, String> publicKeyToSshKey,
+            Function<String, PublicKey> sshKeyToPublicKey) {
+         this.publicKeyToSshKey = checkNotNull(publicKeyToSshKey, "publicKeyToSshKey cannot be null");
+         this.sshKeyToPublicKey = checkNotNull(sshKeyToPublicKey, "sshKeyToPublicKey cannot be null");
+      }
+
+      @Override
+      public void write(JsonWriter out, PublicKey value) throws IOException {
+         out.value(publicKeyToSshKey.apply(value));
+      }
+
+      @Override
+      public PublicKey read(JsonReader in) throws IOException {
+         return sshKeyToPublicKey.apply(in.nextString().trim());
+      }
+   }
+
+   @Provides
+   @Singleton
+   public Function<PublicKey, String> publicKeyToSshKey() {
+      return new Function<PublicKey, String>() {
+         @Override
+         public String apply(PublicKey input) {
+            if (input instanceof RSAPublicKey) {
+               return SshKeys.encodeAsOpenSSH((RSAPublicKey) input);
+            } else if (input instanceof DSAPublicKey) {
+               return DSAKeys.encodeAsOpenSSH((DSAPublicKey) input);
+            } else {
+               throw new IllegalArgumentException("Only RSA and DSA keys are supported");
+            }
+         }
+      };
+   }
+
+   @Provides
+   @Singleton
+   public Function<String, PublicKey> sshKeyToPublicKey() {
+      return new Function<String, PublicKey>() {
+         @Override
+         public PublicKey apply(String input) {
+            Iterable<String> parts = Splitter.on(' ').split(input);
+            checkArgument(size(parts) >= 2, "bad format, should be: [ssh-rsa|ssh-dss] AAAAB3...");
+            String type = get(parts, 0);
+
+            try {
+               if ("ssh-rsa".equals(type)) {
+                  RSAPublicKeySpec spec = SshKeys.publicKeySpecFromOpenSSH(input);
+                  return KeyFactory.getInstance("RSA").generatePublic(spec);
+               } else if ("ssh-dss".equals(type)) {
+                  DSAPublicKeySpec spec = DSAKeys.publicKeySpecFromOpenSSH(input);
+                  return KeyFactory.getInstance("DSA").generatePublic(spec);
+               } else if (type.startsWith("ecdsa-sha2-")) {
+                  ECPublicKeySpec spec = ECDSAKeys.publicKeySpecFromOpenSSH(input);
+                  return KeyFactory.getInstance("EC").generatePublic(spec);
+               } else {
+                  throw new IllegalArgumentException("bad format, jclouds supports ssh-rsa, ssh-dss, ecdsa-sha2-nistp[256|384|521]");
+               }
+            } catch (InvalidKeySpecException ex) {
+               throw propagate(ex);
+            } catch (NoSuchAlgorithmException ex) {
+               throw propagate(ex);
+            }
+         }
+      };
+   }
+
+   @Provides
+   @Singleton
+   public Map<Type, Object> provideCustomAdapterBindings(SshPublicKeyAdapter sshPublicKeyAdapter) {
+      return ImmutableMap.<Type, Object> of(PublicKey.class, sshPublicKeyAdapter);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Action.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Action.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Action.java
new file mode 100644
index 0000000..b800105
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Action.java
@@ -0,0 +1,71 @@
+/*
+ * 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.domain;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.Date;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+
+@AutoValue
+public abstract class Action {
+   
+   public enum Status {
+      COMPLETED, IN_PROGRESS, ERRORED;
+
+      Status() {}
+
+      public static Status fromValue(String value) {
+         Optional<Status> status = Enums.getIfPresent(Status.class, value.toUpperCase());
+         if (!status.isPresent()) {
+            String upperCamelValue = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value.toLowerCase());
+            status = Enums.getIfPresent(Status.class, upperCamelValue);
+         }
+         checkArgument(status.isPresent(), "Expected one of %s but was", Joiner.on(',').join(Status.values()), value);
+         return status.get();
+      }
+   }
+
+   public abstract int id();
+   public abstract Status status();
+   public abstract String type();
+   public abstract Date startedAt();
+   @Nullable public abstract Date completedAt();
+   public abstract Integer resourceId();
+   public abstract String resourceType();
+   @Nullable public abstract Region region();
+   @Nullable public abstract String regionSlug();
+
+   @SerializedNames({ "id", "status", "type", "started_at", "completed_at", "resource_id", "resource_type",
+      "region", "region_slug" })
+   public static Action create(int id, Status status, String type, Date startedAt, Date completedAt, int resourceId,
+         String resourceType, Region region, String regionSlug) {
+      return new AutoValue_Action(id, status, type, startedAt, completedAt, resourceId, resourceType, region,
+            regionSlug);
+   }
+
+   Action() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Backup.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Backup.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Backup.java
new file mode 100644
index 0000000..536a187
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Backup.java
@@ -0,0 +1,43 @@
+/*
+ * 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.domain;
+
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Backup {
+   public abstract int id();
+   public abstract String name();
+   public abstract String distribution();
+   @Nullable public abstract String slug();
+   public abstract boolean isPublic();
+   public abstract List<String> regions();
+   public abstract int minDiskSize();
+
+   @SerializedNames({ "id", "name", "distribution", "slug", "public", "regions", "min_disk_size" })
+   public static Backup create(int id, String name, String distribution, String slug, boolean isPublic,
+         List<String> regions, int minDiskSize) {
+      return new AutoValue_Backup(id, name, distribution, slug, isPublic, regions, minDiskSize);
+   }
+
+   Backup() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Distribution.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Distribution.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Distribution.java
new file mode 100644
index 0000000..1ea0f0e
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Distribution.java
@@ -0,0 +1,69 @@
+/*
+ * 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.domain;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.tryFind;
+import static java.util.Arrays.asList;
+
+import java.util.List;
+
+import org.jclouds.compute.domain.OsFamily;
+import com.google.common.base.Predicate;
+
+/**
+ * DigitalOcean image distributions.
+ */
+public enum Distribution {
+   ARCHLINUX(OsFamily.ARCH, "Arch Linux"), 
+   CENTOS(OsFamily.CENTOS, "CentOS"), 
+   DEBIAN(OsFamily.DEBIAN, "Debian"), 
+   FEDORA(OsFamily.FEDORA, "Fedora"), 
+   UBUNTU(OsFamily.UBUNTU, "Ubuntu"), 
+   UNRECOGNIZED(OsFamily.UNRECOGNIZED, ""); 
+
+   private static final List<Distribution> values = asList(Distribution.values());
+
+   private final OsFamily osFamily;
+   private final String value;
+
+   private Distribution(OsFamily osFamily, String value) {
+      this.osFamily = checkNotNull(osFamily, "osFamily cannot be null");
+      this.value = checkNotNull(value, "value cannot be null");
+   }
+
+   public OsFamily osFamily() {
+      return this.osFamily;
+   }
+   
+   public String value() {
+      return this.value;
+   }
+
+   public static Distribution fromValue(String value) {
+      return tryFind(values, hasValue(value)).or(UNRECOGNIZED);
+   }
+
+   private static Predicate<Distribution> hasValue(final String value) {
+      return new Predicate<Distribution>() {
+         @Override
+         public boolean apply(Distribution input) {
+            return input.value.equalsIgnoreCase(value);
+         }
+      };
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Droplet.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Droplet.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Droplet.java
new file mode 100644
index 0000000..4d0dd4c
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Droplet.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.domain;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.copyOf;
+import static com.google.common.collect.Iterables.concat;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Enums;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+
+@AutoValue
+public abstract class Droplet {
+   
+   public enum Status {
+      NEW, ACTIVE, ARCHIVE, OFF;
+
+      public static Status fromValue(String value) {
+         Optional<Status> status = Enums.getIfPresent(Status.class, value.toUpperCase());
+         checkArgument(status.isPresent(), "Expected one of %s but was %s", Joiner.on(',').join(Status.values()), value);
+         return status.get();
+      }
+   }
+   
+   public abstract int id();
+   public abstract String name();
+   public abstract int memory();
+   public abstract int vcpus();
+   public abstract int disk();
+   public abstract boolean locked();
+   public abstract Date createdAt();
+   public abstract Status status();
+   public abstract List<Integer> backupsIds();
+   public abstract List<Integer> snapshotIds();
+   public abstract List<String> features();
+   @Nullable public abstract Region region();
+   @Nullable public abstract Image image();
+   @Nullable public abstract Size size();
+   public abstract String sizeSlug();
+   @Nullable  public abstract Networks networks();
+   @Nullable public abstract Kernel kernel();
+
+   @SerializedNames({ "id", "name", "memory", "vcpus", "disk", "locked", "created_at", "status", "backup_ids",
+         "snapshot_ids", "features", "region", "image", "size", "size_slug", "networks", "kernel" })
+   public static Droplet create(int id, String name, int memory, int vcpus, int disk, boolean locked, Date createdAt,
+         Status status, List<Integer> backupIds, List<Integer> snapshotIds, List<String> features, Region region,
+         Image image, Size size, String sizeSlug, Networks network, Kernel kernel) {
+      return new AutoValue_Droplet(id, name, memory, vcpus, disk, locked, createdAt, status, 
+            backupIds == null ? ImmutableList.<Integer> of() : copyOf(backupIds),
+            snapshotIds == null ? ImmutableList.<Integer> of() : copyOf(snapshotIds), copyOf(features), region, image,
+            size, sizeSlug, network, kernel);
+   }
+
+   public Set<Networks.Address> getPublicAddresses() {
+      return FluentIterable.from(concat(networks().ipv4(), networks().ipv6()))
+            .filter(Networks.Predicates.publicNetworks())
+            .toSet();
+   }
+
+   public Set<Networks.Address> getPrivateAddresses() {
+      return FluentIterable.from(concat(networks().ipv4(), networks().ipv6()))
+            .filter(Networks.Predicates.privateNetworks())
+            .toSet();
+   }
+
+   Droplet() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/DropletCreate.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/DropletCreate.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/DropletCreate.java
new file mode 100644
index 0000000..06ed12b
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/DropletCreate.java
@@ -0,0 +1,66 @@
+/*
+ * 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.domain;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+
+import java.net.URI;
+import java.util.List;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class DropletCreate {
+   public abstract Droplet droplet();
+   public abstract Links links();
+
+   @AutoValue
+   public abstract static class Links {
+      
+      @AutoValue
+      public abstract static class ActionLink {
+         public abstract int id();
+         public abstract String rel();
+         public abstract URI href();
+         
+         @SerializedNames({"id", "rel", "href"})
+         public static ActionLink create(int id, String rel, URI href) {
+            return new AutoValue_DropletCreate_Links_ActionLink(id, rel, href);
+         }
+         
+         ActionLink() {}
+      }
+      
+      public abstract List<ActionLink> actions();
+
+      @SerializedNames({ "actions" })
+      public static Links create(List<ActionLink> actions) {
+         return new AutoValue_DropletCreate_Links(copyOf(actions));
+      }
+
+      Links() {}
+   }
+
+   @SerializedNames({ "droplet", "links" })
+   public static DropletCreate create(Droplet droplet, Links links) {
+      return new AutoValue_DropletCreate(droplet, links);
+   }
+
+   DropletCreate() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Image.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Image.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Image.java
new file mode 100644
index 0000000..dd2c3b6
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Image.java
@@ -0,0 +1,48 @@
+/*
+ * 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.domain;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+
+import java.util.Date;
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Image {
+
+   public abstract int id();
+   public abstract String name();
+   public abstract String type();
+   public abstract String distribution();
+   @Nullable public abstract String slug();
+   public abstract boolean isPublic();
+   public abstract List<String> regions();
+   public abstract Date createdAt();
+
+   @SerializedNames({ "id", "name", "type", "distribution", "slug", "public", "regions", "created_at" })
+   public static Image create(int id, String name, String type, String distribution, String slug, boolean isPublic,
+         List<String> regions, Date createdAt) {
+      return new AutoValue_Image(id, name, type, distribution, slug, isPublic, copyOf(regions), createdAt);
+   }
+
+   Image() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Kernel.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Kernel.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Kernel.java
new file mode 100644
index 0000000..7eb5467
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Kernel.java
@@ -0,0 +1,35 @@
+/*
+ * 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.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Kernel {
+   public abstract int id();
+   public abstract String name();
+   public abstract String version();
+
+   @SerializedNames({ "id", "name", "version" })
+   public static Kernel create(int id, String name, String version) {
+      return new AutoValue_Kernel(id, name, version);
+   }
+
+   Kernel() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Key.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Key.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Key.java
new file mode 100644
index 0000000..c1a7ae3
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Key.java
@@ -0,0 +1,39 @@
+/*
+ * 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.domain;
+
+import java.security.PublicKey;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Key {
+
+   public abstract int id();
+   public abstract String name();
+   public abstract String fingerprint();
+   public abstract PublicKey publicKey();
+
+   @SerializedNames({ "id", "name", "fingerprint", "public_key" })
+   public static Key create(int id, String name, String fingerprint, PublicKey publicKey) {
+      return new AutoValue_Key(id, name, fingerprint, publicKey);
+   }
+
+   Key() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Networks.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Networks.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Networks.java
new file mode 100644
index 0000000..cd3dbd4
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Networks.java
@@ -0,0 +1,77 @@
+/*
+ * 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.domain;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+
+import java.util.List;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Predicate;
+
+@AutoValue
+public abstract class Networks {
+   
+   @AutoValue
+   public abstract static class Address {
+      public abstract String ip();
+      public abstract String netmask();
+      public abstract String gateway();
+      public abstract String type();
+
+      @SerializedNames({ "ip_address", "netmask", "gateway", "type"})
+      public static Address create(String ip, String netmask, String gateway, String type) {
+         return new AutoValue_Networks_Address(ip, netmask, gateway, type);
+      }
+      
+      Address() {}
+   }
+   
+   public abstract List<Address> ipv4();
+   public abstract List<Address> ipv6();
+   
+   @SerializedNames({ "v4", "v6" })
+   public static Networks create(List<Address> ipv4, List<Address> ipv6) {
+      return new AutoValue_Networks(copyOf(ipv4), copyOf(ipv6));
+   }
+   
+   Networks() {}
+
+   public static class Predicates {
+      
+      public static Predicate<Address> publicNetworks() {
+         return new Predicate<Address>() {
+            @Override
+            public boolean apply(Address network) {
+               return network.type().equals("public");
+            }
+         };
+      }
+      
+      public static Predicate<Address> privateNetworks() {
+         return new Predicate<Address>() {
+            @Override
+            public boolean apply(Address network) {
+               return network.type().equals("private");
+            }
+         };
+      }
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/OperatingSystem.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/OperatingSystem.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/OperatingSystem.java
new file mode 100644
index 0000000..041ea20
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/OperatingSystem.java
@@ -0,0 +1,60 @@
+/*
+ * 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.domain;
+
+import static com.google.common.base.Strings.nullToEmpty;
+import static java.util.regex.Pattern.compile;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * The operating system of an image.
+ * <p>
+ * This class parses the <code>name</code> string (e.g. "Ubuntu 12.10 x64") of the images and properly sets each field
+ * to the right value.
+ */
+@AutoValue
+public abstract class OperatingSystem {
+   
+   // Parse something like "12.10 x64" or "Ubuntu 12.10.1 x64" and matches the version and architecture
+   private static final Pattern VERSION_PATTERN = compile("(?:[a-zA-Z\\s]*\\s+)?(\\d+(?:\\.?\\d+)*)?(?:\\s*(x\\d{2}))?.*");
+   private static final String IS_64_BIT = "x64";
+
+   public abstract Distribution distribution();
+   public abstract String version();
+   public abstract String arch();
+
+   public static OperatingSystem create(String name, String distribution) {
+      return new AutoValue_OperatingSystem(Distribution.fromValue(distribution), match(VERSION_PATTERN, name, 1),
+            match(VERSION_PATTERN, name, 2));
+   }
+
+   public boolean is64bit() {
+      return IS_64_BIT.equals(arch());
+   }
+   
+   OperatingSystem() {}
+
+   private static String match(final Pattern pattern, final String input, int group) {
+      Matcher m = pattern.matcher(input);
+      return m.find() ? nullToEmpty(m.group(group)) : "";
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Region.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Region.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Region.java
new file mode 100644
index 0000000..2b3441c
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Region.java
@@ -0,0 +1,39 @@
+/*
+ * 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.domain;
+
+import java.util.List;
+
+import org.jclouds.json.SerializedNames;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Region {
+   public abstract String slug();
+   public abstract String name();
+   public abstract List<String> sizes();
+   public abstract boolean available();
+   public abstract List<String> features();
+
+   @SerializedNames({ "slug", "name", "sizes", "available", "features" })
+   public static Region create(String slug, String name, List<String> sizes, boolean available, List<String> features) {
+      return new AutoValue_Region(slug, name, sizes, available, features);
+   }
+
+   Region() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Size.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Size.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Size.java
new file mode 100644
index 0000000..03d9492
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Size.java
@@ -0,0 +1,46 @@
+/*
+ * 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.domain;
+
+import java.util.List;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Size {
+   public abstract String slug();
+   public abstract boolean available();
+   public abstract float transfer();
+   public abstract float priceMonthly();
+   public abstract float priceHourly();
+   public abstract int memory();
+   public abstract int vcpus();
+   public abstract int disk();
+   public abstract List<String> regions();
+
+   @SerializedNames({ "slug", "available", "transfer", "price_monthly", "price_hourly", "memory", "vcpus", "disk",
+         "regions" })
+   public static Size create(String slug, boolean available, float transfer, float priceMonthly, float priceHourly,
+         int memory, int vcpus, int disk, List<String> regions) {
+      return new AutoValue_Size(slug, available, transfer, priceMonthly, priceHourly, memory, vcpus, disk, regions);
+   }
+
+   Size() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Snapshot.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Snapshot.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Snapshot.java
new file mode 100644
index 0000000..12daaa2
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/Snapshot.java
@@ -0,0 +1,47 @@
+/*
+ * 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.domain;
+
+import java.util.Date;
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class Snapshot {
+   public abstract int id();
+   public abstract String name();
+   public abstract String type();
+   public abstract String distribution();
+   @Nullable public abstract String slug();
+   public abstract boolean isPublic();
+   public abstract List<String> regions();
+   public abstract int minDiskSize();
+   public abstract Date createdAt();
+
+   @SerializedNames({ "id", "name", "type", "distribution", "slug", "public", "regions", "min_disk_size", "created_at"})
+   public static Snapshot create(int id, String name, String type, String distribution, String slug, boolean isPublic,
+         List<String> regions, int minDiskSize, Date createdAt) {
+      return new AutoValue_Snapshot(id, name, type, distribution, slug, isPublic, regions, minDiskSize, createdAt);
+   }
+   
+   Snapshot() {}
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/internal/PaginatedCollection.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/internal/PaginatedCollection.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/internal/PaginatedCollection.java
new file mode 100644
index 0000000..8ca7b21
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/internal/PaginatedCollection.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.digitalocean2.domain.internal;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.net.URI;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Base class for all collections that return paginated results.
+ */
+public abstract class PaginatedCollection<T> extends IterableWithMarker<T> {
+
+   @AutoValue
+   public abstract static class Meta {
+      public abstract long total();
+
+      @SerializedNames({ "total" })
+      public static Meta create(long total) {
+         return new AutoValue_PaginatedCollection_Meta(total);
+      }
+
+      Meta() { }
+   }
+
+   @AutoValue
+   public abstract static class Links {
+
+      @AutoValue
+      public abstract static class Pages {
+         @Nullable public abstract URI first();
+         @Nullable public abstract URI prev();
+         @Nullable public abstract URI next();
+         @Nullable public abstract URI last();
+
+         @SerializedNames({ "first", "prev", "next", "last" })
+         public static Pages create(URI first, URI next, URI prev, URI last) {
+            return new AutoValue_PaginatedCollection_Links_Pages(first, next, prev, last);
+         }
+
+         Pages() { }
+      }
+
+      @Nullable public abstract Pages pages();
+
+      @SerializedNames({ "pages" })
+      public static Links create(Pages pages) {
+         return new AutoValue_PaginatedCollection_Links(pages);
+      }
+
+      Links() { }
+   }
+
+   private final List<T> items;
+   private final Meta meta;
+   private final Links links;
+
+   protected PaginatedCollection(List<T> items, Meta meta, Links links) {
+      this.items = ImmutableList.copyOf(checkNotNull(items, "items cannot be null"));
+      this.meta = checkNotNull(meta, "meta cannot be null");
+      this.links = checkNotNull(links, "links cannot be null");
+   }
+
+   public List<T> items() {
+      return items;
+   }
+
+   public Meta meta() {
+      return meta;
+   }
+
+   public Links links() {
+      return links;
+   }
+
+   @Override public Iterator<T> iterator() {
+      return items.iterator();
+   }
+
+   @Override public Optional<Object> nextMarker() {
+      if (links.pages() == null) {
+         return Optional.absent();
+      }
+      return Optional.fromNullable((Object) links.pages().next());
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
new file mode 100644
index 0000000..b20fc96
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/CreateDropletOptions.java
@@ -0,0 +1,155 @@
+/*
+ * 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.domain.options;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.jclouds.http.HttpRequest;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+import org.jclouds.rest.MapBinder;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Options to customize droplet creation.
+ */
+public class CreateDropletOptions implements MapBinder {
+
+   @Inject private BindToJsonPayload jsonBinder;
+
+   private final Set<Integer> sshKeys;
+   private final boolean backupsEnabled;
+   private final boolean ipv6Enabled;
+   private final boolean privateNetworking;
+   private final String userData;
+
+   private CreateDropletOptions(Set<Integer> sshKeys, boolean backupsEnabled, boolean ipv6Enabled,
+         boolean privateNetworking, @Nullable String userData) {
+      this.sshKeys = sshKeys;
+      this.backupsEnabled = backupsEnabled;
+      this.ipv6Enabled = ipv6Enabled;
+      this.privateNetworking = privateNetworking;
+      this.userData = userData;
+   }
+
+   @AutoValue
+   abstract static class DropletRequest {
+      abstract String name();
+      abstract String region();
+      abstract String size();
+      abstract String image();
+      abstract Set<Integer> sshKeys();
+      abstract Boolean backups();
+      abstract Boolean ipv6();
+      abstract Boolean privateNetworking();
+      @Nullable abstract String userData();
+      
+      @SerializedNames({"name", "region", "size", "image", "ssh_keys", "backups", "ipv6", "private_networking", "user_data"})
+      static DropletRequest create(String name, String region, String size, String image, Set<Integer> sshKeys,
+            Boolean backups, Boolean ipv6, Boolean privateNetworking, String userData) {
+         return new AutoValue_CreateDropletOptions_DropletRequest(name, region, size, image, sshKeys, backups, ipv6,
+               privateNetworking, userData);
+      }
+      
+      DropletRequest() {}
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Map<String, Object> postParams) {
+      DropletRequest droplet = DropletRequest.create(checkNotNull(postParams.get("name"), "name parameter not present").toString(),
+            checkNotNull(postParams.get("region"), "region parameter not present").toString(),
+            checkNotNull(postParams.get("size"), "size parameter not present").toString(),
+            checkNotNull(postParams.get("image"), "image parameter not present").toString(),
+            sshKeys, backupsEnabled, ipv6Enabled, privateNetworking, userData);
+
+      return bindToRequest(request, droplet);
+   }
+
+   @Override
+   public <R extends HttpRequest> R bindToRequest(R request, Object input) {
+      return jsonBinder.bindToRequest(request, input);
+   }
+
+   public Set<Integer> getSshKeys() {
+      return sshKeys;
+   }
+
+   public Boolean getPrivateNetworking() {
+      return privateNetworking;
+   }
+
+   public Boolean getBackupsEnabled() {
+      return backupsEnabled;
+   }
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public static class Builder {
+      private ImmutableSet.Builder<Integer> sshKeyIds = ImmutableSet.builder();
+      private boolean backupsEnabled;
+      private boolean ipv6Enabled;
+      private boolean privateNetworking;
+      private String userData;
+
+      /**
+       * Adds a set of ssh key ids to be added to the droplet.
+       */
+      public Builder addSshKeyIds(Iterable<Integer> sshKeyIds) {
+         this.sshKeyIds.addAll(sshKeyIds);
+         return this;
+      }
+
+      /**
+       * Adds an ssh key id to be added to the droplet.
+       */
+      public Builder addSshKeyId(int sshKeyId) {
+         this.sshKeyIds.add(sshKeyId);
+         return this;
+      }
+
+      /**
+       * Enables a private network interface if the region supports private
+       * networking.
+       */
+      public Builder privateNetworking(boolean privateNetworking) {
+         this.privateNetworking = privateNetworking;
+         return this;
+      }
+
+      /**
+       * Enabled backups for the droplet.
+       */
+      public Builder backupsEnabled(boolean backupsEnabled) {
+         this.backupsEnabled = backupsEnabled;
+         return this;
+      }
+
+      public CreateDropletOptions build() {
+         return new CreateDropletOptions(sshKeyIds.build(), backupsEnabled, ipv6Enabled, privateNetworking, userData);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ImageListOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ImageListOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ImageListOptions.java
new file mode 100644
index 0000000..9f6415d
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ImageListOptions.java
@@ -0,0 +1,74 @@
+/*
+ * 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.domain.options;
+
+/**
+ * Custom options to filter the list of images.
+ */
+public class ImageListOptions extends ListOptions {
+   public static final String TYPE_PARAM = "type";
+   public static final String PRIVATE_PARAM = "private";
+   
+   /**
+    * Configures the type of the images to be retrieved.
+    */
+   public ImageListOptions type(String type) {
+      queryParameters.put(TYPE_PARAM, type);
+      return this;
+   }
+   
+   /**
+    * Get the images of the current user.
+    */
+   public ImageListOptions privateImages(boolean privateImages) {
+      queryParameters.put(PRIVATE_PARAM, String.valueOf(privateImages));
+      return this;
+   }
+   
+   @Override public ImageListOptions perPage(int perPage) {
+      super.perPage(perPage);
+      return this;
+   }
+
+   @Override public ImageListOptions page(int page) {
+      super.page(page);
+      return this;
+   }
+   
+   public static final class Builder {
+      
+      /**
+       * @see {@link ImageListOptions#type(String)}
+       */
+      public static ImageListOptions type(String type) {
+         return new ImageListOptions().type(type);
+      }
+      
+      /**
+       * @see {@link ImageListOptions#privateImages(boolean)}
+       */
+      public static ImageListOptions privateImages(boolean privateImages) {
+         return new ImageListOptions().privateImages(privateImages);
+      }
+      /**
+       * @see {@link ImageListOptions#page(int)}
+       */
+      public static ImageListOptions page(int page) {
+         return new ImageListOptions().page(page);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ListOptions.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ListOptions.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ListOptions.java
new file mode 100644
index 0000000..f859c1c
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/domain/options/ListOptions.java
@@ -0,0 +1,60 @@
+/*
+ * 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.domain.options;
+
+import org.jclouds.http.options.BaseHttpRequestOptions;
+
+/**
+ * Options to customize how paginated lists are returned.
+ */
+public class ListOptions extends BaseHttpRequestOptions {
+   public static final String PAGE_PARAM = "page";
+   public static final String PER_PAGE_PARAM = "per_page";
+   
+   /**
+    * Configures the number of entries to return in each page.
+    */
+   public ListOptions perPage(int perPage) {
+      queryParameters.put(PER_PAGE_PARAM, String.valueOf(perPage));
+      return this;
+   }
+   
+   /**
+    * Configures the number of the page to be returned.
+    */
+   public ListOptions page(int page) {
+      queryParameters.put(PAGE_PARAM, String.valueOf(page));
+      return this;
+   }
+   
+   public static final class Builder {
+      
+      /**
+       * @see {@link ListOptions#perPage(int)}
+       */
+      public static ListOptions perPage(int perPage) {
+         return new ListOptions().perPage(perPage);
+      }
+      
+      /**
+       * @see {@link ListOptions#page(int)}
+       */
+      public static ListOptions page(int page) {
+         return new ListOptions().page(page);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/057be8df/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ActionApi.java
----------------------------------------------------------------------
diff --git a/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ActionApi.java b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ActionApi.java
new file mode 100644
index 0000000..4a7a8bd
--- /dev/null
+++ b/providers/digitalocean2/src/main/java/org/jclouds/digitalocean2/features/ActionApi.java
@@ -0,0 +1,113 @@
+/*
+ * 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.features;
+
+import java.beans.ConstructorProperties;
+import java.io.Closeable;
+import java.net.URI;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404;
+import org.jclouds.Fallbacks.EmptyPagedIterableOnNotFoundOr404;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.collect.PagedIterable;
+import org.jclouds.digitalocean2.DigitalOcean2Api;
+import org.jclouds.digitalocean2.domain.Action;
+import org.jclouds.digitalocean2.domain.internal.PaginatedCollection;
+import org.jclouds.digitalocean2.domain.options.ListOptions;
+import org.jclouds.digitalocean2.functions.BaseToPagedIterable;
+import org.jclouds.http.functions.ParseJson;
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.Json;
+import org.jclouds.oauth.v2.filters.OAuthFilter;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.annotations.Transform;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Provides access to Actions via the REST API.
+ *
+ * @see <a href="https://developers.digitalocean.com/v2/#actions"/>
+ * @see ActionApi
+ */
+@Path("/actions")
+@RequestFilters(OAuthFilter.class)
+@Consumes(MediaType.APPLICATION_JSON)
+public interface ActionApi extends Closeable {
+
+   @Named("action:list")
+   @GET
+   @ResponseParser(ParseActions.class)
+   @Transform(ParseActions.ToPagedIterable.class)
+   @Fallback(EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Action> list();
+   
+   @Named("action:list")
+   @GET
+   @ResponseParser(ParseActions.class)
+   @Fallback(EmptyIterableWithMarkerOnNotFoundOr404.class)
+   IterableWithMarker<Action> list(ListOptions options);
+   
+   static final class ParseActions extends ParseJson<ParseActions.Actions> {
+      @Inject ParseActions(Json json) {
+         super(json, TypeLiteral.get(Actions.class));
+      }
+
+      private static class Actions extends PaginatedCollection<Action> {
+         @ConstructorProperties({ "actions", "meta", "links" })
+         public Actions(List<Action> items, Meta meta, Links links) {
+            super(items, meta, links);
+         }
+      }
+
+      private static class ToPagedIterable extends BaseToPagedIterable<Action, ListOptions> {
+         @Inject ToPagedIterable(DigitalOcean2Api api, Function<URI, ListOptions> linkToOptions) {
+            super(api, linkToOptions);
+         }
+
+         @Override
+         protected IterableWithMarker<Action> fetchPageUsingOptions(ListOptions options, Optional<Object> arg0) {
+            return api.actionApi().list(options);
+         }
+      }
+   }
+
+   @Named("action:get")
+   @GET
+   @SelectJson("action")
+   @Path("/{id}")
+   @Fallback(NullOnNotFoundOr404.class)
+   @Nullable
+   Action get(@PathParam("id") int id);
+   
+}
+