Repository: jclouds-labs Updated Branches: refs/heads/2.0.x 1fbb79721 -> c1fb76203
Improve duplicate ssh key check in Packet Project: http://git-wip-us.apache.org/repos/asf/jclouds-labs/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds-labs/commit/c1fb7620 Tree: http://git-wip-us.apache.org/repos/asf/jclouds-labs/tree/c1fb7620 Diff: http://git-wip-us.apache.org/repos/asf/jclouds-labs/diff/c1fb7620 Branch: refs/heads/2.0.x Commit: c1fb76203c6daa03287af0c6235c6d69c7cdc580 Parents: 1fbb797 Author: Ignasi Barrera <[email protected]> Authored: Wed Jun 14 11:55:30 2017 +0200 Committer: Ignasi Barrera <[email protected]> Committed: Wed Jun 14 12:31:24 2017 +0200 ---------------------------------------------------------------------- .../strategy/CreateSshKeysThenCreateNodes.java | 289 ++++++++++--------- 1 file changed, 154 insertions(+), 135 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/c1fb7620/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java ---------------------------------------------------------------------- diff --git a/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java b/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java index 148079f..1bb8bc1 100644 --- a/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java +++ b/packet/src/main/java/org/jclouds/packet/compute/strategy/CreateSshKeysThenCreateNodes.java @@ -48,6 +48,7 @@ import org.jclouds.packet.domain.SshKey; import org.jclouds.ssh.SshKeyPairGenerator; import org.jclouds.ssh.SshKeys; +import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Multimap; @@ -65,152 +66,170 @@ import static com.google.common.collect.Iterables.size; @Singleton public class CreateSshKeysThenCreateNodes extends CreateNodesWithGroupEncodedIntoNameThenAddToSet { - @Resource - @Named(ComputeServiceConstants.COMPUTE_LOGGER) - protected Logger logger = Logger.NULL; - - private final PacketApi api; - private final SshKeyPairGenerator keyGenerator; - - @Inject - protected CreateSshKeysThenCreateNodes( - CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy, - ListNodesStrategy listNodesStrategy, - GroupNamingConvention.Factory namingConvention, - @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, - CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory, - PacketApi api, SshKeyPairGenerator keyGenerator) { - super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor, - customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory); - this.api = api; - this.keyGenerator = keyGenerator; - } - - @Override - public Map<?, ListenableFuture<Void>> execute(String group, int count, Template template, - Set<NodeMetadata> goodNodes, Map<NodeMetadata, Exception> badNodes, - Multimap<NodeMetadata, CustomizationResponse> customizationResponses) { - - PacketTemplateOptions options = template.getOptions().as(PacketTemplateOptions.class); - Set<String> generatedSshKeyIds = Sets.newHashSet(); - - // If no key has been configured, generate a key pair - if (Strings.isNullOrEmpty(options.getPublicKey())) { - generateKeyPairAndAddKeyToSet(options, generatedSshKeyIds, group); - } - - // If there is a script to run in the node, make sure a private key has - // been configured so jclouds will be able to access the node - if (options.getRunScript() != null && Strings.isNullOrEmpty(options.getLoginPrivateKey())) { - logger.warn(">> A runScript has been configured but no SSH key has been provided." - + " Authentication will delegate to the ssh-agent"); - } - - // If there is a key configured, then make sure there is a key pair for it - if (!Strings.isNullOrEmpty(options.getPublicKey())) { - createKeyPairForPublicKeyInOptionsAndAddToSet(options, generatedSshKeyIds); - } - - Map<?, ListenableFuture<Void>> responses = super.execute(group, count, template, goodNodes, badNodes, - customizationResponses); - - // Key pairs in Packet are only required to create the devices. 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(PacketTemplateOptions options, - Set<String> generatedSshKeyIds) { - logger.debug(">> checking if the key pair already exists..."); - - PublicKey userKey; - Iterable<String> parts = Splitter.on(' ').split(options.getPublicKey()); - checkArgument(size(parts) >= 2, "bad format, should be: ssh-rsa AAAAB3..."); - String type = get(parts, 0); - - try { - if ("ssh-rsa".equals(type)) { - RSAPublicKeySpec spec = SshKeys.publicKeySpecFromOpenSSH(options.getPublicKey()); - userKey = KeyFactory.getInstance("RSA").generatePublic(spec); - } else { - throw new IllegalArgumentException("bad format, ssh-rsa is only supported"); - } - } catch (InvalidKeySpecException ex) { - throw propagate(ex); - } catch (NoSuchAlgorithmException ex) { - throw propagate(ex); - } - String label = computeFingerprint(userKey); - SshKey key = api.sshKeyApi().get(label); - - if (key == null) { - logger.debug(">> key pair not found. creating a new key pair %s ...", label); - SshKey newKey = api.sshKeyApi().create(label, options.getPublicKey()); - logger.debug(">> key pair created! %s", newKey); - generatedSshKeyIds.add(newKey.id()); - } else { - logger.debug(">> key pair found! %s", key); - generatedSshKeyIds.add(key.id()); + @Resource + @Named(ComputeServiceConstants.COMPUTE_LOGGER) + protected Logger logger = Logger.NULL; + + private final PacketApi api; + private final SshKeyPairGenerator keyGenerator; + + @Inject + protected CreateSshKeysThenCreateNodes( + CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy, + ListNodesStrategy listNodesStrategy, + GroupNamingConvention.Factory namingConvention, + @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, + CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory, + PacketApi api, SshKeyPairGenerator keyGenerator) { + super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor, + customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory); + this.api = api; + this.keyGenerator = keyGenerator; + } + + @Override + public Map<?, ListenableFuture<Void>> execute(String group, int count, Template template, + Set<NodeMetadata> goodNodes, Map<NodeMetadata, Exception> badNodes, + Multimap<NodeMetadata, CustomizationResponse> customizationResponses) { + + PacketTemplateOptions options = template.getOptions().as(PacketTemplateOptions.class); + Set<String> generatedSshKeyIds = Sets.newHashSet(); + + // If no key has been configured, generate a key pair + if (Strings.isNullOrEmpty(options.getPublicKey())) { + generateKeyPairAndAddKeyToSet(options, generatedSshKeyIds, group); } - } - private void generateKeyPairAndAddKeyToSet(PacketTemplateOptions options, Set<String> generatedSshKeyIds, String prefix) { - logger.debug(">> creating default keypair for node..."); + // If there is a script to run in the node, make sure a private key has + // been configured so jclouds will be able to access the node + if (options.getRunScript() != null && Strings.isNullOrEmpty(options.getLoginPrivateKey())) { + logger.warn(">> A runScript has been configured but no SSH key has been provided." + + " Authentication will delegate to the ssh-agent"); + } - Map<String, String> defaultKeys = keyGenerator.get(); + // If there is a key configured, then make sure there is a key pair for it + if (!Strings.isNullOrEmpty(options.getPublicKey())) { + createKeyPairForPublicKeyInOptionsAndAddToSet(options, generatedSshKeyIds); + } - SshKey sshKey = api.sshKeyApi().create(prefix + System.getProperty("user.name"), defaultKeys.get("public")); - generatedSshKeyIds.add(sshKey.id()); - logger.debug(">> keypair created! %s", sshKey); + Map<?, ListenableFuture<Void>> responses = super.execute(group, count, template, goodNodes, badNodes, + customizationResponses); - // If a private key has not been explicitly set, configure the generated one - if (Strings.isNullOrEmpty(options.getLoginPrivateKey())) { - options.overrideLoginPrivateKey(defaultKeys.get("private")); - } - } + // Key pairs in Packet are only required to create the devices. 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); - private void registerAutoGeneratedKeyPairCleanupCallbacks(Map<?, ListenableFuture<Void>> responses, - final Set<String> 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()); + return responses; + } - // 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); - } + private void createKeyPairForPublicKeyInOptionsAndAddToSet(PacketTemplateOptions options, + Set<String> generatedSshKeyIds) { + logger.debug(">> checking if the key pair already exists..."); + PublicKey userKey = readPublicKey(options.getPublicKey()); + final String fingerprint = computeFingerprint(userKey); + + synchronized (CreateSshKeysThenCreateNodes.class) { + boolean keyExists = api.sshKeyApi().list().concat().anyMatch(new Predicate<SshKey>() { @Override - public void onFailure(Throwable t) { - cleanupAutoGeneratedKeyPairs(generatedSshKeyIds); + public boolean apply(SshKey input) { + return input.fingerprint().equals(fingerprint); } + }); + + if (!keyExists) { + logger.debug(">> key pair not found. creating a new key pair %s ...", fingerprint); + SshKey newKey = api.sshKeyApi().create(fingerprint, options.getPublicKey()); + logger.debug(">> key pair created! %s", newKey); + generatedSshKeyIds.add(newKey.id()); + } else { + logger.debug(">> key pair found for key %s", fingerprint); + } + } + } + + private static PublicKey readPublicKey(String publicKey) { + Iterable<String> parts = Splitter.on(' ').split(publicKey); + checkArgument(size(parts) >= 2, "bad format, should be: ssh-rsa AAAAB3..."); + String type = get(parts, 0); + + try { + if ("ssh-rsa".equals(type)) { + RSAPublicKeySpec spec = SshKeys.publicKeySpecFromOpenSSH(publicKey); + return KeyFactory.getInstance("RSA").generatePublic(spec); + } else { + throw new IllegalArgumentException("bad format, ssh-rsa is only supported"); + } + } catch (InvalidKeySpecException ex) { + throw propagate(ex); + } catch (NoSuchAlgorithmException ex) { + throw propagate(ex); + } + } + + private void generateKeyPairAndAddKeyToSet(PacketTemplateOptions options, Set<String> generatedSshKeyIds, + String prefix) { + logger.debug(">> creating default keypair for node..."); + + Map<String, String> defaultKeys = keyGenerator.get(); + + SshKey sshKey = api.sshKeyApi().create(namingConvention.create().uniqueNameForGroup(prefix), + defaultKeys.get("public")); + generatedSshKeyIds.add(sshKey.id()); + logger.debug(">> keypair created! %s", sshKey); - private void cleanupAutoGeneratedKeyPairs(Set<String> generatedSshKeyIds) { - logger.debug(">> cleaning up auto-generated key pairs..."); - for (String sshKeyId : generatedSshKeyIds) { - try { - api.sshKeyApi().delete(sshKeyId); - } catch (Exception ex) { - logger.warn(">> could not delete key pair %s: %s", sshKeyId, ex.getMessage()); - } - } + // If a private key has not been explicitly set, configure the generated + // one + if (Strings.isNullOrEmpty(options.getLoginPrivateKey())) { + options.overrideLoginPrivateKey(defaultKeys.get("private")); + } + } + + private void registerAutoGeneratedKeyPairCleanupCallbacks(Map<?, ListenableFuture<Void>> responses, + final Set<String> 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<String> generatedSshKeyIds) { + logger.debug(">> cleaning up auto-generated key pairs..."); + for (String sshKeyId : generatedSshKeyIds) { + try { + api.sshKeyApi().delete(sshKeyId); + } catch (Exception ex) { + logger.warn(">> could not delete key pair %s: %s", sshKeyId, ex.getMessage()); + } } - }, userExecutor); - } - - private static String computeFingerprint(PublicKey key) { - if (key instanceof RSAPublicKey) { - RSAPublicKey rsaKey = (RSAPublicKey) key; - return SshKeys.fingerprint(rsaKey.getPublicExponent(), rsaKey.getModulus()); - } else { - throw new IllegalArgumentException("Only RSA keys are supported"); - } - } + } + }, userExecutor); + } + + private static String computeFingerprint(PublicKey key) { + if (key instanceof RSAPublicKey) { + RSAPublicKey rsaKey = (RSAPublicKey) key; + return SshKeys.fingerprint(rsaKey.getPublicExponent(), rsaKey.getModulus()); + } else { + throw new IllegalArgumentException("Only RSA keys are supported"); + } + } }
