de.tuhrig.deployman.launch.Launcher.java Source code

Java tutorial

Introduction

Here is the source code for de.tuhrig.deployman.launch.Launcher.java

Source

/**
 * DeployMan # Thomas Uhrig (Stuttgart, 2014) # www.tuhrig.de
 */
package de.tuhrig.deployman.launch;

import static de.tuhrig.deployman.DeployMan.EC2_INSTANCE_KEY;
import static de.tuhrig.deployman.DeployMan.REPO_PROFILE;
import static de.tuhrig.deployman.DeployMan.SLASH;
import static de.tuhrig.deployman.DeployMan.getUserProperty;
import static de.tuhrig.deployman.DeployMan.readUserProperties;
import static de.tuhrig.deployman.DeployMan.sdf;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;

import com.amazonaws.services.autoscaling.AmazonAutoScalingClient;
import com.amazonaws.services.autoscaling.model.AlreadyExistsException;
import com.amazonaws.services.autoscaling.model.AutoScalingInstanceDetails;
import com.amazonaws.services.autoscaling.model.CreateAutoScalingGroupRequest;
import com.amazonaws.services.autoscaling.model.CreateLaunchConfigurationRequest;
import com.amazonaws.services.autoscaling.model.InstanceMonitoring;
import com.amazonaws.services.autoscaling.model.PutScalingPolicyRequest;
import com.amazonaws.services.autoscaling.model.PutScalingPolicyResult;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient;
import com.amazonaws.services.cloudwatch.model.ComparisonOperator;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.PutMetricAlarmRequest;
import com.amazonaws.services.cloudwatch.model.StandardUnit;
import com.amazonaws.services.cloudwatch.model.Statistic;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.BlockDeviceMapping;
import com.amazonaws.services.ec2.model.IamInstanceProfileSpecification;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.rds.model.CreateDBInstanceRequest;
import com.amazonaws.services.rds.model.DBInstance;
import com.amazonaws.services.rds.model.Endpoint;
import com.google.gson.Gson;

import de.tuhrig.deployman.DeployMan;
import de.tuhrig.deployman.aws.AutoScaling;
import de.tuhrig.deployman.aws.CloudWatch;
import de.tuhrig.deployman.aws.Ec2;
import de.tuhrig.deployman.aws.Rds;
import de.tuhrig.deployman.console.Console;
import de.tuhrig.deployman.launch.enums.Comment;
import de.tuhrig.deployman.launch.enums.Script;
import de.tuhrig.deployman.launch.enums.Variable;
import de.tuhrig.deployman.launch.formation.Container;
import de.tuhrig.deployman.launch.formation.Credential;
import de.tuhrig.deployman.launch.formation.Database;
import de.tuhrig.deployman.launch.formation.Formation;
import de.tuhrig.deployman.launch.formation.Machine;
import de.tuhrig.deployman.launch.formation.Scaling;
import de.tuhrig.deployman.repo.LocaleRepository;

/**
 * This class launches formations. It can either launch a EC2 virtual machine setup or a RDS
 * database setup. It can also launch a formation file which contains both, an EC2 setup and a RDS
 * setup.
 * 
 * @author tuhrig
 */
public class Launcher {
    public static final String HOME = "/home/ubuntu";
    public static final String DEPLOYMENT_LOG_FILE = HOME + "/deployman.log";
    public static final String DOCKER_LOG_FILE = HOME + "/docker.log";

    private Ec2 ec2Client = new Ec2();
    private Console console;

    public Launcher() {
        this(DeployMan.console);
    }

    public Launcher(Console console) {
        this.console = console;
    }

    public void run(String formationFile) {
        Formation formation = Formation.read(formationFile);
        run(formation);
    }

    public void run(Formation formation) {
        this.console.writeNl("Start formation " + formation.getFile());

        DBInstance dbInstance = null;
        Instance instance = null;
        List<Instance> instances = null;

        if (formation.hasDatabaseDefinition())
            dbInstance = runDatabaseLaunch(formation);

        // if there is a auto scaling group defined in the
        // formation file, we prever it over the machine
        // definition!
        if (formation.hasAutoScalingDefinition())
            instances = runAutoScalingLaunch(formation);
        else if (formation.hasInstanceDefinition())
            instance = runVirtualMachineLaunch(formation);

        // we print the results at the end to present a nice
        // overview to the user after the launch processes
        // have finished
        if (dbInstance != null)
            this.console.printDatabase(dbInstance);

        if (instance != null)
            this.console.printInstanceInfo(instance);

        if (instances != null)
            this.console.printEc2Instances(instances);
    }

