com.appdynamics.connectors.AWSConnector.java Source code

Java tutorial

Introduction

Here is the source code for com.appdynamics.connectors.AWSConnector.java

Source

/**
 * Copyright 2013 AppDynamics, Inc.
 *
 * Licensed 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 com.appdynamics.connectors;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.AssociateAddressRequest;
import com.amazonaws.services.ec2.model.AuthorizeSecurityGroupIngressRequest;
import com.amazonaws.services.ec2.model.AvailabilityZone;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.DeregisterImageRequest;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceState;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.IpPermission;
import com.amazonaws.services.ec2.model.RebootInstancesRequest;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.SecurityGroup;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
import com.amazonaws.util.Base64;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.singularity.ee.agent.resolver.AgentResolutionEncoder;
import com.singularity.ee.connectors.api.ConnectorException;
import com.singularity.ee.connectors.api.IConnector;
import com.singularity.ee.connectors.api.IControllerServices;
import com.singularity.ee.connectors.api.InvalidObjectException;
import com.singularity.ee.connectors.entity.api.IAccount;
import com.singularity.ee.connectors.entity.api.IComputeCenter;
import com.singularity.ee.connectors.entity.api.IImage;
import com.singularity.ee.connectors.entity.api.IImageStore;
import com.singularity.ee.connectors.entity.api.IMachine;
import com.singularity.ee.connectors.entity.api.IMachineDescriptor;
import com.singularity.ee.connectors.entity.api.IProperty;
import com.singularity.ee.connectors.entity.api.MachineState;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.singularity.ee.connectors.entity.api.MachineState.STARTED;
import static com.singularity.ee.connectors.entity.api.MachineState.STARTING;
import static com.singularity.ee.connectors.entity.api.MachineState.STOPPED;
import static com.singularity.ee.connectors.entity.api.MachineState.STOPPING;
import static com.singularity.ee.controller.KAppServerConstants.CONTROLLER_SERVICES_HOST_NAME_PROPERTY_KEY;
import static com.singularity.ee.controller.KAppServerConstants.CONTROLLER_SERVICES_PORT_PROPERTY_KEY;
import static com.singularity.ee.controller.KAppServerConstants.DEFAULT_CONTROLLER_PORT_VALUE;

public class AWSConnector implements IConnector {
    private static Logger logger = Logger.getLogger(AWSConnector.class.getName());

    private static final Map<String, String> regionVsURLs;

    static {
        Map<String, String> tmpRegionVsURLs = new HashMap<String, String>();

        tmpRegionVsURLs.put("ap-southeast-1", "ec2.ap-southeast-1.amazonaws.com");
        tmpRegionVsURLs.put("eu-west-1", "ec2.eu-west-1.amazonaws.com");
        tmpRegionVsURLs.put("us-east-1", "ec2.us-east-1.amazonaws.com");
        tmpRegionVsURLs.put("us-west-1", "ec2.us-west-1.amazonaws.com");
        tmpRegionVsURLs.put("us-west-2", "ec2.us-west-2.amazonaws.com");
        tmpRegionVsURLs.put("ap-southeast-2", "ec2.ap-southeast-2.amazonaws.com");
        tmpRegionVsURLs.put("ap-northeast-1", "ec2.ap-northeast-1.amazonaws.com");
        tmpRegionVsURLs.put("sa-east-1", "ec2.sa-east-1.amazonaws.com");

        regionVsURLs = Collections.unmodifiableMap(tmpRegionVsURLs);
    }

    private IControllerServices controllerServices;

    /**
     * Public no-arg constructor is required by the connector framework.
     */
    public AWSConnector() {
    }

    public void setControllerServices(IControllerServices controllerServices) {
        this.controllerServices = controllerServices;
    }

    public int getAgentPort() {
        return controllerServices.getDefaultAgentPort();
    }

    public IMachine createMachine(IComputeCenter computeCenter, IImage image, IMachineDescriptor machineDescriptor)
            throws InvalidObjectException, ConnectorException {
        boolean succeeded = false;
        Exception createFailureRootCause = null;
        Instance instance = null;

        try {
            IProperty[] macProps = machineDescriptor.getProperties();

            AmazonEC2 connector = getConnector(image, computeCenter, controllerServices);
            String amiName = Utils.getAMIName(image.getProperties(), controllerServices);
            List<String> securityGroups = getSecurityGroup(macProps);
            validateAndConfigureSecurityGroups(securityGroups, connector);

            controllerServices.getStringPropertyByName(macProps, Utils.SECURITY_GROUP)
                    .setValue(getSecurityGroupsAsString(securityGroups));

            String keyPair = Utils.getKeyPair(macProps, controllerServices);

            InstanceType instanceType = getInstanceType(macProps);

            String zone = Utils.getZone(macProps, controllerServices);
            String kernel = Utils.getKernel(macProps, controllerServices);
            String ramdisk = Utils.getRamDisk(macProps, controllerServices);

            String controllerHost = System.getProperty(CONTROLLER_SERVICES_HOST_NAME_PROPERTY_KEY,
                    InetAddress.getLocalHost().getHostName());

            int controllerPort = Integer.getInteger(CONTROLLER_SERVICES_PORT_PROPERTY_KEY,
                    DEFAULT_CONTROLLER_PORT_VALUE);

            IAccount account = computeCenter.getAccount();

            String accountName = account.getName();
            String accountAccessKey = account.getAccessKey();

            AgentResolutionEncoder agentResolutionEncoder = new AgentResolutionEncoder(controllerHost,
                    controllerPort, accountName, accountAccessKey);

            String userData = agentResolutionEncoder.encodeAgentResolutionInfo();

            String instanceName = Utils.getInstanceName(macProps, controllerServices);

            logger.info("Starting EC2 machine of Image :" + amiName + " Name :" + instanceName + " security :"
                    + securityGroups + " keypair :" + keyPair + " instance :" + instanceType + " zone :" + zone
                    + " kernel :" + kernel + " ramdisk :" + ramdisk + " userData :" + userData);

            RunInstancesRequest runInstancesRequest = new RunInstancesRequest(amiName, 1, 1);
            runInstancesRequest.setSecurityGroups(securityGroups);
            runInstancesRequest.setUserData(Base64.encodeAsString(userData.getBytes()));
            runInstancesRequest.setKeyName(keyPair);
            runInstancesRequest.setInstanceType(instanceType);
            runInstancesRequest.setKernelId(kernel);
            runInstancesRequest.setRamdiskId(ramdisk);

            Reservation reservation = connector.runInstances(runInstancesRequest).getReservation();
            List<Instance> instances = reservation.getInstances();

            if (instances.size() == 0)
                throw new ConnectorException("Cannot create instance for image :" + image.getName());

            instance = instances.get(0);

            //Set name for the instance
            if (!Strings.isNullOrEmpty(instanceName)) {
                CreateTagsRequest createTagsRequest = new CreateTagsRequest();
                createTagsRequest.withResources(instance.getInstanceId()).withTags(new Tag("Name", instanceName));
                connector.createTags(createTagsRequest);
            }

            logger.info("EC2 machine started; id:" + instance.getInstanceId());

            IMachine machine;

            if (Strings.isNullOrEmpty(instance.getPublicDnsName())) {
                machine = controllerServices.createMachineInstance(instance.getInstanceId(),
                        agentResolutionEncoder.getUniqueHostIdentifier(), computeCenter, machineDescriptor, image,
                        getAgentPort());
            } else {
                machine = controllerServices.createMachineInstance(instance.getInstanceId(),
                        agentResolutionEncoder.getUniqueHostIdentifier(), instance.getPublicDnsName(),
                        computeCenter, machineDescriptor, image, getAgentPort());
            }

            if (kernel == null) {
                controllerServices.getStringPropertyByName(macProps, Utils.KERNEL).setValue(instance.getKernelId());
            }

            if (zone == null) {
                DescribeAvailabilityZonesResult describeAvailabilityZonesResult = connector
                        .describeAvailabilityZones();
                List<AvailabilityZone> availabilityZones = describeAvailabilityZonesResult.getAvailabilityZones();
                controllerServices.getStringPropertyByName(macProps, Utils.ZONE)
                        .setValue(availabilityZones.get(0).getZoneName());
            }

            controllerServices.getStringPropertyByName(macProps, Utils.INSTANCE_TYPE)
                    .setValue(instance.getInstanceType());

            succeeded = true;

            return machine;

        } catch (InvalidObjectException e) {
            createFailureRootCause = e;
            throw e;
        } catch (ConnectorException e) {
            createFailureRootCause = e;
            throw e;
        } catch (Exception e) {
            createFailureRootCause = e;
            throw new ConnectorException(e.getMessage(), e);
        } finally {
            // We have to make sure to terminate any orphan EC2 instances if 
            // the machine create fails.
            if (!succeeded && instance != null) {
                try {
                    ConnectorLocator.getInstance().getConnector(computeCenter, controllerServices)
                            .terminateInstances(
                                    new TerminateInstancesRequest(Lists.newArrayList(instance.getInstanceId())));
                } catch (Exception e) {
                    throw new ConnectorException("Machine create failed, but terminate failed as well! "
                            + "We have an orphan EC2 instance with id: " + instance.getInstanceId()
                            + " that must be shut down manually. Root cause for machine "
                            + "create failure is following: ", createFailureRootCause);
                }
            }
        }
    }

    public void refreshMachineState(IMachine machine) throws InvalidObjectException, ConnectorException {
        AmazonEC2 connector = getConnector(machine.getImage(), machine.getComputeCenter(), controllerServices);

        Instance ec2Instance = getEc2Instance(machine, connector);

        MachineState currentState = machine.getState();

        if (ec2Instance == null) {
            // machine not found. MUST have been terminated
            if (currentState != STOPPED)
                machine.setState(STOPPED);
        } else {
            MachineState newState = getMachineState(ec2Instance);

            if (newState == STARTED) {
                // IP may only be set when the machine is running
                String elasticIp = Utils.getElasticIP(machine.getMachineDescriptor().getProperties(),
                        controllerServices);

                elasticIp = elasticIp == null ? "" : elasticIp.trim();

                if (elasticIp.length() > 0) {
                    // associate the address
                    setElasticIp(machine, elasticIp, connector);
                } else {
                    machine.setIpAddress(ec2Instance.getPublicDnsName());
                }
            }

            if (newState != currentState) {
                machine.setState(newState);
            }
        }
    }

    public void restartMachine(IMachine machine) throws InvalidObjectException, ConnectorException {
        try {
            getConnector(machine.getImage(), machine.getComputeCenter(), controllerServices)
                    .rebootInstances(new RebootInstancesRequest(Lists.newArrayList(machine.getName())));
        } catch (Exception e) {
            throw new ConnectorException("Machine restart failed: " + machine.getName(), e);
        }
    }

    public void terminateMachine(IMachine machine) throws InvalidObjectException, ConnectorException {
        AmazonEC2 connector = null;

        try {
            connector = getConnector(machine.getImage(), machine.getComputeCenter(), controllerServices);

            // First check if the instance still exists          
            Instance ec2Instance = getEc2Instance(machine, connector);

            if (ec2Instance == null) {
                // The instance doesn't exist so no need to terminate
                return;
            } else {
                connector.terminateInstances(new TerminateInstancesRequest(Lists.newArrayList(machine.getName())));
            }
        } catch (Exception e) {
            if (connector != null) {
                // Check again if the instance exists - we may get here because 
                // of a race condition in terminating the instance manually and 
                // terminating it here through the connector.
                Instance ec2Instance = getEc2Instance(machine, connector);

                if (ec2Instance == null) {
                    // Terminate failed but the machine doesnt exist so we are ok.
                    return;
                }
            }

            throw new ConnectorException("Machine terminate failed: " + machine.getName(), e);
        }
    }

    public void deleteImage(IImage image) throws InvalidObjectException, ConnectorException {
        String imageAMI = Utils.getAMIName(image.getProperties(), controllerServices);

        try {
            getConnector(image, image.getImageStore().getProperties(), controllerServices)
                    .deregisterImage(new DeregisterImageRequest(imageAMI));
        } catch (AmazonServiceException e) {
            logger.log(Level.WARNING, "", e);

            throw new InvalidObjectException("Failed to delete the image. Cause " + e.getMessage(), e);
        } catch (AmazonClientException e) {
            logger.log(Level.WARNING, "", e);

            throw new InvalidObjectException("Failed to delete the image. Cause " + e.getMessage(), e);
        }
    }

    public void refreshImageState(IImage image) throws InvalidObjectException, ConnectorException {
        // TODO Implement: See CORE-1081

    }

    public void configure(IComputeCenter computeCenter) throws InvalidObjectException, ConnectorException {
    }

    public void configure(IImageStore imageStore) throws InvalidObjectException, ConnectorException {
        // do nothing
    }

    public void configure(IImage image) throws InvalidObjectException, ConnectorException {
        // do nothing
    }

    public void unconfigure(IComputeCenter computeCenter) throws InvalidObjectException, ConnectorException {
    }

    public void unconfigure(IImageStore imageStore) throws InvalidObjectException, ConnectorException {
        // do nothing
    }

    public void unconfigure(IImage image) throws InvalidObjectException, ConnectorException {
        // do nothing
    }

    public void validate(IComputeCenter computeCenter) throws InvalidObjectException, ConnectorException {
        validate(computeCenter.getProperties());
    }

    public void validate(IImageStore imageStore) throws InvalidObjectException, ConnectorException {
        validate(imageStore.getProperties());
    }

    public void validate(IImage image) throws InvalidObjectException, ConnectorException {
        // do nothing
    }

    private List<String> getSecurityGroup(IProperty[] macProps) {
        String propertyValue = Utils.getSecurityGroup(macProps, controllerServices);

        List<String> securityGroups;

        if (propertyValue == null) {
            securityGroups = new ArrayList<String>(1);
            securityGroups.add(Utils.DEFAULT_SECURITY_GROUP);
        } else {
            // security group names are separated by ,
            securityGroups = Arrays.asList(propertyValue.split(","));
        }

        return securityGroups;
    }

    private String getSecurityGroupsAsString(List<String> securityGroups) {
        StringBuilder buffer = new StringBuilder();

        for (String group : securityGroups) {
            if (buffer.length() != 0)
                buffer.append(",");

            buffer.append(group);
        }

        return buffer.toString();
    }

    private void validateAndConfigureSecurityGroups(List<String> securityGroupNames, AmazonEC2 connector)
            throws ConnectorException {
        DescribeSecurityGroupsRequest describeSecurityGroupsRequest = new DescribeSecurityGroupsRequest();
        DescribeSecurityGroupsResult describeSecurityGroupsResult = connector
                .describeSecurityGroups(describeSecurityGroupsRequest.withGroupNames(securityGroupNames));

        String controllerIp = "0.0.0.0/0";
        int agentPort = controllerServices.getDefaultAgentPort();

        // check if any one of the security group
        // already has agent port and controller ip
        List<SecurityGroup> securityGroups = describeSecurityGroupsResult.getSecurityGroups();
        for (SecurityGroup securityGroup : securityGroups) {
            List<IpPermission> ipPermissions = securityGroup.getIpPermissions();
            for (IpPermission permission : ipPermissions) {
                if (permission.getIpRanges().contains(controllerIp)
                        && (agentPort >= permission.getFromPort() && agentPort <= permission.getToPort())) {
                    return;
                }
            }
        }

        String securityGroup = null;

        if (securityGroups.contains(Utils.DEFAULT_SECURITY_GROUP)) {
            securityGroup = Utils.DEFAULT_SECURITY_GROUP;
        } else {
            securityGroup = securityGroups.get(0).getGroupName();
        }

        IpPermission ipPermission = new IpPermission();
        ipPermission.setFromPort(agentPort);
        ipPermission.setToPort(agentPort);
        ipPermission.setIpProtocol("tcp");
        ipPermission.setIpRanges(Lists.newArrayList(controllerIp));
        connector.authorizeSecurityGroupIngress(
                new AuthorizeSecurityGroupIngressRequest(securityGroup, Lists.newArrayList(ipPermission)));
    }

    private InstanceType getInstanceType(IProperty[] macProps) {
        String instanceTypeString = Utils.getInstanceType(macProps, controllerServices);

        InstanceType instanceType = InstanceType.M1Small;
        try {
            instanceType = InstanceType.fromValue(instanceTypeString);
        } catch (IllegalArgumentException e) { //Should never occur
            logger.log(Level.INFO, "Invalid instance type. Using m1.small", e);
        }
        return instanceType;
    }

    private Instance getEc2Instance(IMachine machine, AmazonEC2 connector) throws ConnectorException {
        DescribeInstancesResult describeInstancesResult = null;
        try {
            DescribeInstancesRequest describeInstancesRequest = new DescribeInstancesRequest();
            describeInstancesResult = connector
                    .describeInstances(describeInstancesRequest.withInstanceIds(machine.getName()));
            List<Reservation> reservations = describeInstancesResult.getReservations();
            if (reservations.size() == 0) {
                // machine not found MUST have been terminated
                return null;
            }

            // always it will be in the first reservation as the query is on
            // only one machine
            List<Instance> instances = reservations.get(0).getInstances();
            for (Instance instance : instances) {
                if (instance.getInstanceId().equals(machine.getName())) {
                    // found the machine
                    return instance;
                }
            }
        } catch (Exception e) {
            throw new ConnectorException(e);
        }

        return null;
    }

    private MachineState getMachineState(Instance instance) {
        InstanceState state = instance.getState();
        String stateName = state.getName();

        if (stateName.equals("running")) {
            return STARTED;
        } else if (stateName.equals("pending")) {
            return STARTING;
        } else if (stateName.equals("shutting-down")) {
            return STOPPING;
        } else if (stateName.equals("terminated")) {
            return STOPPED;
        } else if (stateName.equals("stopped")) {
            return STARTED; // we dont need to create a new state as workflow dont support this
        } else {
            throw new IllegalStateException("State " + stateName + " is not known");
        }
    }

    private void setElasticIp(IMachine machine, String elasticIp, AmazonEC2 connector) throws ConnectorException {
        try {
            connector.associateAddress(new AssociateAddressRequest(machine.getName(), elasticIp));

            machine.setIpAddress(elasticIp);
        } catch (AmazonServiceException e) {
            throw new ConnectorException(e);
        } catch (AmazonClientException e) {
            throw new ConnectorException(e);
        }
    }

    private void validate(IProperty[] properties) throws InvalidObjectException {
        AmazonEC2 connector = ConnectorLocator.getInstance().getConnector(properties, controllerServices);

        // this will validate the access and secret keys
        try {
            connector.describeRegions();
        } catch (AmazonServiceException e) {
            logger.log(Level.INFO, "", e);
            throw new InvalidObjectException("The specified " + Utils.ACCESS_KEY_PROP + " and/or "
                    + Utils.SECRET_KEY_PROP + " is not valid.", e);
        } catch (AmazonClientException e) {

            logger.log(Level.INFO, "", e);
            throw new InvalidObjectException("The specified " + Utils.ACCESS_KEY_PROP + " and/or "
                    + Utils.SECRET_KEY_PROP + " is not valid.", e);
        }
    }

    private AmazonEC2 getConnector(IImage image, IComputeCenter computeCenter,
            IControllerServices controllerServices) {
        IProperty[] properties = computeCenter.getProperties();

        return getConnector(image, properties, controllerServices);
    }

    private AmazonEC2 getConnector(IImage image, IProperty[] properties, IControllerServices controllerServices) {
        AmazonEC2 connector = ConnectorLocator.getInstance().getConnector(properties, controllerServices);

        if (image == null) {
            return connector;
        }

        String region = Utils.getRegion(image.getProperties(), controllerServices);

        if (!Strings.isNullOrEmpty(region)) {
            region = region.toLowerCase();

            connector.setEndpoint(regionVsURLs.get(region));
        }

        return connector;
    }

}