JCLOUDS-707: Improve SSH key pair creation

The SSH key pair creation has been improved to avoid having multiple
public keys with the same fingerprint:

* Create the SSH key pairs before creating the nodes.
* Only auto-generate key pairs if the user has not configured a
  public key.
* If the user has provided a public key, try to find one with the
  same fingerprint and reuse it.
* To make the migration to v2 easier, now the fingerprint of the
  public key is used for the name of the key pair.


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

Branch: refs/heads/master
Commit: 3eaa646dc29b6a85b4fc35f6539a61173a2d3d9e
Parents: 927ad1d
Author: Ignasi Barrera <[email protected]>
Authored: Mon Sep 8 12:33:56 2014 +0200
Committer: Ignasi Barrera <[email protected]>
Committed: Wed Sep 24 14:57:41 2014 +0200

----------------------------------------------------------------------
 ...DigitalOceanComputeServiceContextModule.java |   3 +
 .../options/DigitalOceanTemplateOptions.java    |  57 ++---
 .../strategy/CreateKeyPairsThenCreateNodes.java | 206 +++++++++++++++++++
 .../DigitalOceanComputeServiceAdapter.java      |  48 +----
 .../config/DigitalOceanHttpApiModule.java       |   8 +
 .../config/DigitalOceanParserModule.java        |  84 +++++---
 .../predicates/SameFingerprint.java             |  61 ++++++
 .../digitalocean/strategy/ListSshKeys.java      |  80 +++++++
 .../predicates/SameFingerprintTest.java         |  72 +++++++
 .../strategy/ListSshKeysLiveTest.java           |  83 ++++++++
 10 files changed, 603 insertions(+), 99 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/compute/config/DigitalOceanComputeServiceContextModule.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/config/DigitalOceanComputeServiceContextModule.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/config/DigitalOceanComputeServiceContextModule.java
index 5204bed..aa68f91 100644
--- 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/config/DigitalOceanComputeServiceContextModule.java
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/config/DigitalOceanComputeServiceContextModule.java
@@ -35,6 +35,7 @@ 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.digitalocean.DigitalOceanApi;
 import org.jclouds.digitalocean.compute.extensions.DigitalOceanImageExtension;
 import org.jclouds.digitalocean.compute.functions.DropletStatusToStatus;
@@ -44,6 +45,7 @@ import 
org.jclouds.digitalocean.compute.functions.RegionToLocation;
 import org.jclouds.digitalocean.compute.functions.SizeToHardware;
 import 
org.jclouds.digitalocean.compute.functions.TemplateOptionsToStatementWithoutPublicKey;
 import org.jclouds.digitalocean.compute.options.DigitalOceanTemplateOptions;
+import org.jclouds.digitalocean.compute.strategy.CreateKeyPairsThenCreateNodes;
 import 
org.jclouds.digitalocean.compute.strategy.DigitalOceanComputeServiceAdapter;
 import org.jclouds.digitalocean.domain.Droplet;
 import org.jclouds.digitalocean.domain.Event;
@@ -88,6 +90,7 @@ public class DigitalOceanComputeServiceContextModule extends
       install(new LocationsFromComputeServiceAdapterModule<Droplet, Size, 
Image, Region>() {
       });
 
+      
bind(CreateNodesInGroupThenAddToSet.class).to(CreateKeyPairsThenCreateNodes.class);
       bind(TemplateOptions.class).to(DigitalOceanTemplateOptions.class);
       
bind(TemplateOptionsToStatement.class).to(TemplateOptionsToStatementWithoutPublicKey.class);
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/compute/options/DigitalOceanTemplateOptions.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/options/DigitalOceanTemplateOptions.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/options/DigitalOceanTemplateOptions.java
index 3a25388..bc3dade 100644
--- 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/options/DigitalOceanTemplateOptions.java
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/options/DigitalOceanTemplateOptions.java
@@ -16,6 +16,7 @@
  */
 package org.jclouds.digitalocean.compute.options;
 