    /**
     * Starts a auto scaling launch configuration. This method is a little but complicated, since the
     * process to start a auto scaling configuration involves some steps: - create a launch
     * configuration which defines the machines to start - create an auto scaling definition which
     * defines how many machines should be started if something happens. - create an auto scaling
     * policy to which we add a metric - create the metric which is checked by the auto scaling -
     * print the starting instances
     */
    private List<Instance> runAutoScalingLaunch(Formation formation) {
        this.console.write("Run auto scaling...");

        Machine machine = formation.getMachine();
        Scaling scaling = machine.getScaling();

        AmazonAutoScalingClient autoScaling = new AutoScaling().getClient();

        try {
            CreateLaunchConfigurationRequest request = createLaunchConfigurationRequest(formation);
            autoScaling.createLaunchConfiguration(request);
            this.console.write("Created launch configuration " + scaling.getName());
        } catch (AlreadyExistsException e) {
            this.console.write("Launch configuration " + scaling.getName() + " already exists"); //$NON-NLS-2$
        }

        //
        //
        //

        try {
            CreateAutoScalingGroupRequest request = createAutoScalingRequest(scaling);
            autoScaling.createAutoScalingGroup(request);
            this.console.write("Created auto scaling group " + scaling.getGroup());
        } catch (AlreadyExistsException e) {
            this.console.write("Auto scaling group " + scaling.getGroup() + " already exists"); //$NON-NLS-2$
        }

        //
        //
        //

        PutScalingPolicyRequest request = new PutScalingPolicyRequest().withAutoScalingGroupName(scaling.getGroup())
                .withPolicyName(scaling.getPolicy()).withScalingAdjustment(1)
                .withAdjustmentType("ChangeInCapacity");

        PutScalingPolicyResult result = autoScaling.putScalingPolicy(request);

        this.console.write("Put scaling policy " + scaling.getPolicy());

        //
        //
        //

        // Scale Up

        Dimension dimension = new Dimension().withName("AutoScalingGroupName").withValue(scaling.getGroup());

        List<String> actions = new ArrayList<>();
        actions.add(result.getPolicyARN());

        PutMetricAlarmRequest upRequest = new PutMetricAlarmRequest().withAlarmName(scaling.getAlarm())
                .withMetricName("CPUUtilization").withDimensions(dimension).withNamespace("AWS/EC2")
                .withComparisonOperator(ComparisonOperator.GreaterThanThreshold).withStatistic(Statistic.Average)
                .withUnit(StandardUnit.Percent).withThreshold(60d).withPeriod(300).withEvaluationPeriods(2)
                .withAlarmActions(actions);

        AmazonCloudWatchClient cloudWatch = new CloudWatch().getClient();
        cloudWatch.putMetricAlarm(upRequest);

        this.console.write("Put alarm " + scaling.getAlarm());
        this.console.newLine();

        List<Instance> instances = new ArrayList<>();

        for (AutoScalingInstanceDetails instance : autoScaling.describeAutoScalingInstances()
                .getAutoScalingInstances()) {

            if (instance.getAutoScalingGroupName().equals(scaling.getGroup())) {

                instances.add(new Ec2().getEC2InstanceById(instance.getInstanceId()));
            }
        }

        return instances;
    }

    private CreateLaunchConfigurationRequest createLaunchConfigurationRequest(Formation formation) {
        Machine machine = formation.getMachine();
        Scaling scaling = machine.getScaling();

        Properties generalProperties = getMetaInformationProperties(formation);
        CloudInitScript initScript = getParallelCloudInitScript(machine, generalProperties);
        IamInstanceProfileSpecification profil = this.ec2Client
                .getIamInstanceProfileSpecification(getUserProperty(REPO_PROFILE));

        return new CreateLaunchConfigurationRequest().withLaunchConfigurationName(scaling.getName())
                .withInstanceType(machine.getInstanceType()).withImageId(machine.getImageId())
                .withUserData(initScript.renderAsBase64()).withSecurityGroups(machine.getSecurityGroup())
                .withIamInstanceProfile(profil.getName())
                // the name NOT the ARN!
                .withKeyName(getUserProperty(EC2_INSTANCE_KEY))
                .withInstanceMonitoring(new InstanceMonitoring().withEnabled(false));

    }

    private CreateAutoScalingGroupRequest createAutoScalingRequest(Scaling scaling) {
        return new CreateAutoScalingGroupRequest().withAutoScalingGroupName(scaling.getGroup())
                .withLaunchConfigurationName(scaling.getName()).withAvailabilityZones(scaling.getZone())
                .withMaxSize(scaling.getMax()).withMinSize(scaling.getMin()).withLoadBalancerNames(scaling.getLb())
                .withHealthCheckType("EC2").withHealthCheckGracePeriod(300).withDefaultCooldown(600);
    }

