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 <[email protected]>
Authored: Sat Jun 27 19:52:05 2015 -0600
Committer: Ignasi Barrera <[email protected]>
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);
+      }
+   }
+}

Reply via email to