+import static com.google.common.base.Objects.equal;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.util.Set;
@@ -23,6 +24,7 @@ import java.util.Set;
 import org.jclouds.compute.options.TemplateOptions;
 
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableSet;
 
 /**
@@ -33,6 +35,7 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
    private Set<Integer> sshKeyIds = ImmutableSet.of();
    private Boolean privateNetworking;
    private Boolean backupsEnabled;
+   private boolean autoCreateKeyPair = true;
 
    /**
     * Enables a private network interface if the region supports private 
networking.
@@ -58,6 +61,14 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
       return this;
    }
 
+   /**
+    * Sets whether an SSH key pair should be created automatically.
+    */
+   public DigitalOceanTemplateOptions autoCreateKeyPair(boolean 
autoCreateKeyPair) {
+      this.autoCreateKeyPair = autoCreateKeyPair;
+      return this;
+   }
+
    public Set<Integer> getSshKeyIds() {
       return sshKeyIds;
    }
@@ -70,6 +81,10 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
       return backupsEnabled;
    }
 
+   public boolean getAutoCreateKeyPair() {
+      return autoCreateKeyPair;
+   }
+
    @Override
    public DigitalOceanTemplateOptions clone() {
       DigitalOceanTemplateOptions options = new DigitalOceanTemplateOptions();
@@ -88,18 +103,14 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
          if (backupsEnabled != null) {
             eTo.backupsEnabled(backupsEnabled);
          }
+         eTo.autoCreateKeyPair(autoCreateKeyPair);
          eTo.sshKeyIds(sshKeyIds);
       }
    }
 
    @Override
    public int hashCode() {
-      final int prime = 31;
-      int result = super.hashCode();
-      result = prime * result + (backupsEnabled == null ? 0 : 
backupsEnabled.hashCode());
-      result = prime * result + (privateNetworking == null ? 0 : 
privateNetworking.hashCode());
-      result = prime * result + (sshKeyIds == null ? 0 : sshKeyIds.hashCode());
-      return result;
+      return Objects.hashCode(super.hashCode(), backupsEnabled, 
privateNetworking, autoCreateKeyPair, sshKeyIds);
    }
 
    @Override
@@ -114,28 +125,9 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
          return false;
       }
       DigitalOceanTemplateOptions other = (DigitalOceanTemplateOptions) obj;
-      if (backupsEnabled == null) {
-         if (other.backupsEnabled != null) {
-            return false;
-         }
-      } else if (!backupsEnabled.equals(other.backupsEnabled)) {
-         return false;
-      }
-      if (privateNetworking == null) {
-         if (other.privateNetworking != null) {
-            return false;
-         }
-      } else if (!privateNetworking.equals(other.privateNetworking)) {
-         return false;
-      }
-      if (sshKeyIds == null) {
-         if (other.sshKeyIds != null) {
-            return false;
-         }
-      } else if (!sshKeyIds.equals(other.sshKeyIds)) {
-         return false;
-      }
-      return true;
+      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
@@ -146,6 +138,7 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
       if (!sshKeyIds.isEmpty()) {
          toString.add("sshKeyIds", sshKeyIds);
       }
+      toString.add("shouldAutoCreateKeyPair", autoCreateKeyPair);
       return toString;
    }
 
@@ -174,5 +167,13 @@ public class DigitalOceanTemplateOptions extends 
TemplateOptions implements Clon
          DigitalOceanTemplateOptions options = new 
DigitalOceanTemplateOptions();
          return options.sshKeyIds(sshKeyIds);
       }