    private DBInstance runDatabaseLaunch(Formation formation) {
        Rds rdsClient = new Rds();

        // if ( true )
        // return null;

        Database database = formation.getDatabase();
        DBInstance dbInstance = null;

        if (rdsClient.databaseExists(database.getInstanceIdentifier())) {
            this.console.writeNl("Database already exists");
            dbInstance = rdsClient.getDatabase(database.getInstanceIdentifier());
        } else {
            this.console.writeNl("Create database");

            CreateDBInstanceRequest request = new CreateDBInstanceRequest().withEngine(database.getEngine())
                    .withEngineVersion(database.getEngineVersion()).withLicenseModel(database.getLicense())
                    .withDBInstanceClass(database.getInstanceClass()).withMultiAZ(database.getMultiAz())
                    .withAutoMinorVersionUpgrade(database.getAutoMinorVersionUpgrade())
                    .withAllocatedStorage(database.getAllocatedStorage())
                    .withDBInstanceIdentifier(database.getInstanceIdentifier())
                    .withMasterUsername(database.getUsername()).withMasterUserPassword(database.getPassword())
                    .withDBName(database.getName()).withPort(database.getPort());
            // .withVpcSecurityGroupIds( database.getSecurityGroup() );

            dbInstance = rdsClient.getClient().createDBInstance(request);
        }

        this.console.writeNl("Starting...");

        waitForDatabaseState(dbInstance.getDBInstanceIdentifier(), "available");

        // sleep 10 seconds to get it really ready...
        // no idea why the state 'available isn't enough
        sleep(10000);

        dbInstance = rdsClient.getDatabase(dbInstance.getDBInstanceIdentifier());
        runSetup(dbInstance, database);
        return dbInstance;
    }

    private Instance runVirtualMachineLaunch(Formation formation) {
        Properties generalProperties = getMetaInformationProperties(formation);
        Machine machine = formation.getMachine();
        CloudInitScript initScript = getParallelCloudInitScript(machine, generalProperties);

        initScript.save();
        this.console.newLine();

        // if ( true )
        // return null;

        BlockDeviceMapping volumn = this.ec2Client.getBlockDeviceMapping("/dev/sda1", 20);
        IamInstanceProfileSpecification profil = this.ec2Client
                .getIamInstanceProfileSpecification(getUserProperty(REPO_PROFILE));

        RunInstancesRequest request = new RunInstancesRequest().withInstanceType(machine.getInstanceType())
                .withImageId(machine.getImageId()).withIamInstanceProfile(profil).withMinCount(1).withMaxCount(1)
                .withBlockDeviceMappings(volumn).withUserData(initScript.renderAsBase64())
                .withSecurityGroupIds(machine.getSecurityGroup()).withKeyName(getUserProperty(EC2_INSTANCE_KEY));

        String instanceId = runInstance(request);

        this.console.writeNl("Starting...");

        waitForInstanceState(instanceId, "running");
        assigneIpIfAvailable(instanceId, machine);
        assigneName(instanceId, machine);

        return this.ec2Client.getEC2InstanceById(instanceId);
    }

    private void assigneName(String instanceId, Machine machine) {
        // tag the machine with its name which must not be unique
        new Ec2().tag(instanceId, machine.getName());
    }

    private String runInstance(RunInstancesRequest request) {
        AmazonEC2 ec2 = this.ec2Client.getClient();

        RunInstancesResult runInstances = ec2.runInstances(request);

        List<Instance> instances = runInstances.getReservation().getInstances();

        return instances.get(0).getInstanceId();
    }

    /**
     * Returns a CloudInit script which performs the complete initialization of a machine. The script
     * will execute all steps in a sequential order after each other. Note: This method is currently
     * not used since it is slower as the parallel deployment.
     */
    // private CloudInitScript getSequentialCloudInitScript( Machine machine, Properties
    // generalProperties )
    // {
    // CloudInitScript initScript = getBaseScript( machine, generalProperties );
    //
    // // a counter to distinguish different configurations
    // // with the same name
    // int index = 0;
    //
    // for ( Container container : machine.getContainers() )
    // {
    // Properties containerProperties = getContainerProperties( container, index );
    //
    // CloudInitScript script = new CloudInitScript( "# container init" ).withProperties(
    // containerProperties )
    // .withFile( "copy_image.sh" )
    // .withFile( "copy_config.sh" )
    // .withFile( "docker_load.sh" )
    // .withFile( "sync_config.sh" )
    // .withCommand( container.getCommand() );
    //
    // initScript = initScript.withScript( script );
    //
    // // increment the counter
    // index++;
    // }
    //
    //    CloudInitScript script = new CloudInitScript( "# done" ).withFile( "final_message.sh" );  //$NON-NLS-2$
    // return initScript.withScript( script );
    // }

