blob: b9da3dc416698af58db9b73927a8b167c9358547 [file] [log] [blame]
/*
* 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);
}
}