+
+      /**
+       * @see DigitalOceanTemplateOptions#autoCreateKeyPair
+       */
+      public static DigitalOceanTemplateOptions 
shouldAutoCreateKeyPair(boolean shouldAutoCreateKeyPair) {
+         DigitalOceanTemplateOptions options = new 
DigitalOceanTemplateOptions();
+         return options.autoCreateKeyPair(shouldAutoCreateKeyPair);
+      }
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/CreateKeyPairsThenCreateNodes.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/CreateKeyPairsThenCreateNodes.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/CreateKeyPairsThenCreateNodes.java
new file mode 100644
index 0000000..b9da3dc
--- /dev/null
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/CreateKeyPairsThenCreateNodes.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean.compute.strategy;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.tryFind;
+
+import java.security.PublicKey;
+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.digitalocean.DigitalOceanApi;
+import org.jclouds.digitalocean.compute.options.DigitalOceanTemplateOptions;
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.predicates.SameFingerprint;
+import org.jclouds.digitalocean.strategy.ListSshKeys;
+import org.jclouds.logging.Logger;
+import org.jclouds.ssh.SshKeyPairGenerator;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+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 DigitalOceanApi api;
+   private final SshKeyPairGenerator keyGenerator;
+   private final ListSshKeys listSshKeys;
+   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,
+         DigitalOceanApi api, SshKeyPairGenerator keyGenerator, 
ListSshKeys.Factory listSshKeysFactory,
+         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(listSshKeysFactory, "listSshKeysFactory cannot be null");
+      checkNotNull(userExecutor, "userExecutor cannot be null");
+      this.listSshKeys = listSshKeysFactory.create(userExecutor);
+      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) 
{
+
+      DigitalOceanTemplateOptions options = 
template.getOptions().as(DigitalOceanTemplateOptions.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);
+      }
+
+      // If there is a script to run in the node, make sure a pivate 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(DigitalOceanTemplateOptions 
options,
+         Set<Integer> generatedSshKeyIds) {
+      logger.debug(">> checking if the key pair already exists...");
+
+      PublicKey userKey = sshKeyToPublicKey.apply(options.getPublicKey());
+      Optional<SshKey> key = tryFind(listSshKeys.execute(), new 
SameFingerprint(userKey));
+
+      if (!key.isPresent()) {
+         logger.debug(">> key pair not found. creating a new one...");
+
+         String userFingerprint = SameFingerprint.computeFingerprint(userKey);
+         SshKey newKey = api.getKeyPairApi().create(userFingerprint, 
options.getPublicKey());
+
+         generatedSshKeyIds.add(newKey.getId());
+         logger.debug(">> key pair created! %s", newKey);
+      } else {
+         logger.debug(">> key pair found! %s", key.get());
+         generatedSshKeyIds.add(key.get().getId());
+      }
+   }
+
+   private void generateKeyPairAndAddKeyToSet(DigitalOceanTemplateOptions 
options, Set<Integer> generatedSshKeyIds) {
+      logger.debug(">> creating default keypair for node...");
+
+      Map<String, String> defaultKeys = keyGenerator.get();
+
+      PublicKey defaultPublicKey = 
sshKeyToPublicKey.apply(defaultKeys.get("public"));
+      String fingerprint = 
SameFingerprint.computeFingerprint(defaultPublicKey);
+      SshKey defaultKey = api.getKeyPairApi().create(fingerprint, 
defaultKeys.get("public"));
+
+      generatedSshKeyIds.add(defaultKey.getId());
+
+      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.getKeyPairApi().delete(sshKeyId);
+               } catch (Exception ex) {
+                  logger.warn(">> could not delete key pair %s: %s", sshKeyId, 
ex.getMessage());
+               }
+            }
+         }
+
+      }, userExecutor);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
index e036108..db1704c 100644
--- 
a/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/compute/strategy/DigitalOceanComputeServiceAdapter.java
@@ -24,8 +24,6 @@ import static 
org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_S
 import static 
org.jclouds.compute.config.ComputeServiceProperties.TIMEOUT_NODE_TERMINATED;
 import static 
org.jclouds.digitalocean.compute.util.LocationNamingUtils.extractRegionId;
 
-import java.util.Map;
-
 import javax.annotation.Resource;
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -40,14 +38,11 @@ import org.jclouds.digitalocean.domain.DropletCreation;
 import org.jclouds.digitalocean.domain.Image;
 import org.jclouds.digitalocean.domain.Region;
 import org.jclouds.digitalocean.domain.Size;
-import org.jclouds.digitalocean.domain.SshKey;
 import org.jclouds.digitalocean.domain.options.CreateDropletOptions;
 import org.jclouds.domain.LoginCredentials;
 import org.jclouds.logging.Logger;
-import org.jclouds.ssh.SshKeyPairGenerator;
 
 import com.google.common.base.Predicate;