    private void assigneIpIfAvailable(String instanceId, Machine machine) {
        // if the formation has an elastic ip (which is optional)
        // we associate this ip with the instance we created before
        String elasticIp = machine.getElasticIp();
        if (elasticIp != null && !elasticIp.equals(""))
            this.ec2Client.associate(instanceId, elasticIp);
    }

    /**
     * Returns a CloudInit script which performs the complete initialization of a machine. The script
     * will execute the donwload and load steps in parallel in an arbitrary order. All containers are
     * started in the order of the formations file, but directly after each other.
     */
    private CloudInitScript getParallelCloudInitScript(Machine machine, Properties generalProperties) {
        CloudInitScript initScript = generateBaseScript(machine, generalProperties);
        CloudInitScript getScript = new CloudInitScript(Comment.COPY_IMAGES_AND_CONFIGS);
        CloudInitScript runScript = new CloudInitScript(Comment.EXEC_IMAGES);

        // a counter to distinguish different configurations
        // with the same name
        int index = 0;

        for (Container container : machine.getContainers()) {
            Properties properties = getContainerProperties(container, index);

            CloudInitScript imageDownloadScript = generateDownloadScriptForContainer(container, properties);
            CloudInitScript imageRunScript = generateRunScriptForContainer(machine, container, properties);

            getScript.withScript(imageDownloadScript);
            runScript.withScript(imageRunScript);

            // increment the counter
            index++;
        }

        CloudInitScript wait = new CloudInitScript(Comment.WAIT_FOR_SUBSHELL).withFile(Script.WAIT);

        getScript.withScript(wait);

        initScript.withScript(getScript).withScript(runScript);

        CloudInitScript script = new CloudInitScript(Comment.DONE).withFile(Script.FINAL_MESSAGE);
        return initScript.withScript(script);
    }

    private CloudInitScript generateDownloadScriptForContainer(Container container,
            Properties containerProperties) {
        if (container.hasImage())
            return getImageByDownloadFromRegistry(container, containerProperties);
        return getImageByDownloadOfTarball(containerProperties);
    }

    private CloudInitScript generateRunScriptForContainer(Machine machine, Container container,
            Properties containerProperties) {
        if (machine.hasAutoSync()) {
            return new CloudInitScript(Comment.RUN_IMAGE).withProperties(containerProperties)
                    .withFile(Script.SYNC_CONFIG).withCommand(container.getCommand());
        }

        return new CloudInitScript(Comment.RUN_IMAGE).withProperties(containerProperties)
                .withCommand(container.getCommand());
    }

    private CloudInitScript getImageByDownloadOfTarball(Properties containerProperties) {
        return new CloudInitScript(Comment.DOWNLOAD_TARBALL).withProperties(containerProperties)
                .withFile(Script.COPY_AND_LOAD_TARBALL_PARALLEL).withFile(Script.COPY_CONFIG_PARALLEL);
    }

    private CloudInitScript getImageByDownloadFromRegistry(Container container, Properties containerProperties) {
        if (container.hasCredential()) {
            return new CloudInitScript(Comment.DOWNLOAD_IMAGE).withProperties(containerProperties)
                    .withFile(Script.DOCKER_LOGIN).withFile(Script.DOWNLOAD_IMAGE)
                    .withFile(Script.COPY_CONFIG_PARALLEL);
        }

        return new CloudInitScript(Comment.DOWNLOAD_IMAGE).withProperties(containerProperties)
                .withFile(Script.DOWNLOAD_IMAGE).withFile(Script.COPY_CONFIG_PARALLEL);
    }

    private Properties getContainerProperties(Container setup, int index) {
        Properties properties = readUserProperties();
        addContainerProperties(properties, setup, index);
        return properties;
    }

    private void addContainerProperties(Properties properties, Container container, int index) {
        properties.put(Variable.TARBALL_KEY, container.getTarball());
        properties.put(Variable.IMAGE_NAME, container.getImage());
        properties.put(Variable.CONFIG_KEY, container.getConfig());
        properties.put(Variable.TARBALL_NAME, container.getTarballFileName());
        properties.put(Variable.CONFIG_FOLDER, HOME + "/config-" + container.getTarballName() + "-" + index);
        properties.put(Variable.HOME_DIRECTORY, HOME);

        if (container.hasCredential()) {
            Credential credential = container.getCredential();
            addCredentialProperties(properties, credential);
        }
    }

