/* * Copyright 2008-2012 Amazon Technologies, 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://aws.amazon.com/apache2.0 * * This file 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.amazonaws.eclipse.ec2.ui.views.instances; import java.io.File; import java.io.IOException; import java.text.ParseException; import java.util.List; import java.util.logging.Logger; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.statushandlers.StatusManager; import com.amazonaws.eclipse.core.AccountInfo; import com.amazonaws.eclipse.core.AwsToolkitCore; import com.amazonaws.eclipse.core.regions.Region; import com.amazonaws.eclipse.core.regions.RegionUtils; import com.amazonaws.eclipse.ec2.AmiToolsVersion; import com.amazonaws.eclipse.ec2.Ec2Plugin; import com.amazonaws.eclipse.ec2.RemoteCommandUtils; import com.amazonaws.eclipse.ec2.ShellCommandException; import com.amazonaws.eclipse.ec2.ShellCommandResults; import com.amazonaws.eclipse.ec2.ui.ShellCommandErrorDialog; import com.amazonaws.services.ec2.AmazonEC2; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.RegisterImageRequest; /** * Eclipse Job encapsulating the logic to bundle a specified instance as a new AMI. */ class BundleJob extends Job { private final Instance instance; private final String bundleName; private final String s3Bucket; private final AccountInfo accountInfo; /** Shared utilities for executing remote commands */ private final static RemoteCommandUtils remoteCommandUtils = new RemoteCommandUtils(); /** Shared logger */ private static final Logger logger = Logger.getLogger(BundleJob.class.getName()); /** The minimum required version of the EC2 AMI Tools */ private static final AmiToolsVersion requiredVersion = new AmiToolsVersion(1, 3, 31780); /** * Creates a new Bundle Job to bundle the specified instances and store the * data in the specified S3 bucket with the specified bundle name. * * @param instance * The instance to bundle into an AMI. * @param s3Bucket * The S3 bucket to store the bundled image in. * @param bundleName * The name of the AMI manifest. */ public BundleJob(Instance instance, String s3Bucket, String bundleName) { super("Bundling instance " + instance.getInstanceId()); this.instance = instance; this.bundleName = bundleName; this.s3Bucket = s3Bucket; this.accountInfo = AwsToolkitCore.getDefault().getAccountInfo(); } /* * Job Interface */ /* (non-Javadoc) * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor) */ @Override protected IStatus run(IProgressMonitor monitor) { monitor.beginTask("Bundling", 110); try { if (monitor.isCanceled()) return Status.CANCEL_STATUS; checkRequirements(); if (monitor.isCanceled()) return Status.CANCEL_STATUS; transferKeys(monitor); if (monitor.isCanceled()) return Status.CANCEL_STATUS; bundleVolume(monitor); if (monitor.isCanceled()) return Status.CANCEL_STATUS; deleteKeys(monitor); if (monitor.isCanceled()) return Status.CANCEL_STATUS; uploadBundle(monitor); if (monitor.isCanceled()) return Status.CANCEL_STATUS; final String amiName = registerBundle(monitor); final String message = "Successfully created AMI " + amiName; Display.getDefault().asyncExec(new Runnable() { public void run() { MessageBox messageBox = new MessageBox(new Shell(), SWT.ICON_INFORMATION | SWT.OK); messageBox.setMessage(message); messageBox.setText("AMI Bundling Complete"); messageBox.open(); } }); Status status = new Status(Status.INFO, Ec2Plugin.PLUGIN_ID, message); StatusManager.getManager().handle(status, StatusManager.LOG); logger.info("Successfully created AMI: " + amiName); } catch (final ShellCommandException sce) { Display.getDefault().asyncExec(new Runnable() { public void run() { new ShellCommandErrorDialog(sce).open(); } }); /* * We return a warning status instead of an error status since the * warning status doesn't cause another dialog to pop up (since * we're already displaying our own). */ return new Status(IStatus.WARNING, Ec2Plugin.PLUGIN_ID, "Unable to bundle instance: " + sce.getMessage(), sce); } catch (Exception e) { e.printStackTrace(); return new Status(IStatus.ERROR, Ec2Plugin.PLUGIN_ID, "Unable to bundle instance: " + e.getMessage(), e); } return Status.OK_STATUS; } /* * Private Interface */ /** * Checks requirements such as minimum required AMI tools version on the * remote host before the bundling process begins to try and detect anything * that will cause problems. * * @throws Exception * If this method detected a known problem. */ private void checkRequirements() throws Exception { /* * We seen bundling fail for older AMI tools versions that can't * automatically figure out the proper architecture * (ex: ec2-ami-tools-version 1.3-20041), so we want to warn the user * early if we know there's going to be a problem. */ /* * TODO: We should also do more error checking in general, * such as S3 bucket validation. */ List<ShellCommandResults> results = remoteCommandUtils.executeRemoteCommand("ec2-ami-tools-version", instance); // Find the output from the first successful attempt String output = null; for (ShellCommandResults result : results) { if (result.exitCode == 0) { output = result.output; break; } } // If we can't find the output from a successful run, just bail out if (output == null) { return; } AmiToolsVersion version = null; try { version = new AmiToolsVersion(output); } catch (ParseException pe) { /* * If we can't parse the AMI tools version, we want to bail out * instead of stopping the bundling operation. The version number * format could have changed, for example, and we wouldn't want to * break everybody if that happened. */ return; } if (requiredVersion.isGreaterThan(version)) { throw new Exception("The version of the EC2 AMI Tools on the remote host is too old " + "(" + version.toString() + "). " + "The AWS Toolkit for Eclipse requires version " + requiredVersion.toString() + " or greater. \n\nFor information on updating the EC2 AMI Tools on your " + "instance, consult the EC2 Developer Guide available from: http://aws.amazon.com/documentation "); } } /** * Registers the uploaded bundle as an EC2 AMI. * * @param monitor The progress monitor for this job. * @return The ID of the AMI created when registering the bundle. */ private String registerBundle(IProgressMonitor monitor) { monitor.subTask("Registering bundle"); AmazonEC2 ec2 = Ec2Plugin.getDefault().getDefaultEC2Client(); RegisterImageRequest request = new RegisterImageRequest(); request.setImageLocation(s3Bucket + "/" + bundleName + ".manifest.xml"); String amiName = ec2.registerImage(request).getImageId(); monitor.worked(5); return amiName; } /** * Uploads the bundled volume to S3. * * @param monitor The progress monitor for this job. * @throws IOException * @throws InterruptedException */ private void uploadBundle(IProgressMonitor monitor) throws IOException, InterruptedException { monitor.subTask("Uploading bundle"); String manifestFile = "/mnt/" + bundleName + ".manifest.xml"; String uploadCommand = "ec2-upload-bundle " + " --bucket " + s3Bucket + " --manifest " + manifestFile + " --access-key " + accountInfo.getAccessKey() + " --secret-key " + accountInfo.getSecretKey(); Region region = RegionUtils.getCurrentRegion(); String regionId = region.getId().toLowerCase(); /* * We need to make sure that the bundle is uploaded to the correct S3 * location depending on what region we're going to register the AMI in. */ if (regionId.startsWith("eu")) { uploadCommand += " --location EU"; } else if (regionId.startsWith("us")) { uploadCommand += " --location US"; } remoteCommandUtils .executeRemoteCommand(uploadCommand, instance); monitor.worked(35); } /** * Connects to the EC2 instance to bundle the volume. * * @param monitor The progress monitor for this job. * @throws IOException * @throws InterruptedException */ private void bundleVolume(IProgressMonitor monitor) throws IOException, InterruptedException { monitor.subTask("Bundling volume"); File privateKeyFile = new File(accountInfo.getEc2PrivateKeyFile()); String privateKeyBaseFileName = privateKeyFile.getName(); File certificateFile = new File(accountInfo.getEc2CertificateFile()); String certificateBaseFileName = certificateFile.getName(); String bundleCommand = "ec2-bundle-vol --destination /mnt " + " --privateKey /mnt/" + privateKeyBaseFileName + " --cert /mnt/" + certificateBaseFileName + " --user " + accountInfo.getUserId() + " --batch --prefix " + bundleName; remoteCommandUtils.executeRemoteCommand(bundleCommand, instance); monitor.worked(50); } /** * Securely transfers the user's key and certificate to the EC2 instance so that * the bundle can be signed. * * @param monitor The progress monitor for this job. * @throws IOException * @throws InterruptedException */ private void transferKeys(IProgressMonitor monitor) throws IOException, InterruptedException { monitor.subTask("Transfering keys"); File privateKeyFile = new File(accountInfo.getEc2PrivateKeyFile()); String privateKeyBaseFileName = privateKeyFile.getName(); File certificateFile = new File(accountInfo.getEc2CertificateFile()); String certificateBaseFileName = certificateFile.getName(); remoteCommandUtils.copyRemoteFile(accountInfo.getEc2CertificateFile(), "/mnt/" + certificateBaseFileName, instance); monitor.worked(5); remoteCommandUtils.copyRemoteFile(accountInfo.getEc2PrivateKeyFile(), "/mnt/" + privateKeyBaseFileName, instance); monitor.worked(5); } /** * Deletes the user's key and certificate that were previously transfered. * * @param monitor * The progress monitor for this job. * @throws IOException * If any problems were encountered deleting the EC2 key and * cert. */ private void deleteKeys(IProgressMonitor monitor) { monitor.subTask("Deleting keys"); File privateKeyFile = new File(accountInfo.getEc2PrivateKeyFile()); String privateKeyBaseFileName = privateKeyFile.getName(); File certificateFile = new File(accountInfo.getEc2CertificateFile()); String certificateBaseFileName = certificateFile.getName(); try { remoteCommandUtils.executeRemoteCommand("rm /mnt/" + certificateBaseFileName, instance); } catch (IOException e) { Status status = new Status(IStatus.WARNING, Ec2Plugin.PLUGIN_ID, "Unable to remove EC2 certificate: " + e.getMessage(), e); StatusManager.getManager().handle(status, StatusManager.LOG); } monitor.worked(5); try { remoteCommandUtils.executeRemoteCommand("rm /mnt/" + privateKeyBaseFileName, instance); } catch (IOException e) { Status status = new Status(IStatus.WARNING, Ec2Plugin.PLUGIN_ID, "Unable to remove EC2 private key: " + e.getMessage(), e); StatusManager.getManager().handle(status, StatusManager.LOG); } monitor.worked(5); } }