/******************************************************************************* * Copyright 2012 Urbancode, 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.urbancode.terraform.tasks.aws; import java.io.File; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.log4j.Logger; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.BlockDeviceMapping; import com.amazonaws.services.ec2.model.GroupIdentifier; import com.amazonaws.services.ec2.model.Image; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancing; import com.urbancode.terraform.tasks.aws.helpers.AWSHelper; import com.urbancode.terraform.tasks.common.exceptions.EnvironmentCreationException; import com.urbancode.terraform.tasks.common.exceptions.EnvironmentDestructionException; import com.urbancode.terraform.tasks.common.exceptions.PostCreateException; import com.urbancode.x2o.tasks.Task; public class InstanceTask extends Task { //********************************************************************************************** // CLASS //********************************************************************************************** final static private Logger log = Logger.getLogger(InstanceTask.class); //********************************************************************************************** // INSTANCE //********************************************************************************************** private AmazonEC2 ec2Client; private AmazonElasticLoadBalancing elbClient; private AWSHelper helper; protected ContextAWS context; // by default, do not assign an EIP private boolean elasticIp = false; private String name; private String instanceId; private String amiId; private String akiId; private String ariId; private String subnetName; private String subnetId; private String elasticIpAllocId; private String elasticIpAddress; private String keyRef; private String sizeType; private String userData; private String loadBalancer; private String privateIp; private String zone; private String platform; // default values private int count = 1; private int priority = 1; private BootActionsTask bootActions; private PostCreateActionsTask pca; private List<SecurityGroupRefTask> secRefs = new ArrayList<SecurityGroupRefTask>(); private List<EbsTask> ebsVolumes = new ArrayList<EbsTask>(); //---------------------------------------------------------------------------------------------- public InstanceTask(ContextAWS context) { this.context = context; helper = new AWSHelper(); } //---------------------------------------------------------------------------------------------- public void setPlatform(String platform) { this.platform = platform; } //---------------------------------------------------------------------------------------------- public void setZone(String zone) { this.zone = zone; } //---------------------------------------------------------------------------------------------- public void setPrivateIp(String privateIp) { String ip4RegEx = "[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?\\.[0-9][0-9]?[0-9]?"; Pattern ip4Pattern = Pattern.compile(ip4RegEx); Matcher ip4Matcher = ip4Pattern.matcher(privateIp); if (ip4Matcher.find() && ip4Matcher.end() == privateIp.length()) { this.privateIp = privateIp; } else { log.error("Invalid Private IP Address for instance " + this.getName()); this.privateIp = ""; } } //---------------------------------------------------------------------------------------------- public void setLoadBalancer(String loadBalancer) { this.loadBalancer = loadBalancer; } //---------------------------------------------------------------------------------------------- public void setPriority(int priority) { this.priority = priority; } //---------------------------------------------------------------------------------------------- public void setCount(int count) { this.count = count; } //---------------------------------------------------------------------------------------------- public void setPrivateKeyRef(String keyRef) { this.keyRef = keyRef; } //---------------------------------------------------------------------------------------------- public void setImageSize(String sizeType) { this.sizeType = sizeType; } //---------------------------------------------------------------------------------------------- public void setName(String name) { this.name = name; } //---------------------------------------------------------------------------------------------- public void setId(String id) { this.instanceId = id; } //---------------------------------------------------------------------------------------------- public void setAmiId(String id) { this.amiId = id; } //---------------------------------------------------------------------------------------------- public void setSubnetName(String name) { this.subnetName = name; } //---------------------------------------------------------------------------------------------- public void setSubnetId(String id) { this.subnetId = id; } //---------------------------------------------------------------------------------------------- public void setPublicIp(String elasticIpAddress) { this.elasticIpAddress = elasticIpAddress; } //---------------------------------------------------------------------------------------------- public void setElasticIp(boolean elasticIp) { this.elasticIp = elasticIp; } //---------------------------------------------------------------------------------------------- public void setElasticIpAllocId(String id) { this.elasticIpAllocId = id; } //---------------------------------------------------------------------------------------------- public void setRamdiskId(String ariId) { this.ariId = ariId; } //---------------------------------------------------------------------------------------------- public void setKernelId(String akiId) { this.akiId = akiId; } //---------------------------------------------------------------------------------------------- public String getPlatform() { return platform; } //---------------------------------------------------------------------------------------------- public String getZone() { return zone; } //---------------------------------------------------------------------------------------------- public String getRamdiskId() { return ariId; } //---------------------------------------------------------------------------------------------- public String getKernelId() { return akiId; } //---------------------------------------------------------------------------------------------- public String getPrivateIp() { return privateIp; } //---------------------------------------------------------------------------------------------- public String getLoadBalancer() { return loadBalancer; } //---------------------------------------------------------------------------------------------- public int getPriority() { return priority; } //---------------------------------------------------------------------------------------------- public int getCount() { return count; } //---------------------------------------------------------------------------------------------- public String getId() { return instanceId; } //---------------------------------------------------------------------------------------------- public String getImageSize() { return sizeType; } //---------------------------------------------------------------------------------------------- public String getPrivateKeyRef() { return keyRef; } //---------------------------------------------------------------------------------------------- public String getName() { return name; } //---------------------------------------------------------------------------------------------- public BootActionsTask getBootActions() { return bootActions; } //---------------------------------------------------------------------------------------------- public PostCreateActionsTask getPostCreateActions() { return pca; } //---------------------------------------------------------------------------------------------- public List<SecurityGroupRefTask> getSecurityGroupRefs() { return Collections.unmodifiableList(secRefs); } //---------------------------------------------------------------------------------------------- public List<EbsTask> getEbsVolumes() { return Collections.unmodifiableList(ebsVolumes); } //---------------------------------------------------------------------------------------------- public String getAmiId() { return amiId; } //---------------------------------------------------------------------------------------------- public String getSubnetName() { return subnetName; } //---------------------------------------------------------------------------------------------- public boolean getElasticIp() { return elasticIp; } //---------------------------------------------------------------------------------------------- public String getElasticIpAllocId() { return elasticIpAllocId; } //---------------------------------------------------------------------------------------------- public String getPublicIp() { return elasticIpAddress; } //---------------------------------------------------------------------------------------------- public EbsTask createEbs() { EbsTask ebs = new EbsTask(context); ebsVolumes.add(ebs); return ebs; } //---------------------------------------------------------------------------------------------- public BootActionsTask createBootActions() { this.bootActions = new BootActionsTask(context); return bootActions; } //---------------------------------------------------------------------------------------------- public PostCreateActionsTask createPostCreateActions() { this.pca = new PostCreateActionsTask(context); return pca; } //---------------------------------------------------------------------------------------------- public SecurityGroupRefTask createEc2SecurityGroupRef() { SecurityGroupRefTask group = null; // if the instance does not have a subnet name set, then we'll // check for a zone, which would be required for an EC2 instance if (zone != null && !zone.isEmpty()) { log.debug("Creating EC2 Security Group Ref"); group = new Ec2SecurityGroupRefTask(context); } else { String msg = "No zone set! Unable to create securityGroup"; log.error(msg); // throw } if (group != null) { secRefs.add(group); } return group; } //---------------------------------------------------------------------------------------------- public SecurityGroupRefTask createVpcSecurityGroupRef() { SecurityGroupRefTask group = null; // determine which type of security group we need to make. // We will check if it's a VPC one first, that means the // instance must have a subnet name set. if (subnetName != null && !subnetName.isEmpty()) { log.debug("Creating VPC Security Group Ref"); group = new VpcSecurityGroupRefTask(context); } // if neither of those checks pass, something is wrong... else { String msg = "No subnet set! Unable to create securityGroup"; log.error(msg); // throw } if (group != null) { secRefs.add(group); } return group; } //---------------------------------------------------------------------------------------------- private boolean verifyElasticIp(Instance instance) { boolean result = false; boolean hasEIP = instance.getPublicIpAddress() != null; if (elasticIp == hasEIP) { if (elasticIp == true) { if (instance.getPublicIpAddress().equals(elasticIpAddress)) { result = true; } } else { result = true; } } return result; } //---------------------------------------------------------------------------------------------- private boolean verifyKeyPair(Instance instance) { boolean result = false; String keyName = keyRef; if (instance.getKeyName() != null && keyName != null) { if (instance.getKeyName().equals(keyName)) { result = true; } } return result; } //---------------------------------------------------------------------------------------------- private boolean verifySize(Instance instance) { boolean result = false; String size = sizeType; if (instance.getInstanceType() != null && size != null) { if (instance.getInstanceType().equals(size)) { result = true; } } return result; } //---------------------------------------------------------------------------------------------- private boolean verifySecurityGroups(Instance instance) { boolean result = false; List<String> expectedIds = new ArrayList<String>(); for (SecurityGroupRefTask group : getSecurityGroupRefs()) { expectedIds.add(group.fetchSecurityGroup().getId()); } List<String> foundIds = new ArrayList<String>(); List<GroupIdentifier> gids = instance.getSecurityGroups(); if (gids != null && !gids.isEmpty()) { for (GroupIdentifier gid : gids) { foundIds.add(gid.getGroupId()); } } return result; } //---------------------------------------------------------------------------------------------- public boolean verify() { // will return false if the id is null boolean result = false; if (instanceId != null) { if (ec2Client == null) { ec2Client = context.fetchEC2Client(); } List<String> id = new ArrayList<String>(); id.add(instanceId); List<Instance> instances = helper.getInstances(id, ec2Client); if (instances != null && !instances.isEmpty()) { for (Instance instance : instances) { if (instance.getImageId().equals(amiId)) { String subId = ((EnvironmentTaskAWS)context.getEnvironment()).getVpc() .findSubnetForName(subnetName).getId(); if (instance.getSubnetId() != null && instance.getSubnetId().equals(subId) && verifyElasticIp(instance) && verifyKeyPair(instance) && verifySize(instance) && verifySecurityGroups(instance)) { result = true; } } } } } return result; } //---------------------------------------------------------------------------------------------- private String setupBootActions() { String actions = ""; log.debug("Setting up Boot Actions"); if (getBootActions() != null) { getBootActions().setPlatform(platform); getBootActions().create(); actions = getBootActions().getUserData(); actions = context.resolve(actions); } log.info("Instance is being launched with following user-data script:\n\n" + actions); return actions; } //---------------------------------------------------------------------------------------------- private String setupType() { String size = sizeType; String defaultType = "t1.micro"; if (size.indexOf(".") == -1) { size = null; } if (size == null || size.isEmpty()) { log.warn("No instance size specified. Default to " + defaultType); size = defaultType; } return size; } //---------------------------------------------------------------------------------------------- private String setupKeyPair() { String keyPair = keyRef; if (keyPair == null || keyPair.isEmpty()) { log.warn("No key-pair specified. You may not be able to connect to instance " + name); } return keyPair; } //---------------------------------------------------------------------------------------------- private List<BlockDeviceMapping> setupEbs() { List<BlockDeviceMapping> blockMaps = new ArrayList<BlockDeviceMapping>(); if (ebsVolumes != null) { for (EbsTask ebs : ebsVolumes) { ebs.create(); blockMaps.add(ebs.getBlockDeviceMapping()); } } return blockMaps; } //---------------------------------------------------------------------------------------------- private List<String> findSecurityGroups() { List<String> groupIds = new ArrayList<String>(); if (getSecurityGroupRefs() != null) { for (SecurityGroupRefTask ref : getSecurityGroupRefs()) { SecurityGroupTask found = ref.fetchSecurityGroup(); if (found != null) { groupIds.add(found.getId()); } } } return groupIds; } //---------------------------------------------------------------------------------------------- private void updateMachineInfo() throws RemoteException { if (ec2Client == null) { throw new RemoteException("No connection to EC2"); } // update the akiId and ariId with what Amazon shows Instance instance = helper.getInstanceById(instanceId, ec2Client); if (instance != null) { log.info("Verifying Kernel Id..."); log.info("Expected: " + akiId); akiId = instance.getKernelId(); log.info("Found: " + akiId); log.info("Verifying Ramdisk Id..."); log.info("Expected: " + ariId); ariId = instance.getRamdiskId(); log.info("Found: " + ariId); } } //---------------------------------------------------------------------------------------------- private void assignElasticIp(String ipToAssign) throws RemoteException { if (ec2Client == null) { throw new RemoteException("No connection to EC2"); } if (ipToAssign == null || ipToAssign.isEmpty()) { if (subnetName != null && subnetId != null) { String allocId = helper.requestElasticIp(ec2Client); setElasticIpAllocId(allocId); String eip = helper.assignElasticIp(instanceId, allocId, ec2Client); setPublicIp(eip); } else { log.error("Cannot assign Elastic Ip to non-VPC instance"); } } } //---------------------------------------------------------------------------------------------- private void registerWithLoadBalancer() throws RemoteException, EnvironmentCreationException { if (ec2Client == null) { throw new RemoteException("No connection to EC2"); } if (loadBalancer != null && !loadBalancer.isEmpty()) { // we need to find the fullName of the load-balancer with name loadBalancer if (context != null && context.getEnvironment() != null && context.getEnvironment() instanceof EnvironmentTaskAWS) { EnvironmentTaskAWS env = (EnvironmentTaskAWS) context.getEnvironment(); if (env.getLoadBalancers() != null) { for (LoadBalancerTask load : env.getLoadBalancers()) { if (load.getName().equals(loadBalancer)) { // found it boolean allowed = false; // check if (zone != null && !zone.isEmpty()) { if (load.containsZone(zone)) { allowed = true; } else { log.error("Load balancer " + load.getName() + " does not " + "contain " + "zone " + zone + " - could not add " + "instance " + name + " to load balancer"); } } else if (subnetName != null && !subnetName.isEmpty()) { if (load.containsSubnet(subnetName)) { allowed = true; } else { log.error("Load balancer " + load.getName() + " does not " + "contain " + "subnet " + subnetName + " - could not " + "add instance " + name + " to load balancer"); } } if (allowed) { List<String> tmp = new ArrayList<String>(); tmp.add(instanceId); helper.updateInstancesOnLoadBalancer(load.getFullName(), tmp, true, elbClient); } else { log.error("Instance " + name + " cannot be registered on load " + "balancer " + load.getName()); throw new EnvironmentCreationException("Subnet " + subnetName + " or zone " + zone + " is not associated with load " + "balancer " + load.getFullName()); } break; } } } } } } //---------------------------------------------------------------------------------------------- private void deregisterWithLoadBalancer() { if (context != null && context.getEnvironment() != null && context.getEnvironment() instanceof EnvironmentTaskAWS) { EnvironmentTaskAWS env = (EnvironmentTaskAWS) context.getEnvironment(); if (env.getLoadBalancers() != null) { for (LoadBalancerTask load : env.getLoadBalancers()) { if (load.getName().equals(loadBalancer)) { // found it, deregistering instance with load balancer List<String> tmp = new ArrayList<String>(); tmp.add(instanceId); helper.updateInstancesOnLoadBalancer(load.getFullName(), tmp, false, elbClient); break; } } } } } //---------------------------------------------------------------------------------------------- private void waitForInstance() throws RemoteException, InterruptedException { if (instanceId != null && ec2Client != null) { // wait for instance to start and pass status checks helper.waitForState(instanceId, "running", 8, ec2Client); helper.waitForStatus(instanceId, "ok", 8, ec2Client); } } //---------------------------------------------------------------------------------------------- private void postStartup() throws RemoteException, InterruptedException { if (ec2Client == null) { throw new RemoteException("No connection to EC2"); } updateMachineInfo(); waitForInstance(); // name Instances String serverName = context.getEnvironment().getName() + "-" + name; helper.tagInstance(instanceId, "Name", serverName, ec2Client); // tag the instance with the environment name helper.tagInstance(instanceId, "terraform.environment", context.getEnvironment().getName(), ec2Client); } //---------------------------------------------------------------------------------------------- private void startPostCreateActions(String keyPair) throws PostCreateException { if (pca != null) { if (elasticIpAddress != null && !elasticIpAddress.isEmpty()) { pca.setHost(elasticIpAddress); } else { log.warn("Trying to do PostCreateActions on instance with no public ip!" + "\nName: " + name + "\nId: " + instanceId); } if (keyPair != null && !keyPair.isEmpty()) { String basePath = System.getProperty("user.home") + File.separator + ".terraform"; String keyPairPath = basePath + File.separator + keyPair + ".pem"; pca.setIdFile(keyPairPath); } else { log.warn("Trying to do PostCreateActions on instance " + name + " ( " + instanceId + " ) with no ssh key!"); } pca.create(); } } //---------------------------------------------------------------------------------------------- private void updatePlatform() { // update the platform List<String> imageId = new ArrayList<String>(); imageId.add(amiId); List<Image> images = helper.getImages(null, imageId, ec2Client); if (images != null) { Image image = images.get(0); platform = image.getPlatform(); } if (platform == null || "".equals(platform)) { platform = "linux"; } } //---------------------------------------------------------------------------------------------- @Override public void create() throws EnvironmentCreationException { String size; String keyPair; boolean verified = false; log.debug("Creating instance " + name); context.setProperty("server.name", name); // check AWS connections if (ec2Client == null) { ec2Client = context.fetchEC2Client(); } if (elbClient == null) { elbClient = context.fetchELBClient(); } updatePlatform(); try { // setup userData = setupBootActions(); size = setupType(); keyPair = setupKeyPair(); if (!verified) { setId(null); log.info("Starting creation"); List<String> groupIds = findSecurityGroups(); List<BlockDeviceMapping> blockMaps = setupEbs(); if (amiId == null) { String msg = "No AMI ID specified for instance " + name + ". There is no image" + " to use."; log.fatal(msg); throw new EnvironmentCreationException(msg); } // launch the instance and set the Id instanceId = helper.launchAmi(amiId, subnetId, keyPair, size, userData, groupIds, blockMaps, ariId, akiId, zone, privateIp, ec2Client); Thread.sleep(1000); postStartup(); // give instance elastic ip if (elasticIp) { // if we send null, it will grab a new EIP and assign it assignElasticIp(elasticIpAddress); } Instance instance = helper.getInstanceById(instanceId, ec2Client); // set public Ip if (zone != null && !zone.isEmpty()) { setPublicIp(instance.getPublicIpAddress()); context.setProperty(getName() + ".public.ip", getPublicIp()); } // set private Ip privateIp = instance.getPrivateIpAddress(); context.setProperty(getName() + ".private.ip", getPrivateIp()); // register with LB registerWithLoadBalancer(); // do PostCreateActions startPostCreateActions(keyPair); } } catch (Exception e) { log.error("Did not start instance " + name + " completely"); throw new EnvironmentCreationException("Failed to create Instance " + name, e); } finally { ec2Client = null; elbClient = null; } } //---------------------------------------------------------------------------------------------- @Override public void destroy() throws EnvironmentDestructionException { if (ec2Client == null) { ec2Client = context.fetchEC2Client(); } if (elbClient == null) { elbClient = context.fetchELBClient(); } try { log.info("Shutting down instance " + getId()); List<String> instanceIds = new ArrayList<String>(); instanceIds.add(instanceId); try { deregisterWithLoadBalancer(); } catch (AmazonServiceException e) { // swallow exception if we get invalidInstance // this will ignore the exception if you try to // deregister and instance that is not registered with // the load balancer - allowing the instance to terminate // completely. if (!e.getErrorCode().equals("InvalidInstance")) { throw e; } } if (elasticIpAllocId != null) { String assocId = helper.getAssociationIdForAllocationId(elasticIpAllocId, ec2Client); if (assocId != null && !assocId.isEmpty()) { helper.disassociateElasticIp(assocId, ec2Client); helper.releaseElasticIp(getElasticIpAllocId(), ec2Client); setElasticIpAllocId(null); setPublicIp(null); } else { log.error("Could not find asssociation Id for instance " + getName() + "\nUnable to disassociate and release elastic ip with " + "allocation id: " + elasticIpAllocId) ; } } if (instanceIds != null && !instanceIds.isEmpty()) { helper.terminateInstances(instanceIds, ec2Client); helper.waitForState(getId(), "terminated", 8, ec2Client); setId(null); setSubnetId(null); } if (bootActions != null) { bootActions.destroy(); } if (secRefs != null) { for (SecurityGroupRefTask group : secRefs) { group.destroy(); } } } catch (Exception e) { log.error("Did not destroy instance " + name + " completely", e); throw new EnvironmentDestructionException("Failed to destroy instance " + name, e); } finally { ec2Client = null; elbClient = null; log.info("Instance Destroyed."); } } //---------------------------------------------------------------------------------------------- @Override public InstanceTask clone() { InstanceTask result = new InstanceTask(context); // attributes result.setAmiId(amiId); result.setKernelId(akiId); result.setRamdiskId(ariId); result.setElasticIp(elasticIp); result.setImageSize(sizeType); result.setName(name); result.setPrivateKeyRef(keyRef); result.setSubnetName(subnetName); result.setZone(zone); // post create actions task BootActionsTask pcat = result.createBootActions(); if (getBootActions() != null) { if (getBootActions().getShell() != null) { pcat.setShell(getBootActions().getShell()); } if (getBootActions().getScript() != null) { for (BootActionSubTask script : getBootActions().getScript()) { ScriptTask scriptTask = (ScriptTask) script; ScriptTask nScript = pcat.createScript(); nScript.setCmds(script.getCmds()); nScript.setShell(scriptTask.getShell()); nScript.setUrl(scriptTask.getUrl()); for (ParamTask param : scriptTask.getParams()) { ParamTask nParam = nScript.createParam(); nParam.setValue(param.getValue()); } } } } PostCreateActionsTask pcat2 = result.createPostCreateActions(); if (getPostCreateActions() != null) { if (getPostCreateActions().getPostCreateActions() != null) { for (PostCreateActionTask ssh : getPostCreateActions().getPostCreateActions()) { if (ssh instanceof SshTask) { SshTask sshTask = pcat2.createSsh(); sshTask.setCmds(((SshTask) ssh).getCmds()); sshTask.setPassword(ssh.getPassword()); sshTask.setPort(ssh.getPort()); sshTask.setUser(ssh.getUser()); } } } } // sec group refs if (getSecurityGroupRefs() != null) { for (SecurityGroupRefTask secGroup : getSecurityGroupRefs()) { SecurityGroupRefTask nSecGroup = null; if (secGroup instanceof Ec2SecurityGroupRefTask) { nSecGroup = result.createEc2SecurityGroupRef(); } else if (secGroup instanceof VpcSecurityGroupRefTask) { nSecGroup = result.createVpcSecurityGroupRef(); } else { log.error("Unable to clone SecurityGroupRefTasks"); } nSecGroup.setSecurityGroupName(secGroup.getSecurityGroupName()); } } // ebsVolumes if (getEbsVolumes() != null) { for (EbsTask ebs : getEbsVolumes()) { EbsTask nEbs = result.createEbs(); nEbs.setVolumeSize(ebs.getVolumeSize()); nEbs.setVolumeId(ebs.getVolumeId()); nEbs.setSnapshotId(ebs.getSnapshotId()); nEbs.setPersist(ebs.getPersist()); nEbs.setName(ebs.getName()); nEbs.setDeviceName(ebs.getDeviceName()); } } return result; } }