    private void addCredentialProperties(Properties properties, Credential credential) {
        properties.put(Variable.REGISTRY_EMAIL, credential.getEmail());
        properties.put(Variable.REGISTRY_NAME, credential.getName());
        properties.put(Variable.REGISTRY_PASSWORD, credential.getPassword());
        properties.put(Variable.REGISTRY_SERVER, credential.getServer());
    }

    private CloudInitScript generateBaseScript(Machine machine, Properties generalProperties) {
        CloudInitScript baseScript = new CloudInitScript(Comment.BASH).withProperties(generalProperties)
                .withFile(Script.LOG_HEADER).withFile(Script.SET_TIMEZONE).withFile(Script.LOG_INFO);

        if (machine.installDocker())
            baseScript.withFile(Script.INSTALL_DOCKER);

        if (machine.installAwsCli())
            baseScript.withFile(Script.INSTALL_AWSCLI);

        if (machine.openDocker())
            baseScript.withFile(Script.OPEN_DOCKER);

        return baseScript;
    }

    private void waitForDatabaseState(String dbInstance, String state) {
        while (!new Rds().getDatabase(dbInstance).getDBInstanceStatus().equalsIgnoreCase(state)) {
            sleep(4000);
        }
    }

    private void waitForInstanceState(String instanceId, String state) {
        while (!new Ec2().getEC2InstanceById(instanceId).getState().getName().equalsIgnoreCase(state)) {
            sleep(3000);
        }
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            //
        }
    }

    /**
     * Runs the given database configuration (~setup) on an AWS database instance. Make sure the setup
     * fits the database (e.g. a MSSQL setup for a MSSQL database).
     */
    private void runSetup(DBInstance dbInstance, Database database) {
        String setup = database.getSetup();
        String buildFilePath = new LocaleRepository().getLocation() + SLASH + setup + "/build.xml";
        Endpoint endpoint = dbInstance.getEndpoint();
        File buildFile = new File(buildFilePath);

        this.console.write("Run database setup " + buildFilePath);
        this.console.write("Endpoint " + endpoint);

        Project project = new Project();
        project.setUserProperty(Variable.ANT_FILE, buildFile.getAbsolutePath());
        project.setUserProperty(Variable.DEST_ROOT_LOCAL, new LocaleRepository().getLocation() + SLASH + "tmp");
        project.setUserProperty(Variable.DB_SERVER, endpoint.getAddress());
        project.setUserProperty(Variable.DB_PORT, endpoint.getPort().toString());
        project.setUserProperty(Variable.DB_USER, database.getUsername());
        project.setUserProperty(Variable.DB_PASSWORD, database.getPassword());
        project.setUserProperty(Variable.ENV_NLS_LANG, "American_America.UTF8");
        project.setUserProperty(Variable.HEADLESS, "true");
        project.init();

        DefaultLogger consoleLogger = createConsoleLogger();
        project.addBuildListener(consoleLogger);

        ProjectHelper helper = ProjectHelper.getProjectHelper();
        project.addReference("ant.projectHelper", helper);
        helper.parse(project, buildFile);
        project.executeTarget(project.getDefaultTarget());

        this.console.newLine();
    }

    private DefaultLogger createConsoleLogger() {
        DefaultLogger consoleLogger = new DefaultLogger();
        consoleLogger.setErrorPrintStream(System.err);
        consoleLogger.setOutputPrintStream(System.out);
        consoleLogger.setMessageOutputLevel(Project.MSG_INFO);
        return consoleLogger;
    }

    public Properties getMetaInformationProperties(Formation formation) {
        Gson gson = new Gson();
        Machine machine = formation.getMachine();
        List<Container> containers = machine.getContainers();

        Properties properties = readUserProperties();
        properties.put(Variable.TIMESTAMP, sdf.format(new Date()));
        properties.put(Variable.HOST, new DeployMan().getLocalHostName());
        properties.put(Variable.FORMATION, gson.toJson(formation));
        properties.put(Variable.CONTAINERS, containers.size());
        properties.put(Variable.HOME_DIRECTORY, HOME);
        properties.put(Variable.LOG_DEPLOYMENT, DEPLOYMENT_LOG_FILE);
        properties.put(Variable.LOG_DOCKER, DOCKER_LOG_FILE);

        return properties;
    }
}