org.jclouds.aliyun.ecs.compute.strategy.CreateResourcesThenCreateNodes.java Source code

Java tutorial

Introduction

Here is the source code for org.jclouds.aliyun.ecs.compute.strategy.CreateResourcesThenCreateNodes.java

Source

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