| /* |
| * 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"); |
| } |
| } |
| } |