Index: src/main/java/hudson/plugins/ec2/EC2Cloud.java =================================================================== --- src/main/java/hudson/plugins/ec2/EC2Cloud.java (revision 29721) +++ src/main/java/hudson/plugins/ec2/EC2Cloud.java (working copy) @@ -1,490 +1,68 @@ package hudson.plugins.ec2; -import com.xerox.amazonws.ec2.EC2Exception; -import com.xerox.amazonws.ec2.InstanceType; -import com.xerox.amazonws.ec2.Jec2; -import com.xerox.amazonws.ec2.KeyPairInfo; -import com.xerox.amazonws.ec2.ReservationDescription; -import com.xerox.amazonws.ec2.ReservationDescription.Instance; -import hudson.model.Computer; -import hudson.model.Descriptor; -import hudson.model.Hudson; -import hudson.model.Label; -import hudson.model.Node; -import hudson.slaves.Cloud; -import hudson.slaves.NodeProvisioner.PlannedNode; +import hudson.Extension; import hudson.util.FormValidation; -import hudson.util.Secret; -import hudson.util.StreamTaskListener; -import java.net.MalformedURLException; - -import org.jets3t.service.Constants; -import org.jets3t.service.S3Service; -import org.jets3t.service.S3ServiceException; -import org.jets3t.service.impl.rest.httpclient.RestS3Service; -import org.jets3t.service.security.AWSCredentials; -import org.jets3t.service.utils.ServiceUtils; +import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; -import java.io.BufferedReader; import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; +import java.net.MalformedURLException; import java.net.URL; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import java.util.logging.Logger; - -import org.jets3t.service.Jets3tProperties; -import static java.util.logging.Level.WARNING; /** - * Hudson's view of EC2. + * An Amazon EC2 Cloud. + * The original implementation of {@link GenericEC2Cloud}. * * @author Kohsuke Kawaguchi */ -public abstract class EC2Cloud extends Cloud { - - private final String accessId; - private final Secret secretKey; - private final EC2PrivateKey privateKey; - - /** - * Upper bound on how many instances we may provision. - */ - public final int instanceCap; - private final List templates; - private transient KeyPairInfo usableKeyPair; - - protected EC2Cloud(String id, String accessId, String secretKey, String privateKey, String instanceCapStr, List templates) { - super(id); - this.accessId = accessId.trim(); - this.secretKey = Secret.fromString(secretKey.trim()); - this.privateKey = new EC2PrivateKey(privateKey); - if(instanceCapStr.equals("")) - this.instanceCap = Integer.MAX_VALUE; - else - this.instanceCap = Integer.parseInt(instanceCapStr); - if(templates==null) templates=Collections.emptyList(); - this.templates = templates; - readResolve(); // set parents - } - - public abstract URL getEc2EndpointUrl() throws IOException; - public abstract URL getS3EndpointUrl() throws IOException; - - protected Object readResolve() { - for (SlaveTemplate t : templates) - t.parent = this; - return this; - } - - public String getAccessId() { - return accessId; - } - - public String getSecretKey() { - return secretKey.getEncryptedValue(); - } - - public EC2PrivateKey getPrivateKey() { - return privateKey; - } - - public String getInstanceCapStr() { - if(instanceCap==Integer.MAX_VALUE) - return ""; - else - return String.valueOf(instanceCap); - } - - public List getTemplates() { - return Collections.unmodifiableList(templates); - } - - public SlaveTemplate getTemplate(String ami) { - for (SlaveTemplate t : templates) - if(t.ami.equals(ami)) - return t; - return null; - } - - /** - * Gets {@link SlaveTemplate} that has the matching {@link Label}. - */ - public SlaveTemplate getTemplate(Label label) { - for (SlaveTemplate t : templates) - if(t.containsLabel(label)) - return t; - return null; - } - - /** - * Gets the {@link KeyPairInfo} used for the launch. - */ - public synchronized KeyPairInfo getKeyPair() throws EC2Exception, IOException { - if(usableKeyPair==null) - usableKeyPair = privateKey.find(connect()); - return usableKeyPair; - } - - /** - * Counts the number of instances in EC2 currently running. - * - *

- * This includes those instances that may be started outside Hudson. - */ - public int countCurrentEC2Slaves() throws EC2Exception { - int n=0; - for (ReservationDescription r : connect().describeInstances(Collections.emptyList())) { - for (Instance i : r.getInstances()) { - if(!i.isTerminated()) - n++; - } - } - return n; - } - - /** - * Debug command to attach to a running instance. - */ - public void doAttach(StaplerRequest req, StaplerResponse rsp, @QueryParameter String id) throws ServletException, IOException, EC2Exception { - checkPermission(PROVISION); - SlaveTemplate t = getTemplates().get(0); - - StringWriter sw = new StringWriter(); - StreamTaskListener listener = new StreamTaskListener(sw); - EC2Slave node = t.attach(id,listener); - Hudson.getInstance().addNode(node); - - rsp.sendRedirect2(req.getContextPath()+"/computer/"+node.getNodeName()); - } - - public void doProvision(StaplerRequest req, StaplerResponse rsp, @QueryParameter String ami) throws ServletException, IOException { - checkPermission(PROVISION); - if(ami==null) { - sendError("The 'ami' query parameter is missing",req,rsp); - return; - } - SlaveTemplate t = getTemplate(ami); - if(t==null) { - sendError("No such AMI: "+ami,req,rsp); - return; - } - - StringWriter sw = new StringWriter(); - StreamTaskListener listener = new StreamTaskListener(sw); - try { - EC2Slave node = t.provision(listener); - Hudson.getInstance().addNode(node); - - rsp.sendRedirect2(req.getContextPath()+"/computer/"+node.getNodeName()); - } catch (EC2Exception e) { - e.printStackTrace(listener.error(e.getMessage())); - sendError(sw.toString(),req,rsp); - } - } - - public Collection provision(Label label, int excessWorkload) { - try { - - final SlaveTemplate t = getTemplate(label); - - List r = new ArrayList(); - for( ; excessWorkload>0; excessWorkload-- ) { - if(countCurrentEC2Slaves()>=instanceCap) - break; // maxed out - - r.add(new PlannedNode(t.getDisplayName(), - Computer.threadPoolForRemoting.submit(new Callable() { - public Node call() throws Exception { - // TODO: record the output somewhere - EC2Slave s = t.provision(new StreamTaskListener(System.out)); - Hudson.getInstance().addNode(s); - // EC2 instances may have a long init script. If we declare - // the provisioning complete by returning without the connect - // operation, NodeProvisioner may decide that it still wants - // one more instance, because it sees that (1) all the slaves - // are offline (because it's still being launched) and - // (2) there's no capacity provisioned yet. - // - // deferring the completion of provisioning until the launch - // goes successful prevents this problem. - s.toComputer().connect(false).get(); - return s; - } - }) - ,t.getNumExecutors())); - } - return r; - } catch (EC2Exception e) { - LOGGER.log(WARNING,"Failed to count the # of live instances on EC2",e); - return Collections.emptyList(); - } - } - - public boolean canProvision(Label label) { - return getTemplate(label)!=null; - } - +public class EC2Cloud extends GenericEC2Cloud { /** - * Gets the first {@link EC2Cloud} instance configured in the current Hudson, or null if no such thing exists. + * Represents the region. Can be null for backward compatibility reasons. */ - public static EC2Cloud get() { - return Hudson.getInstance().clouds.get(EC2Cloud.class); - } + private AwsRegion region; - /** - * Connects to EC2 and returns {@link Jec2}, which can then be used to communicate with EC2. - */ - public Jec2 connect() throws EC2Exception { - try { - return connect(accessId, secretKey, getEc2EndpointUrl()); - } catch (IOException e) { - throw new EC2Exception("Failed to retrieve the endpoint",e); - } + @DataBoundConstructor + public EC2Cloud(AwsRegion region, String accessId, String secretKey, String privateKey, String instanceCapStr, List templates) { + super("ec2-"+region.name(), accessId, secretKey, privateKey, instanceCapStr, templates); + this.region = region; } - /*** - * Connect to an EC2 instance. - * @return Jec2 - */ - public static Jec2 connect(String accessId, String secretKey, URL endpoint) { - return connect(accessId, Secret.fromString(secretKey), endpoint); + public AwsRegion getRegion() { + if (region==null) + region = AwsRegion.US_EAST_1; // backward data compatibility with earlier versions + return region; } - /*** - * Connect to an EC2 instance. - * @return Jec2 - */ - public static Jec2 connect(String accessId, Secret secretKey, URL endpoint) { - int ec2Port = portFromURL(endpoint); - boolean SSL = isSSL(endpoint); - Jec2 result = new Jec2(accessId, secretKey.toString(), SSL, endpoint.getHost(), ec2Port); - String path = endpoint.getPath(); - if (path.length() != 0) /* '/' is the default, not '' */ - result.setResourcePrefix(path); - return result; - } - - /*** - * Convert a configured hostname like 'us-east-1' to a FQDN or ip address - */ - public static String convertHostName(String ec2HostName) { - if (ec2HostName == null || ec2HostName.length()==0) - ec2HostName = "us-east-1"; - if (!ec2HostName.contains(".")) - ec2HostName = ec2HostName + ".ec2.amazonaws.com"; - return ec2HostName; - } - - /*** - * Convert a configured s3 endpoint to a FQDN or ip address - */ - public static String convertS3HostName(String s3HostName) { - if (s3HostName == null || s3HostName.length()==0) - s3HostName = "s3"; - if (!s3HostName.contains(".")) - s3HostName = s3HostName + ".amazonaws.com"; - return s3HostName; - } - - /*** - * Convert a user entered string into a port number - * "" -> -1 to indicate default based on SSL setting - */ - public static Integer convertPort(String ec2Port) { - if (ec2Port == null || ec2Port.length() == 0) - return -1; - else - return Integer.parseInt(ec2Port); - } - - /** - * Connects to S3 and returns {@link S3Service}. - */ - public S3Service connectS3() throws S3ServiceException, IOException { - URL s3 = getS3EndpointUrl(); - - return new RestS3Service(new AWSCredentials(accessId,secretKey.toString()), - null, null, buildJets3tProperties(s3)); - } - - /** - * Builds the connection parameters for S3. - */ - protected Jets3tProperties buildJets3tProperties(URL s3) { - Jets3tProperties props = Jets3tProperties.getInstance(Constants.JETS3T_PROPERTIES_FILENAME); - final String s3Host = s3.getHost(); - if (!s3Host.equals("s3.amazonaws.com")) - props.setProperty("s3service.s3-endpoint", s3Host); - int s3Port = portFromURL(s3); - if (s3Port != -1) - props.setProperty("s3service.s3-endpoint-http-port", String.valueOf(s3Port)); - if (s3.getPath().length() > 1) - props.setProperty("s3service.s3-endpoint-virtual-path", s3.getPath()); - props.setProperty("s3service.https-only", String.valueOf(isSSL(s3))); - return props; - } - - /** - * Computes the presigned URL for the given S3 resource. - * - * @param path - * String like "/bucketName/folder/folder/abc.txt" that represents the resource to request. - */ - public URL buildPresignedURL(String path) throws IOException, S3ServiceException { - long expires = System.currentTimeMillis()/1000+60*60; - String token = "GET\n\n\n" + expires + "\n" + path; - - String url = "http://s3.amazonaws.com"+path+"?AWSAccessKeyId="+accessId+"&Expires="+expires+"&Signature="+ - URLEncoder.encode( - ServiceUtils.signWithHmacSha1(secretKey.toString(),token),"UTF-8"); - return new URL(url); + @Override + public URL getEc2EndpointUrl() { + return getRegion().ec2Endpoint; } - /* Parse a url or return a sensible error */ - public static URL checkEndPoint(String url) throws FormValidation { - try { - return new URL(url); - } catch (MalformedURLException ex) { - throw FormValidation.error("Endpoint URL is not a valid URL"); - } + @Override + public URL getS3EndpointUrl() { + return getRegion().s3Endpoint; } - - public static abstract class DescriptorImpl extends Descriptor { - public InstanceType[] getInstanceTypes() { - return InstanceType.values(); - } - - /** - * TODO: once 1.304 is released, revert to FormValidation.validateBase64 - */ - private FormValidation validateBase64(String value, boolean allowWhitespace, boolean allowEmpty, String errorMessage) { - try { - String v = value; - if(!allowWhitespace) { - if(v.indexOf(' ')>=0 || v.indexOf('\n')>=0) - return FormValidation.error(errorMessage); - } - v=v.trim(); - if(!allowEmpty && v.length()==0) - return FormValidation.error(errorMessage); - - com.trilead.ssh2.crypto.Base64.decode(v.toCharArray()); - return FormValidation.ok(); - } catch (IOException e) { - return FormValidation.error(errorMessage); - } + @Extension + public static class DescriptorImpl extends GenericEC2Cloud.DescriptorImpl { + public String getDisplayName() { + return "Amazon EC2"; } - public FormValidation doCheckAccessId(@QueryParameter String value) throws IOException, ServletException { - return validateBase64(value,false,false,Messages.EC2Cloud_InvalidAccessId()); + public FormValidation doTestConnection( + @QueryParameter AwsRegion region, + @QueryParameter String accessId, + @QueryParameter String secretKey, + @QueryParameter String privateKey) throws IOException, ServletException { + return super.doTestConnection(region.ec2Endpoint,accessId,secretKey,privateKey); } - public FormValidation doCheckSecretKey(@QueryParameter String value) throws IOException, ServletException { - return validateBase64(value,false,false,Messages.EC2Cloud_InvalidSecretKey()); + public FormValidation doGenerateKey( + StaplerResponse rsp, @QueryParameter AwsRegion region, @QueryParameter String accessId, @QueryParameter String secretKey) throws IOException, ServletException { + return super.doGenerateKey(rsp,region.ec2Endpoint,accessId,secretKey); } - - public FormValidation doCheckPrivateKey(@QueryParameter String value) throws IOException, ServletException { - boolean hasStart=false,hasEnd=false; - BufferedReader br = new BufferedReader(new StringReader(value)); - String line; - while ((line = br.readLine()) != null) { - if (line.equals("-----BEGIN RSA PRIVATE KEY-----")) - hasStart=true; - if (line.equals("-----END RSA PRIVATE KEY-----")) - hasEnd=true; - } - if(!hasStart) - return FormValidation.error("This doesn't look like a private key at all"); - if(!hasEnd) - return FormValidation.error("The private key is missing the trailing 'END RSA PRIVATE KEY' marker. Copy&paste error?"); - return FormValidation.ok(); - } - - protected FormValidation doTestConnection( URL ec2endpoint, - String accessId, String secretKey, String privateKey) throws IOException, ServletException { - try { - Jec2 jec2 = connect(accessId, secretKey, ec2endpoint); - jec2.describeInstances(Collections.emptyList()); - - if(accessId==null) - return FormValidation.error("Access ID is not specified"); - if(secretKey==null) - return FormValidation.error("Secret key is not specified"); - if(privateKey==null) - return FormValidation.error("Private key is not specified. Click 'Generate Key' to generate one."); - - if(privateKey.trim().length()>0) { - // check if this key exists - EC2PrivateKey pk = new EC2PrivateKey(privateKey); - if(pk.find(jec2)==null) - return FormValidation.error("The private key entered below isn't registered to EC2 (fingerprint is "+pk.getFingerprint()+")"); - } - - return FormValidation.ok(Messages.EC2Cloud_Success()); - } catch (EC2Exception e) { - LOGGER.log(WARNING, "Failed to check EC2 credential",e); - return FormValidation.error(e.getMessage()); - } - } - - public FormValidation doGenerateKey(StaplerResponse rsp, URL ec2EndpointUrl, String accessId, String secretKey - ) throws IOException, ServletException { - try { - Jec2 jec2 = connect(accessId, secretKey, ec2EndpointUrl); - List existingKeys = jec2.describeKeyPairs(Collections.emptyList()); - - int n = 0; - while(true) { - boolean found = false; - for (KeyPairInfo k : existingKeys) { - if(k.getKeyName().equals("hudson-"+n)) - found=true; - } - if(!found) - break; - n++; - } - - KeyPairInfo key = jec2.createKeyPair("hudson-" + n); - - - rsp.addHeader("script","findPreviousFormItem(button,'privateKey').value='"+key.getKeyMaterial().replace("\n","\\n")+"'"); - - return FormValidation.ok(Messages.EC2Cloud_Success()); - } catch (EC2Exception e) { - LOGGER.log(WARNING, "Failed to check EC2 credential",e); - return FormValidation.error(e.getMessage()); - } - } - } - - private static final Logger LOGGER = Logger.getLogger(EC2Cloud.class.getName()); - - private static boolean isSSL(URL endpoint) { - return endpoint.getProtocol().equals("https"); - } - - private static int portFromURL(URL endpoint) { - int ec2Port = endpoint.getPort(); - if (ec2Port == -1) { - ec2Port = endpoint.getDefaultPort(); - } - return ec2Port; - } - - static { - // backward compatibility. EC2Cloud used to be a concrete class that represents AmazonEC2Cloud - Hudson.XSTREAM.alias(EC2Cloud.class.getName(),AmazonEC2Cloud.class); } } Index: src/main/java/hudson/plugins/ec2/SlaveTemplate.java =================================================================== --- src/main/java/hudson/plugins/ec2/SlaveTemplate.java (revision 29721) +++ src/main/java/hudson/plugins/ec2/SlaveTemplate.java (working copy) @@ -43,7 +43,7 @@ public final String numExecutors; public final String remoteAdmin; public final String rootCommandPrefix; - protected transient EC2Cloud parent; + protected transient GenericEC2Cloud parent; private transient /*almost final*/ Set

+ Specifies the geographic region in which your slaves will run. Pick the region closest to you. + Regions can be thought of as + independent instances of EC2/S3. See online resources + for more about what regions are. +
\ No newline at end of file Index: src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly =================================================================== --- src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly (revision 29921) +++ src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/config-entries.jelly (working copy) @@ -1,21 +0,0 @@ - - - ${it.displayName} - - - - - - - - - - - - - - - - - - Index: src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-region.html =================================================================== --- src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-region.html (revision 29721) +++ src/main/resources/hudson/plugins/ec2/AmazonEC2Cloud/help-region.html (working copy) @@ -1,6 +0,0 @@ -
- Specifies the geographic region in which your slaves will run. Pick the region closest to you. - Regions can be thought of as - independent instances of EC2/S3. See online resources - for more about what regions are. -
\ No newline at end of file Index: src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java =================================================================== --- src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java (revision 29721) +++ src/test/java/hudson/plugins/ec2/AmazonEC2CloudTest.java (working copy) @@ -9,7 +9,7 @@ */ public class AmazonEC2CloudTest extends HudsonTestCase { public void testConfigRoundtrip() throws Exception { - AmazonEC2Cloud orig = new AmazonEC2Cloud(AwsRegion.US_EAST_1, "abc", "def", "ghi", "3", Collections.emptyList()); + EC2Cloud orig = new EC2Cloud(AwsRegion.US_EAST_1, "abc", "def", "ghi", "3", Collections.emptyList()); hudson.clouds.add(orig); submit(createWebClient().goTo("configure").getFormByName("config"));