blob: 8d302a244e6dd9b191d7fe775c666ea062cc0760 [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.aliyun.ecs.compute.strategy;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
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;
import org.jclouds.Constants;
import org.jclouds.aliyun.ecs.ECSComputeServiceApi;
import org.jclouds.aliyun.ecs.compute.options.ECSServiceTemplateOptions;
import org.jclouds.aliyun.ecs.domain.IpProtocol;
import org.jclouds.aliyun.ecs.domain.KeyPair;
import org.jclouds.aliyun.ecs.domain.KeyPairRequest;
import org.jclouds.aliyun.ecs.domain.SecurityGroup;
import org.jclouds.aliyun.ecs.domain.SecurityGroupRequest;
import org.jclouds.aliyun.ecs.domain.Tag;
import org.jclouds.aliyun.ecs.domain.VPCRequest;
import org.jclouds.aliyun.ecs.domain.VSwitch;
import org.jclouds.aliyun.ecs.domain.VSwitchRequest;
import org.jclouds.aliyun.ecs.domain.Zone;
import org.jclouds.aliyun.ecs.domain.options.CreateSecurityGroupOptions;
import org.jclouds.aliyun.ecs.domain.options.CreateVPCOptions;
import org.jclouds.aliyun.ecs.domain.options.CreateVSwitchOptions;
import org.jclouds.aliyun.ecs.domain.options.ListVSwitchesOptions;
import org.jclouds.aliyun.ecs.domain.options.TagOptions;
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.logging.Logger;
import org.jclouds.ssh.SshKeys;
import javax.annotation.Nullable;
import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.get;
import static com.google.common.collect.Iterables.size;
import static org.jclouds.aliyun.ecs.domain.ResourceType.SECURITYGROUP;
import static org.jclouds.compute.util.ComputeServiceUtils.getPortRangesFromList;
@Singleton
public class CreateResourcesThenCreateNodes extends CreateNodesWithGroupEncodedIntoNameThenAddToSet {
public static final String INTERNET = "0.0.0.0/0";
public static final String DEFAULT_CIDR_BLOCK = "172.16.1.0/24";
public static final String JCLOUDS_KEYPAIR_IMPORTED = "jclouds-imported";
public static final String PORT_RANGE_FORMAT = "%d/%d";
protected static final String DEFAULT_DESCRIPTION_SUFFIX = "created by jclouds";
protected static final String VSWITCH_PREFIX = "vswitch";
protected static final String VPC_PREFIX = "vpc";
private final ECSComputeServiceApi api;
@Resource
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
protected Logger logger = Logger.NULL;
@Inject
protected CreateResourcesThenCreateNodes(CreateNodeWithGroupEncodedIntoName addNodeWithGroupStrategy,
ListNodesStrategy listNodesStrategy, GroupNamingConvention.Factory namingConvention,
@Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor,
CustomizeNodeAndAddToGoodMapOrPutExceptionIntoBadMap.Factory customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory,
ECSComputeServiceApi api) {
super(addNodeWithGroupStrategy, listNodesStrategy, namingConvention, userExecutor,
customizeNodeAndAddToGoodMapOrPutExceptionIntoBadMapFactory);
this.api = api;
}
@Override
public Map<?, ListenableFuture<Void>> execute(String group, int count, Template template,
Set<NodeMetadata> goodNodes, Map<NodeMetadata, Exception> badNodes,
Multimap<NodeMetadata, CustomizationResponse> customizationResponses) {
String regionId = template.getLocation().getId();
ECSServiceTemplateOptions options = template.getOptions().as(ECSServiceTemplateOptions.class);
Optional<SecurityGroup> securityGroupOptional = tryFindSecurityGroupInRegion(regionId, options.getGroups());
String vpcIdFromSecurityGroup;
String vpcId;
if (securityGroupOptional.isPresent()) {
vpcIdFromSecurityGroup = securityGroupOptional.get().vpcId();
if (!Strings.isNullOrEmpty(options.getVSwitchId())) {
validateVSwitchId(regionId, options.getVSwitchId(), securityGroupOptional.get().name(), vpcIdFromSecurityGroup);
} else {
String message = String.format("Security group (%s) belongs to VPC (%s). Please specify a vSwitch Id of that VPC (%s) using ECSServiceTemplateOptions.vSwitchId",
securityGroupOptional.get().name(),
vpcIdFromSecurityGroup,
vpcIdFromSecurityGroup);
throw new IllegalStateException(message);
}
} else {
if (!Strings.isNullOrEmpty(options.getVSwitchId())) {
VSwitch vSwitch = tryFindVSwitch(regionId, options.getVSwitchId());
vpcId = vSwitch.vpcId();
} else {
vpcId = createDefaultVPC(regionId, group);
String vSwitchId = createDefaultVSwitch(regionId, vpcId, group);
options.vSwitchId(vSwitchId);
}
String createdSecurityGroupId = createSecurityGroupForOptions(group, regionId, vpcId, options);
options.securityGroups(createdSecurityGroupId);
}
// If keys haven't been configured, generate a key pair
if (Strings.isNullOrEmpty(options.getPublicKey()) &&
Strings.isNullOrEmpty(options.getLoginPrivateKey())) {
String uniqueNameForGroup = namingConvention.create().uniqueNameForGroup(group);
KeyPairRequest keyPairRequest = generateKeyPair(regionId, uniqueNameForGroup);
options.keyPairName(keyPairRequest.getKeyPairName());
options.overrideLoginPrivateKey(keyPairRequest.getPrivateKeyBody());
}
// 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 public key configured, then make sure there is a key pair for it
if (!Strings.isNullOrEmpty(options.getPublicKey())) {
KeyPair keyPair = getOrImportKeyPairForPublicKey(options, regionId);
options.keyPairName(keyPair.name());
}
Map<?, ListenableFuture<Void>> responses = super.execute(group, count, template, goodNodes, badNodes, customizationResponses);
// Key pairs are only required to create the devices.
// Better to delete the auto-generated key pairs when they are mo more required
registerAutoGeneratedKeyPairCleanupCallbacks(responses, regionId, options.getKeyPairName());
return responses;
}
private void validateVSwitchId(String regionId,
String vSwitchId,
String securityGroupName,
String vpcIdFromSecurityGroup) {
Optional<VSwitch> optionalVSwitch = tryFindVSwitchInVPC(regionId, vpcIdFromSecurityGroup, vSwitchId);
if (!optionalVSwitch.isPresent()) {
String message = String.format("security group (%s) and vSwitch (%s) must be in the same VPC_PREFIX (%s)",
securityGroupName,
optionalVSwitch.get().name(),
vpcIdFromSecurityGroup);
throw new IllegalStateException(message);
}
}
private String createDefaultVPC(String regionId, String group) {
String vpcName = String.format("%s-%s", VPC_PREFIX, group);
VPCRequest vpcRequest = api.vpcApi().create(regionId, CreateVPCOptions.Builder.vpcName(vpcName).description(String.format("%s - %s", VPC_PREFIX, DEFAULT_DESCRIPTION_SUFFIX)));
return vpcRequest.getVpcId();
}
private String createDefaultVSwitch(String regionId, String vpcId, String name) {
String vSwitchName = String.format("%s-%s", VSWITCH_PREFIX, name);
Zone zone = Iterables.getFirst(api.regionAndZoneApi().describeZones(regionId), null);
VSwitchRequest vSwitchRequest = api.vSwitchApi().create(zone.id(), DEFAULT_CIDR_BLOCK, vpcId,
CreateVSwitchOptions.Builder.vSwitchName(vSwitchName).description(String.format("%s - %s", vSwitchName, DEFAULT_DESCRIPTION_SUFFIX)));
return vSwitchRequest.getVSwitchId();
}
private KeyPair getOrImportKeyPairForPublicKey(ECSServiceTemplateOptions options, String regionId) {
logger.debug(">> checking if the key pair already exists...");
PublicKey userKey = readPublicKey(options.getPublicKey());
final String fingerprint = computeFingerprint(userKey);
KeyPair keyPair;
synchronized (CreateResourcesThenCreateNodes.class) {
Optional<KeyPair> keyPairOptional = Iterables
.tryFind(api.sshKeyPairApi().list(regionId).concat(), new Predicate<KeyPair>() {
@Override
public boolean apply(KeyPair input) {
return input.keyPairFingerPrint().equals(fingerprint.replace(":", ""));
}
});
if (!keyPairOptional.isPresent()) {
logger.debug(">> key pair not found. Importing a new key pair %s ...", fingerprint);
keyPair = api.sshKeyPairApi().importKeyPair(
regionId,
options.getPublicKey(),
namingConvention.create().uniqueNameForGroup(JCLOUDS_KEYPAIR_IMPORTED));
logger.debug(">> key pair imported! %s", keyPair);
} else {
logger.debug(">> key pair found for key %s", fingerprint);
keyPair = keyPairOptional.get();
}
return keyPair;
}
}
private KeyPairRequest generateKeyPair(String regionId, String uniqueNameForGroup) {
logger.debug(">> creating default keypair for node...");
KeyPairRequest keyPairRequest = api.sshKeyPairApi().create(regionId, uniqueNameForGroup);
logger.debug(">> keypair created! %s", keyPairRequest);
return keyPairRequest;
}
private Optional<SecurityGroup> tryFindSecurityGroupInRegion(String regionId, final Set<String> securityGroups) {
checkArgument(securityGroups.size() <= 1, "Only one security group can be configured for each network interface");
final String securityGroupId = Iterables.get(securityGroups, 0, null);
if (securityGroupId != null) {
return api.securityGroupApi().list(regionId).concat().firstMatch(new Predicate<SecurityGroup>() {
@Override
public boolean apply(@Nullable SecurityGroup input) {
return securityGroupId.equals(input.id());
}
});
}
return Optional.absent();
}
private VSwitch tryFindVSwitch(String regionId, String vSwitchId) {
ListVSwitchesOptions listVSwitchesOptions = ListVSwitchesOptions.Builder.vSwitchId(vSwitchId);
Optional<VSwitch> optionalVSwitch = api.vSwitchApi().list(regionId, listVSwitchesOptions).first();
if (!optionalVSwitch.isPresent()) {
String message = String.format("Cannot find a valid vSwitch with id (%s) within region (%s)",
vSwitchId,
regionId);
throw new IllegalStateException(message);
}
return optionalVSwitch.get();
}
private Optional<VSwitch> tryFindVSwitchInVPC(String regionId, String vpcId, String vSwitchId) {
ListVSwitchesOptions listVSwitchesOptions = ListVSwitchesOptions.Builder.vpcId(vpcId).vSwitchId(vSwitchId);
return api.vSwitchApi().list(regionId, listVSwitchesOptions).first();
}
private String createSecurityGroupForOptions(String group, String regionId, String vpcId,
ECSServiceTemplateOptions options) {
String name = namingConvention.create().sharedNameForGroup(group);
SecurityGroupRequest securityGroupRequest = api.securityGroupApi().create(regionId,
CreateSecurityGroupOptions.Builder
.securityGroupName(name)
.vpcId(vpcId));
// add rules
Map<Integer, Integer> portRanges = getPortRangesFromList(options.getInboundPorts());
for (Map.Entry<Integer, Integer> portRange : portRanges.entrySet()) {
String range = String.format(PORT_RANGE_FORMAT, portRange.getKey(), portRange.getValue());
// TODO makes protocol and source CIDR configurable?
api.securityGroupApi().addInboundRule(
regionId,
securityGroupRequest.getSecurityGroupId(),
IpProtocol.TCP,
range,
INTERNET);
}
api.tagApi().add(regionId, securityGroupRequest.getSecurityGroupId(), SECURITYGROUP,
TagOptions.Builder
.tag(1, Tag.DEFAULT_OWNER_KEY, Tag.DEFAULT_OWNER_VALUE)
.tag(2, Tag.GROUP, group));
return securityGroupRequest.getSecurityGroupId();
}
private void registerAutoGeneratedKeyPairCleanupCallbacks(Map<?, ListenableFuture<Void>> responses,
final String regionId, final String keyPairName) {
// 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(keyPairName);
}
@Override
public void onFailure(Throwable t) {
cleanupAutoGeneratedKeyPairs(keyPairName);
}
private void cleanupAutoGeneratedKeyPairs(String keyPairName) {
logger.debug(">> cleaning up auto-generated key pairs...");
try {
api.sshKeyPairApi().delete(regionId, keyPairName);
} catch (Exception ex) {
logger.warn(">> could not delete key pair %s: %s", keyPairName, ex.getMessage());
}
}
}, userExecutor);
}
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 new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
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");
}
}
}