-import com.google.common.base.Strings;
 import com.google.common.primitives.Ints;
 
 /**
@@ -60,18 +55,16 @@ public class DigitalOceanComputeServiceAdapter implements 
ComputeServiceAdapter<
    protected Logger logger = Logger.NULL;
 
    private final DigitalOceanApi api;
-   private final SshKeyPairGenerator keyGenerator;
    private final Predicate<Integer> nodeRunningPredicate;
    private final Predicate<Integer> nodeStoppedPredicate;
    private final Predicate<Integer> nodeTerminatedPredicate;
 
    @Inject
-   DigitalOceanComputeServiceAdapter(DigitalOceanApi api, SshKeyPairGenerator 
keyGenerator,
+   DigitalOceanComputeServiceAdapter(DigitalOceanApi api,
          @Named(TIMEOUT_NODE_RUNNING) Predicate<Integer> nodeRunningPredicate,
          @Named(TIMEOUT_NODE_SUSPENDED) Predicate<Integer> 
nodeStoppedPredicate,
          @Named(TIMEOUT_NODE_TERMINATED) Predicate<Integer> 
nodeTerminatedPredicate) {
       this.api = checkNotNull(api, "api cannot be null");
-      this.keyGenerator = checkNotNull(keyGenerator, "keyGenerator cannot be 
null");
       this.nodeRunningPredicate = checkNotNull(nodeRunningPredicate, 
"nodeRunningPredicate cannot be null");
       this.nodeStoppedPredicate = checkNotNull(nodeStoppedPredicate, 
"nodeStoppedPredicate cannot be null");
       this.nodeTerminatedPredicate = checkNotNull(nodeTerminatedPredicate, 
"nodeTerminatedPredicate cannot be null");
@@ -83,23 +76,6 @@ public class DigitalOceanComputeServiceAdapter implements 
ComputeServiceAdapter<
       DigitalOceanTemplateOptions templateOptions = 
template.getOptions().as(DigitalOceanTemplateOptions.class);
       CreateDropletOptions.Builder options = CreateDropletOptions.builder();
 
-      // Create a default keypair for the node so it has a known private key
-      Map<String, String> defaultKeys = keyGenerator.get();
-      logger.debug(">> creating default keypair for node...");
-      SshKey defaultKey = api.getKeyPairApi().create(name, 
defaultKeys.get("public"));
-      logger.debug(">> keypair created! %s", defaultKey);
-      options.addSshKeyId(defaultKey.getId());
-
-      // Check if there is a key to authorize in the portable options
-      if (!Strings.isNullOrEmpty(template.getOptions().getPublicKey())) {
-         logger.debug(">> creating user keypair for node...");
-         // The DigitalOcean API accepts multiple key pairs with the same 
name. It will be useful to identify all
-         // keypairs associated with the node when it comes to destroy it
-         SshKey key = api.getKeyPairApi().create(name, 
template.getOptions().getPublicKey());
-         logger.debug(">> keypair created! %s", key);
-         options.addSshKeyId(key.getId());
-      }
-
       // DigitalOcean specific options
       if (!templateOptions.getSshKeyIds().isEmpty()) {
          options.addSshKeyIds(templateOptions.getSshKeyIds());
@@ -124,7 +100,7 @@ public class DigitalOceanComputeServiceAdapter implements 
ComputeServiceAdapter<
       Droplet droplet = api.getDropletApi().get(dropletCreation.getId());
 
       LoginCredentials defaultCredentials = 
LoginCredentials.builder().user("root")
-            .privateKey(defaultKeys.get("private")).build();
+            .privateKey(templateOptions.getLoginPrivateKey()).build();
 
       return new NodeAndInitialCredentials<Droplet>(droplet, 
String.valueOf(droplet.getId()), defaultCredentials);
    }
@@ -174,30 +150,10 @@ public class DigitalOceanComputeServiceAdapter implements 
ComputeServiceAdapter<
 
    @Override
    public void destroyNode(String id) {
-      Droplet droplet = api.getDropletApi().get(Integer.parseInt(id));
-      final String nodeName = droplet.getName();
-
       // We have to wait here, as the api does not properly populate the state
       // but fails if there is a pending event
       int event = api.getDropletApi().destroy(Integer.parseInt(id), true);
       nodeTerminatedPredicate.apply(event);
-
-      // Destroy the keypairs created for the node
-      Iterable<SshKey> keys = filter(api.getKeyPairApi().list(), new 
Predicate<SshKey>() {
-         @Override
-         public boolean apply(SshKey input) {
-            return input.getName().equals(nodeName);
-         }
-      });
-
-      for (SshKey key : keys) {
-         try {
-            logger.info(">> deleting keypair %s...", key);
-            api.getKeyPairApi().delete(key.getId());
-         } catch (RuntimeException ex) {
-            logger.warn(ex, ">> could not delete keypair %s. You can safely 
delete this key pair manually", key);
-         }
-      }
    }
 
    @Override

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanHttpApiModule.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanHttpApiModule.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanHttpApiModule.java
index ed83c08..3657198 100644
--- 
a/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanHttpApiModule.java
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanHttpApiModule.java
@@ -19,6 +19,7 @@ package org.jclouds.digitalocean.config;
 import org.jclouds.digitalocean.DigitalOceanApi;
 import org.jclouds.digitalocean.handlers.DigitalOceanErrorHandler;
 import 
org.jclouds.digitalocean.http.ResponseStatusFromPayloadHttpCommandExecutorService;
+import org.jclouds.digitalocean.strategy.ListSshKeys;
 import org.jclouds.http.HttpCommandExecutorService;
 import org.jclouds.http.HttpErrorHandler;
 import org.jclouds.http.annotation.ClientError;
@@ -31,6 +32,7 @@ import org.jclouds.rest.config.HttpApiModule;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 /**
  * Configures the DigitalOcean connection.
@@ -39,6 +41,12 @@ import com.google.inject.Scopes;
 public class DigitalOceanHttpApiModule extends HttpApiModule<DigitalOceanApi> {
 
    @Override
+   protected void configure() {
+      super.configure();
+      install(new FactoryModuleBuilder().build(ListSshKeys.Factory.class));
+   }
+
+   @Override
    protected void bindErrorHandlers() {
       
bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(DigitalOceanErrorHandler.class);
       
bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(DigitalOceanErrorHandler.class);

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanParserModule.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanParserModule.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanParserModule.java
index e50cd3f..9170c8e 100644
--- 
a/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanParserModule.java
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/config/DigitalOceanParserModule.java
@@ -17,6 +17,7 @@
 package org.jclouds.digitalocean.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;
@@ -34,6 +35,7 @@ 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.digitalocean.ssh.DSAKeys;
@@ -41,6 +43,7 @@ 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;
@@ -62,40 +65,71 @@ public class DigitalOceanParserModule extends 
AbstractModule {
    @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 {
-         if (value instanceof RSAPublicKey) {
-            out.value(SshKeys.encodeAsOpenSSH((RSAPublicKey) value));
-         } else if (value instanceof DSAPublicKey) {
-            out.value(DSAKeys.encodeAsOpenSSH((DSAPublicKey) value));
-         } else {
-            throw new IllegalArgumentException("Only RSA and DSA keys are 
supported");
-         }
+         out.value(publicKeyToSshKey.apply(value));
       }
 
       @Override
       public PublicKey read(JsonReader in) throws IOException {
-         String input = in.nextString().trim();
-         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);
+         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("bad format, should be: 
[ssh-rsa|ssh-dss] AAAAB3...");
+               throw new IllegalArgumentException("Only RSA and DSA keys are 
supported");
             }
-         } catch (InvalidKeySpecException ex) {
-            throw propagate(ex);
-         } catch (NoSuchAlgorithmException ex) {
-            throw propagate(ex);
          }
-      }
+      };
+   }
+
+   @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 {
+                  throw new IllegalArgumentException("bad format, should be: 
[ssh-rsa|ssh-dss] AAAAB3...");
+               }
+            } catch (InvalidKeySpecException ex) {
+               throw propagate(ex);
+            } catch (NoSuchAlgorithmException ex) {
+               throw propagate(ex);
+            }
+         }
+      };
    }
 
    @Provides

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/predicates/SameFingerprint.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/predicates/SameFingerprint.java
 
b/digitalocean/src/main/java/org/jclouds/digitalocean/predicates/SameFingerprint.java
new file mode 100644
index 0000000..f7d414a
--- /dev/null
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/predicates/SameFingerprint.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.digitalocean.predicates;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.RSAPublicKey;
+
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.ssh.DSAKeys;
+import org.jclouds.ssh.SshKeys;
+
+import com.google.common.base.Predicate;
+
+/**
+ * Predicate to compare SSH keys by fingerprint.
+ */
+public class SameFingerprint implements Predicate<SshKey> {
+
+   public final String fingerprint;
+
+   public SameFingerprint(PublicKey key) {
+      this.fingerprint = computeFingerprint(checkNotNull(key, "key cannot be 
null"));
+   }
+
+   @Override
+   public boolean apply(SshKey key) {
+      checkNotNull(key, "key cannot be null");
+      checkNotNull(key.getPublicKey(), "public key cannot be null");
+      return fingerprint.equals(computeFingerprint(key.getPublicKey()));
+   }
+
+   public 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 {
+         throw new IllegalArgumentException("Only RSA and DSA keys are 
supported");
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/main/java/org/jclouds/digitalocean/strategy/ListSshKeys.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/main/java/org/jclouds/digitalocean/strategy/ListSshKeys.java 
b/digitalocean/src/main/java/org/jclouds/digitalocean/strategy/ListSshKeys.java
new file mode 100644
index 0000000..a51cb60
--- /dev/null
+++ 
b/digitalocean/src/main/java/org/jclouds/digitalocean/strategy/ListSshKeys.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean.strategy;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.util.concurrent.Futures.allAsList;
+import static com.google.common.util.concurrent.Futures.getUnchecked;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.digitalocean.DigitalOceanApi;
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.features.KeyPairApi;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * The {@link org.jclouds.digitalocean.features.KeyPairApi} only returns the 
id and name of each key but not the actual
+ * public key when listing all keys.
+ * <p>
+ * This strategy provides a helper to get all the keys with all details 
populated.
+ */
+@Singleton
+public class ListSshKeys {
+
+   public interface Factory {
+      ListSshKeys create(ListeningExecutorService executor);
+   }
+
+   private final KeyPairApi keyPairApi;
+   private final ListeningExecutorService executor;
+
+   @Inject
+   ListSshKeys(DigitalOceanApi api, @Assisted ListeningExecutorService 
executor) {
+      checkNotNull(api, "api cannot be null");
+      this.executor = checkNotNull(executor, "executor cannot be null");
+      this.keyPairApi = api.getKeyPairApi();
+   }
+
+   public List<SshKey> execute() {
+      List<SshKey> keys = keyPairApi.list();
+
+      ListenableFuture<List<SshKey>> futures = allAsList(transform(keys,
+            new Function<SshKey, ListenableFuture<SshKey>>() {
+               @Override
+               public ListenableFuture<SshKey> apply(final SshKey input) {
+                  return executor.submit(new Callable<SshKey>() {
+                     @Override
+                     public SshKey call() throws Exception {
+                        return keyPairApi.get(input.getId());
+                     }
+                  });
+               }
+            }));
+
+      return getUnchecked(futures);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/test/java/org/jclouds/digitalocean/predicates/SameFingerprintTest.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/test/java/org/jclouds/digitalocean/predicates/SameFingerprintTest.java
 
b/digitalocean/src/test/java/org/jclouds/digitalocean/predicates/SameFingerprintTest.java
new file mode 100644
index 0000000..be204a6
--- /dev/null
+++ 
b/digitalocean/src/test/java/org/jclouds/digitalocean/predicates/SameFingerprintTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean.predicates;
+
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.ssh.DSAKeys;
+import org.jclouds.ssh.SshKeys;
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the {@link SameFingerprint} class.
+ */
+@Test(groups = "unit", testName = "SameFingerprintTest")
+public class SameFingerprintTest {
+
+   @Test(expectedExceptions = NullPointerException.class, 
expectedExceptionsMessageRegExp = "key cannot be null")
+   public void testPublicKeyCannotBeNull() {
+      new SameFingerprint(null);
+   }
+
+   @Test(expectedExceptions = NullPointerException.class, 
expectedExceptionsMessageRegExp = "public key cannot be null")
+   public void testPublicKeyInSshKeyCannotBeNull() throws IOException, 
InvalidKeySpecException,
+   NoSuchAlgorithmException {
+      String rsa = 
Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-rsa.txt"));
+      PublicKey key = 
KeyFactory.getInstance("RSA").generatePublic(SshKeys.publicKeySpecFromOpenSSH(rsa));
+
+      SameFingerprint predicate = new SameFingerprint(key);
+      predicate.apply(new SshKey(0, "foo", null));
+   }
+
+   @Test
+   public void testSameFingerPrintRSA() throws IOException, 
InvalidKeySpecException, NoSuchAlgorithmException {
+      String rsa = 
Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-rsa.txt"));
+      PublicKey key = 
KeyFactory.getInstance("RSA").generatePublic(SshKeys.publicKeySpecFromOpenSSH(rsa));
+
+      SameFingerprint predicate = new SameFingerprint(key);
+      assertTrue(predicate.apply(new SshKey(0, "foo", key)));
+   }
+
+   @Test
+   public void testSameFingerPrintDSA() throws IOException, 
InvalidKeySpecException, NoSuchAlgorithmException {
+      String dsa = 
Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-dsa.txt"));
+      PublicKey key = 
KeyFactory.getInstance("DSA").generatePublic(DSAKeys.publicKeySpecFromOpenSSH(dsa));
+
+      SameFingerprint predicate = new SameFingerprint(key);
+      assertTrue(predicate.apply(new SshKey(0, "foo", key)));
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/3eaa646d/digitalocean/src/test/java/org/jclouds/digitalocean/strategy/ListSshKeysLiveTest.java
----------------------------------------------------------------------
diff --git 
a/digitalocean/src/test/java/org/jclouds/digitalocean/strategy/ListSshKeysLiveTest.java
 
b/digitalocean/src/test/java/org/jclouds/digitalocean/strategy/ListSshKeysLiveTest.java
new file mode 100644
index 0000000..336a166
--- /dev/null
+++ 
b/digitalocean/src/test/java/org/jclouds/digitalocean/strategy/ListSshKeysLiveTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.digitalocean.strategy;
+
+import static com.google.common.collect.Iterables.all;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.jclouds.digitalocean.domain.SshKey;
+import org.jclouds.digitalocean.internal.BaseDigitalOceanLiveTest;
+import org.jclouds.util.Strings2;
+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.util.concurrent.MoreExecutors;
+
+/**
+ * Live tests for the {@link ListSshKeys} strategy.
+ */
+@Test(groups = "live", testName = "ListSshKeysLiveTest")
+public class ListSshKeysLiveTest extends BaseDigitalOceanLiveTest {
+
+   private ListSshKeys strategy;
+
+   private SshKey rsaKey;
+
+   private SshKey dsaKey;
+
+   @Override
+   protected void initialize() {
+      super.initialize();
+      strategy = new ListSshKeys(api, 
MoreExecutors.newDirectExecutorService());
+   }
+
+   @BeforeClass
+   public void setupKeys() throws IOException {
+      String rsa = 
Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-rsa.txt"));
+      String dsa = 
Strings2.toStringAndClose(getClass().getResourceAsStream("/ssh-dsa.txt"));
+
+      rsaKey = api.getKeyPairApi().create("jclouds-test-rsa", rsa);
+      dsaKey = api.getKeyPairApi().create("jclouds-test-dsa", dsa);
+   }
+
+   @AfterClass(alwaysRun = true)
+   public void cleanupKeys() {
+      if (rsaKey != null) {
+         api.getKeyPairApi().delete(rsaKey.getId());
+      }
+      if (dsaKey != null) {
+         api.getKeyPairApi().delete(dsaKey.getId());
+      }
+   }
+
+   public void testListWithDetails() {
+      List<SshKey> keys = strategy.execute();
+
+      assertTrue(keys.size() >= 2);
+      assertTrue(all(keys, new Predicate<SshKey>() {
+         @Override
+         public boolean apply(SshKey input) {
+            return input.getPublicKey() != null;
+         }
+      }));
+   }
+}

Reply via email to